# 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