diff --git a/PAYMENT_IMPLEMENTATION_SUMMARY.md b/PAYMENT_IMPLEMENTATION_SUMMARY.md
new file mode 100644
index 0000000..579343f
--- /dev/null
+++ b/PAYMENT_IMPLEMENTATION_SUMMARY.md
@@ -0,0 +1,103 @@
+# 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
new file mode 100644
index 0000000..8b672e9
--- /dev/null
+++ b/PAYMENT_INTEGRATION_PLAN.md
@@ -0,0 +1,825 @@
+# 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/v0/post/application.php b/api/v0/post/application.php
index 5e61f19..6a8a550 100644
--- a/api/v0/post/application.php
+++ b/api/v0/post/application.php
@@ -247,10 +247,13 @@ if (!empty($post_content['sn']) && !empty($post_content['testdetails'])) {
$sw_version = substr($sw_version, 0, -4);
}
+ // Translate hardware version to standardized format
+ $translated_hw_version = translateDeviceHardwareVersion($hw_version);
+
//Update Equipment record
$sql = "UPDATE equipment SET hw_version = ?, sw_version = ? $whereclause";
$stmt = $pdo->prepare($sql);
- $stmt->execute([$hw_version,$sw_version]);
+ $stmt->execute([$translated_hw_version,$sw_version]);
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
//Update equipment status ++++++++++++++++++++++++++
diff --git a/api/v1/get/products_software.php b/api/v1/get/products_software.php
index d97bf4b..ad1d860 100644
--- a/api/v1/get/products_software.php
+++ b/api/v1/get/products_software.php
@@ -46,7 +46,8 @@ if(isset($get_content) && $get_content!=''){
$clause .= ' AND e.serialnumber = :'.$v[0];
}
elseif ($v[0] == 'hw_version') {
- //build up search
+ //build up search - translate hardware version for comparison
+ $criterias[$v[0]] = translateDeviceHardwareVersion($criterias[$v[0]]);
$clause .= ' AND ps.hw_version = :'.$v[0];
}
elseif ($v[0] == 'status') {
@@ -152,9 +153,11 @@ if (!isset($criterias['productrowid']) && isset($criterias['sn']) && $criterias[
//check if current version is send and update the equipment record
if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){
+ // Translate hardware version to standardized format
+ $translated_hw_version = translateDeviceHardwareVersion($criterias['hw_version']);
$sql = 'UPDATE equipment SET hw_version = ?, updatedby = ? WHERE serialnumber = ? ';
$stmt = $pdo->prepare($sql);
- $stmt->execute([$criterias['hw_version'],$username,$criterias['sn']]);
+ $stmt->execute([$translated_hw_version,$username,$criterias['sn']]);
}
//GET PRODUCTCODE, SW_VERSION_UPGRADE, HW_VERSION from equipment SN
diff --git a/api/v2/get/products_software.php b/api/v2/get/products_software.php
index ec50e8b..a952f40 100644
--- a/api/v2/get/products_software.php
+++ b/api/v2/get/products_software.php
@@ -48,7 +48,8 @@ if(isset($get_content) && $get_content!=''){
$clause .= ' AND ps.status = :'.$v[0];
}
elseif ($v[0] == 'hw_version') {
- //build up search
+ //build up search - translate hardware version for comparison
+ $criterias[$v[0]] = translateDeviceHardwareVersion($criterias[$v[0]]);
$clause .= ' AND ps.hw_version = :'.$v[0];
}
else {//create clause
@@ -149,9 +150,11 @@ if (!isset($criterias['productrowid']) && isset($criterias['sn']) && $criterias[
//check if current version is send and update the equipment record
if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){
+ // Translate hardware version to standardized format
+ $translated_hw_version = translateDeviceHardwareVersion($criterias['hw_version']);
$sql = 'UPDATE equipment SET hw_version = ?, updatedby = ? WHERE serialnumber = ? ';
$stmt = $pdo->prepare($sql);
- $stmt->execute([$criterias['hw_version'],$username,$criterias['sn']]);
+ $stmt->execute([$translated_hw_version,$username,$criterias['sn']]);
}
//GET PRODUCTCODE, SW_VERSION_UPGRADE, HW_VERSION from equipment SN
diff --git a/api/v2/get/software_available.php b/api/v2/get/software_available.php
index 118a914..3cb21b4 100644
--- a/api/v2/get/software_available.php
+++ b/api/v2/get/software_available.php
@@ -14,6 +14,7 @@ $pdo = dbConnect($dbname);
//NEW ARRAY
$criterias = [];
$clause = '';
+$debug = [];
//Check for $_GET variables and build up clause
if(isset($get_content) && $get_content!=''){
@@ -27,6 +28,11 @@ if(isset($get_content) && $get_content!=''){
}
}
+if (debug) {
+ $debug['request_parameters'] = $criterias;
+ $debug['timestamp'] = date('Y-m-d H:i:s');
+}
+
// IF SN IS PROVIDED, CHECK FOR AVAILABLE UPGRADES
if (isset($criterias['sn']) && $criterias['sn'] != ''){
@@ -42,9 +48,11 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
//check if current hw_version is send and update the equipment record
if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){
+ // Translate hardware version to standardized format
+ $translated_hw_version = translateDeviceHardwareVersion($criterias['hw_version']);
$sql = 'UPDATE equipment SET hw_version = ?, updatedby = ? WHERE serialnumber = ? ';
$stmt = $pdo->prepare($sql);
- $stmt->execute([$criterias['hw_version'],$username,$criterias['sn']]);
+ $stmt->execute([$translated_hw_version,$username,$criterias['sn']]);
}
//GET EQUIPMENT AND PRODUCT DATA BASED ON SERIAL NUMBER
@@ -72,8 +80,46 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
$sw_version_license = $equipment_data['sw_version_license'];
$equipment_rowid = $equipment_data['equipment_rowid'];
- //GET ALL DATA: active assignments, version details, and upgrade paths
- //Filter on active status, hw_version compatibility, and exclude current version
+ if (debug) {
+ $debug['equipment_data'] = [
+ 'product_rowid' => $product_rowid,
+ 'productcode' => $productcode,
+ 'current_sw_version_raw' => $current_sw_version,
+ 'hw_version' => $hw_version
+ ];
+ }
+
+ // Normalize software version for comparison (lowercase, trim leading zeros)
+ $current_sw_version = strtolower(ltrim($current_sw_version, '0'));
+
+ // Translate incoming hw_version parameter for comparison if provided
+ $comparison_hw_version = $hw_version;
+ $hw_version_from_request = null;
+ if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){
+ $hw_version_from_request = $criterias['hw_version'];
+ $comparison_hw_version = translateDeviceHardwareVersion($criterias['hw_version']);
+ }
+
+ if (debug) {
+ $debug['normalized_data'] = [
+ 'current_sw_version' => $current_sw_version,
+ 'hw_version_from_request' => $hw_version_from_request,
+ 'comparison_hw_version' => $comparison_hw_version,
+ 'hw_version_valid' => ($comparison_hw_version !== '')
+ ];
+ }
+
+ // Check if hardware version is invalid (all zeros)
+ if ($hw_version_from_request && $comparison_hw_version === '') {
+ $messages = ["software_available" => "error", "error" => "Invalid hardware version (000000) - device may not be properly initialized"];
+ if (debug) {
+ $messages['debug'] = $debug;
+ }
+ echo json_encode($messages, JSON_UNESCAPED_UNICODE);
+ exit;
+ }
+
+ //GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version
$sql = 'SELECT
psv.rowID as version_id,
psv.version,
@@ -82,59 +128,121 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
psv.mandatory,
psv.latest,
psv.hw_version,
- psv.file_path,
- pup.price,
- pup.currency,
- pup.from_version_id,
- from_ver.version as from_version
+ psv.file_path
FROM products_software_assignment psa
JOIN products_software_versions psv ON psa.software_version_id = psv.rowID
- LEFT JOIN products_software_upgrade_paths pup ON pup.to_version_id = psv.rowID AND pup.is_active = 1
- LEFT JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID
WHERE psa.product_id = ?
AND psa.status = 1
- AND (psv.hw_version = ? OR psv.hw_version IS NULL OR psv.hw_version = "")
- AND psv.version != ?';
+ AND (psv.hw_version = ? OR psv.hw_version IS NULL OR psv.hw_version = "")';
$stmt = $pdo->prepare($sql);
- $stmt->execute([$product_rowid, $hw_version, $current_sw_version ?? '']);
+ $stmt->execute([$product_rowid, $comparison_hw_version]);
$versions = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ if (debug) {
+ $debug['active_assignments'] = [
+ 'count' => count($versions),
+ 'versions' => array_map(function($v) {
+ return [
+ 'version_id' => $v['version_id'],
+ 'version' => $v['version'],
+ 'name' => $v['name'],
+ 'hw_version' => $v['hw_version'],
+ 'latest' => $v['latest']
+ ];
+ }, $versions)
+ ];
+ }
+
if (empty($versions)) {
// No versions available
$software_available = "no";
+ if (debug) {
+ $debug['decision'] = 'No active software assignments found';
+ }
} else {
$has_priced_options = false;
$has_latest_version_different = false;
-
+
+ if (debug) {
+ $debug['version_checks'] = [];
+ }
+
foreach ($versions as $version) {
+ //Normalize version for comparison (lowercase, trim leading zeros)
+ $normalized_version = strtolower(ltrim($version['version'], '0'));
+
+ //Skip if this is the current version
+ if ($current_sw_version && $normalized_version == $current_sw_version) {
+ continue;
+ }
+
//Check if this version should be shown (same logic as software_update)
$show_version = false;
+ $final_price = '0.00';
+ $decision_reason = '';
+
+ if (debug) {
+ $version_check = [
+ 'version' => $version['version'],
+ 'name' => $version['name'],
+ 'normalized' => $normalized_version,
+ 'is_current' => ($current_sw_version && $normalized_version == $current_sw_version)
+ ];
+ }
+
if (!$current_sw_version || $current_sw_version == '') {
//No current version - show all
$show_version = true;
- } elseif ($version['from_version'] == $current_sw_version) {
- //Upgrade path exists from current version
- $show_version = true;
+ $decision_reason = 'No current version - showing all';
} else {
- //Check if any upgrade paths exist for this version
+ //Check if this version is part of ANY upgrade path system (either FROM or TO)
$sql = 'SELECT COUNT(*) as path_count
FROM products_software_upgrade_paths
- WHERE to_version_id = ? AND is_active = 1';
+ WHERE (to_version_id = ? OR from_version_id = ?) AND is_active = 1';
$stmt = $pdo->prepare($sql);
- $stmt->execute([$version['version_id']]);
+ $stmt->execute([$version['version_id'], $version['version_id']]);
$path_check = $stmt->fetch(PDO::FETCH_ASSOC);
- if ($path_check['path_count'] == 0) {
- //No paths exist at all - show as free upgrade
- $show_version = true;
+ if (debug) {
+ $version_check['path_count'] = $path_check['path_count'];
}
+
+ if ($path_check['path_count'] == 0) {
+ //Not part of any upgrade path system - show as free upgrade
+ $show_version = true;
+ $decision_reason = 'No upgrade paths defined - showing as free';
+ } else {
+ //Part of an upgrade path system
+ //Only show if there's an explicit path FROM current version TO this version
+ $sql = 'SELECT pup.price, pup.currency
+ FROM products_software_upgrade_paths pup
+ JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID
+ WHERE pup.to_version_id = ?
+ AND LOWER(TRIM(LEADING "0" FROM from_ver.version)) = ?
+ AND pup.is_active = 1';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$version['version_id'], $current_sw_version]);
+ $upgrade_path = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($upgrade_path) {
+ //Valid upgrade path found FROM current version
+ $show_version = true;
+ $final_price = $upgrade_path['price'] ?? '0.00';
+ $decision_reason = 'Found upgrade path from current with price: ' . $final_price;
+ } else {
+ $decision_reason = 'Has upgrade paths but none from current version';
+ }
+ }
+ }
+
+ if (debug) {
+ $version_check['show_version'] = $show_version;
+ $version_check['reason'] = $decision_reason;
}
if ($show_version) {
//Check if there's a valid license for this upgrade
- $final_price = $version['price'] ?? '0.00';
-
if ($final_price > 0 && $sw_version_license) {
//Check if the license is valid
$sql = 'SELECT status, start_at, expires_at
@@ -162,26 +270,52 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
}
// Check if there's a "latest" flagged version that's different from current
- if ($version['latest'] == 1 && $version['version'] != $current_sw_version) {
+ if ($version['latest'] == 1 && $normalized_version != $current_sw_version) {
$has_latest_version_different = true;
}
+
+ if (debug) {
+ $version_check['final_price'] = $final_price;
+ $version_check['has_priced_option'] = ($final_price > 0);
+ $version_check['is_latest_different'] = ($version['latest'] == 1 && $normalized_version != $current_sw_version);
+ }
+ }
+
+ if (debug) {
+ $debug['version_checks'][] = $version_check;
}
}
// Apply the logic:
// 1. If there are priced options -> "yes"
- // 2. If no priced options but current version != latest flagged version -> "yes"
+ // 2. If no priced options but current version != latest flagged version -> "yes"
// 3. Default -> "no"
if ($has_priced_options) {
$software_available = "yes";
+ $availability_reason = "Has priced upgrade options available";
} elseif ($has_latest_version_different) {
$software_available = "yes";
+ $availability_reason = "Has free latest version available";
} else {
$software_available = "no";
+ $availability_reason = "No upgrades available or already on latest";
+ }
+
+ if (debug) {
+ $debug['final_decision'] = [
+ 'has_priced_options' => $has_priced_options,
+ 'has_latest_version_different' => $has_latest_version_different,
+ 'software_available' => $software_available,
+ 'reason' => $availability_reason
+ ];
}
}
$messages = ["software_available" => $software_available];
+
+ if (debug) {
+ debuglog(json_encode($debug));
+ }
}
} else {
$messages = ["error" => "No serialnumber found"];
diff --git a/api/v2/get/software_download.php b/api/v2/get/software_download.php
index 4d6d736..5260d91 100644
--- a/api/v2/get/software_download.php
+++ b/api/v2/get/software_download.php
@@ -9,8 +9,6 @@ defined($security_key) or exit;
//Connect to DB
$pdo = dbConnect($dbname);
-var_dump($_GET);
-
// STEP 1: Validate token parameter exists
if (!isset($_GET['token']) || $_GET['token'] == '') {
http_response_code(400);
@@ -135,8 +133,10 @@ if ($assignment['assigned'] == 0) {
}
// STEP 6: Hardware version compatibility
-if ($version['hw_version'] && $version['hw_version'] != '' && $equipment['hw_version']) {
- if ($version['hw_version'] != $equipment['hw_version']) {
+// Only check if version has hw_version requirement (not NULL or empty)
+// Match logic from software_update.php line 103
+if ($version['hw_version'] && $version['hw_version'] != '') {
+ if ($equipment['hw_version'] && $version['hw_version'] != $equipment['hw_version']) {
http_response_code(403);
log_download([
'user_id' => $user_data['id'],
diff --git a/api/v2/get/software_update.php b/api/v2/get/software_update.php
index 7abf15e..3a71457 100644
--- a/api/v2/get/software_update.php
+++ b/api/v2/get/software_update.php
@@ -13,6 +13,7 @@ $pdo = dbConnect($dbname);
//NEW ARRAY
$criterias = [];
$clause = '';
+$debug = [];
//Check for $_GET variables and build up clause
if(isset($get_content) && $get_content!=''){
@@ -26,6 +27,11 @@ if(isset($get_content) && $get_content!=''){
}
}
+if (debug) {
+ $debug['request_parameters'] = $criterias;
+ $debug['timestamp'] = date('Y-m-d H:i:s');
+}
+
// IF SN IS PROVIDED, HANDLE UPGRADE OPTIONS
if (isset($criterias['sn']) && $criterias['sn'] != ''){
@@ -41,9 +47,11 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
//check if current hw_version is send and update the equipment record
if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){
+ // Translate hardware version to standardized format
+ $translated_hw_version = translateDeviceHardwareVersion($criterias['hw_version']);
$sql = 'UPDATE equipment SET hw_version = ?, updatedby = ? WHERE serialnumber = ? ';
$stmt = $pdo->prepare($sql);
- $stmt->execute([$criterias['hw_version'],$username,$criterias['sn']]);
+ $stmt->execute([$translated_hw_version,$username,$criterias['sn']]);
}
//GET EQUIPMENT AND PRODUCT DATA BASED ON SERIAL NUMBER
@@ -71,8 +79,47 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
$sw_version_license = $equipment_data['sw_version_license'];
$equipment_rowid = $equipment_data['equipment_rowid'];
- //GET ALL DATA: active assignments, version details, and upgrade paths
- //Filter on active status and hw_version compatibility
+ if (debug) {
+ $debug['equipment_data'] = [
+ 'product_rowid' => $product_rowid,
+ 'productcode' => $productcode,
+ 'current_sw_version_raw' => $current_sw_version,
+ 'hw_version' => $hw_version,
+ 'sw_version_license' => $sw_version_license
+ ];
+ }
+
+ // Normalize software version for comparison (lowercase, trim leading zeros)
+ $current_sw_version = strtolower(ltrim($current_sw_version, '0'));
+
+ // Translate incoming hw_version parameter for comparison if provided
+ $comparison_hw_version = $hw_version;
+ $hw_version_from_request = null;
+ if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){
+ $hw_version_from_request = $criterias['hw_version'];
+ $comparison_hw_version = translateDeviceHardwareVersion($criterias['hw_version']);
+ }
+
+ if (debug) {
+ $debug['normalized_data'] = [
+ 'current_sw_version' => $current_sw_version,
+ 'hw_version_from_request' => $hw_version_from_request,
+ 'comparison_hw_version' => $comparison_hw_version,
+ 'hw_version_valid' => ($comparison_hw_version !== '')
+ ];
+ }
+
+ // Check if hardware version is invalid (all zeros)
+ if ($hw_version_from_request && $comparison_hw_version === '') {
+ $messages = ["error" => "Invalid hardware version (000000) - device may not be properly initialized"];
+ if (debug) {
+ $messages['debug'] = $debug;
+ }
+ echo json_encode($messages, JSON_UNESCAPED_UNICODE);
+ exit;
+ }
+
+ //GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version
$sql = 'SELECT
psv.rowID as version_id,
psv.version,
@@ -81,60 +128,157 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
psv.mandatory,
psv.latest,
psv.hw_version,
- psv.file_path,
- pup.price,
- pup.currency,
- pup.from_version_id,
- from_ver.version as from_version
+ psv.file_path
FROM products_software_assignment psa
JOIN products_software_versions psv ON psa.software_version_id = psv.rowID
- LEFT JOIN products_software_upgrade_paths pup ON pup.to_version_id = psv.rowID AND pup.is_active = 1
- LEFT JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID
WHERE psa.product_id = ?
AND psa.status = 1
- AND (psv.hw_version = ? OR psv.hw_version IS NULL OR psv.hw_version = "")
- AND (? IS NULL OR ? = "" OR psv.version != ?)';
+ AND (psv.hw_version = ? OR psv.hw_version IS NULL OR psv.hw_version = "")';
$stmt = $pdo->prepare($sql);
- $stmt->execute([$product_rowid, $hw_version, $current_sw_version, $current_sw_version, $current_sw_version]);
+ $stmt->execute([$product_rowid, $comparison_hw_version]);
$versions = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ if (debug) {
+ $debug['active_assignments'] = [
+ 'count' => count($versions),
+ 'versions' => array_map(function($v) {
+ return [
+ 'version_id' => $v['version_id'],
+ 'version' => $v['version'],
+ 'name' => $v['name'],
+ 'hw_version' => $v['hw_version'],
+ 'latest' => $v['latest']
+ ];
+ }, $versions)
+ ];
+ }
+
if (empty($versions)) {
$messages = ["error" => "No active software assignments found for product"];
+ if (debug) {
+ $messages['debug'] = $debug;
+ }
} else {
+ // First check if current version has paid upgrade paths FROM it
+ $has_paid_upgrade_from_current = false;
+ if ($current_sw_version) {
+ $sql = 'SELECT COUNT(*) as paid_count
+ FROM products_software_upgrade_paths pup
+ JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID
+ WHERE LOWER(TRIM(LEADING "0" FROM from_ver.version)) = ?
+ AND pup.price > 0
+ AND pup.is_active = 1';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$current_sw_version]);
+ $paid_check = $stmt->fetch(PDO::FETCH_ASSOC);
+ $has_paid_upgrade_from_current = ($paid_check['paid_count'] > 0);
+ }
+
+ if (debug) {
+ $debug['has_paid_upgrade_from_current'] = $has_paid_upgrade_from_current;
+ $debug['version_decisions'] = [];
+ }
+
foreach ($versions as $version) {
- //Check if this version should be shown:
- //1. If there's a matching upgrade path from current version, show it
- //2. If no current version exists, show all
- //3. If there's no upgrade path but also no paths exist for this version at all, show it (free upgrade)
+ //Normalize version for comparison (lowercase, trim leading zeros)
+ $normalized_version = strtolower(ltrim($version['version'], '0'));
+ $is_current_version = ($current_sw_version && $normalized_version == $current_sw_version);
+
+ //All versions with matching HW are potential upgrades
$show_version = false;
- if (!$current_sw_version || $current_sw_version == '') {
- //No current version - show all
- $show_version = true;
- } elseif ($version['from_version'] == $current_sw_version) {
- //Upgrade path exists from current version
- $show_version = true;
- } else {
- //Check if any upgrade paths exist for this version
- $sql = 'SELECT COUNT(*) as path_count
- FROM products_software_upgrade_paths
- WHERE to_version_id = ? AND is_active = 1';
- $stmt = $pdo->prepare($sql);
- $stmt->execute([$version['version_id']]);
- $path_check = $stmt->fetch(PDO::FETCH_ASSOC);
+ $final_price = '0.00';
+ $final_currency = '';
+ $is_current = false;
+ $decision_reason = '';
- if ($path_check['path_count'] == 0) {
- //No paths exist at all - show as free upgrade
+ if (debug) {
+ $version_debug = [
+ 'version' => $version['version'],
+ 'name' => $version['name'],
+ 'normalized_version' => $normalized_version,
+ 'is_current_version' => $is_current_version,
+ 'latest' => $version['latest']
+ ];
+ }
+
+ if (!$current_sw_version || $current_sw_version == '') {
+ //No current version - show all as free upgrades
+ if (!$is_current_version) {
$show_version = true;
+ $decision_reason = 'No current version stored - showing as free upgrade';
+ } else {
+ $decision_reason = 'Skipped - is current version but no upgrades scenario';
}
+ } else {
+ //Check if this is the current version and should be shown as disabled
+ if ($is_current_version && $has_paid_upgrade_from_current && $version['latest'] == 1) {
+ //Show current version as disabled only if it's the latest AND there's a paid upgrade available
+ $show_version = true;
+ $is_current = true;
+ $final_price = '0.00';
+ $final_currency = '';
+ $decision_reason = 'Showing as CURRENT - is latest version with paid upgrade available';
+ } else if ($is_current_version && !($has_paid_upgrade_from_current && $version['latest'] == 1)) {
+ $decision_reason = 'Skipped - is current version but not (latest + has_paid_upgrade)';
+ } else if (!$is_current_version) {
+ //Check if this version is part of ANY upgrade path system (either FROM or TO)
+ $sql = 'SELECT COUNT(*) as path_count
+ FROM products_software_upgrade_paths
+ WHERE (to_version_id = ? OR from_version_id = ?) AND is_active = 1';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$version['version_id'], $version['version_id']]);
+ $path_check = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (debug) {
+ $version_debug['upgrade_path_count'] = $path_check['path_count'];
+ }
+
+ if ($path_check['path_count'] == 0) {
+ //Not part of any upgrade path system - show as free upgrade
+ $show_version = true;
+ $decision_reason = 'Showing as FREE - no upgrade paths defined for this version';
+ } else {
+ //Part of an upgrade path system
+ //Only show if there's an explicit path FROM current version TO this version
+ $sql = 'SELECT pup.price, pup.currency
+ FROM products_software_upgrade_paths pup
+ JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID
+ WHERE pup.to_version_id = ?
+ AND LOWER(TRIM(LEADING "0" FROM from_ver.version)) = ?
+ AND pup.is_active = 1';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$version['version_id'], $current_sw_version]);
+ $upgrade_path = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($upgrade_path) {
+ //Valid upgrade path found FROM current version
+ $show_version = true;
+ $final_price = $upgrade_path['price'] ?? '0.00';
+ $final_currency = $upgrade_path['currency'] ?? '';
+ $decision_reason = 'Showing - found upgrade path FROM current (' . $current_sw_version . ') with price: ' . $final_price . ' ' . $final_currency;
+ } else {
+ $decision_reason = 'Skipped - has upgrade paths but none FROM current version (' . $current_sw_version . ')';
+ }
+ //If no path from current version exists, don't show (show_version stays false)
+ }
+ }
+ }
+
+ if (debug) {
+ $version_debug['decision'] = [
+ 'show_version' => $show_version,
+ 'is_current' => $is_current,
+ 'final_price' => $final_price,
+ 'final_currency' => $final_currency,
+ 'reason' => $decision_reason
+ ];
}
if ($show_version) {
//Check if there's a valid license for this upgrade
- $final_price = $version['price'] ?? '0.00';
- $final_currency = $version['currency'] ?? '';
-
+ $license_applied = false;
if ($final_price > 0 && $sw_version_license) {
//Check if the license is valid
$sql = 'SELECT status, start_at, expires_at
@@ -151,7 +295,17 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
//Check if license is within valid date range
if ((!$start_at || $start_at <= $now) && (!$expires_at || $expires_at >= $now)) {
+ $original_price = $final_price;
$final_price = '0.00';
+ $license_applied = true;
+
+ if (debug) {
+ $version_debug['license_applied'] = [
+ 'license_key' => $sw_version_license,
+ 'original_price' => $original_price,
+ 'new_price' => $final_price
+ ];
+ }
}
}
}
@@ -169,9 +323,14 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
"source" => '',
"source_type" => '',
"price" => $final_price,
- "currency" => $final_currency
+ "currency" => $final_currency,
+ "is_current" => $is_current
];
}
+
+ if (debug) {
+ $debug['version_decisions'][] = $version_debug;
+ }
}
//GENERATE DOWNLOAD TOKENS FOR EACH OPTION
@@ -180,13 +339,38 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
$download_token = create_download_url_token($criterias['sn'], $option['version_id']);
// Create secure download URL
- $download_url = 'https://'.$_SERVER['SERVER_NAME'].'/api.php/v2/software_download/token='.$download_token;
+ $download_url = 'https://'.$_SERVER['SERVER_NAME'].'/api.php/v2/software_download?token='.$download_token;
// Set source as download URL
$option['source'] = $download_url;
$option['source_type'] = 'token_url';
}
+
+ if (debug) {
+ $debug['final_output'] = [
+ 'total_versions_shown' => count($output),
+ 'versions' => array_map(function($o) {
+ return [
+ 'name' => $o['name'],
+ 'version' => $o['version'],
+ 'price' => $o['price'],
+ 'is_current' => $o['is_current']
+ ];
+ }, $output)
+ ];
+ }
+
$messages = $output;
+
+ if (debug && !empty($output)) {
+ // Add debug as separate field in response
+ foreach ($messages as &$msg) {
+ $msg['_debug'] = $debug;
+ break; // Only add to first item
+ }
+ } elseif (debug && empty($output)) {
+ $messages = ['message' => 'No upgrades available', 'debug' => $debug];
+ }
}
}
}
diff --git a/api/v2/post/history.php b/api/v2/post/history.php
index 2ad217f..75eead9 100644
--- a/api/v2/post/history.php
+++ b/api/v2/post/history.php
@@ -233,10 +233,13 @@ if (isset($post_content['sn']) && isset($post_content['payload'])){
$sw_version = substr($sw_version, 0, -4);
}
+ // Translate hardware version to standardized format
+ $translated_hw_version = translateDeviceHardwareVersion($hw_version);
+
//Update Equipment record
$sql = "UPDATE equipment SET hw_version = ?, sw_version = ? $whereclause";
$stmt = $pdo->prepare($sql);
- $stmt->execute([$hw_version,$sw_version]);
+ $stmt->execute([$translated_hw_version,$sw_version]);
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
//Update equipment status ++++++++++++++++++++++++++
diff --git a/api/v2/post/products_software_upgrade_paths.php b/api/v2/post/products_software_upgrade_paths.php
index d3849bb..6b6f9a4 100644
--- a/api/v2/post/products_software_upgrade_paths.php
+++ b/api/v2/post/products_software_upgrade_paths.php
@@ -62,16 +62,25 @@ $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
+//VALIDATE: Prevent FROM and TO being the same version
+if (($command == 'insert' || $command == 'update') &&
+ isset($criterias['from_version_id']) && isset($criterias['to_version_id']) &&
+ $criterias['from_version_id'] == $criterias['to_version_id']) {
+ http_response_code(400);
+ echo json_encode(["error" => "FROM version cannot be the same as TO version in upgrade path"]);
+ exit;
+}
+
//QUERY AND VERIFY ALLOWED
if ($command == 'update' && isAllowed('products_software_upgrade_paths',$profile,$permission,'U') === 1){
-
+
$sql = 'UPDATE products_software_upgrade_paths SET '.$clause.' WHERE rowID = ? ';
$execute_input[] = $id;
$stmt = $pdo->prepare($sql);
$stmt->execute($execute_input);
-}
+}
elseif ($command == 'insert' && isAllowed('products_software_upgrade_paths',$profile,$permission,'C') === 1){
-
+
//INSERT NEW ITEM
$sql = 'INSERT INTO products_software_upgrade_paths ('.$clause_insert.') VALUES ('.$input_insert.')';
$stmt = $pdo->prepare($sql);
diff --git a/api/v2/post/products_software_versions.php b/api/v2/post/products_software_versions.php
index 52f4191..60417ab 100644
--- a/api/v2/post/products_software_versions.php
+++ b/api/v2/post/products_software_versions.php
@@ -41,8 +41,11 @@ else {
//do nothing
}
+//translate HW_VERSION to correct string
+if (isset($post_content['hw_version']) && $post_content['hw_version'] !=''){$post_content['hw_version'] =translateDeviceHardwareVersion($post_content['hw_version']); }
+
//CREATE NEW ARRAY AND MAP TO CLAUSE
-if(isset($post_content) && $post_content!=''){
+if(isset($post_content) && $post_content!=''){
foreach ($post_content as $key => $var){
if ($key == 'submit' || $key == 'rowID'){
//do nothing
diff --git a/assets/functions.php b/assets/functions.php
index 608a441..69c7486 100644
--- a/assets/functions.php
+++ b/assets/functions.php
@@ -5191,4 +5191,101 @@ function updateSoftwareVersionStatus($pdo, $serialnumber = null) {
error_log('Database error in updateSoftwareVersionStatus: ' . $e->getMessage());
return false;
}
+}
+
+// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
+// Hardware Version Translation Functions
+// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+/**
+ * Translates hardware version to standardized format
+ * Examples:
+ * - r80, R80, 80 -> r08
+ * - r70, R70, 70 -> r07
+ * - r60, R60, 60 -> r06
+ * etc.
+ *
+ * @param string $hw_version - Input hardware version
+ * @return string - Standardized hardware version
+ */
+function translateHardwareVersion($hw_version) {
+ if (empty($hw_version) || $hw_version == '') {
+ return $hw_version;
+ }
+
+ // Remove any whitespace and convert to lowercase for processing
+ $hw_clean = strtolower(trim($hw_version));
+
+ // Treat all-zeros as invalid/empty hardware version
+ if (preg_match('/^0+$/', $hw_clean)) {
+ return '';
+ }
+
+ // Define translation mapping
+ $translation_map = [
+ // r80/R80/80 variants -> r08
+ 'r80' => 'r08',
+ '80' => 'r08',
+
+ // r70/R70/70 variants -> r07
+ 'r70' => 'r07',
+ '70' => 'r07',
+
+ // r60/R60/60 variants -> r06
+ 'r60' => 'r06',
+ '60' => 'r06',
+
+ // Already correct format, just ensure lowercase
+ 'r08' => 'r08',
+ '08' => 'r08',
+ 'r07' => 'r07',
+ '07' => 'r07',
+ 'r06' => 'r06',
+ '06' => 'r06',
+ ];
+
+ // Check if we have a direct mapping
+ if (isset($translation_map[$hw_clean])) {
+ return $translation_map[$hw_clean];
+ }
+
+ // Handle pattern matching for other potential formats
+ // Extract numeric value from various formats (00000080, r90, 90, etc.)
+ if (preg_match('/^r?0*(\d{1,2})$/', $hw_clean, $matches)) {
+ $number = intval($matches[1]);
+ if ($number >= 10 && $number <= 99) {
+ // Convert to zero-padded format: 80 -> 08, 70 -> 07, etc.
+ // Take the tens digit and format as 0X: 80->08, 70->07, 60->06
+ $tensDigit = intval($number / 10);
+ $padded = '0' . $tensDigit;
+ return 'r' . $padded;
+ }
+ }
+
+ // If no translation found, return original input unchanged
+ return $hw_version;
+}
+
+/**
+ * Translates hardware version received from device/API to standardized DB format
+ * This should be called before storing hw_version in the database
+ *
+ * @param string $device_hw_version - Hardware version from device
+ * @return string - Standardized hardware version for database storage
+ */
+function translateDeviceHardwareVersion($device_hw_version) {
+ return translateHardwareVersion($device_hw_version);
+}
+
+/**
+ * Translates hardware version from database to match device format if needed
+ * This can be used for display or API responses
+ *
+ * @param string $db_hw_version - Hardware version from database
+ * @return string - Hardware version (currently returns same as input)
+ */
+function translateDbHardwareVersion($db_hw_version) {
+ // For now, we keep the standardized format from DB
+ // This function exists for future reverse translation if needed
+ return $db_hw_version;
}
\ No newline at end of file
diff --git a/assets/softwaretool.js b/assets/softwaretool.js
new file mode 100644
index 0000000..cb371c8
--- /dev/null
+++ b/assets/softwaretool.js
@@ -0,0 +1,1099 @@
+const serialResultsDiv = document.getElementById("serialResults");
+const readBar = document.getElementById("readBar");
+
+// Buffer for accumulating received data before logging
+let receivedDataBuffer = '';
+
+// Software tool specific variables
+let deviceSerialNumber = "";
+let deviceVersion = "";
+let deviceHwVersion = "";
+let selectedSoftwareUrl = "";
+
+// Serial port variables (port, writer, textEncoder, writableStreamClosed declared in PHP)
+let reader;
+let readableStreamClosed;
+let keepReading = true;
+
+// Function to log communication to API (reused from scripts.js)
+async function logCommunication(data, direction) {
+ // Only log if debug mode is enabled
+ if (typeof DEBUG === 'undefined' || !DEBUG) {
+ return;
+ }
+
+ try {
+ const serviceToken = document.getElementById("servicetoken")?.innerHTML || '';
+
+ let serialNumber = '';
+ if (deviceSerialNumber) {
+ serialNumber = deviceSerialNumber;
+ }
+
+ const logData = {
+ data: data,
+ direction: direction,
+ timestamp: new Date().toISOString(),
+ serial_number: serialNumber,
+ maintenance_run: 0
+ };
+
+ const url = link + '/v2/com_log/log';
+ const bearer = 'Bearer ' + serviceToken;
+
+ const response = await fetch(url, {
+ method: 'POST',
+ withCredentials: true,
+ credentials: 'include',
+ headers: {
+ 'Authorization': bearer,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(logData)
+ });
+
+ if (!response.ok) {
+ console.warn('Failed to log communication:', response.status);
+ }
+ } catch (error) {
+ console.warn('Error logging communication:', error);
+ }
+}
+
+// Progress bar function (reused from scripts.js)
+function progressBar(percentage, message, color){
+ readBar.style.background = color;
+ readBar.style.width = percentage +"%";
+ readBar.innerHTML = message;
+}
+
+// Connect device for software tool
+async function connectDeviceForSoftware() {
+ //clear input
+ readBar.innerHTML = '';
+ serialResultsDiv.innerHTML = '';
+ document.getElementById("softwareCheckStatus").style.display = "none";
+ document.getElementById("softwareOptions").style.display = "none";
+ document.getElementById("noUpdatesMessage").style.display = "none";
+ document.getElementById("uploadSection").style.display = "none";
+
+ // Reset data
+ receivedDataBuffer = '';
+ deviceSerialNumber = "";
+ deviceVersion = "";
+ deviceHwVersion = "";
+
+ //set progress bar
+ progressBar("1", "", "");
+
+ // Check if DEBUG mode is enabled - use mock device data
+ if (typeof DEBUG !== 'undefined' && DEBUG) {
+ // TEST MODE: Use mock device data
+ deviceSerialNumber = "22110095";
+ deviceVersion = "03e615af";
+ deviceHwVersion = "0000080";
+
+ document.getElementById("Device_output").style.display = "block";
+ serialResultsDiv.innerHTML = `DEBUG MODE - Simulated Device Data:
SN=${deviceSerialNumber}
FW=${deviceVersion}
HW=${deviceHwVersion}`;
+ progressBar("60", "DEBUG MODE - Device data read - SN: " + deviceSerialNumber, "#ff9800");
+
+ await logCommunication(`DEBUG MODE: Simulated device - SN=${deviceSerialNumber}, Version=${deviceVersion}, HW=${deviceHwVersion}`, 'handshake');
+
+ // Proceed to check software availability
+ checkSoftwareAvailability();
+ return;
+ }
+
+ // Check if port is already open - if so, refresh the page to start clean
+ if (port) {
+ await logCommunication('Port already in use - refreshing page for clean reconnect', 'info');
+ location.reload();
+ return;
+ }
+
+ // Reset flags for new connection
+ keepReading = true;
+
+ try {
+ // Prompt user to select any serial port.
+ const filters = [{ usbVendorId: 1027, usbProductId: 24597 }];
+ port = await navigator.serial.requestPort({ filters });
+
+ // Log selected port details
+ const portInfo = port.getInfo();
+ const portDetails = {
+ processStep: 'Software',
+ usbVendorId: portInfo.usbVendorId,
+ usbProductId: portInfo.usbProductId,
+ readable: !!port.readable,
+ writable: !!port.writable,
+ opened: port.readable !== null && port.writable !== null
+ };
+ await logCommunication(`Selected USB device - ${JSON.stringify(portDetails)}`, 'connected');
+
+ await port.open({
+ baudRate: 56700,
+ dataBits: 8,
+ stopBits: 1,
+ parity: 'none',
+ flowControl: 'none'
+ });
+
+ progressBar("10", "Connecting", "#04AA6D");
+
+ // Log successful connection
+ await logCommunication('Port opened successfully', 'connected');
+
+ textEncoder = new TextEncoderStream();
+ writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
+ writer = textEncoder.writable.getWriter();
+
+ progressBar("20", "Connected, reading device...", "#04AA6D");
+
+ // Read device output
+ await readDeviceOutput();
+
+ // Close the port immediately after reading
+ await closePortAfterRead();
+
+ // Check for software updates (port is now closed)
+ checkSoftwareAvailability();
+
+ } catch (error) {
+ await logCommunication(`Connection error: ${error.message}`, 'error');
+ progressBar("0", "Error: " + error.message, "#ff6666");
+ }
+}
+
+async function readDeviceOutput() {
+ const textDecoder = new TextDecoderStream();
+ readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
+ reader = textDecoder.readable.getReader();
+
+ document.getElementById("Device_output").style.display = "block";
+
+ let dataCompleteMarkerFound = false;
+ const startTime = Date.now();
+ const timeout = 10000; // 10 second timeout
+
+ try {
+ progressBar("40", "Reading device...", "#04AA6D");
+
+ while (keepReading) {
+ const { value, done } = await reader.read();
+ if (done || !keepReading) {
+ break;
+ }
+
+ receivedDataBuffer += value;
+ serialResultsDiv.innerHTML = receivedDataBuffer;
+
+ // Check if we have received PLUGTY which comes after all device data
+ // Or wait for at least FWDATE which comes near the end
+ if (receivedDataBuffer.indexOf("PLUGTY") > 0 || receivedDataBuffer.indexOf("FWDATE=") > 0) {
+ dataCompleteMarkerFound = true;
+ }
+
+ // Only parse and exit if we found the completion marker
+ if (dataCompleteMarkerFound) {
+ // Parse device info from complete output
+ parseDeviceInfo(receivedDataBuffer);
+
+ // Check if we have all required data
+ if (deviceSerialNumber && deviceVersion && deviceHwVersion) {
+ progressBar("60", "Device data read - SN: " + deviceSerialNumber, "#04AA6D");
+ await logCommunication(`Device identification data received: SN=${deviceSerialNumber}, Version=${deviceVersion}, HW=${deviceHwVersion}`, 'handshake');
+
+ // Exit cleanly
+ break;
+ }
+ }
+
+ // Timeout check
+ if (Date.now() - startTime > timeout) {
+ await logCommunication('Device read timeout - proceeding with partial data', 'error');
+ parseDeviceInfo(receivedDataBuffer);
+ break;
+ }
+ }
+
+ progressBar("65", "Device reading completed", "#04AA6D");
+
+ } catch (error) {
+ if (keepReading) {
+ await logCommunication(`Read error: ${error.message}`, 'error');
+ progressBar("0", "Error reading device: " + error.message, "#ff6666");
+ }
+ } finally {
+ // Clean up reader and cancel the stream
+ try {
+ if (reader) {
+ await reader.cancel();
+ reader.releaseLock();
+ }
+ } catch (e) {
+ console.log('Reader cleanup error:', e);
+ }
+ }
+}
+
+function parseDeviceInfo(data) {
+ // Extract SN, FW (firmware/software version), and HW from device output
+ // Device format: SN=12345678;FW=12345678;HW=12345678;STATE=...
+ // Match exact logic from scripts.js and readdevice.js
+
+ // Use exact same approach as scripts.js line 153 and readdevice.js line 649
+ const x = Array.from(new Set(data.split(";"))).toString();
+
+ // Parse SN= (8 characters, exact logic from scripts.js line 185-189)
+ if (x.indexOf("SN=") > 0 && !deviceSerialNumber) {
+ const a = x.indexOf("SN=");
+ const b = a + 3;
+ const c = b + 8;
+ deviceSerialNumber = x.substring(b, c);
+ console.log("Found SN:", deviceSerialNumber);
+ }
+
+ // Parse FW= (8 characters, exact logic from readdevice.js line 672-676)
+ if (x.indexOf("FW=") > 0 && !deviceVersion) {
+ const a = x.indexOf("FW=");
+ const b = a + 3;
+ const c = b + 8;
+ deviceVersion = x.substring(b, c);
+ console.log("Found FW/Version:", deviceVersion);
+ }
+
+ // Parse HW= (8 characters, exact logic from readdevice.js line 665-670)
+ if (x.indexOf("HW=") > 0 && !deviceHwVersion) {
+ const a = x.indexOf("HW=");
+ const b = a + 3;
+ const c = b + 8;
+ deviceHwVersion = x.substring(b, c);
+ console.log("Found HW Version:", deviceHwVersion);
+ }
+}
+
+async function closePortAfterRead() {
+ if (port) {
+ try {
+ await logCommunication('Closing port after reading device data', 'info');
+
+ // Reader is already cancelled and released by readDeviceOutput finally block
+ // Now abort the writer
+ if (writer) {
+ try {
+ await writer.abort();
+ } catch (e) {
+ console.log('Writer abort error:', e);
+ }
+ }
+
+ // Give time for streams to cleanup
+ await new Promise(resolve => setTimeout(resolve, 300));
+
+ // Close the port
+ await port.close();
+ await logCommunication('Port closed successfully', 'info');
+
+ // Reset for next connection
+ reader = null;
+ writer = null;
+ readableStreamClosed = null;
+ writableStreamClosed = null;
+ port = null;
+ } catch (error) {
+ console.error('Error closing port after read:', error);
+ await logCommunication(`Error closing port: ${error.message}`, 'error');
+
+ // Force reset even on error
+ reader = null;
+ writer = null;
+ readableStreamClosed = null;
+ writableStreamClosed = null;
+ port = null;
+ }
+ }
+}
+
+async function closePort() {
+ if (port) {
+ try {
+ keepReading = false;
+
+ // Wait for read operations to complete
+ await new Promise(resolve => setTimeout(resolve, 300));
+
+ // Release reader if still locked
+ if (reader) {
+ try {
+ reader.releaseLock();
+ } catch (e) {
+ console.log('Reader release error:', e);
+ }
+ }
+
+ // Wait a bit more
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ // Now close the port
+ try {
+ await port.close();
+ await logCommunication('Port closed', 'info');
+ } catch (e) {
+ console.log('Port close error (may already be closed):', e);
+ }
+
+ // Reset for next connection
+ reader = null;
+ port = null;
+ } catch (error) {
+ console.error('Error closing port:', error);
+ }
+ }
+}
+
+async function checkSoftwareAvailability() {
+ if (!deviceSerialNumber) {
+ progressBar("0", "Error: Serial number not found", "#ff6666");
+ await logCommunication('Serial number not found in device output', 'error');
+ return;
+ }
+
+ document.getElementById("softwareCheckStatus").style.display = "block";
+ progressBar("70", "Checking for software updates...", "#04AA6D");
+
+ try {
+ // Call software_available API
+ const availableUrl = link + "/v2/software_available/sn=" + deviceSerialNumber +
+ (deviceVersion ? "&version=" + deviceVersion : "") +
+ (deviceHwVersion ? "&hw_version=" + deviceHwVersion : "");
+
+ console.log("Calling API:", availableUrl);
+ await logCommunication(`Checking software availability for SN: ${deviceSerialNumber}`, 'sent');
+
+ const availableResponse = await fetch(availableUrl, {
+ method: "GET",
+ headers: {
+ "Authorization": "Bearer " + document.getElementById("servicetoken").textContent,
+ "Content-Type": "application/json"
+ }
+ });
+
+ const availableData = await availableResponse.json();
+ console.log("Available response:", availableData);
+
+ await logCommunication(`Software availability response: ${JSON.stringify(availableData)}`, 'received');
+
+ if (availableData.software_available === "error" || availableData.error) {
+ // Error checking for updates (e.g., invalid hardware version)
+ document.getElementById("softwareCheckStatus").style.display = "none";
+ progressBar("0", "Error: " + (availableData.error || "Unable to check for updates"), "#ff6666");
+ alert("Error checking for updates: " + (availableData.error || "Unknown error"));
+ } else if (availableData.software_available === "yes") {
+ // Software updates available, fetch options
+ progressBar("80", "Software updates found, loading options...", "#04AA6D");
+ await fetchSoftwareOptions();
+ } else {
+ // No updates available
+ document.getElementById("softwareCheckStatus").style.display = "none";
+ document.getElementById("noUpdatesMessage").style.display = "block";
+ progressBar("100", "No software updates available", "#04AA6D");
+ }
+ } catch (error) {
+ await logCommunication(`Software check error: ${error.message}`, 'error');
+ progressBar("0", "Error checking software: " + error.message, "#ff6666");
+ await closePort();
+ }
+}
+
+async function fetchSoftwareOptions() {
+ try {
+ // Call software_update API to get options
+ const updateUrl = link + "/v2/software_update/sn=" + deviceSerialNumber +
+ (deviceVersion ? "&version=" + deviceVersion : "") +
+ (deviceHwVersion ? "&hw_version=" + deviceHwVersion : "");
+
+ console.log("Fetching options from:", updateUrl);
+ await logCommunication(`Fetching software options for SN: ${deviceSerialNumber}`, 'sent');
+
+ const updateResponse = await fetch(updateUrl, {
+ method: "GET",
+ headers: {
+ "Authorization": "Bearer " + document.getElementById("servicetoken").textContent,
+ "Content-Type": "application/json"
+ }
+ });
+
+ const options = await updateResponse.json();
+ console.log("Software options:", options);
+
+ await logCommunication(`Software options response: ${JSON.stringify(options)}`, 'received');
+
+ if (options.error) {
+ document.getElementById("softwareCheckStatus").style.display = "none";
+ document.getElementById("noUpdatesMessage").style.display = "block";
+ progressBar("100", "No software updates available", "#04AA6D");
+ await closePort();
+ return;
+ }
+
+ // Display options in table
+ displaySoftwareOptions(options);
+ document.getElementById("softwareCheckStatus").style.display = "none";
+ document.getElementById("softwareOptions").style.display = "block";
+ progressBar("100", "Software options loaded", "#04AA6D");
+
+ } catch (error) {
+ await logCommunication(`Software options error: ${error.message}`, 'error');
+ progressBar("0", "Error loading options: " + error.message, "#ff6666");
+ await closePort();
+ }
+}
+
+function displaySoftwareOptions(options) {
+ const grid = document.getElementById("softwareOptionsGrid");
+ grid.innerHTML = "";
+
+ options.forEach((option, index) => {
+ const price = parseFloat(option.price);
+ const isFree = price === 0;
+ const isCurrent = option.is_current === true || option.is_current === 1;
+
+ // Create card
+ const card = document.createElement("div");
+ card.style.cssText = `
+ background: ${isCurrent ? '#f5f5f5' : 'white'};
+ border: 2px solid ${isCurrent ? '#bbb' : (isFree ? '#e0e0e0' : '#e0e0e0')};
+ border-radius: 4px;
+ padding: 15px;
+ transition: 0.3s;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ overflow: hidden;
+ transform: translateY(0px);
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px;
+ opacity: ${isCurrent ? '0.6' : '1'};
+ pointer-events: ${isCurrent ? 'none' : 'auto'};
+ `;
+
+ if (!isCurrent) {
+ card.onmouseenter = () => {
+ card.style.transform = 'translateY(-5px)';
+ card.style.boxShadow = '0 8px 16px rgba(0,0,0,0.15)';
+ };
+ card.onmouseleave = () => {
+ card.style.transform = 'translateY(0)';
+ card.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
+ };
+ }
+
+ // Badge for current/free/paid
+ const badge = document.createElement("div");
+ badge.style.cssText = `
+ position: absolute;
+ top: 15px;
+ right: 15px;
+ background: ${isCurrent ? '#6c757d' : '#04AA6D'};
+ color: white;
+ padding: 5px 12px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: bold;
+ display:none;
+ `;
+
+ if (isCurrent) {
+ badge.textContent = "CURRENT VERSION";
+ } else if (isFree) {
+ badge.textContent = "Included";
+ }
+
+ if (isCurrent || isFree) {
+ card.appendChild(badge);
+ }
+
+ // Name
+ const name = document.createElement("h4");
+ name.style.cssText = `
+ margin: 0 0 10px 0;
+ color: #333;
+ font-size: 20px;
+ font-weight: 600;
+ `;
+ name.textContent = option.name || "Software Update";
+ card.appendChild(name);
+
+ // Version
+ const version = document.createElement("div");
+ version.style.cssText = `
+ color: #666;
+ font-size: 14px;
+ margin-bottom: 15px;
+ `;
+ version.innerHTML = ` Version: ${option.version || "N/A"}`;
+ card.appendChild(version);
+
+ // Description
+ const desc = document.createElement("p");
+ desc.style.cssText = `
+ color: #555;
+ font-size: 14px;
+ line-height: 1.6;
+ margin: 0 0 20px 0;
+ flex-grow: 1;
+ `;
+ desc.textContent = option.description || "No description available";
+ card.appendChild(desc);
+
+ // Price section
+ const priceSection = document.createElement("div");
+ priceSection.style.cssText = `
+ border-top: 1px solid #e0e0e0;
+ padding-top: 15px;
+ margin-top: auto;
+ `;
+
+ const priceText = document.createElement("div");
+ priceText.style.cssText = `
+ font-size: 24px;
+ font-weight: bold;
+ color: ${isCurrent ? '#6c757d' : (isFree ? '#04AA6D' : '#333')};
+ margin-bottom: 15px;
+ `;
+
+ if (isCurrent) {
+ priceText.textContent = "INSTALLED";
+ } else {
+ priceText.textContent = isFree ? "Included" : `${option.currency || "€"} ${price.toFixed(2)}`;
+ }
+
+ priceSection.appendChild(priceText);
+
+ // Action button
+ const actionBtn = document.createElement("button");
+ actionBtn.className = "btn";
+ actionBtn.style.cssText = `
+ width: 100%;
+ background: ${isCurrent ? '#6c757d' : '#04AA6D'};
+ color: white;
+ border: none;
+ padding: 12px;
+ border-radius: 6px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: ${isCurrent ? 'not-allowed' : 'pointer'};
+ transition: background 0.3s ease;
+ opacity: ${isCurrent ? '0.5' : '1'};
+ `;
+
+ if (isCurrent) {
+ actionBtn.innerHTML = ' Currently Installed';
+ actionBtn.disabled = true;
+ } else if (isFree) {
+ actionBtn.innerHTML = '';
+ actionBtn.onclick = () => selectUpgrade(option);
+ actionBtn.onmouseenter = () => actionBtn.style.background = '#038f5a';
+ actionBtn.onmouseleave = () => actionBtn.style.background = '#04AA6D';
+ } else {
+ actionBtn.innerHTML = '';
+ actionBtn.onclick = () => selectUpgrade(option);
+ actionBtn.onmouseenter = () => actionBtn.style.background = '#038f5a';
+ actionBtn.onmouseleave = () => actionBtn.style.background = '#04AA6D';
+ }
+
+ priceSection.appendChild(actionBtn);
+
+ card.appendChild(priceSection);
+ grid.appendChild(card);
+ });
+}
+
+async function selectUpgrade(option) {
+ const price = parseFloat(option.price || 0);
+ const isFree = price === 0;
+
+ // If paid upgrade, show payment modal first
+ if (!isFree) {
+ showPaymentModal(option);
+ return;
+ }
+
+ // Free upgrade - show confirmation modal first
+ showFreeInstallModal(option);
+}
+
+function showFreeInstallModal(option) {
+ // Create modal overlay
+ const modal = document.createElement("div");
+ modal.id = "freeInstallModal";
+ modal.style.cssText = `
+ display: flex;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,0.5);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+ `;
+
+ // Create modal content
+ const modalContent = document.createElement("div");
+ modalContent.style.cssText = `
+ background: white;
+ border-radius: 8px;
+ max-width: 500px;
+ width: 90%;
+ max-height: 90vh;
+ overflow-y: auto;
+ margin: 20px;
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
+ `;
+
+ modalContent.innerHTML = `
+
Version: ${option.version || "N/A"}
+${option.description || ""}
+Version: ${option.version || "N/A"}
+${option.description || ""}
+'.$firmwaretool_p.'