Add PayPal webhook handler and marketing styles

- Implemented PayPal webhook for handling payment notifications, including signature verification and transaction updates.
- Created invoice generation and license management for software upgrades upon successful payment.
- Added comprehensive logging for debugging purposes.
- Introduced new CSS styles for the marketing file management system, including layout, toolbar, breadcrumb navigation, search filters, and file management UI components.
This commit is contained in:
“VeLiTi”
2026-01-09 15:19:28 +01:00
parent 08263c7933
commit 2520fb2b75
38 changed files with 4166 additions and 1107 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,103 +0,0 @@
# Payment Integration Implementation Summary
## Overview
Complete payment integration for software upgrades using existing ecommerce infrastructure (transaction API, invoice API, PHPMailer, DomPDF).
## New Files to Create
### 1. `/webhook_mollie.php` (Root directory)
**Purpose**: Mollie webhook handler specifically for software upgrades
**Based on**: Existing webhook.php from commerce product
**Key features**:
- ✅ Uses `/v2/transactions/` API for status updates (consistent with commerce)
- ✅ Uses `/v2/invoice/` API for invoice generation
- ✅ Creates PDF invoice with DomPDF
- ✅ Sends email via PHPMailer
- ✅ Creates software license
- ✅ Multi-language support
- ✅ Sends to bookkeeping if configured
**Webhook URL**: `https://yourdomain.com/webhook_mollie.php`
### 2. `/api/v2/post/payment.php`
**Purpose**: Create Mollie payment for software upgrade
**Input**: serial_number, version_id, user_data
**Output**: {checkout_url, payment_id}
**Security**: Server-side price calculation
### 3. `/api/v2/get/payment.php`
**Purpose**: Retrieve payment status
**Input**: ?payment_id=xxx
**Output**: {payment_id, payment_status, serial_number, equipment_id, ...}
## Modified Files
### 1. `/assets/functions.php`
**Add new functions**:
- `generateUniqueLicenseKey()` - Generate unique license keys
- `generateSoftwareInvoice($invoice_data, $order_id, $language)` - Generate HTML invoice for software upgrades
- Based on existing `generateInvoice()` function
- Custom template for software licenses
- Shows: Device serial number, software version, license key, expiry date
- Returns: [$html_content, $customer_email, $order_id]
### 2. `/assets/softwaretool.js`
**Modify**:
- `processPayment()` - Call `/v2/post/payment` API
- `downloadAndInstallSoftware()` - Add serial number verification
### 3. `/softwaretool.php`
**Add**: Payment return detection (`?payment_id=xxx`)
## Database
**No changes needed** - Uses existing:
- `transactions` table (txn_id, payment_status, payment_amount, etc.)
- `transactions_items` table (item_id, item_options with JSON)
## Payment Status Codes (Matching Commerce System)
- `0` = Pending
- `1` = Paid
- `101` = Open/Pending (Mollie)
- `102` = Failed
- `103` = Expired
- `999` = Canceled
## Implementation Order
1. ✅ Add Mollie constants to config.php
2. Create helper functions in functions.php:
- `generateUniqueLicenseKey()`
- `generateSoftwareInvoice()`
3. Create `/api/v2/post/payment.php`
4. Create `/api/v2/get/payment.php`
5. Create `/webhook_mollie.php`
6. Modify frontend JavaScript
7. Modify softwaretool.php
8. Test in DEBUG mode
9. Test with Mollie sandbox
10. Deploy to production
## Key Benefits
1. **Consistent with ecommerce** - Uses same API structure
2. **Professional invoices** - PDF generation + email delivery
3. **Complete audit trail** - Transactions + invoices + licenses
4. **Multi-language** - Invoice language based on customer country
5. **Bookkeeping integration** - Auto-send to bookkeeping email
6. **Refund handling** - Webhook detects refunds (TODO: disable license)
## Invoice Email Template
The email will include:
- Subject: "Software Upgrade - Invoice: [ORDER_ID]"
- HTML body with invoice details
- PDF attachment (Invoice_[ORDER_ID].pdf)
- Sent to customer email + bookkeeping (if configured)
**Invoice contains:**
- Customer details (name, address, email)
- Order ID / Transaction ID
- Software upgrade details (version, device serial number)
- License key + expiry date (2099-12-31)
- Price breakdown
- Company information

View File

@@ -1,825 +0,0 @@
# Plan: Payment Flow with Redirect for Software Upgrade Tool
## User Request
Design the payment flow for software upgrades using Mollie (payment provider) with the following requirements:
1. User initiates paid upgrade
2. System redirects to Mollie for payment
3. After successful payment, Mollie redirects back to software tool
4. System creates license connected to serialnumber
5. Download and upload to device starts automatically
## Key Challenge
**User Experience**: How to resume the upgrade flow after payment redirect, ensuring seamless transition from payment completion back to automatic download/upload.
---
## Current System Analysis
### Existing Infrastructure
**Transactions Table** - Ready for payment tracking (txn_id, payment_status, payment_amount)
**Licenses Table** - Has transaction_id field for linking (currently unused)
**Payment Modal UI** - Frontend form exists in softwaretool.js (lines 455-572)
**Payment Provider Integration** - No Mollie/Stripe/PayPal implementation exists
**Webhook Handlers** - No callback endpoints implemented
**Redirect Handling** - No return_url/cancel_url pattern
**License Auto-creation** - No logic to create licenses after successful payment
**Payment Session State** - No state persistence across redirect cycle
### Current Payment Flow (Simulated)
```
softwaretool.js:
1. User clicks "Purchase & Install" → showPaymentModal()
2. User fills form → processPayment()
3. [SIMULATED 2-second delay - no actual payment]
4. downloadAndInstallSoftware() → triggers upload.js
```
**Problem**: Step 3 will become a redirect to Mollie, breaking the flow and losing all state.
---
## User's Preferred Flow (APPROVED)
The user wants a simpler, more elegant approach:
1. **Payment creates license** - Mollie webhook creates license linked to serial number
2. **Return to software tool** - User redirected back with upgrade information in URL
3. **Reconnect device** - User connects device (may be different device!)
4. **Re-check software options** - System calls `software_update` API again
5. **License automatically applied** - Paid upgrade now shows as FREE (license found)
6. **Install button changes** - "Purchase & Install" becomes "Install Now" (free)
7. **User proceeds** - Click install to download and upload
### Key Benefits
- ✅ No complex state management needed
- ✅ Existing license checking logic handles everything
- ✅ User can connect different device (license is separate)
- ✅ Clean separation: payment → license → upgrade check
- ✅ Works with existing `software_update.php` license validation (lines 274-311)
### Critical Security Check
**IMPORTANT**: Before starting upload, verify serial number matches the one from payment.
- Store `serial_number` in payment session/URL
- When user returns and reconnects device, compare:
- `serialnumber_from_payment` vs `serialnumber_from_device`
- If mismatch: Show warning "Different device detected - license applied to original device (SN: XXXXX)"
---
## Proposed Solution Architecture
### Database Changes
**No new tables needed** - Use existing `transactions` and `transactions_items` tables
**`transactions` table fields:**
- `txn_id` (varchar 255, UNIQUE) - Store Mollie payment_id here
- `payment_status` (int 11) - Payment status code (need to define: 0=pending, 1=paid, 2=failed, 3=canceled, etc.)
- `payment_amount` (decimal 7,2) - Price
- `payer_email` (varchar 255) - Customer email
- `first_name`, `last_name` - Customer name
- `address_*` fields - Customer address
- `account_id` (varchar 255) - Can store serial_number here or user account
- `payment_method` (int 11) - Payment method ID
- `created`, `updated` - Timestamps
**`transactions_items` table fields:**
- `txn_id` (varchar 255) - Links to transactions.txn_id
- `item_id` (int 11) - Store version_id (products_software_versions.rowID)
- `item_price` (decimal 7,2) - Software version price
- `item_quantity` (int 11) - Always 1 for software upgrades
- `item_options` (varchar 255) - Store JSON with: `{"serial_number": "22110095", "equipment_id": 123, "hw_version": "r08"}`
- `created`, `updated` - Timestamps
**Payment Status Codes** (matching existing webhook.php):
- `0` = Pending (initial state, before Mollie call)
- `1` = Paid (payment successful)
- `101` = Open/Pending (Mollie isPending or isOpen)
- `102` = Failed (Mollie isFailed)
- `103` = Expired (Mollie isExpired)
- `999` = Canceled (Mollie isCanceled)
### API Endpoints Needed (Following Standard Structure)
1. **POST /api/v2/post/payment.php** - Initiates Mollie payment (create action)
2. **GET /api/v2/get/payment.php** - Retrieves payment status and details
3. **NEW `webhook_mollie.php`** - Separate webhook for software upgrades (based on webhook.php structure, but simplified for this use case)
### Simplified Flow Diagram
```
[User] → [Select Paid Upgrade] → [Payment Modal]
processPayment() calls POST /v2/post/payment
- Store pending payment in DB
- Call Mollie API: create payment
- Get checkout URL
- Redirect user to Mollie
User pays at Mollie ←→ [Mollie Payment Page]
┌───────────────────────┴───────────────────────┐
↓ ↓
[Mollie redirects user back] [Mollie webhook fires asynchronously]
softwaretool.php?payment_id={payment_id} NEW webhook_mollie.php receives POST
- Calls GET /v2/get/payment?payment_id=X - Fetches payment from Mollie API
- Shows status message - Updates transaction status (1=paid)
- Display device connection button - Creates license in products_software_licenses
- Updates equipment.sw_version_license
[User clicks "Connect Device"]
connectDeviceForSoftware()
- User connects device (may be different device!)
- Read SN, FW, HW from device
checkSoftwareAvailability() → calls /v2/software_update
- Existing license validation (lines 274-311) finds license
- Paid upgrade now shows price = 0.00
- Button text changes: "Purchase & Install" → "Install Now"
[User clicks "Install Now"]
selectUpgrade(option) → sees price = 0, skips payment modal
downloadAndInstallSoftware()
- CRITICAL: Verify serial number matches payment
- If mismatch: Show warning but allow (license already applied)
- Download firmware
- Trigger upload.js
```
### Key Design Decisions
**1. Leverage Existing License Logic**
- No need to manually check licenses in frontend
- `software_update.php` lines 274-311 already handle this perfectly
- When license exists and is valid, price automatically becomes 0.00
- Frontend just needs to check `if (price === 0)` to show different button
**2. Minimal State Management**
- Store only essential data in `transactions` and `transactions_items`
- URL parameters carry context back (payment_id)
- No need to persist entire upgrade state
- User reconnects device = fresh state from device
**3. Serial Number Verification**
- Store `serial_number` in `transactions_items.item_options` JSON
- After return, when user reconnects device, compare:
- `serialnumber_from_payment` (from item_options JSON)
- `deviceSerialNumber` (from connected device)
- If mismatch: Show warning "Different device detected. License was applied to device SN: XXXXX"
- Allow upload to proceed (license is already created for original SN)
**4. Separate Webhook for Software Upgrades**
- Create new `webhook_mollie.php` based on structure from existing webhook.php
- Specifically designed for software upgrade payments (no invoice generation needed)
- Simplified logic: Just update transaction status and create license
- Webhook URL: `https://site.com/webhook_mollie.php`
- Webhook is authoritative for license creation
- Return URL handler just shows status message
- Race condition safe: user may see "payment successful" before webhook fires
---
## Implementation Plan
### Phase 1: Database & Payment Infrastructure
**1.1 Database Table - No Changes Needed**
```
The existing transactions and transactions_items tables will be used.
No schema modifications required.
```
**1.2 Create `/api/v2/post/payment.php`**
```php
<?php
defined($security_key) or exit;
// POST endpoint for payment creation
// Input (JSON): serial_number, version_id, user_data (name, email, address)
// Output (JSON): {checkout_url: "https://mollie.com/...", payment_id: "tr_xxx"}
//Connect to DB
$pdo = dbConnect($dbname);
//CONTENT FROM API (POST)
$post_content = json_decode($input, true);
// SECURITY: Never trust price/currency from frontend!
// Steps:
1. Validate inputs (serial_number, version_id, user_data)
2. SERVER-SIDE: Calculate actual price using software_update logic:
a. Get equipment data from serial_number
b. Get version data from version_id
c. Check upgrade path pricing (same logic as software_update.php lines 237-253)
d. Check license validity (same logic as software_update.php lines 274-311)
e. Calculate FINAL price server-side
3. Verify price > 0 (free upgrades shouldn't reach payment API)
4. Call Mollie API FIRST to get payment_id:
$mollie->payments->create([
'amount' => ['currency' => 'EUR', 'value' => $final_price],
'description' => 'Software upgrade to version X',
'redirectUrl' => 'https://site.com/softwaretool.php?payment_return=1&payment_id={payment_id}',
'webhookUrl' => 'https://site.com/webhook_mollie.php', // NEW webhook for software upgrades
'metadata' => ['order_id' => $mollie_payment_id] // for compatibility
])
5. Store transaction in DB with Mollie payment_id:
INSERT INTO transactions (txn_id, payment_amount, payment_status, payer_email, first_name, last_name, address_*, account_id, ...)
VALUES ($mollie_payment_id, $final_price, 0, ...) -- 0 = pending
6. Store transaction item:
INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, ...)
VALUES ($mollie_payment_id, $version_id, $final_price, 1, '{"serial_number":"...", "equipment_id":...}', ...)
7. Return JSON: {checkout_url: $mollie_checkout_url, payment_id: $mollie_payment_id}
```
**1.3 Create `/api/v2/get/payment.php`**
```php
<?php
defined($security_key) or exit;
// GET endpoint for payment status retrieval
// Input (URL): ?payment_id=tr_xxx
// Output (JSON): {payment_id, serial_number, version_id, payment_status, price, currency, user_data}
//Connect to DB
$pdo = dbConnect($dbname);
//NEW ARRAY
$criterias = [];
//Check for $_GET variables
if(isset($get_content) && $get_content!=''){
$requests = explode("&", $get_content);
foreach ($requests as $y){
$v = explode("=", $y);
$criterias[$v[0]] = $v[1];
}
}
// Steps:
1. Validate payment_id from URL
2. Fetch transaction: SELECT * FROM transactions WHERE txn_id = ?
3. Fetch transaction item: SELECT * FROM transactions_items WHERE txn_id = ?
4. Parse item_options JSON to get serial_number, equipment_id
5. Return JSON with payment details:
{
"payment_id": txn_id,
"payment_status": payment_status, // 0=pending, 1=paid, 2=failed, 3=canceled
"payment_amount": payment_amount,
"serial_number": from item_options JSON,
"equipment_id": from item_options JSON,
"version_id": item_id,
"payer_email": payer_email,
"customer_name": first_name + " " + last_name
}
6. If not found, return error
```
**1.4 Create NEW `webhook_mollie.php`**
```php
<?php
// NEW FILE - Webhook for software upgrade payments
// Based on structure from existing webhook.php from commerce product
// Uses existing transaction API + invoice API + email system
require_once 'assets/config.php';
require_once 'assets/functions.php';
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//LOGIN TO API (same as commerce webhook.php)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$data = json_encode(array("clientID" => clientID, "clientsecret" => clientsecret), JSON_UNESCAPED_UNICODE);
$responses = ioAPIv2('/v2/authorization', $data,'');
//Decode Payload
if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';}
$clientsecret = $responses['token'];
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// BASEURL is required for invoice template
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$base_url = 'https://'.$_SERVER['SERVER_NAME'].'/';
define('base_url', $base_url);
try {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Initialize the Mollie API library
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
require "initialize.php"; // Mollie initialization (from commerce webhook)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Retrieve the payment's current state
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payment = $mollie->payments->get($_POST["id"]);
$orderId = $payment->metadata->order_id;
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Update the transaction using existing API
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($payment->isPaid() && !$payment->hasRefunds() && !$payment->hasChargebacks()) {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// PAID - Update transaction status via API
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 1), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
$transaction = json_decode($transaction,true);
if ($transaction !== null && !empty($transaction)) {
if(count($transaction) > 0) {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// CREATE LICENSE for software upgrade
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$pdo = dbConnect($dbname);
// Fetch transaction items to find software upgrade
$sql = 'SELECT * FROM transactions_items WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$orderId]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($items as $item) {
if (!empty($item['item_options'])) {
$options = json_decode($item['item_options'], true);
// Check if this is a software upgrade (has serial_number and equipment_id)
if (isset($options['serial_number']) && isset($options['equipment_id'])) {
// Check if license already exists for this transaction
$sql = 'SELECT rowID FROM products_software_licenses WHERE transaction_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$orderId]);
$existing_license = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$existing_license) {
// Generate unique license key
$license_key = generateUniqueLicenseKey();
// Create license
$sql = 'INSERT INTO products_software_licenses
(license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$license_key,
$options['equipment_id'],
'upgrade',
1, // active
date('Y-m-d H:i:s'),
'2099-12-31 23:59:59', // effectively permanent
$orderId,
date('Y-m-d H:i:s'),
'webhook' // created by webhook
]);
// Update equipment.sw_version_license
$sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$license_key, $options['equipment_id']]);
error_log("Webhook: License created for equipment_id: " . $options['equipment_id'] . ", license_key: " . $license_key);
}
}
}
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Generate INVOICE RECORD via API
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payload = json_encode(array("txn_id" => $transaction['transaction_id']), JSON_UNESCAPED_UNICODE);
$invoice = ioAPIv2('/v2/invoice/',$payload,$clientsecret);
$invoice = json_decode($invoice,true);
if ($invoice !== null && !empty($invoice)) {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Generate INVOICE PDF and send email
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$invoice_cust = ioAPIv2('/v2/invoice/list=invoice&id='.$invoice['invoice_id'],'',$clientsecret);
$invoice_cust = json_decode($invoice_cust,true);
// Determine invoice language
if (!empty($invoice_cust['customer']['language'])) {
$invoice_language = strtoupper($invoice_cust['customer']['language']);
} elseif (!empty($invoice_cust['customer']['country']) && isset($available_languages[strtoupper($invoice_cust['customer']['country'])])) {
$invoice_language = $available_languages[strtoupper($invoice_cust['customer']['country'])];
} else {
$invoice_language = 'US'; // Default fallback
}
// Generate invoice HTML (using custom template for software upgrades)
list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
//CREATE PDF using DomPDF
$dompdf->loadHtml($data);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$subject = ($invoice_software_subject ?? 'Software Upgrade - Invoice: ').$order_id;
$attachment = $dompdf->output();
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Send email via PHPMailer
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
send_mail_by_PHPMailer($customer_email, $subject, $data, $attachment, $subject);
if(invoice_bookkeeping){
send_mail_by_PHPMailer(email_bookkeeping, $subject, $data, $attachment, $subject);
}
}
}
}
} elseif ($payment->isOpen()) {
// OPEN/PENDING (101)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 101), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
} elseif ($payment->isPending()) {
// PENDING (101)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 101), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
} elseif ($payment->isFailed()) {
// FAILED (102)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 102), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
} elseif ($payment->isExpired()) {
// EXPIRED (103)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 103), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
} elseif ($payment->isCanceled()) {
// CANCELED (999)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 999), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
} elseif ($payment->hasRefunds()) {
// REFUNDED (1 + refund flag)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 1), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
// TODO: Disable license on refund
}
} catch (\Mollie\Api\Exceptions\ApiException $e) {
error_log("Webhook API call failed: " . htmlspecialchars($e->getMessage()));
http_response_code(500);
echo "API call failed: " . htmlspecialchars($e->getMessage());
} catch (Exception $e) {
error_log("Webhook error: " . htmlspecialchars($e->getMessage()));
http_response_code(500);
}
```
**Key Features (matching commerce webhook.php):**
- ✅ Uses `/v2/transactions/` API for status updates
- ✅ Uses `/v2/invoice/` API for invoice generation
- ✅ Generates PDF invoice with DomPDF
- ✅ Sends email via PHPMailer
- ✅ Creates license for software upgrade
- ✅ Uses same payment status codes (0, 1, 101, 102, 103, 999)
- ✅ Handles refunds (TODO: disable license)
- ✅ Multi-language invoice support
- ✅ Sends to bookkeeping if configured
### Phase 2: Frontend Integration
**2.1 Modify `processPayment()` in softwaretool.js (lines 574-608)**
```javascript
async function processPayment(paymentData, option, modal) {
try {
progressBar("10", "Processing payment...", "#04AA6D");
// SECURITY: Only send serial_number and version_id
// Server will calculate the price to prevent tampering
const response = await fetch(link + "/v2/post/payment", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + document.getElementById("servicetoken").textContent
},
body: JSON.stringify({
serial_number: deviceSerialNumber,
version_id: option.version_id,
user_data: paymentData // name, email, address only
// REMOVED: price, currency - server calculates these
})
});
const result = await response.json();
if (result.checkout_url) {
await logCommunication(`Redirecting to payment provider`, 'sent');
// Redirect to Mollie checkout
window.location.href = result.checkout_url;
} else {
throw new Error(result.error || "Failed to create payment");
}
} catch (error) {
await logCommunication(`Payment error: ${error.message}`, 'error');
progressBar("0", "Payment failed: " + error.message, "#ff6666");
alert("Payment failed: " + error.message);
}
}
```
**2.2 Remove equipment_id tracking - NOT NEEDED**
```javascript
// SECURITY: We don't need to track equipment_id in frontend
// The server will look it up from serial_number in the payment/create API
// This prevents tampering with equipment_id
```
**2.3 Add Serial Number Verification in `downloadAndInstallSoftware()` (lines 610-699)**
```javascript
async function downloadAndInstallSoftware(option) {
// Check if we're returning from payment
const urlParams = new URLSearchParams(window.location.search);
const paymentId = urlParams.get('payment_id');
if (paymentId) {
// Verify serial number matches payment using GET /v2/get/payment
const response = await fetch(link + `/v2/get/payment?payment_id=${paymentId}`, {
method: "GET",
headers: {
"Authorization": "Bearer " + document.getElementById("servicetoken").textContent
}
});
const paymentData = await response.json();
if (paymentData.serial_number !== deviceSerialNumber) {
const confirmed = confirm(
`WARNING: Different device detected!\n\n` +
`License was created for device: ${paymentData.serial_number}\n` +
`Currently connected device: ${deviceSerialNumber}\n\n` +
`The license is already applied to the original device. ` +
`Do you want to continue with this device anyway?`
);
if (!confirmed) {
progressBar("0", "Upload canceled by user", "#ff6666");
return;
}
}
}
// Continue with existing download logic...
selectedSoftwareUrl = option.source;
// ... rest of function unchanged
}
```
**Note**: Serial number verification uses existing GET /v2/get/payment endpoint (no separate verify endpoint needed)
### Phase 3: Return URL Handling
**3.1 Modify `softwaretool.php` to detect return from payment**
```php
// Add near top of softwaretool.php (after includes, before $view)
<?php
$payment_return = isset($_GET['payment_id']) ? $_GET['payment_id'] : null;
if ($payment_return) {
// Optionally fetch payment status via GET /v2/get/payment
// and show appropriate message banner at top of page
// "Payment successful! Please reconnect your device to continue."
// User will then click "Connect Device" button
// After connection, checkSoftwareAvailability() will run
// License will be found via existing logic, price will be 0.00
}
?>
```
**3.2 Optional: Auto-trigger device connection after payment return**
```javascript
// In softwaretool.js, check URL on page load
window.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('payment_id')) {
// Show message: "Payment successful! Please reconnect your device."
// Optionally auto-show device connection UI
}
});
```
### Phase 4: Testing Strategy
**4.1 DEBUG Mode Testing (Complete Simulation)**
```php
// In /api/v2/post/payment.php, check if DEBUG mode
if (defined('debug') && debug) {
// FULL SIMULATION: No Mollie API connection, no device connection
$fake_payment_id = 'DEBUG_' . uniqid();
// 1. Store transaction with status 0 (pending)
$sql = 'INSERT INTO transactions
(txn_id, payment_amount, payment_status, payer_email, first_name, last_name,
address_street, address_city, address_state, address_zip, address_country, account_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$fake_payment_id,
$final_price,
0, // 0 = pending
$post_content['user_data']['email'],
$post_content['user_data']['first_name'] ?? '',
$post_content['user_data']['last_name'] ?? '',
$post_content['user_data']['address_street'] ?? '',
$post_content['user_data']['address_city'] ?? '',
$post_content['user_data']['address_state'] ?? '',
$post_content['user_data']['address_zip'] ?? '',
$post_content['user_data']['address_country'] ?? '',
$post_content['serial_number'] // store serial number in account_id
]);
// 2. Store transaction item
$item_options = json_encode([
'serial_number' => $post_content['serial_number'],
'equipment_id' => $equipment_id,
'hw_version' => $hw_version
]);
$sql = 'INSERT INTO transactions_items
(txn_id, item_id, item_price, item_quantity, item_options)
VALUES (?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$fake_payment_id,
$post_content['version_id'],
$final_price,
1,
$item_options
]);
// 3. Immediately simulate webhook success (update status to paid + create license)
$sql = 'UPDATE transactions SET payment_status = 1 WHERE txn_id = ?'; // 1 = paid
$stmt = $pdo->prepare($sql);
$stmt->execute([$fake_payment_id]);
// 4. Create license
$license_key = generateUniqueLicenseKey();
$sql = 'INSERT INTO products_software_licenses
(license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$license_key,
$equipment_id,
'upgrade',
1,
date('Y-m-d H:i:s'),
'2099-12-31 23:59:59',
$fake_payment_id,
date('Y-m-d H:i:s'),
$username
]);
// 5. Update equipment.sw_version_license
$sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$license_key, $equipment_id]);
// 6. Return fake checkout URL that redirects immediately
$messages = [
'checkout_url' => 'https://'.$_SERVER['SERVER_NAME'].'/softwaretool.php?payment_return=1&payment_id=' . $fake_payment_id,
'payment_id' => $fake_payment_id
];
echo json_encode($messages);
exit;
}
```
**Note**: In DEBUG mode, the entire payment + license creation flow is simulated without:
- Calling Mollie API
- Requiring physical device connection (works with DEBUG mode mock device data in softwaretool.js)
**4.2 Mollie Sandbox Testing**
1. Use Mollie test API key
2. Test successful payment flow
3. Test failed payment flow
4. Test canceled payment flow
5. Test webhook delivery
6. Test license creation
**4.3 Serial Number Mismatch Testing**
1. Complete payment with device A (SN: 22110095)
2. Disconnect device A
3. Connect device B (different SN)
4. Verify warning appears
5. Verify license was created for device A
---
## Critical Files to Modify
### New Files
- `/api/v2/post/payment.php` - Payment creation (POST)
- `/api/v2/get/payment.php` - Payment status retrieval (GET)
- `/webhook_mollie.php` - Mollie webhook handler for software upgrades (based on existing webhook.php structure)
- `generateSoftwareInvoice()` function in `/assets/functions.php` - Invoice template for software upgrades
### Modified Files
- `/assets/softwaretool.js`:
- `processPayment()` (lines 574-608) - Call POST /v2/post/payment instead of simulation
- `downloadAndInstallSoftware()` (lines 610-699) - Add serial number verification using GET /v2/get/payment
- Add payment return detection on page load (optional)
- `/softwaretool.php`:
- Add payment return URL detection (check for ?payment_id=X)
- Optionally show success message banner after payment
- `/api/v2/get/software_update.php`:
- **No changes needed** (existing license logic at lines 274-311 works perfectly!)
### Database & Helper Functions
- No new tables needed (using existing `transactions` and `transactions_items`)
- Add helper function `generateUniqueLicenseKey()` in `assets/functions.php`
- Payment status codes already defined in existing webhook.php (0, 1, 101, 102, 103, 999)
---
## Security Architecture Summary
### ✅ **SECURE APPROACH: Server-Side Price Validation**
**Frontend sends:**
- `serial_number` (from connected device)
- `version_id` (which version they want)
- `user_data` (name, email, address)
**Backend does:**
1. Look up equipment from `serial_number`
2. Look up version from `version_id`
3. **Calculate actual price using same logic as software_update.php**:
- Check upgrade path pricing (lines 244-260)
- Check if license exists and reduces price (lines 274-311)
- Get final server-calculated price
4. Verify price > 0 (reject free upgrades)
5. Create Mollie payment with **SERVER-CALCULATED price**
6. Store pending payment with correct price
### ❌ **INSECURE APPROACH: Never Do This**
```javascript
// WRONG - User can modify price in browser console!
body: JSON.stringify({
serial_number: deviceSerialNumber,
version_id: option.version_id,
price: 0.01, // <-- Tampered from 49.99!
currency: "EUR"
})
```
**Why this is dangerous:**
- User can open browser console
- Change `option.price = 0.01` before payment
- Backend trusts this value = user pays 1 cent for €49.99 upgrade
### ✅ **CORRECT APPROACH**
```javascript
// SECURE - Only send identifiers, server calculates price
body: JSON.stringify({
serial_number: deviceSerialNumber, // Who is buying
version_id: option.version_id, // What they want
user_data: paymentData // Customer info
// NO PRICE - server calculates it!
})
```
---
## Configuration & Requirements (USER CONFIRMED)
1.**Mollie API Credentials**: User has Mollie info - will be added as constants in `config.php`
- `MOLLIE_API_KEY_TEST` (for sandbox)
- `MOLLIE_API_KEY_LIVE` (for production)
2.**License Duration**: `expires_at = '2099-12-31 23:59:59'` (effectively permanent until 2099)
3.**Multiple Devices**: One license per device (license linked to specific equipment_id)
4.**DEBUG Mode**: Full payment process simulation without Mollie connection AND without device connection
5.**Transaction Logging**: Use existing ecommerce transaction APIs:
- `transactions` table - main transaction record
- `transaction_items` table - line items (software upgrade details)
---
## Next Steps After Plan Approval
1. ✅ Add Mollie constants to `config.php`:
```php
define('MOLLIE_API_KEY_TEST', 'test_xxxxx'); // User will provide
define('MOLLIE_API_KEY_LIVE', 'live_xxxxx'); // User will provide
```
2. ✅ Mollie PHP SDK already installed (used by existing webhook.php)
3. ✅ No database changes needed (using existing `transactions` and `transactions_items` tables)
4. Create helper functions in `assets/functions.php`:
- `generateUniqueLicenseKey()` - Generate unique license keys
- `generateSoftwareInvoice()` - Generate HTML invoice for software upgrades (based on existing generateInvoice())
5. ✅ Payment status codes already defined in existing webhook.php
6. Implement NEW backend files:
- `/api/v2/post/payment.php` (with DEBUG mode support)
- `/api/v2/get/payment.php`
- `/webhook_mollie.php` (based on existing webhook.php structure)
8. Modify frontend JavaScript in `/assets/softwaretool.js`:
- Update `processPayment()` to call POST /v2/post/payment
- Add serial number verification in `downloadAndInstallSoftware()`
9. Modify `/softwaretool.php` to detect payment return
10. Test in DEBUG mode (full simulation without Mollie or device)
11. Test with Mollie sandbox
12. Deploy to production

