Files
assetmgt/PAYMENT_INTEGRATION_PLAN.md
“VeLiTi” 653e33d7e9 Add software tool functionality with device connection and upgrade options
- Implemented the software tool page with user interface for connecting devices.
- Added functionality to display connection status and software upgrade options.
- Included a help modal with step-by-step instructions for users.
- Integrated error handling and user permission checks.
- Enhanced user experience with dynamic content updates and visual feedback.
2025-12-21 14:16:55 +01:00

34 KiB

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
defined($security_key) or exit;

// POST endpoint for payment creation
// Input (JSON): serial_number, version_id, user_data (name, email, address)
// Output (JSON): {checkout_url: "https://mollie.com/...", payment_id: "tr_xxx"}

//Connect to DB
$pdo = dbConnect($dbname);

//CONTENT FROM API (POST)
$post_content = json_decode($input, true);

// SECURITY: Never trust price/currency from frontend!
// Steps:
1. Validate inputs (serial_number, version_id, user_data)
2. SERVER-SIDE: Calculate actual price using software_update logic:
   a. Get equipment data from serial_number
   b. Get version data from version_id
   c. Check upgrade path pricing (same logic as software_update.php lines 237-253)
   d. Check license validity (same logic as software_update.php lines 274-311)
   e. Calculate FINAL price server-side
3. Verify price > 0 (free upgrades shouldn't reach payment API)
4. Call Mollie API FIRST to get payment_id:
   $mollie->payments->create([
     'amount' => ['currency' => 'EUR', 'value' => $final_price],
     'description' => 'Software upgrade to version X',
     'redirectUrl' => 'https://site.com/softwaretool.php?payment_return=1&payment_id={payment_id}',
     'webhookUrl' => 'https://site.com/webhook_mollie.php',  // NEW webhook for software upgrades
     'metadata' => ['order_id' => $mollie_payment_id]  // for compatibility
   ])
5. Store transaction in DB with Mollie payment_id:
   INSERT INTO transactions (txn_id, payment_amount, payment_status, payer_email, first_name, last_name, address_*, account_id, ...)
   VALUES ($mollie_payment_id, $final_price, 0, ...) -- 0 = pending
6. Store transaction item:
   INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, ...)
   VALUES ($mollie_payment_id, $version_id, $final_price, 1, '{"serial_number":"...", "equipment_id":...}', ...)
7. Return JSON: {checkout_url: $mollie_checkout_url, payment_id: $mollie_payment_id}

1.3 Create /api/v2/get/payment.php

<?php
defined($security_key) or exit;

// GET endpoint for payment status retrieval
// Input (URL): ?payment_id=tr_xxx
// Output (JSON): {payment_id, serial_number, version_id, payment_status, price, currency, user_data}

//Connect to DB
$pdo = dbConnect($dbname);

//NEW ARRAY
$criterias = [];

//Check for $_GET variables
if(isset($get_content) && $get_content!=''){
    $requests = explode("&", $get_content);
    foreach ($requests as $y){
        $v = explode("=", $y);
        $criterias[$v[0]] = $v[1];
    }
}

// Steps:
1. Validate payment_id from URL
2. Fetch transaction: SELECT * FROM transactions WHERE txn_id = ?
3. Fetch transaction item: SELECT * FROM transactions_items WHERE txn_id = ?
4. Parse item_options JSON to get serial_number, equipment_id
5. Return JSON with payment details:
   {
     "payment_id": txn_id,
     "payment_status": payment_status, // 0=pending, 1=paid, 2=failed, 3=canceled
     "payment_amount": payment_amount,
     "serial_number": from item_options JSON,
     "equipment_id": from item_options JSON,
     "version_id": item_id,
     "payer_email": payer_email,
     "customer_name": first_name + " " + last_name
   }
6. If not found, return error

1.4 Create NEW webhook_mollie.php

<?php
// NEW FILE - Webhook for software upgrade payments
// Based on structure from existing webhook.php from commerce product
// Uses existing transaction API + invoice API + email system

require_once 'assets/config.php';
require_once 'assets/functions.php';

//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//LOGIN TO API (same as commerce webhook.php)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$data = json_encode(array("clientID" => clientID, "clientsecret" => clientsecret), JSON_UNESCAPED_UNICODE);
$responses = ioAPIv2('/v2/authorization', $data,'');
//Decode Payload
if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';}
$clientsecret = $responses['token'];

