diff --git a/.DS_Store b/.DS_Store index 9d4c452..1b6483b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/PAYMENT_IMPLEMENTATION_SUMMARY.md b/PAYMENT_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 579343f..0000000 --- a/PAYMENT_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -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 diff --git a/PAYMENT_INTEGRATION_PLAN.md b/PAYMENT_INTEGRATION_PLAN.md deleted file mode 100644 index 8b672e9..0000000 --- a/PAYMENT_INTEGRATION_PLAN.md +++ /dev/null @@ -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 - 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 - 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) - -``` - -**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 - diff --git a/api.php b/api.php index b4d3888..3bb45e5 100644 --- a/api.php +++ b/api.php @@ -154,7 +154,8 @@ if($is_jwt_valid && str_contains($version, 'v')) { // First check if endPoint is fileUpload //------------------------------------------ $fileUploadEndpoints = [ - 'media_upload' + 'media_upload', + 'marketing_upload' ]; $isFileUploadEndpoint = in_array($collection, $fileUploadEndpoints); diff --git a/api/.DS_Store b/api/.DS_Store index 362cceb..25bea50 100644 Binary files a/api/.DS_Store and b/api/.DS_Store differ diff --git a/api/v1/.DS_Store b/api/v1/.DS_Store index ad7d9dd..cb2f10e 100644 Binary files a/api/v1/.DS_Store and b/api/v1/.DS_Store differ diff --git a/api/v2/.DS_Store b/api/v2/.DS_Store index 16d8e39..c28ab40 100644 Binary files a/api/v2/.DS_Store and b/api/v2/.DS_Store differ diff --git a/api/v2/get/equipment_history.php b/api/v2/get/equipment_history.php new file mode 100644 index 0000000..1fc241e --- /dev/null +++ b/api/v2/get/equipment_history.php @@ -0,0 +1,116 @@ + 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' + ]); +} + +?> \ No newline at end of file diff --git a/api/v2/get/equipments.php b/api/v2/get/equipments.php index 13eb35e..ab70944 100644 --- a/api/v2/get/equipments.php +++ b/api/v2/get/equipments.php @@ -275,7 +275,7 @@ else { } //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); diff --git a/api/v2/get/marketing_files.php b/api/v2/get/marketing_files.php new file mode 100644 index 0000000..b0f5e3f --- /dev/null +++ b/api/v2/get/marketing_files.php @@ -0,0 +1,155 @@ +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; \ No newline at end of file diff --git a/api/v2/get/marketing_folders.php b/api/v2/get/marketing_folders.php new file mode 100644 index 0000000..92f0853 --- /dev/null +++ b/api/v2/get/marketing_folders.php @@ -0,0 +1,172 @@ +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; \ No newline at end of file diff --git a/api/v2/get/marketing_tags.php b/api/v2/get/marketing_tags.php new file mode 100644 index 0000000..8bc7431 --- /dev/null +++ b/api/v2/get/marketing_tags.php @@ -0,0 +1,115 @@ +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); \ No newline at end of file diff --git a/api/v2/get/service.php b/api/v2/get/service.php new file mode 100644 index 0000000..9fe1e98 --- /dev/null +++ b/api/v2/get/service.php @@ -0,0 +1,41 @@ + \ No newline at end of file diff --git a/api/v2/get/software_available.php b/api/v2/get/software_available.php index ef5f27f..8f4e4e6 100644 --- a/api/v2/get/software_available.php +++ b/api/v2/get/software_available.php @@ -62,6 +62,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ e.sw_version as current_sw_version, e.hw_version, e.sw_version_license, + e.sw_version_upgrade, e.rowID as equipment_rowid FROM equipment e 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']; $hw_version = $equipment_data['hw_version']; $sw_version_license = $equipment_data['sw_version_license']; + $sw_version_upgrade = $equipment_data['sw_version_upgrade']; $equipment_rowid = $equipment_data['equipment_rowid']; if (debug) { @@ -85,7 +87,8 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ 'product_rowid' => $product_rowid, 'productcode' => $productcode, '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; } + // 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 $sql = 'SELECT psv.rowID as version_id, diff --git a/api/v2/get/software_update.php b/api/v2/get/software_update.php index 6742cd0..d56a26e 100644 --- a/api/v2/get/software_update.php +++ b/api/v2/get/software_update.php @@ -61,6 +61,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ e.sw_version as current_sw_version, e.hw_version, e.sw_version_license, + e.sw_version_upgrade, e.rowID as equipment_rowid FROM equipment e 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']; $hw_version = $equipment_data['hw_version']; $sw_version_license = $equipment_data['sw_version_license']; + $sw_version_upgrade = $equipment_data['sw_version_upgrade']; $equipment_rowid = $equipment_data['equipment_rowid']; if (debug) { @@ -85,7 +87,8 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ 'productcode' => $productcode, 'current_sw_version_raw' => $current_sw_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; } + // 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 $sql = 'SELECT psv.rowID as version_id, diff --git a/api/v2/post/marketing_delete.php b/api/v2/post/marketing_delete.php new file mode 100644 index 0000000..ed9036e --- /dev/null +++ b/api/v2/post/marketing_delete.php @@ -0,0 +1,93 @@ +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']); +} + +?> \ No newline at end of file diff --git a/api/v2/post/marketing_folders.php b/api/v2/post/marketing_folders.php new file mode 100644 index 0000000..ab29f94 --- /dev/null +++ b/api/v2/post/marketing_folders.php @@ -0,0 +1,105 @@ +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']); +} + +?> \ No newline at end of file diff --git a/api/v2/post/marketing_upload.php b/api/v2/post/marketing_upload.php new file mode 100644 index 0000000..a84a066 --- /dev/null +++ b/api/v2/post/marketing_upload.php @@ -0,0 +1,302 @@ +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; +} + +?> \ No newline at end of file diff --git a/api/v2/post/payment.php b/api/v2/post/payment.php index 84b4c54..e9e559e 100644 --- a/api/v2/post/payment.php +++ b/api/v2/post/payment.php @@ -6,7 +6,7 @@ defined($security_key) or exit; //------------------------------------------ // 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 $pdo = dbConnect($dbname); @@ -25,6 +25,8 @@ if (empty($post_content['serial_number']) || empty($post_content['version_id'])) $serial_number = $post_content['serial_number']; $version_id = $post_content['version_id']; $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 @@ -137,67 +139,150 @@ if ($final_price <= 0) { } //+++++++++++++++++++++++++++++++++++++++++++++++++++++ -// STEP 6: DEBUG MODE - Log but continue to real Mollie +// STEP 6: DEBUG MODE - Log //+++++++++++++++++++++++++++++++++++++++++++++++++++++ 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"); } //+++++++++++++++++++++++++++++++++++++++++++++++++++++ -// STEP 7: Call Mollie API to create payment +// STEP 7: Create payment based on provider //+++++++++++++++++++++++++++++++++++++++++++++++++++++ try { - // Initialize Mollie - require dirname(__FILE__, 4).'/initialize.php'; - - // Format price for Mollie (must be string with 2 decimals) + // Format price (must be string with 2 decimals) $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)); - // Build webhook URL and redirect URL with actual transaction ID + // Build URLs $protocol = 'https'; $hostname = $_SERVER['SERVER_NAME']; $path = '/'; - $webhook_url = "{$protocol}://{$hostname}{$path}webhook_mollie.php"; $redirect_url = "{$protocol}://{$hostname}{$path}?page=softwaretool&payment_return=1&order_id={$txn_id}"; if (debug) { debuglog("DEBUG: Transaction ID: {$txn_id}"); - debuglog("DEBUG: redirectUrl being sent to Mollie: " . $redirect_url); + debuglog("DEBUG: Redirect URL: " . $redirect_url); } - // Create payment with Mollie - $payment = $mollie->payments->create([ - 'amount' => [ - 'currency' => $final_currency ?: 'EUR', - 'value' => "{$formatted_price}" - ], - 'description' => "Software upgrade Order #{$txn_id}", - 'redirectUrl' => "{$redirect_url}", - 'webhookUrl' => "{$webhook_url}", - 'metadata' => [ - 'order_id' => $txn_id, - 'serial_number' => $serial_number, - 'version_id' => $version_id, - 'equipment_id' => $equipment_id - ] - ]); + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + // 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(); - $mollie_payment_id = $payment->id; - $checkout_url = $payment->getCheckoutUrl(); + // 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 + $payment = $mollie->payments->create([ + 'amount' => [ + 'currency' => $final_currency ?: 'EUR', + 'value' => "{$formatted_price}" + ], + 'description' => "Software upgrade Order #{$txn_id}", + 'redirectUrl' => "{$redirect_url}", + 'webhookUrl' => "{$webhook_url}", + 'metadata' => [ + 'order_id' => $txn_id, + 'serial_number' => $serial_number, + 'version_id' => $version_id, + 'equipment_id' => $equipment_id + ] + ]); + + $payment_id = $payment->id; + $checkout_url = $payment->getCheckoutUrl(); + + if (debug) { + debuglog("DEBUG: Mollie payment created successfully"); + debuglog("DEBUG: Payment ID: $payment_id"); + debuglog("DEBUG: Redirect URL sent: $redirect_url"); + debuglog("DEBUG: Checkout URL: $checkout_url"); + } + + $payment_method_id = 0; // Mollie + $payment_metadata = 'mollie_payment_id'; + } if (debug) { - debuglog("DEBUG: Mollie payment created successfully"); - debuglog("DEBUG: Payment ID: $mollie_payment_id"); - debuglog("DEBUG: Redirect URL sent: $redirect_url"); - debuglog("DEBUG: Redirect URL from Mollie object: " . $payment->redirectUrl); - debuglog("DEBUG: Full payment object: " . json_encode($payment)); + debuglog("DEBUG: Payment created via $payment_provider"); + debuglog("DEBUG: Payment ID: $payment_id"); debuglog("DEBUG: Checkout URL: $checkout_url"); } @@ -218,7 +303,7 @@ try { VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; $stmt = $pdo->prepare($sql); $stmt->execute([ - $txn_id, // Use generated transaction ID, not Mollie payment ID + $txn_id, $final_price, 0, // 0 = pending $user_data['email'] ?? '', @@ -230,7 +315,7 @@ try { $user_data['postal'] ?? '', $user_data['country'] ?? '', $serial_number, - 0, // payment method + $payment_method_id, // 0 = Mollie, 1 = PayPal $partner_product, date('Y-m-d H:i:s') ]); @@ -245,14 +330,14 @@ try { 'serial_number' => $serial_number, 'equipment_id' => $equipment_id, '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); $sql = 'INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, created) VALUES (?, ?, ?, ?, ?, ?)'; $stmt = $pdo->prepare($sql); $stmt->execute([ - $transaction_id, // Use database transaction ID (not txn_id string, not mollie_payment_id) + $transaction_id, $version_id, $final_price, 1, @@ -265,7 +350,7 @@ try { //+++++++++++++++++++++++++++++++++++++++++++++++++++++ $messages = json_encode([ 'checkout_url' => $checkout_url, - 'payment_id' => $mollie_payment_id + 'payment_id' => $payment_id ], JSON_UNESCAPED_UNICODE); echo $messages; @@ -275,4 +360,27 @@ try { 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'] ?? ''; +} + ?> diff --git a/assets/.DS_Store b/assets/.DS_Store index 1ba4cf5..7502f1f 100644 Binary files a/assets/.DS_Store and b/assets/.DS_Store differ diff --git a/assets/database/marketing_install.sql b/assets/database/marketing_install.sql new file mode 100644 index 0000000..7fb34e3 --- /dev/null +++ b/assets/database/marketing_install.sql @@ -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 \ No newline at end of file diff --git a/assets/functions.php b/assets/functions.php index ed0e09f..647f357 100644 --- a/assets/functions.php +++ b/assets/functions.php @@ -1353,6 +1353,47 @@ function ioAPIv2($api_call, $data, $token){ 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 //------------------------------------------ @@ -1491,7 +1532,12 @@ function getProfile($profile, $permission){ 'com_log' => 'U', 'software_update' => '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]] diff --git a/assets/images/.DS_Store b/assets/images/.DS_Store index 12648ae..a97d4fb 100644 Binary files a/assets/images/.DS_Store and b/assets/images/.DS_Store differ diff --git a/assets/marketing.js b/assets/marketing.js new file mode 100644 index 0000000..30f8c9c --- /dev/null +++ b/assets/marketing.js @@ -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 = '
File Type: ${file.file_type.toUpperCase()}
+Size: ${file.file_size_formatted}
+Created: ${this.formatDate(file.created)}
+'.$marketing_p.'
-
- ';
- }
- if ( $filetype == "pdf"){
- echo'
-
- '.ucfirst(substr(substr($file, 0, strpos($file, ".")),0 ,25)).'
- ';
- }
- if ( $filetype == "mp4"){
- echo'
-
- ';
- }
-
- echo'
-
- ' . (${$payment_method} ?? $order['header']['payment_method'] ). '
-' . (${$payment_status} ?? $order['header']['payment_status'] ). '
-'.getRelativeTime($order['header']['created']). '
diff --git a/products_software_upgrade_paths_manage.php b/products_software_upgrade_paths_manage.php index 592a0be..487f54d 100644 --- a/products_software_upgrade_paths_manage.php +++ b/products_software_upgrade_paths_manage.php @@ -28,7 +28,7 @@ $path = [ 'from_version_id' => '', 'to_version_id' => '', 'price' => '', - 'currency' => 'USD', + 'currency' => 'EUR', 'description' => '', 'is_active' => 1, 'created' => '', diff --git a/settings/settingsmenu.php b/settings/settingsmenu.php index 85cf303..b481eb6 100644 --- a/settings/settingsmenu.php +++ b/settings/settingsmenu.php @@ -336,6 +336,7 @@ $page_rows_invoice = 25; //invoices $page_rows_dealers = 25; //dealers $page_rows_software_versions = 50; //software versions $page_rows_software_assignment = 50; //software assignment +$page_rows_folders = 25; //marketing folders //------------------------------------------ // Languages supported diff --git a/settings/settingsprofiles.php b/settings/settingsprofiles.php index 4ec0352..283f91d 100644 --- a/settings/settingsprofiles.php +++ b/settings/settingsprofiles.php @@ -6,7 +6,7 @@ define('superuser_profile','admin,dashboard,profile,application,assets,firmwaret /*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'); /*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*/ define('build','dashboard,profile,application,buildtool,buildtool,firmwaretool,products_software'); /*Commerce*/ @@ -14,7 +14,7 @@ define('commerce','admin,dashboard,profile,application,catalog,categories,catego /*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'); /*Firmware*/ -define('firmware','application,firmwaretool,products_software'); +define('firmware','application,firmwaretool,software_available,software_download,software_update,softwaretool,transactions,transactions_items,products_software'); /*Garage*/ define('garage','dashboard,profile,application,cartest,cartest_manage,cartests,products_versions'); /*Interface*/ diff --git a/settings/settingsviews.php b/settings/settingsviews.php index 0e7e408..9a4956d 100644 --- a/settings/settingsviews.php +++ b/settings/settingsviews.php @@ -44,11 +44,14 @@ $all_views = [ "equipment", "equipment_data", "equipment_healthindex", + "equipment_history", "equipment_manage", "equipment_manage_edit", "equipments", "equipments_mass_update", + "factuur", "firmwaretool", + "functions", "generate_download_token", "histories", "history", @@ -63,6 +66,12 @@ $all_views = [ "mailer", "maintenance", "marketing", + "marketing_delete", + "marketing_files", + "marketing_folders", + "marketing_migrate", + "marketing_tags", + "marketing_upload", "media", "media_manage", "media_scanner", @@ -114,6 +123,7 @@ $all_views = [ "rmas", "sales", "security", + "service", "servicereport", "servicereports", "settings", @@ -141,6 +151,7 @@ $all_views = [ "users", "vin", "webhook_mollie", + "webhook_paypal", ]; ?> \ No newline at end of file diff --git a/softwaretool.php b/softwaretool.php index e259d50..1bdf7cc 100644 --- a/softwaretool.php +++ b/softwaretool.php @@ -20,6 +20,72 @@ $bearertoken = createCommunicationToken($_SESSION['userkey']); //+++++++++++++++++++++++++++++++++++++++++++++++++++++ $payment_return = isset($_GET['order_id']) ? $_GET['order_id'] : 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'); @@ -205,6 +271,9 @@ echo '