View File

@@ -154,7 +154,8 @@ if($is_jwt_valid && str_contains($version, 'v')) {
// First check if endPoint is fileUpload // First check if endPoint is fileUpload
//------------------------------------------ //------------------------------------------
$fileUploadEndpoints = [ $fileUploadEndpoints = [
'media_upload' 'media_upload',
'marketing_upload'
]; ];
$isFileUploadEndpoint = in_array($collection, $fileUploadEndpoints); $isFileUploadEndpoint = in_array($collection, $fileUploadEndpoints);

BIN
api/.DS_Store vendored

Binary file not shown.

BIN
api/v1/.DS_Store vendored

Binary file not shown.

BIN
api/v2/.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,116 @@
<?php
defined($security_key) or exit;
// Database connection
$pdo = dbConnect($dbname);
// ============================================
// Input Validation & Sanitization
// ============================================
$filters = [
'serialnumber' => isset($_GET['serialnumber']) ? trim($_GET['serialnumber']) : null,
'type' => isset($_GET['type']) ? trim($_GET['type']) : null,
'start' => isset($_GET['start']) ? trim($_GET['start']) : date("Y-m-d", strtotime("-270 days")),
'end' => isset($_GET['end']) ? trim($_GET['end']) : date("Y-m-d", strtotime("+1 days"))
];
// ============================================
// Build Query with Prepared Statements
// ============================================
$whereClauses = [];
$params = [];
// Serial Number Filter
if ($filters['serialnumber']) {
$whereClauses[] = 'h.description LIKE :serialnumber';
$params[':serialnumber'] = "%historycreated%SN%:" . $filters['serialnumber'] . "%";
$whereClauses[] = 'h.type != :excluded_type';
$params[':excluded_type'] = 'SRIncluded';
}
// Type Filter
if ($filters['type']) {
if ($filters['type'] === 'latest') {
// Get only the latest record per equipment
if ($filters['serialnumber']) {
$whereClauses[] = 'h.rowID IN (
SELECT MAX(h2.rowID)
FROM equipment_history h2
GROUP BY h2.equipmentid
)';
} else {
$whereClauses[] = "h.description LIKE '%historycreated%'";
$whereClauses[] = 'h.rowID IN (
SELECT MAX(h2.rowID)
FROM equipment_history h2
WHERE h2.description LIKE :history_created
GROUP BY h2.equipmentid
)';
$params[':history_created'] = '%historycreated%';
}
} else {
// Specific type filter
$whereClauses[] = 'h.type = :type';
$params[':type'] = $filters['type'];
}
}
// Default filter if no other filters applied
if (empty($whereClauses)) {
$whereClauses[] = "h.description LIKE '%historycreated%'";
}
// Date Range Filter
$whereClauses[] = 'h.created BETWEEN :start_date AND :end_date';
$params[':start_date'] = $filters['start'];
$params[':end_date'] = $filters['end'];
// ============================================
// Execute Query
// ============================================
$whereClause = 'WHERE ' . implode(' AND ', $whereClauses);
$sql = "SELECT h.rowID, h.description
FROM equipment_history h
$whereClause
ORDER BY h.created DESC";
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ============================================
// Format Response
// ============================================
$results = [];
foreach ($messages as $message) {
$record = json_decode($message['description'], true);
// Handle JSON decode errors
if (json_last_error() !== JSON_ERROR_NONE) {
continue; // Skip invalid JSON
}
$record['historyID'] = (int)$message['rowID'];
$results[] = $record;
}
// Set proper headers
header('Content-Type: application/json; charset=utf-8');
echo json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (PDOException $e) {
// Log error (don't expose to client in production)
error_log("Database error: " . $e->getMessage());
//header('Content-Type: application/json; charset=utf-8', true, 500);
echo json_encode([
'error' => 'An error occurred while processing your request'
]);
}
?>

View File

@@ -275,7 +275,7 @@ else {
} }
//SQL for Paging //SQL for Paging
$sql = 'SELECT e.rowID as equipmentID, e.*, p.productcode, p.productname, p.product_media from equipment e LEFT JOIN products p ON e.productrowid = p.rowID '.$whereclause.' ORDER BY '.$sort.' LIMIT :page,:num_products'; $sql = 'SELECT e.rowID as equipmentID, e.*, p.productcode, p.productname, p.product_media, psl.starts_at,psl.expires_at,psl.status as license_status from equipment e LEFT JOIN products p ON e.productrowid = p.rowID LEFT JOIN products_software_licenses psl ON e.sw_version_license = psl.license_key '.$whereclause.' ORDER BY '.$sort.' LIMIT :page,:num_products';
} }
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);

View File

@@ -0,0 +1,155 @@
<?php
defined($security_key) or exit;
//------------------------------------------
// Marketing Files
//------------------------------------------
//Connect to DB
$pdo = dbConnect($dbname);
//SoldTo is empty
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
$whereclause = '';
// For testing, disable account hierarchy filtering
// list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'get');
//NEW ARRAY
$criterias = [];
$clause = '';
//Check for $_GET variables and build up clause
if(isset($get_content) && $get_content!=''){
//GET VARIABLES FROM URL
$requests = explode("&", $get_content);
//Check for keys and values
foreach ($requests as $y){
$v = explode("=", $y);
//INCLUDE VARIABLES IN ARRAY
$criterias[$v[0]] = $v[1];
if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='list' || $v[0] == 'action' || $v[0] =='success_msg' || $v[0] == '_t'){
//do nothing
}
elseif ($v[0] == 'folder_id') {
if ($v[1] === 'null' || $v[1] === '') {
$clause .= ' AND folder_id IS NULL';
} else {
$clause .= ' AND folder_id = :folder_id';
}
}
elseif ($v[0] == 'search') {
$clause .= ' AND (title LIKE :search OR original_filename LIKE :search)';
}
elseif ($v[0] == 'tag') {
$clause .= ' AND EXISTS (SELECT 1 FROM marketing_file_tags ft JOIN marketing_tags t ON ft.tag_id = t.id WHERE ft.file_id = mf.id AND t.tag_name = :tag)';
}
elseif ($v[0] == 'file_type') {
$clause .= ' AND file_type = :file_type';
}
else {
// Ignore unknown parameters
}
}
if ($whereclause == '' && $clause !=''){
$whereclause = 'WHERE '.substr($clause, 4);
} else {
$whereclause .= $clause;
}
}
//Set page
$pagina = 1;
if(isset($criterias['p']) && $criterias['p'] !='') {
$pagina = $criterias['p'];
}
//Set limit
$limit = 50;
if(isset($criterias['limit']) && $criterias['limit'] !='') {
$limit = intval($criterias['limit']);
}
$offset = ($pagina - 1) * $limit;
//check for totals call
if(isset($criterias['totals'])){
$sql = 'SELECT COUNT(*) as found FROM marketing_files mf '.$whereclause.' ';
$stmt = $pdo->prepare($sql);
// Bind parameters
if (!empty($criterias)) {
foreach ($criterias as $key => $value) {
if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'limit' && $key !== 'action') {
if ($key == 'search') {
$stmt->bindValue(':'.$key, '%'.$value.'%');
} elseif ($key == 'folder_id' && ($value === 'null' || $value === '')) {
continue;
} else {
$stmt->bindValue(':'.$key, $value);
}
}
}
}
$stmt->execute();
$found = $stmt->fetchColumn();
echo $found;
exit;
}
// Main query
$sql = "SELECT
mf.*,
GROUP_CONCAT(mt.tag_name) as tags
FROM marketing_files mf
LEFT JOIN marketing_file_tags mft ON mf.id = mft.file_id
LEFT JOIN marketing_tags mt ON mft.tag_id = mt.id
" . $whereclause . "
GROUP BY mf.id
ORDER BY mf.created DESC
LIMIT " . $limit . " OFFSET " . $offset;
$stmt = $pdo->prepare($sql);
// Bind parameters
if (!empty($criterias)) {
foreach ($criterias as $key => $value) {
if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'limit') {
if ($key == 'search') {
$stmt->bindValue(':'.$key, '%'.$value.'%');
} elseif ($key == 'folder_id' && ($value === 'null' || $value === '')) {
continue;
} else {
$stmt->bindValue(':'.$key, $value);
}
}
}
}
$stmt->execute();
$marketing_files = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Process each file
foreach ($marketing_files as &$file) {
// Process tags
$file['tags'] = $file['tags'] ? explode(',', $file['tags']) : [];
// Format file size
$bytes = $file['file_size'];
if ($bytes >= 1073741824) {
$file['file_size_formatted'] = number_format($bytes / 1073741824, 2) . ' GB';
} elseif ($bytes >= 1048576) {
$file['file_size_formatted'] = number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
$file['file_size_formatted'] = number_format($bytes / 1024, 2) . ' KB';
} else {
$file['file_size_formatted'] = $bytes . ' B';
}
}
// Return result
echo json_encode($marketing_files, JSON_UNESCAPED_UNICODE);
exit;

View File