//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// BASEURL is required for invoice template
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$base_url = 'https://'.$_SERVER['SERVER_NAME'].'/';
define('base_url', $base_url);

try {
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++
    // Initialize the Mollie API library
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++
    require "initialize.php"; // Mollie initialization (from commerce webhook)

    //+++++++++++++++++++++++++++++++++++++++++++++++++++++
    //Retrieve the payment's current state
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++
    $payment = $mollie->payments->get($_POST["id"]);
    $orderId = $payment->metadata->order_id;

    //+++++++++++++++++++++++++++++++++++++++++++++++++++++
    // Update the transaction using existing API
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++

    if ($payment->isPaid() && !$payment->hasRefunds() && !$payment->hasChargebacks()) {
        //+++++++++++++++++++++++++++++++++++++++++++++++++++++
        // PAID - Update transaction status via API
        //+++++++++++++++++++++++++++++++++++++++++++++++++++++
        $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 1), JSON_UNESCAPED_UNICODE);
        $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
        $transaction = json_decode($transaction,true);

        if ($transaction !== null && !empty($transaction)) {
            if(count($transaction) > 0) {

                //+++++++++++++++++++++++++++++++++++++++++++++++++++++
                // CREATE LICENSE for software upgrade
                //+++++++++++++++++++++++++++++++++++++++++++++++++++++
                $pdo = dbConnect($dbname);

                // Fetch transaction items to find software upgrade
                $sql = 'SELECT * FROM transactions_items WHERE txn_id = ?';
                $stmt = $pdo->prepare($sql);
                $stmt->execute([$orderId]);
                $items = $stmt->fetchAll(PDO::FETCH_ASSOC);

                foreach ($items as $item) {
                    if (!empty($item['item_options'])) {
                        $options = json_decode($item['item_options'], true);

                        // Check if this is a software upgrade (has serial_number and equipment_id)
                        if (isset($options['serial_number']) && isset($options['equipment_id'])) {

                            // Check if license already exists for this transaction
                            $sql = 'SELECT rowID FROM products_software_licenses WHERE transaction_id = ?';
                            $stmt = $pdo->prepare($sql);
                            $stmt->execute([$orderId]);
                            $existing_license = $stmt->fetch(PDO::FETCH_ASSOC);

                            if (!$existing_license) {
                                // Generate unique license key
                                $license_key = generateUniqueLicenseKey();

                                // Create license
                                $sql = 'INSERT INTO products_software_licenses
                                        (license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby)
                                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
                                $stmt = $pdo->prepare($sql);
                                $stmt->execute([
                                    $license_key,
                                    $options['equipment_id'],
                                    'upgrade',
                                    1, // active
                                    date('Y-m-d H:i:s'),
                                    '2099-12-31 23:59:59', // effectively permanent
                                    $orderId,
                                    date('Y-m-d H:i:s'),
                                    'webhook' // created by webhook
                                ]);

                                // Update equipment.sw_version_license
                                $sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
                                $stmt = $pdo->prepare($sql);
                                $stmt->execute([$license_key, $options['equipment_id']]);

                                error_log("Webhook: License created for equipment_id: " . $options['equipment_id'] . ", license_key: " . $license_key);
                            }
                        }
                    }
                }

                //+++++++++++++++++++++++++++++++++++++++++++++++++++++
                //Generate INVOICE RECORD via API
                //+++++++++++++++++++++++++++++++++++++++++++++++++++++
                $payload = json_encode(array("txn_id" => $transaction['transaction_id']), JSON_UNESCAPED_UNICODE);
                $invoice = ioAPIv2('/v2/invoice/',$payload,$clientsecret);
                $invoice = json_decode($invoice,true);

                if ($invoice !== null && !empty($invoice)) {
                    //+++++++++++++++++++++++++++++++++++++++++++++++++++++
                    //Generate INVOICE PDF and send email
                    //+++++++++++++++++++++++++++++++++++++++++++++++++++++
                    $invoice_cust = ioAPIv2('/v2/invoice/list=invoice&id='.$invoice['invoice_id'],'',$clientsecret);
                    $invoice_cust = json_decode($invoice_cust,true);

                    // Determine invoice language
                    if (!empty($invoice_cust['customer']['language'])) {
                        $invoice_language = strtoupper($invoice_cust['customer']['language']);
                    } elseif (!empty($invoice_cust['customer']['country']) && isset($available_languages[strtoupper($invoice_cust['customer']['country'])])) {
                        $invoice_language = $available_languages[strtoupper($invoice_cust['customer']['country'])];
                    } else {
                        $invoice_language = 'US'; // Default fallback
                    }

                    // Generate invoice HTML (using custom template for software upgrades)
                    list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);

                    //CREATE PDF using DomPDF
                    $dompdf->loadHtml($data);
                    $dompdf->setPaper('A4', 'portrait');
                    $dompdf->render();
                    $subject = ($invoice_software_subject ?? 'Software Upgrade - Invoice: ').$order_id;
                    $attachment = $dompdf->output();

                    //+++++++++++++++++++++++++++++++++++++++++++++++++++++
                    //Send email via PHPMailer
                    //+++++++++++++++++++++++++++++++++++++++++++++++++++++
                    send_mail_by_PHPMailer($customer_email, $subject, $data, $attachment, $subject);

                    if(invoice_bookkeeping){
                        send_mail_by_PHPMailer(email_bookkeeping, $subject, $data, $attachment, $subject);
                    }
                }
            }
        }

    } elseif ($payment->isOpen()) {
        // OPEN/PENDING (101)
        $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 101), JSON_UNESCAPED_UNICODE);
        $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);

    } elseif ($payment->isPending()) {
        // PENDING (101)
        $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 101), JSON_UNESCAPED_UNICODE);
        $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);

    } elseif ($payment->isFailed()) {
        // FAILED (102)
        $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 102), JSON_UNESCAPED_UNICODE);
        $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);

    } elseif ($payment->isExpired()) {
        // EXPIRED (103)
        $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 103), JSON_UNESCAPED_UNICODE);
        $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);

    } elseif ($payment->isCanceled()) {
        // CANCELED (999)
        $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 999), JSON_UNESCAPED_UNICODE);
        $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);

    } elseif ($payment->hasRefunds()) {
        // REFUNDED (1 + refund flag)
        $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 1), JSON_UNESCAPED_UNICODE);
        $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
        // TODO: Disable license on refund
    }

} catch (\Mollie\Api\Exceptions\ApiException $e) {
    error_log("Webhook API call failed: " . htmlspecialchars($e->getMessage()));
    http_response_code(500);
    echo "API call failed: " . htmlspecialchars($e->getMessage());
} catch (Exception $e) {
    error_log("Webhook error: " . htmlspecialchars($e->getMessage()));
    http_response_code(500);
}