@@ -0,0 +1,172 @@
<?php
defined($security_key) or exit;
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
//------------------------------------------
// Marketing Folders
//------------------------------------------
//Connect to DB
$pdo = dbConnect($dbname);
// Function to build hierarchical tree structure
function buildFolderTree($folders, $parentId = null) {
$tree = [];
foreach ($folders as $folder) {
if ($folder['parent_id'] == $parentId) {
$children = buildFolderTree($folders, $folder['id']);
$folder['children'] = $children; // Always include children array, even if empty
$tree[] = $folder;
}
}
return $tree;
}
//SoldTo is empty
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
$whereclause = '';
list($whereclause,$condition) = getWhereclauselvl2('',$permission,$partner,'get');
//NEW ARRAY
$criterias = [];
$clause = '';
//Check for $_GET variables and build up clause
if(isset($get_content) && $get_content!=''){
//GET VARIABLES FROM URL
$requests = explode("&", $get_content);
//Check for keys and values
foreach ($requests as $y){
$v = explode("=", $y);
//INCLUDE VARIABLES IN ARRAY
$criterias[$v[0]] = $v[1];
if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='list' || $v[0] =='success_msg' || $v[0] == 'action' || $v[0] == 'tree'){
//do nothing - these are not SQL parameters
}
elseif ($v[0] == 'parent_id') {
if ($v[1] === 'null' || $v[1] === '') {
$clause .= ' AND parent_id IS NULL';
} else {
$clause .= ' AND parent_id = :parent_id';
}
}
elseif ($v[0] == 'search') {
$clause .= ' AND (folder_name LIKE :search OR description LIKE :search)';
}
else {//create clause
$clause .= ' AND '.$v[0].' = :'.$v[0];
}
}
if ($whereclause == '' && $clause !=''){
$whereclause = 'WHERE '.substr($clause, 4);
} else {
$whereclause .= $clause;
}
}
//Define Query
if(isset($criterias['totals']) && $criterias['totals'] ==''){
//Request for total rows
$sql = 'SELECT count(*) as count FROM marketing_folders '.$whereclause.'';
}
elseif (isset($criterias['list']) && $criterias['list'] =='') {
//SQL for list (no paging)
$sql = "SELECT
mf.*,
(SELECT COUNT(*) FROM marketing_files WHERE folder_id = mf.id) as file_count,
(SELECT COUNT(*) FROM marketing_folders WHERE parent_id = mf.id) as subfolder_count,
CASE
WHEN mf.parent_id IS NOT NULL THEN
(SELECT folder_name FROM marketing_folders WHERE id = mf.parent_id)
ELSE NULL
END as parent_folder_name
FROM marketing_folders mf
" . $whereclause . "
ORDER BY mf.folder_name ASC";
}
else {
//SQL for paging
$sql = "SELECT
mf.*,
(SELECT COUNT(*) FROM marketing_files WHERE folder_id = mf.id) as file_count,
(SELECT COUNT(*) FROM marketing_folders WHERE parent_id = mf.id) as subfolder_count,
CASE
WHEN mf.parent_id IS NOT NULL THEN
(SELECT folder_name FROM marketing_folders WHERE id = mf.parent_id)
ELSE NULL
END as parent_folder_name
FROM marketing_folders mf
" . $whereclause . "
ORDER BY mf.folder_name ASC
LIMIT :page,:num_folders";
}
$stmt = $pdo->prepare($sql);
//Bind to query
if (str_contains($whereclause, ':condition')){
$stmt->bindValue('condition', $condition, PDO::PARAM_STR);
}
if (!empty($criterias)){
foreach ($criterias as $key => $value){
$key_condition = ':'.$key;
if (str_contains($whereclause, $key_condition)){
if ($key == 'search'){
$search_value = '%'.$value.'%';
$stmt->bindValue($key, $search_value, PDO::PARAM_STR);
}
elseif ($key == 'parent_id' && ($value === 'null' || $value === '')) {
// Skip binding for NULL parent_id
continue;
}
else {
$stmt->bindValue($key, $value, PDO::PARAM_STR);
}
}
}
}
//Add paging details
if(isset($criterias['totals']) && $criterias['totals']==''){
$stmt->execute();
$messages = $stmt->fetch();
$messages = $messages[0];
}
elseif(isset($criterias['list']) && $criterias['list']==''){
//Execute Query
$stmt->execute();
//Get results
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
else {
$current_page = isset($criterias['p']) && is_numeric($criterias['p']) ? (int)$criterias['p'] : 1;
$stmt->bindValue('page', ($current_page - 1) * $page_rows_folders, PDO::PARAM_INT);
$stmt->bindValue('num_folders', $page_rows_folders, PDO::PARAM_INT);
//Execute Query
$stmt->execute();
//Get results
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Check if tree structure is requested
if (isset($criterias['tree']) && isset($messages) && is_array($messages)) {
// Build hierarchical tree structure
$messages = buildFolderTree($messages);
}
//------------------------------------------
//JSON_ENCODE
//------------------------------------------
$messages = json_encode($messages, JSON_UNESCAPED_UNICODE);
//Send results
echo $messages;

View File

@@ -0,0 +1,115 @@
<?php
defined($security_key) or exit;
//------------------------------------------
// Marketing Tags
//------------------------------------------
//Connect to DB
$pdo = dbConnect($dbname);
//SoldTo is empty
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
$whereclause = '';
// Tags are global, so no account hierarchy filtering
// list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'get');
//NEW ARRAY
$criterias = [];
$clause = '';
//Check for $_GET variables and build up clause
if(isset($get_content) && $get_content!=''){
//GET VARIABLES FROM URL
$requests = explode("&", $get_content);
//Check for keys and values
foreach ($requests as $y){
$v = explode("=", $y);
//INCLUDE VARIABLES IN ARRAY
$criterias[$v[0]] = $v[1];
if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='list' || $v[0] =='success_msg' || $v[0] == 'action'){
//do nothing
}
elseif ($v[0] == 'search') {
$clause .= ' AND tag_name LIKE :search';
}
elseif ($v[0] == 'used_only') {
if ($v[1] === 'true') {
$clause .= ' AND id IN (SELECT DISTINCT tag_id FROM marketing_file_tags)';
}
}
else {//create clause
$clause .= ' AND '.$v[0].' = :'.$v[0];
}
}
if ($whereclause == '' && $clause !=''){
$whereclause = 'WHERE '.substr($clause, 4);
} else {
$whereclause .= $clause;
}
}
//Set page
$pagina = 1;
if(isset($criterias['p']) && $criterias['p'] !='') {
$pagina = $criterias['p'];
}
//check for totals call
if(isset($criterias['totals'])){
$sql = 'SELECT COUNT(*) as found FROM marketing_tags mt '.$whereclause.' ';
$stmt = $pdo->prepare($sql);
// Bind parameters
if (!empty($criterias)) {
foreach ($criterias as $key => $value) {
if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'used_only') {
if ($key == 'search') {
$stmt->bindValue(':'.$key, '%'.$value.'%');
} else {
$stmt->bindValue(':'.$key, $value);
}
}
}
}
$stmt->execute();
$found = $stmt->fetchColumn();
echo $found;
exit;
}
// Main query
$sql = "SELECT
mt.*,
COUNT(mft.file_id) as usage_count
FROM marketing_tags mt
LEFT JOIN marketing_file_tags mft ON mt.id = mft.tag_id
" . $whereclause . "
GROUP BY mt.id
ORDER BY mt.tag_name ASC";
$stmt = $pdo->prepare($sql);
// Bind parameters
if (!empty($criterias)) {
foreach ($criterias as $key => $value) {
if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'used_only') {
if ($key == 'search') {
$stmt->bindValue(':'.$key, '%'.$value.'%');
} else {
$stmt->bindValue(':'.$key, $value);
}
}
}
}
$stmt->execute();
$marketing_tags = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Return result
echo json_encode($marketing_tags, JSON_UNESCAPED_UNICODE);

41
api/v2/get/service.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
defined($security_key) or exit;
//------------------------------------------
// Application related calls
//------------------------------------------
$request = explode('/', trim($_SERVER['PATH_INFO'],'/'));
$action = $request[2] ?? '';
if ($action == 'init'){
include './settings/systemservicetool_init.php';
echo json_encode($init);
}
elseif ($action == 'questions' && (isset($_GET['type']) && $_GET['type'] != '')){
include './settings/systemservicetool.php';
//build questions
switch ($_GET['type']) {
case 'visual':
$arrayQuestions = $arrayQuestions_visual;
break;
case 'final':
$arrayQuestions = $arrayQuestions_finalize;
break;
case 'cartest':
include './settings/systemcartest.php';
$arrayQuestions = $arrayQuestions_cartest;
break;
}
//Return JSON
echo json_encode($arrayQuestions);
}
else {
http_response_code(400);
}
?>

View File

@@ -62,6 +62,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
e.sw_version as current_sw_version, e.sw_version as current_sw_version,
e.hw_version, e.hw_version,
e.sw_version_license, e.sw_version_license,
e.sw_version_upgrade,
e.rowID as equipment_rowid e.rowID as equipment_rowid
FROM equipment e FROM equipment e
JOIN products p ON e.productrowid = p.rowID JOIN products p ON e.productrowid = p.rowID
@@ -78,6 +79,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
$current_sw_version = $equipment_data['current_sw_version']; $current_sw_version = $equipment_data['current_sw_version'];
$hw_version = $equipment_data['hw_version']; $hw_version = $equipment_data['hw_version'];
$sw_version_license = $equipment_data['sw_version_license']; $sw_version_license = $equipment_data['sw_version_license'];
$sw_version_upgrade = $equipment_data['sw_version_upgrade'];
$equipment_rowid = $equipment_data['equipment_rowid']; $equipment_rowid = $equipment_data['equipment_rowid'];
if (debug) { if (debug) {
@@ -85,7 +87,8 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
'product_rowid' => $product_rowid, 'product_rowid' => $product_rowid,
'productcode' => $productcode, 'productcode' => $productcode,
'current_sw_version_raw' => $current_sw_version, 'current_sw_version_raw' => $current_sw_version,
'hw_version' => $hw_version 'hw_version' => $hw_version,
'sw_version_upgrade' => $sw_version_upgrade
]; ];
} }
@@ -119,6 +122,77 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
exit; exit;
} }
// Check if sw_version_upgrade is set - this overrides normal availability check
if (!empty($sw_version_upgrade)) {
if (debug) {
$debug['sw_version_upgrade_check'] = [
'sw_version_upgrade_id' => $sw_version_upgrade,
'checking_override' => true
];
}
// Check if this version exists and is active
$sql = 'SELECT
psv.rowID as version_id,
psv.version,
psv.name,
psv.description,
psv.mandatory,
psv.latest,
psv.hw_version,
psv.file_path,
psv.status
FROM products_software_versions psv
WHERE psv.rowID = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$sw_version_upgrade]);
$upgrade_version = $stmt->fetch(PDO::FETCH_ASSOC);
if ($upgrade_version && $upgrade_version['status'] == 1) {
// Valid override found - check if different from current version
$normalized_upgrade_version = strtolower(ltrim($upgrade_version['version'], '0'));
if (debug) {
$debug['sw_version_upgrade_check']['found_version'] = [
'version' => $upgrade_version['version'],
'name' => $upgrade_version['name'],
'normalized' => $normalized_upgrade_version,
'status' => $upgrade_version['status'],
'is_different_from_current' => ($current_sw_version != $normalized_upgrade_version)
];
}
if ($current_sw_version && $normalized_upgrade_version == $current_sw_version) {
// Override version is same as current - no upgrade available
$software_available = "no";
if (debug) {
$debug['sw_version_upgrade_check']['decision'] = 'Override version is same as current version';
}
} else {
// Override version is different - upgrade is available
$software_available = "yes";
if (debug) {
$debug['sw_version_upgrade_check']['decision'] = 'Override version is available';
}
}
$messages = ["software_available" => $software_available];
if (debug) {
debuglog(json_encode($debug));
}
echo json_encode($messages, JSON_UNESCAPED_UNICODE);
exit;
} else {
// Override version not found or inactive - fall back to standard check
if (debug) {
$debug['sw_version_upgrade_check']['found_version'] = $upgrade_version ? 'found but inactive' : 'not found';
$debug['sw_version_upgrade_check']['decision'] = 'Falling back to standard check';
}
}
}
//GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version //GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version
$sql = 'SELECT $sql = 'SELECT
psv.rowID as version_id, psv.rowID as version_id,

View File

@@ -61,6 +61,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
e.sw_version as current_sw_version, e.sw_version as current_sw_version,
e.hw_version, e.hw_version,
e.sw_version_license, e.sw_version_license,
e.sw_version_upgrade,
e.rowID as equipment_rowid e.rowID as equipment_rowid
FROM equipment e FROM equipment e
JOIN products p ON e.productrowid = p.rowID JOIN products p ON e.productrowid = p.rowID
@@ -77,6 +78,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
$current_sw_version = $equipment_data['current_sw_version']; $current_sw_version = $equipment_data['current_sw_version'];
$hw_version = $equipment_data['hw_version']; $hw_version = $equipment_data['hw_version'];
$sw_version_license = $equipment_data['sw_version_license']; $sw_version_license = $equipment_data['sw_version_license'];
$sw_version_upgrade = $equipment_data['sw_version_upgrade'];
$equipment_rowid = $equipment_data['equipment_rowid']; $equipment_rowid = $equipment_data['equipment_rowid'];
if (debug) { if (debug) {
@@ -85,7 +87,8 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
'productcode' => $productcode, 'productcode' => $productcode,
'current_sw_version_raw' => $current_sw_version, 'current_sw_version_raw' => $current_sw_version,
'hw_version' => $hw_version, 'hw_version' => $hw_version,
'sw_version_license' => $sw_version_license 'sw_version_license' => $sw_version_license,
'sw_version_upgrade' => $sw_version_upgrade
]; ];
} }
@@ -119,6 +122,95 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
exit; exit;
} }
// Check if sw_version_upgrade is set - this overrides normal availability check
if (!empty($sw_version_upgrade)) {
if (debug) {
$debug['sw_version_upgrade_check'] = [
'sw_version_upgrade_id' => $sw_version_upgrade,
'checking_override' => true
];
}
// Check if this version exists and is active
$sql = 'SELECT
psv.rowID as version_id,
psv.version,
psv.name,
psv.description,
psv.mandatory,
psv.latest,
psv.hw_version,
psv.file_path,
psv.status
FROM products_software_versions psv
WHERE psv.rowID = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$sw_version_upgrade]);
$upgrade_version = $stmt->fetch(PDO::FETCH_ASSOC);
if ($upgrade_version && $upgrade_version['status'] == 1) {
// Valid override found - check if different from current version
$normalized_upgrade_version = strtolower(ltrim($upgrade_version['version'], '0'));
if (debug) {
$debug['sw_version_upgrade_check']['found_version'] = [
'version' => $upgrade_version['version'],
'name' => $upgrade_version['name'],
'normalized' => $normalized_upgrade_version,
'status' => $upgrade_version['status'],
'is_different_from_current' => ($current_sw_version != $normalized_upgrade_version)
];
}
if (!$current_sw_version || $current_sw_version == '' || $normalized_upgrade_version != $current_sw_version) {
// Override version is different from current (or no current) - return only this upgrade
$output[] = [
"productcode" => $productcode,
"name" => $upgrade_version['name'] ?? '',
"version" => $upgrade_version['version'],
"version_id" => $upgrade_version['version_id'],
"description" => $upgrade_version['description'] ?? '',
"hw_version" => $upgrade_version['hw_version'] ?? '',
"mandatory" => $upgrade_version['mandatory'] ?? '',
"latest" => $upgrade_version['latest'] ?? '',
"software" => $upgrade_version['file_path'] ?? '',
"source" => '',
"source_type" => '',
"price" => '0.00',
"currency" => '',
"is_current" => false
];
// Generate download token
$download_token = create_download_url_token($criterias['sn'], $upgrade_version['version_id']);
$download_url = 'https://'.$_SERVER['SERVER_NAME'].'/api.php/v2/software_download?token='.$download_token;
$output[0]['source'] = $download_url;
$output[0]['source_type'] = 'token_url';
if (debug) {
$debug['sw_version_upgrade_check']['decision'] = 'Override version returned as only upgrade';
$output[0]['_debug'] = $debug;
}
} else {
// Override version is same as current - no upgrades
if (debug) {
$debug['sw_version_upgrade_check']['decision'] = 'Override version is same as current version - no upgrades';
$output = ['message' => 'No upgrades available', 'debug' => $debug];
}
}
$messages = $output;
echo json_encode($messages, JSON_UNESCAPED_UNICODE);
exit;
} else {
// Override version not found or inactive - fall back to standard check
if (debug) {
$debug['sw_version_upgrade_check']['found_version'] = $upgrade_version ? 'found but inactive' : 'not found';
$debug['sw_version_upgrade_check']['decision'] = 'Falling back to standard check';
}
}
}
//GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version //GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version
$sql = 'SELECT $sql = 'SELECT
psv.rowID as version_id, psv.rowID as version_id,

View File

@@ -0,0 +1,93 @@
<?php
defined($security_key) or exit;
//------------------------------------------
// Marketing Files Delete
//------------------------------------------
//Connect to DB
$pdo = dbConnect($dbname);
//CONTENT FROM API (POST)
$post_content = json_decode($input,true);
//SoldTo is empty
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
$file_id = $post_content['file_id'] ?? '';
if (empty($file_id)) {
echo json_encode(['error' => 'File ID is required']);
exit;
}
//QUERY AND VERIFY ALLOWED
if (isAllowed('marketing',$profile,$permission,'D') === 1){
// Get file information for cleanup
$file_sql = 'SELECT * FROM marketing_files WHERE id = ? AND accounthierarchy LIKE ?';
$stmt = $pdo->prepare($file_sql);
$stmt->execute([$file_id, '%' . $partner->soldto . '%']);
$file_info = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$file_info) {
echo json_encode(['error' => 'File not found or access denied']);
exit;
}
try {
$pdo->beginTransaction();
// Remove file tags
$delete_tags_sql = 'DELETE FROM marketing_file_tags WHERE file_id = ?';
$stmt = $pdo->prepare($delete_tags_sql);
$stmt->execute([$file_id]);
// Delete file record
$delete_file_sql = 'DELETE FROM marketing_files WHERE id = ? AND accounthierarchy LIKE ?';
$stmt = $pdo->prepare($delete_file_sql);
$stmt->execute([$file_id, '%' . $partner->soldto . '%']);
// Delete physical files
$base_path = dirname(__FILE__, 4) . "/";
$main_file = $base_path . $file_info['file_path'];
$thumbnail_file = $file_info['thumbnail_path'] ? $base_path . $file_info['thumbnail_path'] : null;
$files_deleted = [];
$files_failed = [];
if (file_exists($main_file)) {
if (unlink($main_file)) {
$files_deleted[] = $file_info['file_path'];
} else {
$files_failed[] = $file_info['file_path'];
}
}
if ($thumbnail_file && file_exists($thumbnail_file)) {
if (unlink($thumbnail_file)) {
$files_deleted[] = $file_info['thumbnail_path'];
} else {
$files_failed[] = $file_info['thumbnail_path'];
}
}
$pdo->commit();
echo json_encode([
'success' => true,
'message' => 'File deleted successfully',
'files_deleted' => $files_deleted,
'files_failed' => $files_failed
]);
} catch (Exception $e) {
$pdo->rollback();
echo json_encode(['error' => 'Failed to delete file: ' . $e->getMessage()]);
}
} else {
echo json_encode(['error' => 'Insufficient permissions']);
}
?>

View File

@@ -0,0 +1,105 @@
<?php
defined($security_key) or exit;
//------------------------------------------
// Marketing Folders
//------------------------------------------
//Connect to DB
$pdo = dbConnect($dbname);
//CONTENT FROM API (POST)
$post_content = json_decode($input,true);
//SoldTo is empty
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
//BUILD UP PARTNERHIERARCHY FROM USER
$partner_hierarchy = json_encode(array("salesid"=>$partner->salesid,"soldto"=>$partner->soldto), JSON_UNESCAPED_UNICODE);
$id = $post_content['id'] ?? ''; //check for rowID
$command = ($id == '')? 'insert' : 'update'; //IF rowID = empty then INSERT
if (isset($post_content['delete'])){$command = 'delete';} //change command to delete
$date = date('Y-m-d H:i:s');
//CREATE EMPTY STRINGS
$clause = '';
$clause_insert ='';
$input_insert = '';
if ($command == 'update'){
$post_content['updatedby'] = $username;
$post_content['updated'] = $date;
}
if ($command == 'insert'){
$post_content['createdby'] = $username;
$post_content['accounthierarchy'] = $partner_hierarchy;
}
//CREATE NEW ARRAY AND MAP TO CLAUSE
if(isset($post_content) && $post_content!=''){
foreach ($post_content as $key => $var){
if ($key == 'submit' || $key == 'id' || $key == 'delete'){
//do nothing
}
else {
// Handle empty parent_id as NULL for foreign key constraint
if ($key == 'parent_id' && $var === '') {
$var = null;
}
$criterias[$key] = $var;
$clause .= ' , '.$key.' = ?';
$clause_insert .= ' , '.$key.'';
$input_insert .= ', ?'; // ? for each insert item
$execute_input[]= $var; // Build array for input
}
}
}
//CLEAN UP INPUT
$clause = substr($clause, 2); //Clean clause - remove first comma
$clause_insert = substr($clause_insert, 2); //Clean clause - remove first comma
$input_insert = substr($input_insert, 1); //Clean clause - remove first comma
//QUERY AND VERIFY ALLOWED
if ($command == 'update' && isAllowed('marketing',$profile,$permission,'U') === 1){
$sql = 'UPDATE marketing_folders SET '.$clause.' WHERE id = ? '.$whereclause.'';
$execute_input[] = $id;
$stmt = $pdo->prepare($sql);
$stmt->execute($execute_input);
echo json_encode(['success' => true, 'message' => 'Folder updated successfully']);
}
elseif ($command == 'insert' && isAllowed('marketing',$profile,$permission,'C') === 1){
$sql = 'INSERT INTO marketing_folders ('.$clause_insert.') VALUES ('.$input_insert.')';
$stmt = $pdo->prepare($sql);
$stmt->execute($execute_input);
$folder_id = $pdo->lastInsertId();
echo json_encode(['success' => true, 'rowID' => $folder_id, 'message' => 'Folder created successfully']);
}
elseif ($command == 'delete' && isAllowed('marketing',$profile,$permission,'D') === 1){
// Check if folder has subfolders
$subfolder_sql = 'SELECT COUNT(*) as count FROM marketing_folders WHERE parent_id = ? AND accounthierarchy LIKE ?';
$stmt = $pdo->prepare($subfolder_sql);
$stmt->execute([$id, '%' . $partner->soldto . '%']);
$subfolder_count = $stmt->fetch()['count'];
// Check if folder has files
$files_sql = 'SELECT COUNT(*) as count FROM marketing_files WHERE folder_id = ? AND accounthierarchy LIKE ?';
$stmt = $pdo->prepare($files_sql);
$stmt->execute([$id, '%' . $partner->soldto . '%']);
$files_count = $stmt->fetch()['count'];
if ($subfolder_count > 0 || $files_count > 0) {
echo json_encode(['error' => 'Cannot delete folder that contains subfolders or files']);
} else {
$stmt = $pdo->prepare('DELETE FROM marketing_folders WHERE id = ? '.$whereclause.'');
$stmt->execute([ $id ]);
echo json_encode(['success' => true, 'message' => 'Folder deleted successfully']);
}
} else {
echo json_encode(['error' => 'Insufficient permissions or invalid operation']);
}
?>

View File