Key Features (matching commerce webhook.php):

  • Uses /v2/transactions/ API for status updates
  • Uses /v2/invoice/ API for invoice generation
  • Generates PDF invoice with DomPDF
  • Sends email via PHPMailer
  • Creates license for software upgrade
  • Uses same payment status codes (0, 1, 101, 102, 103, 999)
  • Handles refunds (TODO: disable license)
  • Multi-language invoice support
  • Sends to bookkeeping if configured

Phase 2: Frontend Integration

2.1 Modify processPayment() in softwaretool.js (lines 574-608)

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

// 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)

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

// Add near top of softwaretool.php (after includes, before $view)
<?php
$payment_return = isset($_GET['payment_id']) ? $_GET['payment_id'] : null;

if ($payment_return) {
    // Optionally fetch payment status via GET /v2/get/payment
    // and show appropriate message banner at top of page
    // "Payment successful! Please reconnect your device to continue."
    // User will then click "Connect Device" button
    // After connection, checkSoftwareAvailability() will run
    // License will be found via existing logic, price will be 0.00
}
?>

3.2 Optional: Auto-trigger device connection after payment return

// 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)

// 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

// 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

// 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:
    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)
  7. Modify frontend JavaScript in /assets/softwaretool.js:
    • Update processPayment() to call POST /v2/post/payment
    • Add serial number verification in downloadAndInstallSoftware()
  8. Modify /softwaretool.php to detect payment return
  9. Test in DEBUG mode (full simulation without Mollie or device)
  10. Test with Mollie sandbox
  11. Deploy to production