@@ -0,0 +1,302 @@
<?php
defined($security_key) or exit;
//------------------------------------------
// Marketing Upload
//------------------------------------------
//Connect to DB
$pdo = dbConnect($dbname);
//SoldTo is empty
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
//BUILD UP PARTNERHIERARCHY FROM USER
$partner_hierarchy = $condition;
//QUERY AND VERIFY ALLOWED
if (isAllowed('marketing',$profile,$permission,'C') === 1){
if (!isset($_FILES['file'])) {
echo json_encode(['success' => false, 'error' => 'No file uploaded']);
exit;
}
$file = $_FILES['file'];
$folder_id = $_POST['folder_id'] ?? '';
$tags = isset($_POST['tags']) ? json_decode($_POST['tags'], true) : [];
$title = $_POST['title'] ?? pathinfo($file['name'], PATHINFO_FILENAME);
// Validate file type
$allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'mp4', 'mov', 'avi'];
$filename = $file['name'];
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowedTypes)) {
echo json_encode(['success' => false, 'error' => 'Invalid file type. Allowed: ' . implode(', ', $allowedTypes)]);
exit;
}
$imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$isImage = in_array($ext, $imageTypes);
// For images over 10MB, automatically compress
if ($isImage && $file['size'] > 10000000) {
$compressed = compressImage($file['tmp_name'], $ext, 10000000);
if ($compressed === false) {
echo json_encode(['success' => false, 'error' => 'Failed to compress large image. Please reduce file size manually.']);
exit;
}
// Update file size after compression
$file['size'] = filesize($file['tmp_name']);
}
// Non-images must be under 10MB
if (!$isImage && $file['size'] > 10000000) {
echo json_encode(['success' => false, 'error' => 'File too large. Maximum size is 10MB.']);
exit;
}
// Create unique filename
$unique_filename = uniqid() . '_' . time() . '.' . $ext;
$target_dir = dirname(__FILE__, 4) . "/marketing/uploads/";
$target_file = $target_dir . $unique_filename;
$logical_path = "marketing/uploads/" . $unique_filename;
// Ensure upload directory exists
if (!file_exists($target_dir)) {
mkdir($target_dir, 0755, true);
}
if (move_uploaded_file($file['tmp_name'], $target_file)) {
// Generate thumbnail for images
$thumbnail_path = null;
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$thumb_dir = $target_dir . "thumbs/";
if (!file_exists($thumb_dir)) {
mkdir($thumb_dir, 0755, true);
}
$thumbnail_file = $thumb_dir . $unique_filename;
if (generateThumbnail($target_file, $thumbnail_file, 200, 200)) {
$thumbnail_path = "marketing/uploads/thumbs/" . $unique_filename;
}
}
// Insert into database
$insert_sql = 'INSERT INTO `marketing_files` (`title`, `original_filename`, `file_path`, `thumbnail_path`, `file_type`, `file_size`, `folder_id`, `tags`, `createdby`, `accounthierarchy`) VALUES (?,?,?,?,?,?,?,?,?,?)';
$stmt = $pdo->prepare($insert_sql);
$stmt->execute([
$title,
$filename,
$logical_path,
$thumbnail_path,
$ext,
$file['size'],
$folder_id,
json_encode($tags),
$username,
$partner_hierarchy
]);
$file_id = $pdo->lastInsertId();
// Insert tags into separate table
if (!empty($tags)) {
$tag_sql = 'INSERT IGNORE INTO `marketing_tags` (`tag_name`) VALUES (?)';
$tag_stmt = $pdo->prepare($tag_sql);
$file_tag_sql = 'INSERT INTO `marketing_file_tags` (`file_id`, `tag_id`) SELECT ?, id FROM marketing_tags WHERE tag_name = ?';
$file_tag_stmt = $pdo->prepare($file_tag_sql);
foreach ($tags as $tag) {
$tag_stmt->execute([trim($tag)]);
$file_tag_stmt->execute([$file_id, trim($tag)]);
}
}
echo json_encode([
'success' => true,
'file_id' => $file_id,
'path' => $logical_path,
'thumbnail' => $thumbnail_path,
'message' => 'File uploaded successfully'
]);
} else {
echo json_encode(['success' => false, 'error' => 'Failed to move uploaded file']);
}
} else {
echo json_encode(['success' => false, 'error' => 'Insufficient permissions']);
}
// Function to compress large images
function compressImage($source, $ext, $maxSize) {
$info = @getimagesize($source);
if ($info === false) return false;
$mime = $info['mime'];
// Load image
switch ($mime) {
case 'image/jpeg':
$image = @imagecreatefromjpeg($source);
break;
case 'image/png':
$image = @imagecreatefrompng($source);
break;
case 'image/gif':
$image = @imagecreatefromgif($source);
break;
case 'image/webp':
$image = @imagecreatefromwebp($source);
break;
default:
return false;
}
if ($image === false) return false;
$width = imagesx($image);
$height = imagesy($image);
// Start with 90% quality and reduce dimensions if needed
$quality = 90;
$scale = 1.0;
$tempFile = $source . '.tmp';
// Try progressive compression
while (true) {
// Calculate new dimensions
$newWidth = (int)($width * $scale);
$newHeight = (int)($height * $scale);
// Create resized image
$resized = imagecreatetruecolor($newWidth, $newHeight);
// Preserve transparency for PNG/GIF
if ($mime === 'image/png' || $mime === 'image/gif') {
imagealphablending($resized, false);
imagesavealpha($resized, true);
$transparent = imagecolorallocatealpha($resized, 255, 255, 255, 127);
imagefilledrectangle($resized, 0, 0, $newWidth, $newHeight, $transparent);
}
imagecopyresampled($resized, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
// Save with current quality
if ($ext === 'jpg' || $ext === 'jpeg') {
imagejpeg($resized, $tempFile, $quality);
} elseif ($ext === 'png') {
// PNG compression level (0-9, where 9 is best compression)
$pngQuality = (int)((100 - $quality) / 11);
imagepng($resized, $tempFile, $pngQuality);
} elseif ($ext === 'webp') {
imagewebp($resized, $tempFile, $quality);
} else {
imagegif($resized, $tempFile);
}
imagedestroy($resized);
$fileSize = filesize($tempFile);
// If file is small enough, use it
if ($fileSize <= $maxSize) {
imagedestroy($image);
rename($tempFile, $source);
return true;
}
// If we've reduced too much, give up
if ($quality < 50 && $scale < 0.5) {
imagedestroy($image);
@unlink($tempFile);
return false;
}
// Reduce quality or scale
if ($quality > 50) {
$quality -= 10;
} else {
$scale -= 0.1;
}
}
}
// Function to generate thumbnail
function generateThumbnail($source, $destination, $width, $height) {
$info = getimagesize($source);
if ($info === false) return false;
$mime = $info['mime'];
switch ($mime) {
case 'image/jpeg':
$image = imagecreatefromjpeg($source);
break;
case 'image/png':
$image = imagecreatefrompng($source);
break;
case 'image/gif':
$image = imagecreatefromgif($source);
break;
case 'image/webp':
$image = imagecreatefromwebp($source);
break;
default:
return false;
}
if ($image === false) return false;
$original_width = imagesx($image);
$original_height = imagesy($image);
// Calculate aspect ratio
$aspect_ratio = $original_width / $original_height;
if ($width / $height > $aspect_ratio) {
$new_width = $height * $aspect_ratio;
$new_height = $height;
} else {
$new_height = $width / $aspect_ratio;
$new_width = $width;
}
$thumbnail = imagecreatetruecolor($new_width, $new_height);
// Preserve transparency
imagealphablending($thumbnail, false);
imagesavealpha($thumbnail, true);
$transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
imagefilledrectangle($thumbnail, 0, 0, $new_width, $new_height, $transparent);
imagecopyresampled($thumbnail, $image, 0, 0, 0, 0, $new_width, $new_height, $original_width, $original_height);
// Save thumbnail
switch ($mime) {
case 'image/jpeg':
$result = imagejpeg($thumbnail, $destination, 85);
break;
case 'image/png':
$result = imagepng($thumbnail, $destination, 8);
break;
case 'image/gif':
$result = imagegif($thumbnail, $destination);
break;
case 'image/webp':
$result = imagewebp($thumbnail, $destination, 85);
break;
default:
$result = false;
}
imagedestroy($image);
imagedestroy($thumbnail);
return $result;
}
?>

View File

@@ -6,7 +6,7 @@ defined($security_key) or exit;
//------------------------------------------ //------------------------------------------
// Payment Creation (for Software Upgrades) // Payment Creation (for Software Upgrades)
//------------------------------------------ //------------------------------------------
// This endpoint creates a Mollie payment and stores transaction data // This endpoint creates a payment (Mollie or PayPal) and stores transaction data
//Connect to DB //Connect to DB
$pdo = dbConnect($dbname); $pdo = dbConnect($dbname);
@@ -25,6 +25,8 @@ if (empty($post_content['serial_number']) || empty($post_content['version_id']))
$serial_number = $post_content['serial_number']; $serial_number = $post_content['serial_number'];
$version_id = $post_content['version_id']; $version_id = $post_content['version_id'];
$user_data = $post_content['user_data'] ?? []; $user_data = $post_content['user_data'] ?? [];
// Read payment_provider from top level first, then fallback to user_data
$payment_provider = $post_content['payment_provider'] ?? $user_data['payment_provider'] ?? 'mollie';
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
// STEP 1: Get equipment data from serial_number // STEP 1: Get equipment data from serial_number
@@ -137,41 +139,116 @@ if ($final_price <= 0) {
} }
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
// STEP 6: DEBUG MODE - Log but continue to real Mollie // STEP 6: DEBUG MODE - Log
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
if (debug) { if (debug) {
debuglog("DEBUG MODE: Creating real Mollie payment for testing"); debuglog("DEBUG MODE: Creating $payment_provider payment for testing");
debuglog("DEBUG: Serial Number: $serial_number, Version ID: $version_id, Price: $final_price"); debuglog("DEBUG: Serial Number: $serial_number, Version ID: $version_id, Price: $final_price");
} }
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
// STEP 7: Call Mollie API to create payment // STEP 7: Create payment based on provider
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
try { try {
// Initialize Mollie // Format price (must be string with 2 decimals)
require dirname(__FILE__, 4).'/initialize.php';
// Format price for Mollie (must be string with 2 decimals)
$formatted_price = number_format((float)$final_price, 2, '.', ''); $formatted_price = number_format((float)$final_price, 2, '.', '');
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
// STEP 7A: Generate transaction ID BEFORE creating Mollie payment // STEP 7A: Generate transaction ID BEFORE creating payment
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Generate unique transaction ID (same as placeorder.php)
$txn_id = strtoupper(uniqid('SC') . substr(md5(mt_rand()), 0, 5)); $txn_id = strtoupper(uniqid('SC') . substr(md5(mt_rand()), 0, 5));
// Build webhook URL and redirect URL with actual transaction ID // Build URLs
$protocol = 'https'; $protocol = 'https';
$hostname = $_SERVER['SERVER_NAME']; $hostname = $_SERVER['SERVER_NAME'];
$path = '/'; $path = '/';
$webhook_url = "{$protocol}://{$hostname}{$path}webhook_mollie.php";
$redirect_url = "{$protocol}://{$hostname}{$path}?page=softwaretool&payment_return=1&order_id={$txn_id}"; $redirect_url = "{$protocol}://{$hostname}{$path}?page=softwaretool&payment_return=1&order_id={$txn_id}";
if (debug) { if (debug) {
debuglog("DEBUG: Transaction ID: {$txn_id}"); debuglog("DEBUG: Transaction ID: {$txn_id}");
debuglog("DEBUG: redirectUrl being sent to Mollie: " . $redirect_url); debuglog("DEBUG: Redirect URL: " . $redirect_url);
} }
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Create payment based on selected provider
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($payment_provider === 'paypal') {
//==========================================
// PAYPAL PAYMENT CREATION
//==========================================
$cancel_url = "{$protocol}://{$hostname}{$path}?page=softwaretool&payment_return=cancelled&order_id={$txn_id}";
// Get PayPal access token
$access_token = getPayPalAccessToken();
// Create PayPal order
$order_data = [
'intent' => 'CAPTURE',
'purchase_units' => [[
'custom_id' => $txn_id,
'description' => "Software upgrade Order #{$txn_id}",
'amount' => [
'currency_code' => $final_currency ?: 'EUR',
'value' => $formatted_price
],
'payee' => [
'email_address' => email
]
]],
'application_context' => [
'return_url' => $redirect_url,
'cancel_url' => $cancel_url,
'brand_name' => site_name,
'user_action' => 'PAY_NOW'
]
];
$ch = curl_init(PAYPAL_URL . '/v2/checkout/orders');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($order_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $access_token
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code != 200 && $http_code != 201) {
debuglog("PayPal API Error: HTTP $http_code - Response: $response");
throw new Exception("PayPal order creation failed: HTTP $http_code");
}
$paypal_order = json_decode($response, true);
$payment_id = $paypal_order['id'] ?? null;
// Extract approval URL
$checkout_url = '';
foreach ($paypal_order['links'] ?? [] as $link) {
if ($link['rel'] === 'approve') {
$checkout_url = $link['href'];
break;
}
}
if (!$checkout_url) {
throw new Exception("No approval URL received from PayPal");
}
$payment_method_id = 1; // PayPal
$payment_metadata = 'paypal_order_id';
} else {
//==========================================
// MOLLIE PAYMENT CREATION
//==========================================
// Initialize Mollie
require dirname(__FILE__, 4).'/initialize.php';
$webhook_url = "{$protocol}://{$hostname}{$path}webhook_mollie.php";
// Create payment with Mollie // Create payment with Mollie
$payment = $mollie->payments->create([ $payment = $mollie->payments->create([
'amount' => [ 'amount' => [
@@ -189,15 +266,23 @@ try {
] ]
]); ]);
$mollie_payment_id = $payment->id; $payment_id = $payment->id;
$checkout_url = $payment->getCheckoutUrl(); $checkout_url = $payment->getCheckoutUrl();
if (debug) { if (debug) {
debuglog("DEBUG: Mollie payment created successfully"); debuglog("DEBUG: Mollie payment created successfully");
debuglog("DEBUG: Payment ID: $mollie_payment_id"); debuglog("DEBUG: Payment ID: $payment_id");
debuglog("DEBUG: Redirect URL sent: $redirect_url"); debuglog("DEBUG: Redirect URL sent: $redirect_url");
debuglog("DEBUG: Redirect URL from Mollie object: " . $payment->redirectUrl); debuglog("DEBUG: Checkout URL: $checkout_url");
debuglog("DEBUG: Full payment object: " . json_encode($payment)); }
$payment_method_id = 0; // Mollie
$payment_metadata = 'mollie_payment_id';
}
if (debug) {
debuglog("DEBUG: Payment created via $payment_provider");
debuglog("DEBUG: Payment ID: $payment_id");
debuglog("DEBUG: Checkout URL: $checkout_url"); debuglog("DEBUG: Checkout URL: $checkout_url");
} }
@@ -218,7 +303,7 @@ try {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
$stmt->execute([ $stmt->execute([
$txn_id, // Use generated transaction ID, not Mollie payment ID $txn_id,
$final_price, $final_price,
0, // 0 = pending 0, // 0 = pending
$user_data['email'] ?? '', $user_data['email'] ?? '',
@@ -230,7 +315,7 @@ try {
$user_data['postal'] ?? '', $user_data['postal'] ?? '',
$user_data['country'] ?? '', $user_data['country'] ?? '',
$serial_number, $serial_number,
0, // payment method $payment_method_id, // 0 = Mollie, 1 = PayPal
$partner_product, $partner_product,
date('Y-m-d H:i:s') date('Y-m-d H:i:s')
]); ]);
@@ -245,14 +330,14 @@ try {
'serial_number' => $serial_number, 'serial_number' => $serial_number,
'equipment_id' => $equipment_id, 'equipment_id' => $equipment_id,
'hw_version' => $hw_version, 'hw_version' => $hw_version,
'mollie_payment_id' => $mollie_payment_id // Store Mollie payment ID in options $payment_metadata => $payment_id // Store payment provider ID
], JSON_UNESCAPED_UNICODE); ], JSON_UNESCAPED_UNICODE);
$sql = 'INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, created) $sql = 'INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, created)
VALUES (?, ?, ?, ?, ?, ?)'; VALUES (?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
$stmt->execute([ $stmt->execute([
$transaction_id, // Use database transaction ID (not txn_id string, not mollie_payment_id) $transaction_id,
$version_id, $version_id,
$final_price, $final_price,
1, 1,
@@ -265,7 +350,7 @@ try {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
$messages = json_encode([ $messages = json_encode([
'checkout_url' => $checkout_url, 'checkout_url' => $checkout_url,
'payment_id' => $mollie_payment_id 'payment_id' => $payment_id
], JSON_UNESCAPED_UNICODE); ], JSON_UNESCAPED_UNICODE);
echo $messages; echo $messages;
@@ -275,4 +360,27 @@ try {
exit; exit;
} }
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Helper function to get PayPal access token
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
function getPayPalAccessToken() {
$ch = curl_init(PAYPAL_URL . '/v1/oauth2/token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
curl_setopt($ch, CURLOPT_USERPWD, PAYPAL_CLIENT_ID . ':' . PAYPAL_CLIENT_SECRET);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code != 200) {
throw new Exception("Failed to get PayPal access token: HTTP $http_code");
}
$result = json_decode($response, true);
return $result['access_token'] ?? '';
}
?> ?>

BIN
assets/.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,114 @@
-- Marketing System Database Tables
-- Run this script to create the necessary tables for the marketing file management system
--
-- Usage: Import this file into your MySQL database or run the commands individually
-- Make sure to select the correct database before running these commands
-- Disable foreign key checks temporarily to avoid constraint errors
SET FOREIGN_KEY_CHECKS = 0;
-- Create marketing_folders table
CREATE TABLE IF NOT EXISTS `marketing_folders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`folder_name` varchar(255) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`description` text DEFAULT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`createdby` varchar(100) DEFAULT NULL,
`updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`updatedby` varchar(100) DEFAULT NULL,
`accounthierarchy` text DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `parent_id` (`parent_id`),
KEY `accounthierarchy_idx` (`accounthierarchy`(100))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create marketing_files table
CREATE TABLE IF NOT EXISTS `marketing_files` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`original_filename` varchar(255) NOT NULL,
`file_path` varchar(500) NOT NULL,
`thumbnail_path` varchar(500) DEFAULT NULL,
`file_type` varchar(10) NOT NULL,
`file_size` bigint(20) NOT NULL DEFAULT 0,
`folder_id` int(11) DEFAULT NULL,
`tags` json DEFAULT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`createdby` varchar(100) DEFAULT NULL,
`updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`updatedby` varchar(100) DEFAULT NULL,
`accounthierarchy` text DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `folder_id` (`folder_id`),
KEY `file_type` (`file_type`),
KEY `accounthierarchy_idx` (`accounthierarchy`(100)),
KEY `created_idx` (`created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create marketing_tags table
CREATE TABLE IF NOT EXISTS `marketing_tags` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`tag_name` varchar(100) NOT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `tag_name` (`tag_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create marketing_file_tags junction table
CREATE TABLE IF NOT EXISTS `marketing_file_tags` (
`file_id` int(11) NOT NULL,
`tag_id` int(11) NOT NULL,
PRIMARY KEY (`file_id`, `tag_id`),
KEY `file_id` (`file_id`),
KEY `tag_id` (`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Add foreign key constraints after all tables are created
ALTER TABLE `marketing_folders`
ADD CONSTRAINT `fk_marketing_folders_parent`
FOREIGN KEY (`parent_id`) REFERENCES `marketing_folders`(`id`) ON DELETE CASCADE;
ALTER TABLE `marketing_files`
ADD CONSTRAINT `fk_marketing_files_folder`
FOREIGN KEY (`folder_id`) REFERENCES `marketing_folders`(`id`) ON DELETE SET NULL;
ALTER TABLE `marketing_file_tags`
ADD CONSTRAINT `fk_marketing_file_tags_file`
FOREIGN KEY (`file_id`) REFERENCES `marketing_files`(`id`) ON DELETE CASCADE;
ALTER TABLE `marketing_file_tags`
ADD CONSTRAINT `fk_marketing_file_tags_tag`
FOREIGN KEY (`tag_id`) REFERENCES `marketing_tags`(`id`) ON DELETE CASCADE;
-- Re-enable foreign key checks
SET FOREIGN_KEY_CHECKS = 1;
-- Insert some default sample data (optional)
-- Uncomment the lines below if you want to start with sample folders and tags
-- INSERT INTO `marketing_folders` (`folder_name`, `description`, `createdby`) VALUES
-- ('Product Brochures', 'Marketing brochures and product information', 'system'),
-- ('Technical Specifications', 'Technical documentation and specifications', 'system'),
-- ('Images', 'Product images and photos', 'system'),
-- ('Videos', 'Product videos and demonstrations', 'system');
-- INSERT INTO `marketing_tags` (`tag_name`) VALUES
-- ('brochure'),
-- ('specification'),
-- ('manual'),
-- ('image'),
-- ('video'),
-- ('product'),
-- ('marketing'),
-- ('technical');
-- Create upload directories (Note: This requires manual creation on file system)
-- Create the following directories in your web server:
-- - ./marketing/uploads/
-- - ./marketing/uploads/thumbs/
--
-- Linux/macOS commands:
-- mkdir -p marketing/uploads/thumbs
-- chmod 755 marketing/uploads
-- chmod 755 marketing/uploads/thumbs

View File

@@ -1353,6 +1353,47 @@ function ioAPIv2($api_call, $data, $token){
return $resp; return $resp;
} }
//------------------------------------------
// API TO API version 2 File Upload
//------------------------------------------
function ioAPIv2_FileUpload($api_call, $fileData, $additionalData = [], $token = '') {
include dirname(__FILE__,2).'/settings/settings_redirector.php';
$url = $baseurl . $api_call;
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POST, true);
// Prepare headers (no Content-Type for multipart uploads)
if ($token != '') {
$headers = array("Authorization: Bearer $token");
} else {
$headers = array();
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
// Merge file data with additional data
$postData = array_merge($fileData, $additionalData);
curl_setopt($curl, CURLOPT_POSTFIELDS, $postData);
$resp = curl_exec($curl);
$http_status = curl_getinfo($curl) ?? '200';
curl_close($curl);
if ($http_status['http_code'] == '403' || $http_status['http_code'] == '400') {
$resp = json_encode('NOK');
}
if (debug){
$message = $date.';'.$api_call;
debuglog($message);
}
return $resp;
}
//------------------------------------------ //------------------------------------------
// DEFINE WHERECLAUSE BASED ON ACCOUNTHIERARCHY ALL // DEFINE WHERECLAUSE BASED ON ACCOUNTHIERARCHY ALL
//------------------------------------------ //------------------------------------------
@@ -1491,7 +1532,12 @@ function getProfile($profile, $permission){
'com_log' => 'U', 'com_log' => 'U',
'software_update' => 'R', 'software_update' => 'R',
'software_download' => 'R', 'software_download' => 'R',
'software_available' => 'R' 'software_available' => 'R',
'marketing_files' => 'CRUD',
'marketing_folders' => 'CRUD',
'marketing_tags' => 'CRUD',
'marketing_upload' => 'CRUD',
'marketing_delete' => 'CRUD'
]; ];
// Group permissions: [granting_page => [collection => allowed_actions_string]] // Group permissions: [granting_page => [collection => allowed_actions_string]]

Binary file not shown.

900
assets/marketing.js Normal file
View File

@@ -0,0 +1,900 @@
/**
* Marketing File Management System
* Professional drag-and-drop upload with folder management and tagging
*/
class MarketingFileManager {
constructor() {
this.currentFolder = '';
this.selectedFiles = [];
this.uploadQueue = [];
this.viewMode = 'grid';
this.filters = {
search: '',
tag: '',
fileTypes: []
};
this.folders = []; // Store folders data
this.loadRequestId = 0; // Track the latest load request
this.init();
}
init() {
this.bindEvents();
this.loadFolders();
this.loadTags();
this.loadFiles();
this.setupDragAndDrop();
}
bindEvents() {
// Upload modal
document.getElementById('uploadBtn')?.addEventListener('click', () => {
this.showUploadModal();
});
// Create folder modal
document.getElementById('createFolderBtn')?.addEventListener('click', () => {
this.showFolderModal();
});
// View mode toggle
document.getElementById('gridViewBtn')?.addEventListener('click', () => {
this.setViewMode('grid');
});
document.getElementById('listViewBtn')?.addEventListener('click', () => {
this.setViewMode('list');
});
// Search
document.getElementById('searchInput')?.addEventListener('input', (e) => {
this.filters.search = e.target.value;
this.debounce(this.loadFiles.bind(this), 300)();
});
// Tag filter
document.getElementById('tagFilter')?.addEventListener('change', (e) => {
this.filters.tag = e.target.value;
this.loadFiles();
});
// File type filters
document.querySelectorAll('.file-type-filters input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', () => {
this.updateFileTypeFilters();
});
});
// Modal events
this.bindModalEvents();
// Upload events
this.bindUploadEvents();
}
bindModalEvents() {
// Close modals
document.querySelectorAll('.modal-close, .modal-cancel').forEach(btn => {
btn.addEventListener('click', (e) => {
this.closeModal(e.target.closest('.modal'));
});
});
// Create folder
document.getElementById('createFolder')?.addEventListener('click', () => {
this.createFolder();
});
// Download file
document.getElementById('downloadFile')?.addEventListener('click', () => {
if (this.selectedFile) {
this.downloadFile(this.selectedFile);
}
});
// Delete file
document.getElementById('deleteFile')?.addEventListener('click', () => {
if (this.selectedFile) {
this.deleteFile(this.selectedFile);
}
});
}
bindUploadEvents() {
const fileInput = document.getElementById('fileInput');
const browseBtn = document.getElementById('browseBtn');
const startUpload = document.getElementById('startUpload');
browseBtn?.addEventListener('click', () => {
fileInput.click();
});
fileInput?.addEventListener('change', (e) => {
this.handleFileSelect(e.target.files);
});
startUpload?.addEventListener('click', () => {
this.startUpload();
});
}
setupDragAndDrop() {
const uploadArea = document.getElementById('uploadArea');
const filesContainer = document.getElementById('filesContainer');
if (uploadArea) {
uploadArea.addEventListener('dragover', this.handleDragOver);
uploadArea.addEventListener('drop', (e) => this.handleDrop(e));
}
if (filesContainer) {
filesContainer.addEventListener('dragover', this.handleDragOver);
filesContainer.addEventListener('drop', (e) => this.handleDrop(e));
}
}
handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
e.currentTarget.classList.add('drag-over');
}
handleDrop(e) {
e.preventDefault();
e.stopPropagation();
e.currentTarget.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length > 0) {
this.showUploadModal();
this.handleFileSelect(files);
}
}
async loadFolders() {
try {
const response = await fetch('index.php?page=marketing&action=marketing_folders&tree=true', { cache: 'no-store' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
if (!text || text.trim() === '') {
console.warn('Empty response from folders API');
this.folders = [];
this.renderFolderTree([]);
this.populateFolderSelects([]);
return;
}
const data = JSON.parse(text);
this.folders = data || []; // Store the folders data
// Always render the folder tree (at minimum shows Root)
this.renderFolderTree(this.folders);
this.populateFolderSelects(this.folders);
} catch (error) {
console.error('Error loading folders:', error);
this.folders = [];
// Show at least root folder on error
this.renderFolderTree([]);
this.populateFolderSelects([]);
}
}
async loadTags() {
try {
const response = await fetch('index.php?page=marketing&action=marketing_tags&used_only=true', { cache: 'no-store' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
if (!text || text.trim() === '') {
console.warn('Empty response from tags API');
this.populateTagFilter([]);
return;
}
const data = JSON.parse(text);
// Always populate tag filter (at minimum shows "All Tags")
this.populateTagFilter(data || []);
} catch (error) {
console.error('Error loading tags:', error);
// Show empty tag filter on error
this.populateTagFilter([]);
}
}
async loadFiles() {
const container = document.getElementById('filesContainer');
const loading = document.getElementById('loadingIndicator');
const emptyState = document.getElementById('emptyState');
// Increment request ID to invalidate previous requests
const requestId = ++this.loadRequestId;
// Clear container FIRST to prevent showing old files
container.innerHTML = '';
loading.style.display = 'block';
emptyState.style.display = 'none';
try {
// Use proper folder ID (null for root, or the folder ID)
const folderId = this.currentFolder ? this.currentFolder : 'null';
// Add cache busting to prevent browser caching
let url = `index.php?page=marketing&action=marketing_files&folder_id=${folderId}&limit=50&_t=${Date.now()}`;
if (this.filters.search) {
url += `&search=${encodeURIComponent(this.filters.search)}`;
}
if (this.filters.tag) {
url += `&tag=${encodeURIComponent(this.filters.tag)}`;
}
if (this.filters.fileTypes.length > 0) {
// API expects individual file_type parameter, so we'll filter client-side for now
}
const response = await fetch(url, { cache: 'no-store' });
// Ignore response if a newer request was made
if (requestId !== this.loadRequestId) {
return;
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
if (!text || text.trim() === '') {
console.warn('Empty response from files API');
emptyState.style.display = 'block';
return;
}
const data = JSON.parse(text);
if (data && data.length > 0) {
let files = data;
// Client-side file type filtering
if (this.filters.fileTypes.length > 0) {
files = files.filter(file =>
this.filters.fileTypes.includes(file.file_type.toLowerCase())
);
}
if (files.length === 0) {
emptyState.style.display = 'block';
} else {
this.renderFiles(files);
}
} else {
emptyState.style.display = 'block';
}
} catch (error) {
console.error('Error loading files:', error);
this.showToast('Error loading files', 'error');
} finally {
loading.style.display = 'none';
}
}
renderFolderTree(folders, container = null, level = 0) {
if (!container) {
container = document.getElementById('folderTree');
container.innerHTML = '<div class="folder-item root" data-folder=""><i class="fa fa-home"></i> Root</div>';
// Add click listener to root folder
const rootFolder = container.querySelector('.folder-item.root');
if (rootFolder) {
rootFolder.addEventListener('click', () => {
this.selectFolder('');
});
}
}
folders.forEach(folder => {
const folderItem = document.createElement('div');
folderItem.className = 'folder-item';
folderItem.setAttribute('data-folder', folder.id);
folderItem.style.marginLeft = `${level * 20}px`;
const hasChildren = folder.children && folder.children.length > 0;
const expandIcon = hasChildren ? '<i class="fa fa-chevron-right expand-icon"></i>' : '';
folderItem.innerHTML = `
${expandIcon}
<i class="fa fa-folder"></i>
<span class="folder-name">${this.escapeHtml(folder.folder_name)}</span>
<span class="file-count">(${folder.file_count})</span>
`;
folderItem.addEventListener('click', () => {
this.selectFolder(folder.id);
});
container.appendChild(folderItem);
if (hasChildren) {
this.renderFolderTree(folder.children, container, level + 1);
}
});
}
renderFiles(files) {
const container = document.getElementById('filesContainer');
container.innerHTML = '';
files.forEach(file => {
const fileElement = this.createFileElement(file);
container.appendChild(fileElement);
});
}
createFileElement(file) {
const fileElement = document.createElement('div');
fileElement.className = `file-item ${this.viewMode}-item`;
fileElement.setAttribute('data-file-id', file.id);
const thumbnail = this.getThumbnail(file);
const tags = file.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('');
fileElement.innerHTML = `
<div class="file-thumbnail">
${thumbnail}
<div class="file-overlay">
<button class="preview-btn" title="Preview">
<i class="fa fa-eye"></i>
</button>
<button class="download-btn" title="Download">
<i class="fa fa-download"></i>
</button>
</div>
</div>
<div class="file-info">
<div class="file-name" title="${this.escapeHtml(file.original_filename)}">
${this.escapeHtml(file.title || file.original_filename)}
</div>
<div class="file-meta">
<span class="file-size">${file.file_size_formatted}</span>
<span class="file-type">.${file.file_type.toUpperCase()}</span>
<span class="file-date">${this.formatDate(file.created)}</span>
</div>
<div class="file-tags">
${tags}
</div>
</div>
`;
// Bind events
fileElement.querySelector('.preview-btn').addEventListener('click', () => {
this.previewFile(file);
});
fileElement.querySelector('.download-btn').addEventListener('click', () => {
this.downloadFile(file);
});
fileElement.addEventListener('dblclick', () => {
this.previewFile(file);
});
return fileElement;
}
getThumbnail(file) {
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.file_type.toLowerCase());
if (isImage && file.thumbnail_path) {
return `<img src="${file.thumbnail_path}" alt="${this.escapeHtml(file.title)}" class="thumbnail-img">`;
}
// File type icons
const iconMap = {
pdf: 'fa-file-pdf',
doc: 'fa-file-word',
docx: 'fa-file-word',
xls: 'fa-file-excel',
xlsx: 'fa-file-excel',
mp4: 'fa-file-video',
mov: 'fa-file-video',
avi: 'fa-file-video'
};
const iconClass = iconMap[file.file_type.toLowerCase()] || 'fa-file';
return `<div class="file-icon"><i class="fa ${iconClass}"></i></div>`;
}
showUploadModal() {
const modal = document.getElementById('uploadModal');
this.showModal(modal);
this.populateUploadFolders(this.folders); // Use stored folders data
}
showFolderModal() {
const modal = document.getElementById('folderModal');
this.showModal(modal);
this.populateParentFolders(this.folders); // Use stored folders data
}
showModal(modal) {
modal.style.display = 'flex';
modal.classList.add('show');
document.body.classList.add('modal-open');
}
closeModal(modal) {
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
document.body.classList.remove('modal-open');
}, 300);
}
handleFileSelect(files) {
this.uploadQueue = [];
Array.from(files).forEach(file => {
this.uploadQueue.push({
file: file,
progress: 0,
status: 'pending'
});
});
this.renderUploadQueue();
document.getElementById('startUpload').disabled = this.uploadQueue.length === 0;
}
renderUploadQueue() {
const container = document.getElementById('uploadQueue');
container.innerHTML = '';
this.uploadQueue.forEach((item, index) => {
const queueItem = document.createElement('div');
queueItem.className = 'upload-item';
queueItem.innerHTML = `
<div class="upload-info">
<div class="file-name">${this.escapeHtml(item.file.name)}</div>
<div class="file-size">${this.formatFileSize(item.file.size)}</div>
</div>
<div class="upload-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${item.progress}%"></div>
</div>
<div class="upload-status">${item.status}</div>
</div>
<button class="remove-btn" data-index="${index}">
<i class="fa fa-times"></i>
</button>
`;
queueItem.querySelector('.remove-btn').addEventListener('click', () => {
this.removeFromQueue(index);
});
container.appendChild(queueItem);
});
}
async startUpload() {
const folder = document.getElementById('uploadFolder').value;
const tags = document.getElementById('uploadTags').value
.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
for (let i = 0; i < this.uploadQueue.length; i++) {
const item = this.uploadQueue[i];
await this.uploadFile(item, folder, tags, i);
}
// Switch to the uploaded folder if different from current
if (folder && folder !== this.currentFolder) {
this.currentFolder = folder;
}
this.loadFiles();
this.closeModal(document.getElementById('uploadModal'));
this.showToast('Files uploaded successfully!', 'success');
}
async uploadFile(item, folderId, tags, index) {
const formData = new FormData();
formData.append('file', item.file);
formData.append('folder_id', folderId);
formData.append('tags', JSON.stringify(tags));
formData.append('title', item.file.name.replace(/\.[^/.]+$/, ""));
item.status = 'uploading';
this.updateQueueItem(index, item);
try {
const response = await fetch('index.php?page=marketing&action=marketing_upload', {
method: 'POST',
body: formData,
onUploadProgress: (progressEvent) => {
item.progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
this.updateQueueItem(index, item);
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
if (!text || text.trim() === '') {
throw new Error('Empty response from upload server');
}
const result = JSON.parse(text);
if (result.success) {
item.status = 'completed';
item.progress = 100;
} else {
throw new Error(result.error || 'Upload failed');
}
} catch (error) {
item.status = 'error';
item.error = error.message;
this.showToast(error.message, 'error');
}
this.updateQueueItem(index, item);
}
async createFolder() {
const folderName = document.getElementById('folderName').value.trim();
const parentId = document.getElementById('parentFolder').value;
const description = document.getElementById('folderDescription').value.trim();
if (!folderName) {
this.showToast('Folder name is required', 'error');
return;
}
try {
const formData = new FormData();
formData.append('folder_name', folderName);
formData.append('parent_id', parentId || '');
formData.append('description', description);
const response = await fetch('index.php?page=marketing&action=marketing_folders', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
if (!text || text.trim() === '') {
throw new Error('Empty response from server');
}
const data = JSON.parse(text);
if (data && (data.success || data.rowID)) {
this.closeModal(document.getElementById('folderModal'));
this.loadFolders();
this.showToast('Folder created successfully!', 'success');
} else if (data.error) {
throw new Error(data.error);
} else {
throw new Error('Unexpected response format');
}
} catch (error) {
console.error('Create folder error:', error);
this.showToast(error.message || 'Error creating folder', 'error');
}
}
async deleteFile(file) {
if (!confirm(`Are you sure you want to delete "${file.title || file.original_filename}"?`)) {
return;
}
try {
const formData = new FormData();
formData.append('file_id', file.id);
const response = await fetch('index.php?page=marketing&action=marketing_delete', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
if (!text || text.trim() === '') {
throw new Error('Empty response from delete server');
}
const data = JSON.parse(text);
if (data && (data.success || !data.error)) {
this.closeModal(document.getElementById('previewModal'));
this.loadFiles();
this.showToast('File deleted successfully!', 'success');
} else if (data.error) {
throw new Error(data.error);
} else {
throw new Error('Unexpected response format');
}
} catch (error) {
this.showToast(error.message || 'Error deleting file', 'error');
}
}
previewFile(file) {
this.selectedFile = file;
const modal = document.getElementById('previewModal');
const title = document.getElementById('previewTitle');
const content = document.getElementById('previewContent');
title.textContent = file.title || file.original_filename;
// Generate preview content based on file type
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.file_type.toLowerCase())) {
content.innerHTML = `<img src="${file.file_path}" alt="${this.escapeHtml(file.title)}" style="max-width: 100%; max-height: 500px;">`;
} else if (file.file_type.toLowerCase() === 'mp4') {
content.innerHTML = `<video controls style="max-width: 100%; max-height: 500px;"><source src="${file.file_path}" type="video/mp4"></video>`;
} else {
content.innerHTML = `
<div class="file-preview-info">
<i class="fa ${this.getFileIcon(file.file_type)} fa-4x"></i>
<h4>${this.escapeHtml(file.title || file.original_filename)}</h4>
<p>File Type: ${file.file_type.toUpperCase()}</p>
<p>Size: ${file.file_size_formatted}</p>
<p>Created: ${this.formatDate(file.created)}</p>
</div>
`;
}
this.showModal(modal);
}
downloadFile(file) {
const link = document.createElement('a');
link.href = file.file_path;
link.download = file.original_filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Utility methods
async apiCall(endpoint, params = {}, method = 'GET') {
const url = new URL(`/api.php${endpoint}`, window.location.origin);
let options = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
if (method === 'GET') {
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
} else {
options.body = JSON.stringify(params);
}
const response = await fetch(url, options);
return await response.json();
}
escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text ? text.replace(/[&<>"']/g, m => map[m]) : '';
}
formatDate(dateString) {
return new Date(dateString).toLocaleDateString();
}
formatFileSize(bytes) {
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
showToast(message, type = 'info') {
// Simple toast implementation
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 100);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => document.body.removeChild(toast), 300);
}, 3000);
}
setViewMode(mode) {
this.viewMode = mode;
const container = document.getElementById('filesContainer');
// Update view mode classes
container.className = `files-container ${mode}-view`;
// Update button states
document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById(`${mode}ViewBtn`).classList.add('active');
// Re-render files with new view mode
this.loadFiles();
}
selectFolder(folderId) {
// Clear current folder selection and files BEFORE setting new folder
const container = document.getElementById('filesContainer');
container.innerHTML = '';
// Set new current folder
this.currentFolder = folderId;
// Update UI
this.updateBreadcrumb();
// Update active folder in tree
document.querySelectorAll('.folder-item').forEach(item => {
item.classList.remove('active');
});
const selectedFolder = document.querySelector(`[data-folder="${folderId}"]`);
if (selectedFolder) {
selectedFolder.classList.add('active');
}
// Load files for the new folder
this.loadFiles();
}
updateBreadcrumb() {
// Implement breadcrumb navigation
const nav = document.getElementById('breadcrumbNav');
// This would build breadcrumb based on current folder path
}
updateFileTypeFilters() {
this.filters.fileTypes = [];
document.querySelectorAll('.file-type-filters input[type="checkbox"]:checked').forEach(checkbox => {
const types = checkbox.value.split(',');
this.filters.fileTypes.push(...types);
});
this.loadFiles();
}
populateTagFilter(tags) {
const select = document.getElementById('tagFilter');
select.innerHTML = '<option value="">All Tags</option>';
tags.forEach(tag => {
const option = document.createElement('option');
option.value = tag.tag_name;
option.textContent = `${tag.tag_name} (${tag.usage_count})`;
select.appendChild(option);
});
}
populateFolderSelects(folders) {
this.populateUploadFolders(folders);
this.populateParentFolders(folders);
}
addFolderOptions(select, folders, level = 0) {
folders.forEach(folder => {
const option = document.createElement('option');
option.value = folder.id;
option.textContent = ' '.repeat(level) + folder.folder_name;
select.appendChild(option);
if (folder.children && folder.children.length > 0) {
this.addFolderOptions(select, folder.children, level + 1);
}
});
}
populateUploadFolders(folders = []) {
// Populate upload folder select
const select = document.getElementById('uploadFolder');
if (select) {
select.innerHTML = '<option value="">Root Folder</option>';
this.addFolderOptions(select, folders);
}
}
populateParentFolders(folders = []) {
// Populate parent folder select
const select = document.getElementById('parentFolder');
if (select) {
select.innerHTML = '<option value="">Root Folder</option>';
this.addFolderOptions(select, folders);
}
}
getFileIcon(fileType) {
const iconMap = {
pdf: 'fa-file-pdf',
doc: 'fa-file-word',
docx: 'fa-file-word',
xls: 'fa-file-excel',
xlsx: 'fa-file-excel',
mp4: 'fa-file-video',
mov: 'fa-file-video',
avi: 'fa-file-video'
};
return iconMap[fileType.toLowerCase()] || 'fa-file';
}
updateQueueItem(index, item) {
const queueItems = document.querySelectorAll('.upload-item');
if (queueItems[index]) {
const progressFill = queueItems[index].querySelector('.progress-fill');
const status = queueItems[index].querySelector('.upload-status');
progressFill.style.width = `${item.progress}%`;
status.textContent = item.status;
if (item.status === 'error') {
queueItems[index].classList.add('error');
} else if (item.status === 'completed') {
queueItems[index].classList.add('completed');
}
}
}
removeFromQueue(index) {
this.uploadQueue.splice(index, 1);
this.renderUploadQueue();
document.getElementById('startUpload').disabled = this.uploadQueue.length === 0;
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.marketingManager = new MarketingFileManager();
});

View File

@@ -860,9 +860,9 @@ function showPaymentModal(option) {
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Payment Method *</label> <label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Payment Method *</label>
<select name="payment_method" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"> <select name="payment_method" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<option value="">Select payment method</option> <option value="">Select payment method</option>
<option value="credit_card">Credit Card</option> ${typeof MOLLIE_ENABLED !== 'undefined' && MOLLIE_ENABLED ? '<option value="credit_card">Credit Card</option>' : ''}
<option value="paypal">PayPal</option> ${typeof PAYPAL_ENABLED !== 'undefined' && PAYPAL_ENABLED ? '<option value="paypal">PayPal</option>' : ''}
<option value="bank_transfer">Bank Transfer</option> ${typeof PAY_ON_DELIVERY_ENABLED !== 'undefined' && PAY_ON_DELIVERY_ENABLED ? '<option value="bank_transfer">Bank Transfer</option>' : ''}
</select> </select>
</div> </div>
@@ -906,6 +906,16 @@ function showPaymentModal(option) {
document.getElementById("paymentForm").onsubmit = async (e) => { document.getElementById("paymentForm").onsubmit = async (e) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(e.target); const formData = new FormData(e.target);
const paymentMethod = formData.get("payment_method");
// Auto-determine payment provider based on payment method
let paymentProvider = 'mollie'; // default
if (paymentMethod === 'paypal') {
paymentProvider = 'paypal';
} else if (paymentMethod === 'credit_card' || paymentMethod === 'bank_transfer') {
paymentProvider = 'mollie';
}
const paymentData = { const paymentData = {
name: formData.get("name"), name: formData.get("name"),
email: formData.get("email"), email: formData.get("email"),
@@ -913,7 +923,8 @@ function showPaymentModal(option) {
city: formData.get("city"), city: formData.get("city"),
postal: formData.get("postal"), postal: formData.get("postal"),
country: formData.get("country"), country: formData.get("country"),
payment_method: formData.get("payment_method"), payment_method: paymentMethod,
payment_provider: paymentProvider,
version_id: option.version_id, version_id: option.version_id,
price: price, price: price,
currency: currency currency: currency
@@ -1010,7 +1021,9 @@ async function processPayment(paymentData, option, modal) {
const paymentRequest = { const paymentRequest = {
serial_number: deviceSerialNumber, serial_number: deviceSerialNumber,
version_id: option.version_id, version_id: option.version_id,
user_data: paymentData // name, email, address only payment_method: paymentData.payment_method,
payment_provider: paymentData.payment_provider,
user_data: paymentData // name, email, address, etc.
}; };
// Debug logging // Debug logging
@@ -1018,13 +1031,15 @@ async function processPayment(paymentData, option, modal) {
console.log("=== DEBUG: Payment Request ==="); console.log("=== DEBUG: Payment Request ===");
console.log("Serial Number:", deviceSerialNumber); console.log("Serial Number:", deviceSerialNumber);
console.log("Version ID:", option.version_id); console.log("Version ID:", option.version_id);
console.log("Payment Method:", paymentData.payment_method);
console.log("Payment Provider:", paymentData.payment_provider);
console.log("User Data:", paymentData); console.log("User Data:", paymentData);
console.log("Request payload:", paymentRequest); console.log("Request payload:", paymentRequest);
} }
await logCommunication(`Payment initiated for version ${option.version_id}`, 'sent'); await logCommunication(`Payment initiated for version ${option.version_id} via ${paymentData.payment_provider}`, 'sent');
// Call payment API to create Mollie payment // Call payment API (handles both Mollie and PayPal)
const response = await fetch(link + "/v2/payment", { const response = await fetch(link + "/v2/payment", {
method: "POST", method: "POST",
headers: { headers: {
@@ -1052,16 +1067,16 @@ async function processPayment(paymentData, option, modal) {
} }
if (result.checkout_url) { if (result.checkout_url) {
await logCommunication(`Redirecting to Mollie payment: ${result.payment_id}`, 'sent'); await logCommunication(`Redirecting to ${paymentData.payment_provider} payment: ${result.payment_id}`, 'sent');
if (typeof DEBUG !== 'undefined' && DEBUG) { if (typeof DEBUG !== 'undefined' && DEBUG) {
console.log("DEBUG: Redirecting to Mollie checkout..."); console.log(`DEBUG: Redirecting to ${paymentData.payment_provider} checkout...`);
} }
// Close modal before redirect // Close modal before redirect
document.body.removeChild(modal); document.body.removeChild(modal);
// Redirect to Mollie checkout page // Redirect to payment checkout page
window.location.href = result.checkout_url; window.location.href = result.checkout_url;
} else { } else {
throw new Error(result.error || "No checkout URL received"); throw new Error(result.error || "No checkout URL received");

0
custom/morvalwatches/style/VeLiTi-Logo2.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -32,10 +32,10 @@ $view_contracts = isAllowed('contracts' ,$_SESSION['profile'],$_SESSION['permis
$GET_VALUES = urlGETdetails($_GET) ?? ''; $GET_VALUES = urlGETdetails($_GET) ?? '';
//CALL TO API FOR General information //CALL TO API FOR General information
$api_url = '/v1/equipments/'.$GET_VALUES; $api_url = '/v2/equipments/'.$GET_VALUES;
$responses = ioServer($api_url,''); $responses = ioServer($api_url,'');
//Decode Payload //Decode Payload
if (!empty($responses)){$responses = decode_payload($responses);}else{$responses = null;} if (!empty($responses)){$responses = json_decode($responses);}else{$responses = null;}
$responses = $responses[0]; $responses = $responses[0];
//CALL TO API FOR RELATED //CALL TO API FOR RELATED
@@ -246,6 +246,46 @@ $view .= '<div class="content-block">
<td style="width:25%;">'.$equipment_label6.'</td> <td style="width:25%;">'.$equipment_label6.'</td>
<td>'.$responses->sw_version.'</td> <td>'.$responses->sw_version.'</td>
</tr>'; </tr>';
//Check if license is attached
if (!empty($responses->sw_version_license)) {
$view .= '<tr>
<td style="width:25%;">'.($equipment_license ?? 'License').'</td>
<td>'.$responses->sw_version_license;
// Check if license is active
$current_date = date('Y-m-d H:i:s');
$is_active = false;
if (!empty($responses->license_status) && $responses->license_status == 1) {
$starts_at = $responses->starts_at ?? null;
$expires_at = $responses->expires_at ?? null;
if ($starts_at && $expires_at) {
if ($current_date >= $starts_at && $current_date <= $expires_at) {
$is_active = true;
}
} elseif ($starts_at && !$expires_at) {
if ($current_date >= $starts_at) {
$is_active = true;
}
} elseif (!$starts_at && $expires_at) {
if ($current_date <= $expires_at) {
$is_active = true;
}
} elseif (!$starts_at && !$expires_at) {
$is_active = true;
}
}
if ($is_active) {
$view .= ' / '.$enabled ?? 'Active';
} else {
$view .= ' / '.$disabled ?? 'Inactive';
}
$view .= '</td>
</tr>';
}
//SHOW ONLY SW_UPGRADE WHEN SET //SHOW ONLY SW_UPGRADE WHEN SET
if (isset($products_software) && $products_software !=''){ if (isset($products_software) && $products_software !=''){
foreach ($products_software as $products_soft){ foreach ($products_software as $products_soft){

View File

@@ -53,26 +53,26 @@ if ($equipment_ID !=''){
} }
//GET PRODUCTS //GET PRODUCTS
$api_url = '/v1/products/list='; $api_url = '/v2/products/list=';
$responses = ioServer($api_url,''); $responses = ioServer($api_url,'');
//Decode Payload //Decode Payload
if (!empty($responses)){$products = decode_payload($responses);}else{$products = null;} if (!empty($responses)){$products = json_decode($responses);}else{$products = null;}
if (isset($_GET['equipmentID'])) { if (isset($_GET['equipmentID'])) {
// ID param exists, edit an existing product // ID param exists, edit an existing product
//CALL TO API //CALL TO API
$api_url = '/v1/equipments/equipmentID='.$equipment_ID; $api_url = '/v2/equipments/equipmentID='.$equipment_ID;
$responses = ioServer($api_url,''); $responses = ioServer($api_url,'');
//Decode Payload //Decode Payload
if (!empty($responses)){$responses = decode_payload($responses);}else{$responses = null;} if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = null;}
$equipment = json_decode(json_encode($responses[0]), true); $equipment = $responses[0];
//GET PRODUCTS_SOFTWARE //GET PRODUCTS_SOFTWARE_VERSIONS
$api_url = '/v1/products_software/productrowid='.$equipment['productrowid'].'&status=1'; $api_url = '/v2/products_software_versions/hw_version='.$equipment['hw_version'].'&status=1';
$products_software = ioServer($api_url,''); $products_software = ioServer($api_url,'');
//Decode Payload //Decode Payload
if (!empty($products_software)){$products_software = decode_payload($products_software);}else{$products_software = null;} if (!empty($products_software)){$products_software = json_decode($products_software);}else{$products_software = null;}
//------------------------------------ //------------------------------------
//CHECK IF USER IS ALSO CREATOR OF RECORD THEN OVERRIDE UPDATE_ALLOWED //CHECK IF USER IS ALSO CREATOR OF RECORD THEN OVERRIDE UPDATE_ALLOWED
@@ -174,7 +174,7 @@ if (isset($products_software) && $products_software !=''){
'; ';
foreach ($products_software as $products_soft ){ foreach ($products_software as $products_soft ){
if ($products_soft->hw_version == $equipment['hw_version']){ if ($products_soft->hw_version == $equipment['hw_version']){
$product_software_list .= '<option value="'.$products_soft->rowID.'" '.($equipment['sw_version_upgrade']==$products_soft->rowID?' selected':'').'>'.$products_soft->version.' - '.$products_soft->software.'</option> $product_software_list .= '<option value="'.$products_soft->rowID.'" '.($equipment['sw_version_upgrade']==$products_soft->rowID?' selected':'').'>'.$products_soft->description.' ('.$products_soft->version.')</option>
'; ';
} }
} }

View File

@@ -10,107 +10,362 @@ if (debug && debug_id == $_SESSION['id']){
include_once './assets/functions.php'; include_once './assets/functions.php';
include_once './settings/settings_redirector.php'; include_once './settings/settings_redirector.php';
$page = 'marketing';
//Check if allowed //Check if allowed
if (isAllowed('marketing',$_SESSION['profile'],$_SESSION['permission'],'R') === 0){ if (isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'R') === 0){
header('location: index.php'); header('location: index.php');
exit; exit;
} }
//PAGE Security
$update_allowed = isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'U');
$delete_allowed = isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'D');
$create_allowed = isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'C');
//GET PARAMETERS: //GET PARAMETERS:
$product_group = $_GET['product_group'] ?? ''; $current_folder = $_GET['folder'] ?? '';
$product_content = $_GET['product_content'] ?? ''; $view_mode = $_GET['view'] ?? 'grid';
$search_term = $_GET['search'] ?? '';
$tag_filter = $_GET['tag'] ?? '';
template_header('Marketing', 'marketing'); // Handle AJAX API requests
echo ' if (isset($_GET['action'])) {
<div class="content-title"> $action = $_GET['action'];
<div class="title">
<i class="fa-solid fa-house"></i>
<div class="txt">
<h2>'.$marketing_h2.'</h2>
<p>'.$marketing_p.'</p>
</div>
</div>
</div>
<div class="products content-wrapper">
<div style="display: flex;align-items: center;align-content: center;flex-wrap: nowrap;flex-direction: column;">
<div class="">';
foreach ($marketing_structure as $marketing => $folders){
$style = '';
if (!empty($product_group) && $product_group !== $marketing) {
$style = ' style="opacity: 0.5; color: #999; background-color: #f5f5f5;"';
} elseif (!empty($product_group) && $product_group === $marketing) {
$style = ' style="background-color: #007cba; color: white;"';
}
echo '<a href="index.php?page=marketing&product_group='.$marketing.'" class="btn"'.$style.'>'.$marketing.'</a>';
}
echo'
</div>
<div class="">';
// Only show folders if a product group is selected
if (!empty($product_group) && isset($marketing_structure[$product_group])) {
foreach($marketing_structure[$product_group] as $folder){
echo '<a href="index.php?page=marketing&product_group='.$product_group.'&product_content='.$folder.'" class="btn"> <img src="./assets/images/folder3.png" width="15" height="15" alt=""> '.$folder.'</a>';
}
}
echo '
</div>
</div>';
// Suppress errors for API responses to avoid HTML output breaking JSON
error_reporting(0);
ini_set('display_errors', 0);
if (isset($product_group) && $product_group !='' && isset($product_content) && $product_content !=''){ try {
// Marketing folders
echo ' if ($action === 'marketing_folders') {
<div class="content-block"> if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<div class="products-wrapper">'; // Create folder - use standard format expected by POST API
$dir_name = $main_marketing_dir.$product_group.'/'.$product_content; $payload = [
'folder_name' => $_POST['folder_name'] ?? '',
$files = array_diff(scandir($dir_name), array('.', '..')); 'parent_id' => $_POST['parent_id'] ?? '',
echo''; 'description' => $_POST['description'] ?? ''
// rowID is empty = insert (standard pattern)
foreach ($files as $file) { ];
$filetype = strtolower(pathinfo($file,PATHINFO_EXTENSION)); $response = ioServer('/v2/marketing_folders', json_encode($payload));
} else {
if ( $filetype != '' && $filetype != 'ds_store'){ // Get folders
echo ' $get_values = urlGETdetails($_GET) ?? '';
<div class="product"> $response = ioServer('/v2/marketing_folders/' . $get_values, '');
<a href="'.$dir_name.'/'.$file.'" class="product">
';
if ( $filetype == "jpg" || $filetype == "png" || $filetype == "jpeg" || $filetype == "gif" || $filetype == "png"){
echo'
<img src="'.$dir_name.'/Thumb/'.$file.'" width="200" height="200" alt=""/> </a>
';
} }
if ($filetype == "doc" || $filetype == "docx" || $filetype == "xls"|| $filetype == "xlsx"){ header('Content-Type: application/json');
echo' echo $response;
<img src="./assets/images/brochure.png" width="200" height="200" alt=""> </a> exit;
';
}
if ( $filetype == "pdf"){
echo'
<img src="./assets/images/download-pdf.png" width="200" height="200" alt="'.ucfirst(substr($file, 0, strpos($file, "."))).'"></a>
<span class="name">'.ucfirst(substr(substr($file, 0, strpos($file, ".")),0 ,25)).'</span>
';
}
if ( $filetype == "mp4"){
echo'
<video width="200" height="200" controls>
<source src="'.$dir_name.'/'.$file.'" type="video/mp4">
Your browser does not support the video tag.
</video> </a>
';
} }
echo' // Marketing files
<button class="btn"><a href="'.$dir_name.'/'.$file.'" style="text-decoration: none;color: #ffff;"download="">Download</a></button> if ($action === 'marketing_files') {
</div>'; // Filter out 'page', 'action', and cache busting timestamp from GET parameters
} $filtered_params = $_GET;
unset($filtered_params['page']);
unset($filtered_params['action']);
unset($filtered_params['_t']);
$get_values = urlGETdetails($filtered_params) ?? '';
// API expects path segments, not query string: /v2/marketing_files/params
$api_url = '/v2/marketing_files/' . $get_values;
$response = ioServer($api_url, '');
header('Content-Type: application/json');
echo $response;
exit;
} }
echo '</div> // Marketing tags
</div> if ($action === 'marketing_tags') {
</div> // Filter out 'page' and 'action' from GET parameters
'; $get_values = urlGETdetails($_GET) ?? '';
$response = ioServer('/v2/marketing_tags?' . $get_values, '');
header('Content-Type: application/json');
echo $response;
exit;
}
// Marketing upload
if ($action === 'marketing_upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK && $_FILES['file']['size'] > 0) {
// Use the uploaded file's temp path directly
$temp_path = $_FILES['file']['tmp_name'];
// Get actual MIME type from file content (more secure than trusting browser)
$actual_mime_type = mime_content_type($temp_path);
// Sanitize filename - remove path info and dangerous characters
$safe_filename = basename($_FILES['file']['name']);
$safe_filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $safe_filename);
$fileData = [
'file' => new CURLFile($temp_path, $actual_mime_type, $safe_filename)
];
$additionalData = $_POST; // Include any additional POST data
$token = createCommunicationToken($_SESSION['userkey']);
$response = ioAPIv2_FileUpload('/v2/marketing_upload/', $fileData, $additionalData, $token);
// No need to unlink since we didn't move the file
} else {
$response = json_encode(['error' => 'No file uploaded or upload error']);
}
header('Content-Type: application/json');
echo $response;
exit;
}
// Marketing delete
if ($action === 'marketing_delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$payload = ['file_id' => $_POST['file_id'] ?? ''];
$response = ioServer('/v2/marketing_delete', json_encode($payload));
header('Content-Type: application/json');
echo $response;
exit;
}
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
exit;
}
} }
template_header('Marketing', 'marketing');
?>
<link rel="stylesheet" href="./style/marketing.css">
<div class="content-title">
<div class="title">
<i class="fa-solid fa-rectangle-ad"></i>
<div class="txt">
<h2><?php echo $marketing_h2; ?></h2>
<p><?php echo $marketing_p; ?></p>
</div>
</div>
</div>
<!-- Marketing File Management Interface -->
<div class="marketing-container">
<!-- Toolbar -->
<div class="marketing-toolbar">
<div class="toolbar-left">
<?php if ($create_allowed === 1): ?>
<button id="uploadBtn" class="btn btn-primary">
<i class="fa fa-upload"></i>
</button>
<button id="createFolderBtn" class="btn btn-secondary">
<i class="fa fa-folder-plus"></i>
</button>
<?php endif; ?>
</div>
<div class="toolbar-right">
<!-- Search and Filters -->
<div class="search-container">
<input type="text" id="searchInput" class="search-input" placeholder="Search files..." value="<?php echo htmlspecialchars($search_term); ?>">
<i class="fa fa-search search-icon"></i>
</div>
<select id="tagFilter" class="filter-select">
<option value="">All Tags</option>
</select>
<div class="view-toggle">
<button id="gridViewBtn" class="view-btn <?php echo $view_mode === 'grid' ? 'active' : ''; ?>">
<i class="fa fa-th-large"></i>
</button>
<button id="listViewBtn" class="view-btn <?php echo $view_mode === 'list' ? 'active' : ''; ?>">
<i class="fa fa-list"></i>
</button>
</div>
</div>
</div>
<!-- Content Area -->
<div class="marketing-content">
<!-- Sidebar -->
<div class="marketing-sidebar">
<div class="sidebar-section">
<h3>Folders</h3>
<div id="folderTree" class="folder-tree">
<!-- Folder tree will be loaded here -->
</div>
</div>
<div class="sidebar-section">
<h3>File Types</h3>
<div class="file-type-filters">
<div class="filter-item">
<input type="checkbox" id="filterImages" value="jpg,jpeg,png,gif,webp">
<label for="filterImages">
<i class="fa fa-image"></i> Images
</label>
</div>
<div class="filter-item">
<input type="checkbox" id="filterDocuments" value="pdf,doc,docx">
<label for="filterDocuments">
<i class="fa fa-file-text"></i> Documents
</label>
</div>
<div class="filter-item">
<input type="checkbox" id="filterSpreadsheets" value="xls,xlsx">
<label for="filterSpreadsheets">
<i class="fa fa-file-excel"></i> Spreadsheets
</label>
</div>
<div class="filter-item">
<input type="checkbox" id="filterVideos" value="mp4,mov,avi">
<label for="filterVideos">
<i class="fa fa-file-video"></i> Videos
</label>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="marketing-main">
<!-- Files Grid -->
<div id="filesContainer" class="files-container <?php echo $view_mode; ?>-view">
<!-- Files will be loaded here -->
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="loading-indicator">
<i class="fa fa-spinner fa-spin"></i>
<span>Loading files...</span>
</div>
<!-- Empty State -->
<div id="emptyState" class="empty-state" style="display: none;">
<i class="fa fa-folder-open"></i>
<h3>No files found</h3>
<p>Upload your first file to get started</p>
</div>
</div>
</div>
</div>
<!-- Upload Modal -->
<?php if ($create_allowed === 1): ?>
<div id="uploadModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Upload Files</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="upload-area" id="uploadArea">
<div class="upload-icon">
<i class="fa fa-cloud-upload"></i>
</div>
<h4>Drag & Drop Files Here</h4>
<p>or <button class="browse-btn" id="browseBtn">Browse Files</button></p>
<input type="file" id="fileInput" multiple accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.mp4,.mov,.avi">
</div>
<div id="uploadQueue" class="upload-queue">
<!-- Upload queue items will appear here -->
</div>
<div class="upload-options">
<div class="form-group">
<label for="uploadFolder">Upload to Folder:</label>
<select id="uploadFolder" class="form-control">
<option value="">Root Folder</option>
</select>
</div>
<div class="form-group">
<label for="uploadTags">Tags (comma separated):</label>
<input type="text" id="uploadTags" class="form-control" placeholder="marketing, brochure, product">
</div>
</div>
</div>
<div class="modal-footer">
<button id="startUpload" class="btn btn-primary" disabled>
<i class="fa fa-upload"></i>
</button>
<button class="modal-cancel btn btn-secondary">X</button>
</div>
</div>
</div>
<?php endif; ?>
<!-- Create Folder Modal -->
<?php if ($create_allowed === 1): ?>
<div id="folderModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Create New Folder</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="folderName">Folder Name:</label>
<input type="text" id="folderName" class="form-control" placeholder="Enter folder name">
</div>
<div class="form-group">
<label for="parentFolder">Parent Folder:</label>
<select id="parentFolder" class="form-control">
<option value="">Root Folder</option>
</select>
</div>
<div class="form-group">
<label for="folderDescription">Description:</label>
<textarea id="folderDescription" class="form-control" rows="3" placeholder="Optional description"></textarea>
</div>
</div>
<div class="modal-footer">
<button id="createFolder" class="btn btn-primary">
<i class="fa fa-folder-plus"></i>
</button>
<button class="modal-cancel btn btn-secondary">X</button>
</div>
</div>
</div>
<?php endif; ?>
<!-- File Preview Modal -->
<div id="previewModal" class="modal preview-modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="previewTitle">File Preview</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div id="previewContent" class="preview-content">
<!-- Preview content will be loaded here -->
</div>
</div>
<div class="modal-footer">
<button id="downloadFile" class="btn btn-primary">
<i class="fa fa-download"></i>
</button>
<?php if ($delete_allowed === 1): ?>
<button id="deleteFile" class="btn btn-danger">
<i class="fa fa-trash"></i>
</button>
<?php endif; ?>
</div>
</div>
</div>
<script src="./assets/marketing.js"></script>
<?php
template_footer(); template_footer();
?> ?>

View File

@@ -42,6 +42,23 @@ $order = ioServer($api_url,'');
//Decode Payload //Decode Payload
if (!empty($order)){$order = json_decode($order,true);}else{$order = null;} if (!empty($order)){$order = json_decode($order,true);}else{$order = null;}
//HANDLE STATUS CHANGE
if ($update_allowed === 1){
if (isset($_POST['payment_status'])) {
//GET ALL POST DATA
$data = json_encode($_POST, JSON_UNESCAPED_UNICODE);
//API call
$responses = ioServer('/v2/transactions', $data);
if ($responses === 'NOK'){
} else {
header('Location: index.php?page=order&id='.$_POST['id'].'&success_msg=2');
exit;
}
}
}
// Handle success messages // Handle success messages
if (isset($_GET['success_msg'])) { if (isset($_GET['success_msg'])) {
if ($_GET['success_msg'] == 1) { if ($_GET['success_msg'] == 1) {
@@ -112,11 +129,33 @@ $view .='
<div class="order-detail"> <div class="order-detail">
<h3>Payment Method</h3> <h3>Payment Method</h3>
<p>' . (${$payment_method} ?? $order['header']['payment_method'] ). '</p> <p>' . (${$payment_method} ?? $order['header']['payment_method'] ). '</p>
</div> </div>';
//STATUS CHANGE FORM
if ($update_allowed === 1){
$view .='
<div class="order-detail">
<h3>Payment Status</h3>
<form action="" method="post" style="margin: 0;">
<p><select id="payment_status" name="payment_status" onchange="this.form.submit();" style="border: none; background: transparent; padding: 0; cursor: pointer;">
<option value="0" '.($order['header']['payment_status']==0?' selected':'').'>'.$payment_status_0.'</option>
<option value="1" '.($order['header']['payment_status']==1?' selected':'').'>'.$payment_status_1.'</option>
<option value="101" '.($order['header']['payment_status']==101?' selected':'').'>'.$payment_status_101.'</option>
<option value="102" '.($order['header']['payment_status']==102?' selected':'').'>'.$payment_status_102.'</option>
<option value="103" '.($order['header']['payment_status']==103?' selected':'').'>'.$payment_status_103.'</option>
<option value="999" '.($order['header']['payment_status']==999?' selected':'').'>'.$payment_status_999.'</option>
</select></p>
<input type="hidden" name="id" value="'.$order['header']['id'].'">
</form>
</div>';
} else {
$view .='
<div class="order-detail"> <div class="order-detail">
<h3>Payment Status</h3> <h3>Payment Status</h3>
<p>' . (${$payment_status} ?? $order['header']['payment_status'] ). '</p> <p>' . (${$payment_status} ?? $order['header']['payment_status'] ). '</p>
</div> </div>';
}
$view .='
<div class="order-detail"> <div class="order-detail">
<h3>Date</h3> <h3>Date</h3>
<p>'.getRelativeTime($order['header']['created']). '</p> <p>'.getRelativeTime($order['header']['created']). '</p>

View File

@@ -28,7 +28,7 @@ $path = [
'from_version_id' => '', 'from_version_id' => '',
'to_version_id' => '', 'to_version_id' => '',
'price' => '', 'price' => '',
'currency' => 'USD', 'currency' => 'EUR',
'description' => '', 'description' => '',
'is_active' => 1, 'is_active' => 1,
'created' => '', 'created' => '',

View File

@@ -336,6 +336,7 @@ $page_rows_invoice = 25; //invoices
$page_rows_dealers = 25; //dealers $page_rows_dealers = 25; //dealers
$page_rows_software_versions = 50; //software versions $page_rows_software_versions = 50; //software versions
$page_rows_software_assignment = 50; //software assignment $page_rows_software_assignment = 50; //software assignment
$page_rows_folders = 25; //marketing folders
//------------------------------------------ //------------------------------------------
// Languages supported // Languages supported

View File

@@ -6,7 +6,7 @@ define('superuser_profile','admin,dashboard,profile,application,assets,firmwaret
/*Admin*/ /*Admin*/
define('admin_profile','account,accounts,admin,dashboard,profile,application,assets,buildtool,buildtool,cartest,cartest_manage,cartests,changelog,communication,communication_send,communications,firmwaretool,histories,history,history_manage,marketing,partner,partners,sales,servicereport,servicereports,contract,contract_manage,contracts,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_software,products_versions,report_build,report_contracts_billing,report_healthindex,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_manage,users'); define('admin_profile','account,accounts,admin,dashboard,profile,application,assets,buildtool,buildtool,cartest,cartest_manage,cartests,changelog,communication,communication_send,communications,firmwaretool,histories,history,history_manage,marketing,partner,partners,sales,servicereport,servicereports,contract,contract_manage,contracts,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_software,products_versions,report_build,report_contracts_billing,report_healthindex,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_manage,users');
/*AdminPlus*/ /*AdminPlus*/
define('adminplus_profile','account,account_manage,accounts,admin,config,dashboard,profile,settings,api,application,appointment,assets,billing,buildtool,buildtool,cartest,cartest_manage,cartests,catalog,categories,category,changelog,checkout,com_log,communication,communication_send,communications,cronjob,debug,dev,discount,discounts,firmwaretool,generate_download_token,histories,history,history_manage,identity,identity_dealers,language,licenses,logfile,mailer,maintenance,marketing,media,media_manage,media_scanner,media_upload,order,orders,partner,partners,payment,placeorder,pricelists,pricelists_items,pricelists_manage,profiles,register,render_service_report,reset,sales,security,servicereport,servicereports,shipping,shipping_manage,shopping_cart,software_available,software_download,software_update,softwaretool,tax,taxes,test,transactions,transactions_items,translation_manage,translations,translations_details,unscribe,upgrades,uploader,vin,contract,contract_manage,contracts,dealer,dealer_manage,dealers,dealers_media,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_attributes,products_attributes_items,products_attributes_manage,products_categories,products_configurations,products_media,products_software,products_software_assignment,products_software_assignments,products_software_assignments,products_software_licenses,products_software_upgrade_paths,products_software_upgrade_paths_manage,products_software_version,products_software_version_access_rules_manage,products_software_version_manage,products_software_versions,products_versions,report_build,report_contracts_billing,report_healthindex,report_usage,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_credentials,user_manage,users'); define('adminplus_profile','account,account_manage,accounts,admin,config,dashboard,profile,settings,api,application,appointment,assets,billing,buildtool,buildtool,cartest,cartest_manage,cartests,catalog,categories,category,changelog,checkout,com_log,communication,communication_send,communications,cronjob,debug,dev,discount,discounts,factuur,firmwaretool,functions,generate_download_token,histories,history,history_manage,identity,identity_dealers,initialize,invoice,language,licenses,logfile,mailer,maintenance,marketing,marketing_delete,marketing_files,marketing_folders,marketing_migrate,marketing_tags,marketing_upload,media,media_manage,media_scanner,media_upload,order,orders,partner,partners,payment,placeorder,pricelists,pricelists_items,pricelists_manage,profiles,register,render_service_report,reset,sales,security,service,servicereport,servicereports,shipping,shipping_manage,shopping_cart,software_available,software_download,software_update,softwaretool,tax,taxes,test,transactions,transactions_items,translation_manage,translations,translations_details,unscribe,upgrades,uploader,vin,webhook_mollie,webhook_paypal,contract,contract_manage,contracts,dealer,dealer_manage,dealers,dealers_media,equipment,equipment_data,equipment_healthindex,equipment_history,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_attributes,products_attributes_items,products_attributes_manage,products_categories,products_configurations,products_media,products_software,products_software_assignment,products_software_assignments,products_software_assignments,products_software_licenses,products_software_upgrade_paths,products_software_upgrade_paths_manage,products_software_version,products_software_version_access_rules_manage,products_software_version_manage,products_software_versions,products_versions,report_build,report_contracts_billing,report_healthindex,report_usage,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_credentials,user_manage,users');
/*Build*/ /*Build*/
define('build','dashboard,profile,application,buildtool,buildtool,firmwaretool,products_software'); define('build','dashboard,profile,application,buildtool,buildtool,firmwaretool,products_software');
/*Commerce*/ /*Commerce*/
@@ -14,7 +14,7 @@ define('commerce','admin,dashboard,profile,application,catalog,categories,catego
/*Distribution*/ /*Distribution*/
define('distribution','admin,dashboard,profile,application,assets,firmwaretool,histories,history,history_manage,marketing,partner,partners,servicereport,servicereports,equipment,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_software,products_versions,user,user_manage,users'); define('distribution','admin,dashboard,profile,application,assets,firmwaretool,histories,history,history_manage,marketing,partner,partners,servicereport,servicereports,equipment,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_software,products_versions,user,user_manage,users');
/*Firmware*/ /*Firmware*/
define('firmware','application,firmwaretool,products_software'); define('firmware','application,firmwaretool,software_available,software_download,software_update,softwaretool,transactions,transactions_items,products_software');
/*Garage*/ /*Garage*/
define('garage','dashboard,profile,application,cartest,cartest_manage,cartests,products_versions'); define('garage','dashboard,profile,application,cartest,cartest_manage,cartests,products_versions');
/*Interface*/ /*Interface*/

View File

@@ -44,11 +44,14 @@ $all_views = [
"equipment", "equipment",
"equipment_data", "equipment_data",
"equipment_healthindex", "equipment_healthindex",
"equipment_history",
"equipment_manage", "equipment_manage",
"equipment_manage_edit", "equipment_manage_edit",
"equipments", "equipments",
"equipments_mass_update", "equipments_mass_update",
"factuur",
"firmwaretool", "firmwaretool",
"functions",
"generate_download_token", "generate_download_token",
"histories", "histories",
"history", "history",
@@ -63,6 +66,12 @@ $all_views = [
"mailer", "mailer",
"maintenance", "maintenance",
"marketing", "marketing",
"marketing_delete",
"marketing_files",
"marketing_folders",
"marketing_migrate",
"marketing_tags",
"marketing_upload",
"media", "media",
"media_manage", "media_manage",
"media_scanner", "media_scanner",
@@ -114,6 +123,7 @@ $all_views = [
"rmas", "rmas",
"sales", "sales",
"security", "security",
"service",
"servicereport", "servicereport",
"servicereports", "servicereports",
"settings", "settings",
@@ -141,6 +151,7 @@ $all_views = [
"users", "users",
"vin", "vin",
"webhook_mollie", "webhook_mollie",
"webhook_paypal",
]; ];
?> ?>

View File

@@ -20,6 +20,72 @@ $bearertoken = createCommunicationToken($_SESSION['userkey']);
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payment_return = isset($_GET['order_id']) ? $_GET['order_id'] : null; $payment_return = isset($_GET['order_id']) ? $_GET['order_id'] : null;
$payment_return_status = isset($_GET['payment_return']) ? $_GET['payment_return'] : null; $payment_return_status = isset($_GET['payment_return']) ? $_GET['payment_return'] : null;
$paypal_token = isset($_GET['token']) ? $_GET['token'] : null; // PayPal returns with ?token=
// Handle PayPal return - capture the order directly
if ($paypal_token && $payment_return) {
try {
// Get PayPal access token
$ch = curl_init(PAYPAL_URL . '/v1/oauth2/token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
curl_setopt($ch, CURLOPT_USERPWD, PAYPAL_CLIENT_ID . ':' . PAYPAL_CLIENT_SECRET);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
$response = curl_exec($ch);
curl_close($ch);
$token_data = json_decode($response, true);
$access_token = $token_data['access_token'] ?? '';
if ($access_token) {
// Capture the PayPal order
$capture_url = PAYPAL_URL . "/v2/checkout/orders/{$paypal_token}/capture";
$ch = curl_init($capture_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $access_token
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (debug) {
debuglog("PayPal Capture: HTTP $http_code - $response");
}
// Update transaction status based on capture result
if ($http_code == 200 || $http_code == 201) {
$capture_result = json_decode($response, true);
$capture_status = $capture_result['status'] ?? '';
$payment_status = null;
if ($capture_status === 'COMPLETED') {
$payment_status = 1; // Paid
} elseif ($capture_status === 'PENDING') {
$payment_status = 101; // Pending
}
if ($payment_status !== null) {
$pdo = dbConnect($dbname);
$sql = 'UPDATE transactions SET payment_status = ? WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$payment_status, $payment_return]);
}
}
}
// Redirect to clean URL
header("Location: ?page=softwaretool&payment_return=1&order_id={$payment_return}");
exit;
} catch (Exception $e) {
if (debug) {
debuglog("PayPal Capture Error: " . $e->getMessage());
}
}
}
template_header('Softwaretool', 'softwaretool','view'); template_header('Softwaretool', 'softwaretool','view');
@@ -205,6 +271,9 @@ echo '
<script> <script>
var link = "'.$baseurl.'"; var link = "'.$baseurl.'";
var DEBUG = '.(debug ? 'true' : 'false').'; var DEBUG = '.(debug ? 'true' : 'false').';
var MOLLIE_ENABLED = '.(mollie_enabled ? 'true' : 'false').';
var PAYPAL_ENABLED = '.(paypal_enabled ? 'true' : 'false').';
var PAY_ON_DELIVERY_ENABLED = '.(pay_on_delivery_enabled ? 'true' : 'false').';
var port, textEncoder, writableStreamClosed, writer, historyIndex = -1; var port, textEncoder, writableStreamClosed, writer, historyIndex = -1;
const lineHistory = []; const lineHistory = [];

715
style/marketing.css Normal file
View File

@@ -0,0 +1,715 @@
/* Marketing File Management System Styles */
.marketing-container {
width: 100%;
height: calc(100vh - 200px);
display: flex;
flex-direction: column;
background: var(--color-white, #fff);
}
/* Toolbar */
.marketing-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--color-light-grey, #f8f9fa);
gap: 1rem;
}
.toolbar-left, .toolbar-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.toolbar-center {
flex: 1;
display: flex;
justify-content: center;
}
/* Breadcrumb Navigation */
.breadcrumb-nav {
display: flex;
align-items: center;
gap: 0.5rem;
}
.breadcrumb-item {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
text-decoration: none;
color: var(--color-text, #333);
border-radius: 4px;
transition: background-color 0.2s;
}
.breadcrumb-item:hover {
background-color: var(--color-hover, #e9ecef);
}
.breadcrumb-item::after {
content: '>';
margin-left: 0.5rem;
color: var(--color-text-light, #6c757d);
}
.breadcrumb-item:last-child::after {
display: none;
}
/* Search and Filters */
.search-container {
position: relative;
}
.search-input {
padding: 0.5rem 2rem 0.5rem 0.75rem;
border: 1px solid var(--color-border, #dee2e6);
border-radius: 4px;
width: 250px;
}
.search-icon {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--color-text-light, #6c757d);
}
.filter-select {
padding: 0.5rem;
border: 1px solid var(--color-border, #dee2e6);
border-radius: 4px;
min-width: 120px;
}
/* View Toggle */
.view-toggle {
display: flex;
border: 1px solid var(--color-border, #dee2e6);
border-radius: 4px;
overflow: hidden;
}
.view-btn {
padding: 0.5rem 0.75rem;
border: none;
background: var(--color-white, #fff);
cursor: pointer;
transition: all 0.2s;
}
.view-btn:hover {
background: var(--color-hover, #e9ecef);
}
.view-btn.active {
background: var(--color-primary, #005655);
color: white;
}
/* Content Area */
.marketing-content {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.marketing-sidebar {
width: 250px;
background: var(--color-white, #fff);
border-right: 1px solid var(--color-border, #dee2e6);
overflow-y: auto;
padding: 1rem;
}
.sidebar-section {
margin-bottom: 2rem;
}
.sidebar-section h3 {
font-size: 0.9rem;
font-weight: 600;
color: var(--color-text-dark, #212529);
margin-bottom: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Folder Tree */
.folder-tree {
margin-bottom: 1rem;
}
.folder-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
font-size: 0.9rem;
}
.folder-item:hover {
background-color: var(--color-hover, #e9ecef);
}
.folder-item.active {
background-color: var(--color-primary-light, #cce7f0);
color: var(--color-primary, #007cba);
}
.folder-item.root {
font-weight: 600;
}
.folder-name {
flex: 1;
}
.file-count {
font-size: 0.8rem;
color: var(--color-text-light, #6c757d);
}
.expand-icon {
cursor: pointer;
transition: transform 0.2s;
}
.folder-item.expanded .expand-icon {
transform: rotate(90deg);
}
/* File Type Filters */
.file-type-filters {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.filter-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-item input[type="checkbox"] {
margin: 0;
}
.filter-item label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.9rem;
}
/* Main Content */
.marketing-main {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
/* Files Container */
.files-container {
display: grid;
gap: 1rem;
}
.files-container.grid-view {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
.files-container.list-view {
grid-template-columns: 1fr;
}
/* File Items */
.file-item {
background: var(--color-white, #fff);
border: 1px solid var(--color-border, #dee2e6);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.file-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.grid-item {
text-align: center;
}
.list-item {
display: flex;
align-items: center;
gap: 1rem;
text-align: left;
}
.list-item .file-thumbnail {
width: 60px;
height: 60px;
flex-shrink: 0;
}
.list-item .file-info {
flex: 1;
}
/* File Thumbnail */
.file-thumbnail {
position: relative;
width: 100%;
height: 150px;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-light-grey, #f8f9fa);
border-radius: 4px;
overflow: hidden;
}
.thumbnail-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-icon {
font-size: 3rem;
color: var(--color-text-light, #6c757d);
}
/* File Overlay */
.file-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
opacity: 0;
transition: opacity 0.2s;
}
.file-item:hover .file-overlay {
opacity: 1;
}
.file-overlay button {
background: var(--color-white, #fff);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
transition: all 0.2s;
}
.file-overlay button:hover {
background: var(--color-primary, #007cba);
color: white;
}
/* File Info */
.file-info {
text-align: center;
}
.list-item .file-info {
text-align: left;
}
.file-name {
font-weight: 600;
margin-bottom: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-meta {
display: flex;
justify-content: center;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--color-text-light, #6c757d);
margin-bottom: 0.5rem;
}
.list-item .file-meta {
justify-content: flex-start;
}
.file-tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.25rem;
}
.list-item .file-tags {
justify-content: flex-start;
}
.tag {
background: var(--color-primary-light, #cce7f0);
color: var(--color-primary, #007cba);
padding: 0.125rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
}
/* Loading and Empty States */
.loading-indicator, .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
text-align: center;
color: var(--color-text-light, #6c757d);
}
.loading-indicator i {
font-size: 2rem;
margin-bottom: 1rem;
}
.empty-state i {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Modals */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.show {
opacity: 1;
}
.modal-content {
background: var(--color-white, #fff);
border-radius: 8px;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
transform: translateY(-20px);
transition: transform 0.3s;
}
.modal.show .modal-content {
transform: translateY(0);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
border-bottom: 1px solid var(--color-border, #dee2e6);
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--color-text-light, #6c757d);
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1.5rem;
border-top: 1px solid var(--color-border, #dee2e6);
}
/* Preview Modal */
.preview-modal .modal-content {
max-width: 800px;
}
.preview-content {
text-align: center;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.file-preview-info {
text-align: center;
}
.file-preview-info i {
color: var(--color-text-light, #6c757d);
margin-bottom: 1rem;
}
.file-preview-info h4 {
margin: 1rem 0;
}
/* Upload Area */
.upload-area {
border: 2px dashed var(--color-border, #dee2e6);
border-radius: 8px;
padding: 2rem;
text-align: center;
margin-bottom: 1.5rem;
transition: all 0.2s;
cursor: pointer;
}
.upload-area.drag-over,
.upload-area:hover {
border-color: var(--color-primary, #005655);
background: var(--color-primary-light, #cce7f0);
}
.upload-icon {
font-size: 3rem;
color: var(--color-text-light, #6c757d);
margin-bottom: 1rem;
}
.upload-area h4 {
margin-bottom: 0.5rem;
}
.browse-btn {
background: none;
border: none;
color: var(--color-primary, #005655);
text-decoration: underline;
cursor: pointer;
}
.upload-area input[type="file"] {
display: none;
}
/* Upload Queue */
.upload-queue {
max-height: 300px;
overflow-y: auto;
margin-bottom: 1.5rem;
}
.upload-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
border: 1px solid var(--color-border, #dee2e6);
border-radius: 4px;
margin-bottom: 0.5rem;
}
.upload-item.completed {
border-color: var(--color-success, #28a745);
background: var(--color-success-light, #d4edda);
}
.upload-item.error {
border-color: var(--color-danger, #dc3545);
background: var(--color-danger-light, #f8d7da);
}
.upload-info {
flex: 1;
min-width: 0;
}
.upload-info .file-name {
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.upload-info .file-size {
font-size: 0.8rem;
color: var(--color-text-light, #6c757d);
}
.upload-progress {
flex: 2;
min-width: 0;
}
.progress-bar {
background: var(--color-light-grey, #f8f9fa);
border-radius: 4px;
height: 8px;
overflow: hidden;
margin-bottom: 0.25rem;
}
.progress-fill {
background: var(--color-primary, #007cba);
height: 100%;
transition: width 0.3s;
}
.upload-status {
font-size: 0.8rem;
color: var(--color-text-light, #6c757d);
}
.remove-btn {
background: none;
border: none;
color: var(--color-danger, #dc3545);
cursor: pointer;
padding: 0.25rem;
}
/* Upload Options */
.upload-options {
display: grid;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: 600;
font-size: 0.9rem;
}
.form-control {
padding: 0.5rem;
border: 1px solid var(--color-border, #dee2e6);
border-radius: 4px;
font-size: 0.9rem;
}
.form-control:focus {
outline: none;
border-color: var(--color-primary, #005655);
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.25);
}
/* Toast Notifications */
.toast {
position: fixed;
top: 20px;
right: 20px;
padding: 0.75rem 1rem;
border-radius: 4px;
color: white;
z-index: 1100;
transform: translateX(100%);
transition: transform 0.3s;
}
.toast.show {
transform: translateX(0);
}
.toast-success {
background: var(--color-success, #28a745);
}
.toast-error {
background: var(--color-danger, #dc3545);
}
.toast-info {
background: var(--color-info, #17a2b8);
}
/* Drag and Drop */
.drag-over {
border-color: var(--color-primary, #007cba) !important;
background: var(--color-primary-light, #cce7f0) !important;
}
/* Responsive Design */
@media (max-width: 768px) {
.marketing-toolbar {
flex-direction: column;
gap: 0.75rem;
}
.toolbar-left,
.toolbar-center,
.toolbar-right {
width: 100%;
justify-content: center;
}
.marketing-content {
flex-direction: column;
}
.marketing-sidebar {
width: 100%;
height: auto;
max-height: 200px;
}
.search-input {
width: 100%;
}
.files-container.grid-view {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.modal-content {
width: 95%;
margin: 1rem;
}
}

View File

@@ -216,10 +216,8 @@ try {
date('Y-m-d H:i:s') date('Y-m-d H:i:s')
]); ]);
$invoice_id = $pdo->lastInsertId(); $invoice_id = $pdo->lastInsertId();
debuglog("WEBHOOK: Invoice created with ID: $invoice_id");
} else { } else {
$invoice_id = $existing_invoice['id']; $invoice_id = $existing_invoice['id'];
debuglog("WEBHOOK: Invoice already exists with ID: $invoice_id");
} }
// Fetch full invoice data with customer details for email // Fetch full invoice data with customer details for email
@@ -238,13 +236,9 @@ try {
$stmt->execute([$invoice_id]); $stmt->execute([$invoice_id]);
$invoice_data = $stmt->fetchAll(PDO::FETCH_ASSOC); $invoice_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
debuglog("WEBHOOK: Invoice data fetched: " . print_r($invoice_data, true));
if (!empty($invoice_data)) { if (!empty($invoice_data)) {
debuglog("WEBHOOK: Transforming invoice data...");
// Transform the data (group items like the API does) // Transform the data (group items like the API does)
$invoice_cust = transformOrderData($invoice_data); $invoice_cust = transformOrderData($invoice_data);
debuglog("WEBHOOK: Transformed invoice data: " . print_r($invoice_cust, true));
// Determine invoice language // Determine invoice language
if (!empty($invoice_cust['customer']['language'])) { if (!empty($invoice_cust['customer']['language'])) {
@@ -256,35 +250,22 @@ try {
} }
// Generate invoice HTML (using custom template for software upgrades) // Generate invoice HTML (using custom template for software upgrades)
debuglog("WEBHOOK: Calling generateSoftwareInvoice with language: $invoice_language");
list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language); list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
debuglog("WEBHOOK: Invoice generated - Customer email: $customer_email, Order ID: $order_id");
debuglog("WEBHOOK: Invoice HTML length: " . strlen($data));
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
//CREATE PDF using DomPDF //CREATE PDF using DomPDF
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
debuglog("WEBHOOK: Creating PDF...");
$dompdf->loadHtml($data); $dompdf->loadHtml($data);
$dompdf->setPaper('A4', 'portrait'); $dompdf->setPaper('A4', 'portrait');
$dompdf->render(); $dompdf->render();
$subject = 'Software Upgrade - Invoice: '.$order_id; $subject = 'Software Upgrade - Invoice: '.$order_id;
$attachment = $dompdf->output(); $attachment = $dompdf->output();
debuglog("WEBHOOK: PDF created, size: " . strlen($attachment) . " bytes");
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Send email via PHPMailer //Send email via PHPMailer
//+++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
debuglog("WEBHOOK: Attempting to send email to: $customer_email");
debuglog("WEBHOOK: Email subject: $subject");
debuglog("WEBHOOK: Email config - Host: " . (defined('email_host_name') ? email_host_name : 'NOT DEFINED'));
debuglog("WEBHOOK: Email config - Port: " . (defined('email_outgoing_port') ? email_outgoing_port : 'NOT DEFINED'));
debuglog("WEBHOOK: Email config - Security: " . (defined('email_outgoing_security') ? email_outgoing_security : 'NOT DEFINED'));
debuglog("WEBHOOK: Email config - Username: " . (defined('email') ? email : 'NOT DEFINED'));
// The send_mail function will exit on error and debuglog the error // The send_mail function will exit on error and debuglog the error
$mail_result = send_mail($customer_email, $subject, $data, $attachment, $subject); $mail_result = send_mail($customer_email, $subject, $data, $attachment, $subject);
debuglog("WEBHOOK: Email sent successfully to: $customer_email");
// Send to bookkeeping if configured // Send to bookkeeping if configured
if(invoice_bookkeeping){ if(invoice_bookkeeping){

427
webhook_paypal.php Normal file
View File

@@ -0,0 +1,427 @@
<?php
// PayPal Webhook for software upgrade payments
// Based on webhook_mollie.php structure
// Handles PayPal IPN/webhook notifications for payment status updates
require_once 'settings/config_redirector.php';
require_once 'assets/functions.php';
include dirname(__FILE__).'/settings/settings_redirector.php';
// DEBUG: Log webhook call
debuglog("PAYPAL WEBHOOK CALLED - POST data: " . print_r($_POST, true));
debuglog("PAYPAL WEBHOOK CALLED - RAW input: " . file_get_contents('php://input'));
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//LOGIN TO API (same as webhook_mollie.php)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
debuglog("WEBHOOK: Attempting API authorization...");
debuglog("WEBHOOK: Interface user: " . interface_user);
$data = json_encode(array("clientID" => interface_user, "clientsecret" => interface_pw), JSON_UNESCAPED_UNICODE);
$responses = ioAPIv2('/v2/authorization', $data,'');
debuglog("WEBHOOK: Authorization response: " . $responses);
if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';}
$clientsecret = $responses['token'];
debuglog("WEBHOOK: Token obtained: " . ($clientsecret ? 'YES' : 'NO'));
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// BASEURL is required for invoice template
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$base_url = 'https://'.$_SERVER['SERVER_NAME'].'/';
define('base_url', $base_url);
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Initialize DomPDF for invoice generation
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
use Dompdf\Dompdf;
use Dompdf\Options;
$options = new Options();
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Language mapping for invoices
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$available_languages = [
'NL' => 'NL',
'BE' => 'NL',
'US' => 'US',
'GB' => 'US',
'DE' => 'DE',
'FR' => 'FR',
'ES' => 'ES'
];
try {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Get the webhook event data from PayPal
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$raw_input = file_get_contents('php://input');
$webhook_event = json_decode($raw_input, true);
debuglog("PAYPAL WEBHOOK: Decoded event: " . print_r($webhook_event, true));
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Verify webhook signature (recommended for production)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if (!debug && PAYPAL_WEBHOOK_ID) {
$verified = verifyPayPalWebhookSignature($raw_input, $_SERVER);
if (!$verified) {
debuglog("PAYPAL WEBHOOK ERROR: Signature verification failed");
http_response_code(401);
exit;
}
debuglog("PAYPAL WEBHOOK: Signature verified successfully");
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Extract event type and resource
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$event_type = $webhook_event['event_type'] ?? '';
$resource = $webhook_event['resource'] ?? [];
debuglog("PAYPAL WEBHOOK: Event type: {$event_type}");
// Get order ID from custom_id in the payment resource
$orderId = $resource['custom_id'] ?? $resource['purchase_units'][0]['custom_id'] ?? null;
if (!$orderId) {
debuglog("PAYPAL WEBHOOK ERROR: No custom_id (order_id) in webhook event");
http_response_code(400);
exit;
}
debuglog("PAYPAL WEBHOOK: Order ID: {$orderId}");
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Map PayPal event types to payment status
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payment_status = null;
switch ($event_type) {
case 'PAYMENT.CAPTURE.COMPLETED':
case 'CHECKOUT.ORDER.APPROVED':
$payment_status = 1; // Paid
debuglog("PAYPAL WEBHOOK: Payment completed/approved");
break;
case 'PAYMENT.CAPTURE.PENDING':
case 'CHECKOUT.ORDER.PROCESSED':
$payment_status = 101; // Pending
debuglog("PAYPAL WEBHOOK: Payment pending");
break;
case 'PAYMENT.CAPTURE.DECLINED':
case 'PAYMENT.CAPTURE.FAILED':
$payment_status = 102; // Failed
debuglog("PAYPAL WEBHOOK: Payment failed/declined");
break;
case 'PAYMENT.CAPTURE.REFUNDED':
$payment_status = 104; // Refunded
debuglog("PAYPAL WEBHOOK: Payment refunded");
break;
case 'CHECKOUT.ORDER.VOIDED':
$payment_status = 999; // Canceled
debuglog("PAYPAL WEBHOOK: Payment voided/canceled");
break;
default:
debuglog("PAYPAL WEBHOOK: Unhandled event type: {$event_type}");
http_response_code(200);
echo "OK - Event type not handled";
exit;
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Update transaction status directly in database
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($payment_status !== null) {
debuglog("PAYPAL WEBHOOK: Order ID: $orderId, Payment Status: $payment_status");
$pdo = dbConnect($dbname);
// Update transaction status
$sql = 'UPDATE transactions SET payment_status = ? WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$payment_status, $orderId]);
debuglog("PAYPAL WEBHOOK: Transaction status updated in database");
// Fetch transaction data for license creation
$sql = 'SELECT * FROM transactions WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$orderId]);
$transaction = $stmt->fetch(PDO::FETCH_ASSOC);
debuglog("PAYPAL WEBHOOK: Transaction data: " . print_r($transaction, true));
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Only create license and invoice if payment is PAID
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($payment_status == 1 && $transaction !== null && !empty($transaction)) {
debuglog("PAYPAL WEBHOOK: Payment is PAID, processing license...");
debuglog("PAYPAL WEBHOOK: Transaction ID (auto-increment): " . $transaction['id']);
debuglog("PAYPAL WEBHOOK: Transaction txn_id: " . $transaction['txn_id']);
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// CREATE LICENSE for software upgrade
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$pdo = dbConnect($dbname);
// Fetch transaction items to find software upgrade
$sql = 'SELECT * FROM transactions_items WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$transaction['id']]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
debuglog("PAYPAL WEBHOOK: Found " . count($items) . " transaction items");
debuglog("PAYPAL WEBHOOK: Items data: " . print_r($items, true));
foreach ($items as $item) {
debuglog("PAYPAL WEBHOOK: Processing item: " . print_r($item, true));
if (!empty($item['item_options'])) {
$options = json_decode($item['item_options'], true);
debuglog("PAYPAL WEBHOOK: Item options: " . print_r($options, true));
// Check if this is a software upgrade (has serial_number and equipment_id)
if (isset($options['serial_number']) && isset($options['equipment_id'])) {
debuglog("PAYPAL WEBHOOK: This is a software upgrade item");
// Check if license already exists for this transaction
$sql = 'SELECT rowID FROM products_software_licenses WHERE transaction_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$orderId]);
$existing_license = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$existing_license) {
debuglog("PAYPAL WEBHOOK: No existing license, creating new one...");
// Generate unique license key
$license_key = generateUniqueLicenseKey();
// Create license
$sql = 'INSERT INTO products_software_licenses
(version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, created, createdby)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$item['item_id'], // version_id
1, // license_type (1 = upgrade)
$license_key,
1, // status = active
date('Y-m-d H:i:s'),
'2099-12-31 23:59:59', // effectively permanent
$orderId,
date('Y-m-d H:i:s'),
'webhook_paypal' // created by PayPal webhook
]);
debuglog("PAYPAL WEBHOOK: License created: $license_key");
// Update equipment.sw_version_license
$sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$license_key, $options['equipment_id']]);
debuglog("PAYPAL WEBHOOK: Equipment updated with license: {$options['equipment_id']}");
} else {
debuglog("PAYPAL WEBHOOK: License already exists for order: $orderId");
}
} else {
debuglog("PAYPAL WEBHOOK: Not a software upgrade item (no serial_number/equipment_id)");
}
} else {
debuglog("PAYPAL WEBHOOK: No item_options found");
}
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Generate INVOICE directly in database
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Check if invoice already exists for this transaction
$sql = 'SELECT id FROM invoice WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$transaction['txn_id']]);
$existing_invoice = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$existing_invoice) {
// Create invoice
$sql = 'INSERT INTO invoice (txn_id, payment_status, payment_amount, shipping_amount, discount_amount, tax_amount, created)
VALUES (?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$transaction['txn_id'],
$transaction['payment_status'],
$transaction['payment_amount'],
$transaction['shipping_amount'] ?? 0.00,
$transaction['discount_amount'] ?? 0.00,
$transaction['tax_amount'] ?? 0.00,
date('Y-m-d H:i:s')
]);
$invoice_id = $pdo->lastInsertId();
} else {
$invoice_id = $existing_invoice['id'];
}
// Fetch full invoice data with customer details for email
$sql = 'SELECT tx.*, txi.item_id, txi.item_price, txi.item_quantity, txi.item_options,
p.productcode, p.productname, inv.id as invoice, inv.created as invoice_created,
i.language as user_language
FROM invoice inv
LEFT JOIN transactions tx ON tx.txn_id = inv.txn_id
LEFT JOIN transactions_items txi ON txi.txn_id = tx.id
LEFT JOIN products p ON p.rowID = txi.item_id
LEFT JOIN identity i ON i.userkey = tx.account_id
WHERE inv.id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$invoice_id]);
$invoice_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($invoice_data)) {
// Transform the data (group items like the API does)
$invoice_cust = transformOrderData($invoice_data);
// Determine invoice language
if (!empty($invoice_cust['customer']['language'])) {
$invoice_language = strtoupper($invoice_cust['customer']['language']);
} elseif (!empty($invoice_cust['customer']['country']) && isset($available_languages[strtoupper($invoice_cust['customer']['country'])])) {
$invoice_language = $available_languages[strtoupper($invoice_cust['customer']['country'])];
} else {
$invoice_language = 'US'; // Default fallback
}
// Generate invoice HTML (using custom template for software upgrades)
list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//CREATE PDF using DomPDF
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$dompdf->loadHtml($data);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$subject = 'Software Upgrade - Invoice: '.$order_id;
$attachment = $dompdf->output();
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Send email via PHPMailer
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$mail_result = send_mail($customer_email, $subject, $data, $attachment, $subject);
// Send to bookkeeping if configured
if(invoice_bookkeeping){
debuglog("PAYPAL WEBHOOK: Sending to bookkeeping: " . email_bookkeeping);
send_mail(email_bookkeeping, $subject, $data, $attachment, $subject);
}
} else {
debuglog("PAYPAL WEBHOOK: No invoice data found for invoice_id: $invoice_id");
}
}
}
// Return 200 OK to PayPal
http_response_code(200);
echo "OK";
} catch (Exception $e) {
debuglog("PAYPAL WEBHOOK ERROR: " . $e->getMessage());
debuglog("PAYPAL WEBHOOK ERROR TRACE: " . $e->getTraceAsString());
error_log("PayPal webhook error: " . htmlspecialchars($e->getMessage()));
http_response_code(500);
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Helper function to verify PayPal webhook signature
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
function verifyPayPalWebhookSignature($raw_body, $headers) {
if (!PAYPAL_WEBHOOK_ID || !PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
debuglog("PAYPAL WEBHOOK: Signature verification skipped - credentials not configured");
return true; // Skip verification if not configured
}
// Get required headers
$transmission_id = $headers['HTTP_PAYPAL_TRANSMISSION_ID'] ?? '';
$transmission_time = $headers['HTTP_PAYPAL_TRANSMISSION_TIME'] ?? '';
$cert_url = $headers['HTTP_PAYPAL_CERT_URL'] ?? '';
$auth_algo = $headers['HTTP_PAYPAL_AUTH_ALGO'] ?? '';
$transmission_sig = $headers['HTTP_PAYPAL_TRANSMISSION_SIG'] ?? '';
debuglog("PAYPAL WEBHOOK: Headers - transmission_id: $transmission_id, cert_url: $cert_url");
if (!$transmission_id || !$transmission_sig) {
debuglog("PAYPAL WEBHOOK: Missing signature headers");
return false;
}
try {
// Get PayPal access token
$access_token = getPayPalAccessToken();
// Prepare verification request
$verify_url = PAYPAL_URL . '/v1/notifications/verify-webhook-signature';
$verify_data = [
'transmission_id' => $transmission_id,
'transmission_time' => $transmission_time,
'cert_url' => $cert_url,
'auth_algo' => $auth_algo,
'transmission_sig' => $transmission_sig,
'webhook_id' => PAYPAL_WEBHOOK_ID,
'webhook_event' => json_decode($raw_body, true)
];
debuglog("PAYPAL WEBHOOK: Verification request - webhook_id: " . PAYPAL_WEBHOOK_ID);
debuglog("PAYPAL WEBHOOK: Verification URL: " . $verify_url);
$ch = curl_init($verify_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($verify_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $access_token
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
debuglog("PAYPAL WEBHOOK: Verification response (HTTP $http_code): " . $response);
$result = json_decode($response, true);
$verified = ($http_code == 200 && isset($result['verification_status']) && $result['verification_status'] === 'SUCCESS');
debuglog("PAYPAL WEBHOOK: Signature verification result: " . ($verified ? 'SUCCESS' : 'FAILED'));
return $verified;
} catch (Exception $e) {
debuglog("PAYPAL WEBHOOK: Signature verification error: " . $e->getMessage());
return false;
}
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Helper function to get PayPal access token
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
function getPayPalAccessToken() {
$ch = curl_init(PAYPAL_URL . '/v1/oauth2/token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
curl_setopt($ch, CURLOPT_USERPWD, PAYPAL_CLIENT_ID . ':' . PAYPAL_CLIENT_SECRET);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code != 200) {
throw new Exception("Failed to get PayPal access token: HTTP $http_code");
}
$result = json_decode($response, true);
return $result['access_token'] ?? '';
}
?>