Merge branch 'development'

This commit is contained in:
“VeLiTi”
2025-12-21 14:19:58 +01:00
34 changed files with 2915 additions and 169 deletions

View File

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

825
PAYMENT_INTEGRATION_PLAN.md Normal file
View File

@@ -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
<?php
defined($security_key) or exit;
// POST endpoint for payment creation
// Input (JSON): serial_number, version_id, user_data (name, email, address)
// Output (JSON): {checkout_url: "https://mollie.com/...", payment_id: "tr_xxx"}
//Connect to DB
$pdo = dbConnect($dbname);
//CONTENT FROM API (POST)
$post_content = json_decode($input, true);
// SECURITY: Never trust price/currency from frontend!
// Steps:
1. Validate inputs (serial_number, version_id, user_data)
2. SERVER-SIDE: Calculate actual price using software_update logic:
a. Get equipment data from serial_number
b. Get version data from version_id
c. Check upgrade path pricing (same logic as software_update.php lines 237-253)
d. Check license validity (same logic as software_update.php lines 274-311)
e. Calculate FINAL price server-side
3. Verify price > 0 (free upgrades shouldn't reach payment API)
4. Call Mollie API FIRST to get payment_id:
$mollie->payments->create([
'amount' => ['currency' => 'EUR', 'value' => $final_price],
'description' => 'Software upgrade to version X',
'redirectUrl' => 'https://site.com/softwaretool.php?payment_return=1&payment_id={payment_id}',
'webhookUrl' => 'https://site.com/webhook_mollie.php', // NEW webhook for software upgrades
'metadata' => ['order_id' => $mollie_payment_id] // for compatibility
])
5. Store transaction in DB with Mollie payment_id:
INSERT INTO transactions (txn_id, payment_amount, payment_status, payer_email, first_name, last_name, address_*, account_id, ...)
VALUES ($mollie_payment_id, $final_price, 0, ...) -- 0 = pending
6. Store transaction item:
INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, ...)
VALUES ($mollie_payment_id, $version_id, $final_price, 1, '{"serial_number":"...", "equipment_id":...}', ...)
7. Return JSON: {checkout_url: $mollie_checkout_url, payment_id: $mollie_payment_id}
```
**1.3 Create `/api/v2/get/payment.php`**
```php
<?php
defined($security_key) or exit;
// GET endpoint for payment status retrieval
// Input (URL): ?payment_id=tr_xxx
// Output (JSON): {payment_id, serial_number, version_id, payment_status, price, currency, user_data}
//Connect to DB
$pdo = dbConnect($dbname);
//NEW ARRAY
$criterias = [];
//Check for $_GET variables
if(isset($get_content) && $get_content!=''){
$requests = explode("&", $get_content);
foreach ($requests as $y){
$v = explode("=", $y);
$criterias[$v[0]] = $v[1];
}
}
// Steps:
1. Validate payment_id from URL
2. Fetch transaction: SELECT * FROM transactions WHERE txn_id = ?
3. Fetch transaction item: SELECT * FROM transactions_items WHERE txn_id = ?
4. Parse item_options JSON to get serial_number, equipment_id
5. Return JSON with payment details:
{
"payment_id": txn_id,
"payment_status": payment_status, // 0=pending, 1=paid, 2=failed, 3=canceled
"payment_amount": payment_amount,
"serial_number": from item_options JSON,
"equipment_id": from item_options JSON,
"version_id": item_id,
"payer_email": payer_email,
"customer_name": first_name + " " + last_name
}
6. If not found, return error
```
**1.4 Create NEW `webhook_mollie.php`**
```php
<?php
// NEW FILE - Webhook for software upgrade payments
// Based on structure from existing webhook.php from commerce product
// Uses existing transaction API + invoice API + email system
require_once 'assets/config.php';
require_once 'assets/functions.php';
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//LOGIN TO API (same as commerce webhook.php)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$data = json_encode(array("clientID" => clientID, "clientsecret" => clientsecret), JSON_UNESCAPED_UNICODE);
$responses = ioAPIv2('/v2/authorization', $data,'');
//Decode Payload
if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';}
$clientsecret = $responses['token'];
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// BASEURL is required for invoice template
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$base_url = 'https://'.$_SERVER['SERVER_NAME'].'/';
define('base_url', $base_url);
try {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Initialize the Mollie API library
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
require "initialize.php"; // Mollie initialization (from commerce webhook)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Retrieve the payment's current state
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payment = $mollie->payments->get($_POST["id"]);
$orderId = $payment->metadata->order_id;
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Update the transaction using existing API
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($payment->isPaid() && !$payment->hasRefunds() && !$payment->hasChargebacks()) {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// PAID - Update transaction status via API
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 1), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
$transaction = json_decode($transaction,true);
if ($transaction !== null && !empty($transaction)) {
if(count($transaction) > 0) {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// CREATE LICENSE for software upgrade
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$pdo = dbConnect($dbname);
// Fetch transaction items to find software upgrade
$sql = 'SELECT * FROM transactions_items WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$orderId]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($items as $item) {
if (!empty($item['item_options'])) {
$options = json_decode($item['item_options'], true);
// Check if this is a software upgrade (has serial_number and equipment_id)
if (isset($options['serial_number']) && isset($options['equipment_id'])) {
// Check if license already exists for this transaction
$sql = 'SELECT rowID FROM products_software_licenses WHERE transaction_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$orderId]);
$existing_license = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$existing_license) {
// Generate unique license key
$license_key = generateUniqueLicenseKey();
// Create license
$sql = 'INSERT INTO products_software_licenses
(license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$license_key,
$options['equipment_id'],
'upgrade',
1, // active
date('Y-m-d H:i:s'),
'2099-12-31 23:59:59', // effectively permanent
$orderId,
date('Y-m-d H:i:s'),
'webhook' // created by webhook
]);
// Update equipment.sw_version_license
$sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$license_key, $options['equipment_id']]);
error_log("Webhook: License created for equipment_id: " . $options['equipment_id'] . ", license_key: " . $license_key);
}
}
}
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Generate INVOICE RECORD via API
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payload = json_encode(array("txn_id" => $transaction['transaction_id']), JSON_UNESCAPED_UNICODE);
$invoice = ioAPIv2('/v2/invoice/',$payload,$clientsecret);
$invoice = json_decode($invoice,true);
if ($invoice !== null && !empty($invoice)) {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Generate INVOICE PDF and send email
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$invoice_cust = ioAPIv2('/v2/invoice/list=invoice&id='.$invoice['invoice_id'],'',$clientsecret);
$invoice_cust = json_decode($invoice_cust,true);
// Determine invoice language
if (!empty($invoice_cust['customer']['language'])) {
$invoice_language = strtoupper($invoice_cust['customer']['language']);
} elseif (!empty($invoice_cust['customer']['country']) && isset($available_languages[strtoupper($invoice_cust['customer']['country'])])) {
$invoice_language = $available_languages[strtoupper($invoice_cust['customer']['country'])];
} else {
$invoice_language = 'US'; // Default fallback
}
// Generate invoice HTML (using custom template for software upgrades)
list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
//CREATE PDF using DomPDF
$dompdf->loadHtml($data);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$subject = ($invoice_software_subject ?? 'Software Upgrade - Invoice: ').$order_id;
$attachment = $dompdf->output();
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Send email via PHPMailer
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
send_mail_by_PHPMailer($customer_email, $subject, $data, $attachment, $subject);
if(invoice_bookkeeping){
send_mail_by_PHPMailer(email_bookkeeping, $subject, $data, $attachment, $subject);
}
}
}
}
} elseif ($payment->isOpen()) {
// OPEN/PENDING (101)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 101), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
} elseif ($payment->isPending()) {
// PENDING (101)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 101), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
} elseif ($payment->isFailed()) {
// FAILED (102)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 102), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
} elseif ($payment->isExpired()) {
// EXPIRED (103)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 103), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
} elseif ($payment->isCanceled()) {
// CANCELED (999)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 999), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
} elseif ($payment->hasRefunds()) {
// REFUNDED (1 + refund flag)
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 1), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
// TODO: Disable license on refund
}
} catch (\Mollie\Api\Exceptions\ApiException $e) {
error_log("Webhook API call failed: " . htmlspecialchars($e->getMessage()));
http_response_code(500);
echo "API call failed: " . htmlspecialchars($e->getMessage());
} catch (Exception $e) {
error_log("Webhook error: " . htmlspecialchars($e->getMessage()));
http_response_code(500);
}
```
**Key Features (matching commerce webhook.php):**
- ✅ Uses `/v2/transactions/` API for status updates
- ✅ Uses `/v2/invoice/` API for invoice generation
- ✅ Generates PDF invoice with DomPDF
- ✅ Sends email via PHPMailer
- ✅ Creates license for software upgrade
- ✅ Uses same payment status codes (0, 1, 101, 102, 103, 999)
- ✅ Handles refunds (TODO: disable license)
- ✅ Multi-language invoice support
- ✅ Sends to bookkeeping if configured
### Phase 2: Frontend Integration
**2.1 Modify `processPayment()` in softwaretool.js (lines 574-608)**
```javascript
async function processPayment(paymentData, option, modal) {
try {
progressBar("10", "Processing payment...", "#04AA6D");
// SECURITY: Only send serial_number and version_id
// Server will calculate the price to prevent tampering
const response = await fetch(link + "/v2/post/payment", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + document.getElementById("servicetoken").textContent
},
body: JSON.stringify({
serial_number: deviceSerialNumber,
version_id: option.version_id,
user_data: paymentData // name, email, address only
// REMOVED: price, currency - server calculates these
})
});
const result = await response.json();
if (result.checkout_url) {
await logCommunication(`Redirecting to payment provider`, 'sent');
// Redirect to Mollie checkout
window.location.href = result.checkout_url;
} else {
throw new Error(result.error || "Failed to create payment");
}
} catch (error) {
await logCommunication(`Payment error: ${error.message}`, 'error');
progressBar("0", "Payment failed: " + error.message, "#ff6666");
alert("Payment failed: " + error.message);
}
}
```
**2.2 Remove equipment_id tracking - NOT NEEDED**
```javascript
// SECURITY: We don't need to track equipment_id in frontend
// The server will look it up from serial_number in the payment/create API
// This prevents tampering with equipment_id
```
**2.3 Add Serial Number Verification in `downloadAndInstallSoftware()` (lines 610-699)**
```javascript
async function downloadAndInstallSoftware(option) {
// Check if we're returning from payment
const urlParams = new URLSearchParams(window.location.search);
const paymentId = urlParams.get('payment_id');
if (paymentId) {
// Verify serial number matches payment using GET /v2/get/payment
const response = await fetch(link + `/v2/get/payment?payment_id=${paymentId}`, {
method: "GET",
headers: {
"Authorization": "Bearer " + document.getElementById("servicetoken").textContent
}
});
const paymentData = await response.json();
if (paymentData.serial_number !== deviceSerialNumber) {
const confirmed = confirm(
`WARNING: Different device detected!\n\n` +
`License was created for device: ${paymentData.serial_number}\n` +
`Currently connected device: ${deviceSerialNumber}\n\n` +
`The license is already applied to the original device. ` +
`Do you want to continue with this device anyway?`
);
if (!confirmed) {
progressBar("0", "Upload canceled by user", "#ff6666");
return;
}
}
}
// Continue with existing download logic...
selectedSoftwareUrl = option.source;
// ... rest of function unchanged
}
```
**Note**: Serial number verification uses existing GET /v2/get/payment endpoint (no separate verify endpoint needed)
### Phase 3: Return URL Handling
**3.1 Modify `softwaretool.php` to detect return from payment**
```php
// Add near top of softwaretool.php (after includes, before $view)
<?php
$payment_return = isset($_GET['payment_id']) ? $_GET['payment_id'] : null;
if ($payment_return) {
// Optionally fetch payment status via GET /v2/get/payment
// and show appropriate message banner at top of page
// "Payment successful! Please reconnect your device to continue."
// User will then click "Connect Device" button
// After connection, checkSoftwareAvailability() will run
// License will be found via existing logic, price will be 0.00
}
?>
```
**3.2 Optional: Auto-trigger device connection after payment return**
```javascript
// In softwaretool.js, check URL on page load
window.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('payment_id')) {
// Show message: "Payment successful! Please reconnect your device."
// Optionally auto-show device connection UI
}
});
```
### Phase 4: Testing Strategy
**4.1 DEBUG Mode Testing (Complete Simulation)**
```php
// In /api/v2/post/payment.php, check if DEBUG mode
if (defined('debug') && debug) {
// FULL SIMULATION: No Mollie API connection, no device connection
$fake_payment_id = 'DEBUG_' . uniqid();
// 1. Store transaction with status 0 (pending)
$sql = 'INSERT INTO transactions
(txn_id, payment_amount, payment_status, payer_email, first_name, last_name,
address_street, address_city, address_state, address_zip, address_country, account_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$fake_payment_id,
$final_price,
0, // 0 = pending
$post_content['user_data']['email'],
$post_content['user_data']['first_name'] ?? '',
$post_content['user_data']['last_name'] ?? '',
$post_content['user_data']['address_street'] ?? '',
$post_content['user_data']['address_city'] ?? '',
$post_content['user_data']['address_state'] ?? '',
$post_content['user_data']['address_zip'] ?? '',
$post_content['user_data']['address_country'] ?? '',
$post_content['serial_number'] // store serial number in account_id
]);
// 2. Store transaction item
$item_options = json_encode([
'serial_number' => $post_content['serial_number'],
'equipment_id' => $equipment_id,
'hw_version' => $hw_version
]);
$sql = 'INSERT INTO transactions_items
(txn_id, item_id, item_price, item_quantity, item_options)
VALUES (?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$fake_payment_id,
$post_content['version_id'],
$final_price,
1,
$item_options
]);
// 3. Immediately simulate webhook success (update status to paid + create license)
$sql = 'UPDATE transactions SET payment_status = 1 WHERE txn_id = ?'; // 1 = paid
$stmt = $pdo->prepare($sql);
$stmt->execute([$fake_payment_id]);
// 4. Create license
$license_key = generateUniqueLicenseKey();
$sql = 'INSERT INTO products_software_licenses
(license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$license_key,
$equipment_id,
'upgrade',
1,
date('Y-m-d H:i:s'),
'2099-12-31 23:59:59',
$fake_payment_id,
date('Y-m-d H:i:s'),
$username
]);
// 5. Update equipment.sw_version_license
$sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$license_key, $equipment_id]);
// 6. Return fake checkout URL that redirects immediately
$messages = [
'checkout_url' => 'https://'.$_SERVER['SERVER_NAME'].'/softwaretool.php?payment_return=1&payment_id=' . $fake_payment_id,
'payment_id' => $fake_payment_id
];
echo json_encode($messages);
exit;
}
```
**Note**: In DEBUG mode, the entire payment + license creation flow is simulated without:
- Calling Mollie API
- Requiring physical device connection (works with DEBUG mode mock device data in softwaretool.js)
**4.2 Mollie Sandbox Testing**
1. Use Mollie test API key
2. Test successful payment flow
3. Test failed payment flow
4. Test canceled payment flow
5. Test webhook delivery
6. Test license creation
**4.3 Serial Number Mismatch Testing**
1. Complete payment with device A (SN: 22110095)
2. Disconnect device A
3. Connect device B (different SN)
4. Verify warning appears
5. Verify license was created for device A
---
## Critical Files to Modify
### New Files
- `/api/v2/post/payment.php` - Payment creation (POST)
- `/api/v2/get/payment.php` - Payment status retrieval (GET)
- `/webhook_mollie.php` - Mollie webhook handler for software upgrades (based on existing webhook.php structure)
- `generateSoftwareInvoice()` function in `/assets/functions.php` - Invoice template for software upgrades
### Modified Files
- `/assets/softwaretool.js`:
- `processPayment()` (lines 574-608) - Call POST /v2/post/payment instead of simulation
- `downloadAndInstallSoftware()` (lines 610-699) - Add serial number verification using GET /v2/get/payment
- Add payment return detection on page load (optional)
- `/softwaretool.php`:
- Add payment return URL detection (check for ?payment_id=X)
- Optionally show success message banner after payment
- `/api/v2/get/software_update.php`:
- **No changes needed** (existing license logic at lines 274-311 works perfectly!)
### Database & Helper Functions
- No new tables needed (using existing `transactions` and `transactions_items`)
- Add helper function `generateUniqueLicenseKey()` in `assets/functions.php`
- Payment status codes already defined in existing webhook.php (0, 1, 101, 102, 103, 999)
---
## Security Architecture Summary
### ✅ **SECURE APPROACH: Server-Side Price Validation**
**Frontend sends:**
- `serial_number` (from connected device)
- `version_id` (which version they want)
- `user_data` (name, email, address)
**Backend does:**
1. Look up equipment from `serial_number`
2. Look up version from `version_id`
3. **Calculate actual price using same logic as software_update.php**:
- Check upgrade path pricing (lines 244-260)
- Check if license exists and reduces price (lines 274-311)
- Get final server-calculated price
4. Verify price > 0 (reject free upgrades)
5. Create Mollie payment with **SERVER-CALCULATED price**
6. Store pending payment with correct price
### ❌ **INSECURE APPROACH: Never Do This**
```javascript
// WRONG - User can modify price in browser console!
body: JSON.stringify({
serial_number: deviceSerialNumber,
version_id: option.version_id,
price: 0.01, // <-- Tampered from 49.99!
currency: "EUR"
})
```
**Why this is dangerous:**
- User can open browser console
- Change `option.price = 0.01` before payment
- Backend trusts this value = user pays 1 cent for €49.99 upgrade
### ✅ **CORRECT APPROACH**
```javascript
// SECURE - Only send identifiers, server calculates price
body: JSON.stringify({
serial_number: deviceSerialNumber, // Who is buying
version_id: option.version_id, // What they want
user_data: paymentData // Customer info
// NO PRICE - server calculates it!
})
```
---
## Configuration & Requirements (USER CONFIRMED)
1.**Mollie API Credentials**: User has Mollie info - will be added as constants in `config.php`
- `MOLLIE_API_KEY_TEST` (for sandbox)
- `MOLLIE_API_KEY_LIVE` (for production)
2.**License Duration**: `expires_at = '2099-12-31 23:59:59'` (effectively permanent until 2099)
3.**Multiple Devices**: One license per device (license linked to specific equipment_id)
4.**DEBUG Mode**: Full payment process simulation without Mollie connection AND without device connection
5.**Transaction Logging**: Use existing ecommerce transaction APIs:
- `transactions` table - main transaction record
- `transaction_items` table - line items (software upgrade details)
---
## Next Steps After Plan Approval
1. ✅ Add Mollie constants to `config.php`:
```php
define('MOLLIE_API_KEY_TEST', 'test_xxxxx'); // User will provide
define('MOLLIE_API_KEY_LIVE', 'live_xxxxx'); // User will provide
```
2. ✅ Mollie PHP SDK already installed (used by existing webhook.php)
3. ✅ No database changes needed (using existing `transactions` and `transactions_items` tables)
4. Create helper functions in `assets/functions.php`:
- `generateUniqueLicenseKey()` - Generate unique license keys
- `generateSoftwareInvoice()` - Generate HTML invoice for software upgrades (based on existing generateInvoice())
5. ✅ Payment status codes already defined in existing webhook.php
6. Implement NEW backend files:
- `/api/v2/post/payment.php` (with DEBUG mode support)
- `/api/v2/get/payment.php`
- `/webhook_mollie.php` (based on existing webhook.php structure)
8. Modify frontend JavaScript in `/assets/softwaretool.js`:
- Update `processPayment()` to call POST /v2/post/payment
- Add serial number verification in `downloadAndInstallSoftware()`
9. Modify `/softwaretool.php` to detect payment return
10. Test in DEBUG mode (full simulation without Mollie or device)
11. Test with Mollie sandbox
12. Deploy to production

View File

@@ -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 ++++++++++++++++++++++++++

View File

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

View File

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

View File

@@ -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,9 +270,19 @@ 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;
}
}
@@ -174,14 +292,30 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
// 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"];

View File

@@ -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'],

View File

@@ -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 {
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)
// 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) {
//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;
$final_price = '0.00';
$final_currency = '';
$is_current = false;
$decision_reason = '';
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
$show_version = true;
} elseif ($version['from_version'] == $current_sw_version) {
//Upgrade path exists from current 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 {
//Check if any upgrade paths exist for this version
$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 = ? 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_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];
}
}
}
}

View File

@@ -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 ++++++++++++++++++++++++++

View File

@@ -62,6 +62,15 @@ $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){

View File

@@ -41,6 +41,9 @@ 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!=''){
foreach ($post_content as $key => $var){

View File

@@ -5192,3 +5192,100 @@ function updateSoftwareVersionStatus($pdo, $serialnumber = null) {
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;
}

1099
assets/softwaretool.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -113,9 +113,7 @@ $view .= '</div>';
$view .= '<div class="tabs">
<a href="#" class="active">'.$tab1.'</a>
<a href="#">'.$tab3.'</a>
</div>
';
</div>';
//GET PARTNERID
$view_partners = '';
@@ -162,6 +160,9 @@ $view .= '<div class="content-block tab-content active">
</div>
</div>';
$view .= '<div class="tabs">
<a href="#">'.$tab3.'</a>
</div>';
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">
<label for="">'.$general_created.'</label>

View File

@@ -125,24 +125,32 @@ $view .= '<div class="content-block">
<i class="fa-solid fa-bars fa-sm"></i>'.($view_dealer_details_1 ?? 'Descriptions').'
<div class="tabs">
<a href="#" class="active">'.($dealers_short_description ?? 'Short').'</a>
<a href="#">'.($dealers_long_description ?? 'Long').'</a>
<a href="#">'.($dealers_usp1 ?? 'USP1').'</a>
<a href="#">'.($dealers_usp2 ?? 'USP2').'</a>
<a href="#">'.($dealers_usp3 ?? 'USP3').'</a>
</div>
</div>
<div class="table order-table tab-content active">
'.(${$responses['short_description']} ?? $responses['short_description']).'
</div>
<div class="tabs">
<a href="#">'.($dealers_long_description ?? 'Long').'</a>
</div>
<div class="table order-table tab-content">
'.(${$responses['long_description']} ?? $responses['long_description']).'
</div>
<div class="tabs">
<a href="#">'.($dealers_usp1 ?? 'USP1').'</a>
</div>
<div class="table order-table tab-content">
'.(${$responses['usp1']} ?? $responses['usp1']).'
</div>
<div class="tabs">
<a href="#">'.($dealers_usp2 ?? 'USP2').'</a>
</div>
<div class="table order-table tab-content">
'.(${$responses['usp2']} ?? $responses['usp2']).'
</div>
<div class="tabs">
<a href="#">'.($dealers_usp3 ?? 'USP3').'</a>
</div>
<div class="table order-table tab-content">
'.(${$responses['usp3']} ?? $responses['usp3']).'
</div>

View File

@@ -49,9 +49,7 @@ $view .= '</div>';
$view .= '<div class="tabs">
<a href="#" class="active">'.$tab1.'</a>
<a href="#">'.$tab3.'</a>
</div>
';
</div>';
$view .= '<div class="content-block tab-content active">
<div class="form responsive-width-100">
@@ -96,8 +94,11 @@ $view .= '<div class="content-block tab-content active">
}
$view .= '
</div>
</div>';
</div';
$view .= '<div class="tabs">
<a href="#">'.$tab3.'</a>
</div>';
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">
<label for="productcode">'.$general_created.'</label>

View File

@@ -26,8 +26,8 @@ $view = '
<h2>'.$firmwaretool_h2 .'</h2>
<p>'.$firmwaretool_p.'</p>
</div>
</div>
</div>';
</div>';
if (isset($_GET['equipmentID'])){$returnpage = 'equipment&equipmentID='.$_GET['equipmentID']; } else {$returnpage = 'dashboard';}
@@ -35,12 +35,15 @@ if (isset($_GET['equipmentID'])){$returnpage = 'equipment&equipmentID='.$_GET['e
//SHOW BACK BUTTON ONLY FOR PORTAL USERS
if (isAllowed('dashboard',$_SESSION['profile'],$_SESSION['permission'],'R') != 0){
$view .= '
<div class="content-header responsive-flex-column pad-top-5">
<a href="index.php?page='.$returnpage.'" class="btn">←</a>
<div class="title-actions">
<a href="index.php?page='.$returnpage.'" class="btn alt mar-right-2">←</a>
</div>
';
}
$view .= '
</div>';
$view .= '<div class="content-block">
<div class="block-header">
<i class="fa-solid fa-bars fa-sm"></i>

View File

@@ -82,7 +82,13 @@ if (isset($_GET['page']) && $_GET['page'] == 'logout') {
//=====================================
$allowed_views = explode(',',$_SESSION['profile']);
$ignoreViews = ['profile','assets','sales'];
$allowed_views = findExistingView($allowed_views, 'dashboard', $ignoreViews);
// If dashboard is in the profile, prioritize it
if (in_array('dashboard', $allowed_views) && file_exists('dashboard.php')) {
$allowed_views = 'dashboard';
} else {
$allowed_views = findExistingView($allowed_views, 'dashboard', $ignoreViews);
}
//=====================================
//FORWARD THE USER TO THE CORRECT PAGE

View File

@@ -45,7 +45,6 @@ if (isset($_GET['success_msg'])) {
<?php endif; ?>
<div class="tabs">
<a href="#" class="active">US</a>
<a href="#" class="active">NL</a>
</div>
<div class="content-block">
<div class="form responsive-width-100">
@@ -53,7 +52,10 @@ if (isset($_GET['success_msg'])) {
<label for="language_US"></label>
<textarea name="language_US" id="language_US" style="min-height: 100vh;"><?=$contents?></textarea>
</div>
<div class="tab-content active">
<div class="tabs">
<a href="#" class="">NL</a>
</div>
<div class="tab-content">
<label for="language_NL"></label>
<textarea name="language_NL" id="language_NL" style="min-height: 100vh;"><?=$contents2?></textarea>
</div>

View File

@@ -147,7 +147,6 @@ if (file_exists($filelocation_webserver)){
<div class="tabs">
<a href="#" class="active">Application</a>
<a href="#" class="">Webserver</a>
</div>
<div class="content-block">
@@ -156,6 +155,9 @@ if (file_exists($filelocation_webserver)){
<label for="Logfile">Application Log</label>
<textarea name="logfile" id="logfile" style="min-height: 70vh; font-family: 'Courier New', monospace; font-size: 12px; background: #1e1e1e; color: #f8f8f2; border: 1px solid #333; padding: 15px;"><?=$contents?></textarea>
</div>
<div class="tabs">
<a href="#" class="">Webserver</a>
</div>
<div class="tab-content">
<label for="WebserverLog">Webserver Log</label>
<textarea name="" id="webserver-log" style="min-height: 70vh; font-family: 'Courier New', monospace; font-size: 12px; background: #1e1e1e; color: #f8f8f2; border: 1px solid #333; padding: 15px;"><?=$contents_webserver?></textarea>

View File

@@ -70,8 +70,6 @@ $view .='
$view .= '<div class="tabs">
<a href="#" class="active">'.$general_actions .'</a>
<a href="#" class="">Learning</a>
<a href="#" class="">Translations</a>
</div>
';
@@ -96,6 +94,11 @@ $view .= '<div class="content-block tab-content active">
</div>
</div>';
}
$view .= '<div class="tabs">
<a href="#" class="">Learning</a>
</div>
';
if ($update_allowed === 1){
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">
@@ -109,6 +112,11 @@ if ($update_allowed === 1){
</div>';
}
$view .= '<div class="tabs">
<a href="#" class="">Translations</a>
</div>
';
if ($update_allowed === 1){
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">

View File

@@ -120,10 +120,7 @@ $view .= '</div>';
$view .= '<div class="tabs">
<a href="#" class="active">'.$tab1 .'</a>
<a href="#">'.$tab2.'</a>
<a href="#">'.$tab3.'</a>
</div>
';
</div>';
//Define Service and partner enabled
$view .= '<div class="content-block tab-content active">
@@ -163,6 +160,9 @@ $salesid_dropdown = listPartner('salesid',$_SESSION['permission'],$partner_data-
$soldto_dropdown = listPartner('soldto',$_SESSION['permission'],$partner_data->soldto,'');
//DISPLAY
$view .= '<div class="tabs">
<a href="#">'.$tab2.'</a>
</div>';
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">
';
@@ -178,6 +178,9 @@ if ($_SESSION['permission'] == 3 || $_SESSION['permission'] == 4){
</div>
</div>';
$view .= '<div class="tabs">
<a href="#">'.$tab3.'</a>
</div>';
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">
<label for="">'.$general_created.'</label>

View File

@@ -141,9 +141,7 @@ $view .= '</div>';
$view .= '<div class="tabs">
<a href="#" class="active">'.$tab1.'</a>
<a href="#">'.$tab3.'</a>
</div>
';
</div>';
$view .= '<div class="content-block tab-content active">
<div class="form responsive-width-100">
@@ -171,6 +169,9 @@ $view .= '
</div>
</div>';
$view .= '<div class="tabs">
<a href="#">'.$tab3.'</a>
</div>';
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">
<label for="productcode">'.$general_created.'</label>

View File

@@ -188,7 +188,6 @@ $view .= '</div>';
$view .= '<div class="tabs">
<a href="#" class="active">'.$tab1.'</a>
<a href="#">'.$tab3.'</a>
</div>
';
@@ -233,6 +232,11 @@ $view .= '
</div>
</div>';
$view .= '<div class="tabs">
<a href="#">'.$tab3.'</a>
</div>
';
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">
<label for="productcode">'.$general_created.'</label>

View File

@@ -37,12 +37,19 @@ $path = [
'updatedby' => $_SESSION['username']
];
// Determine filter version id from URL (for hw_version filtering)
$filter_version_id = $_GET['from_version_id'] ?? $_GET['to_version_id'] ?? $_GET['id'] ?? '';
// Check if coming from version page (id parameter) or editing existing path
$from_version_page = false;
$to_version_fixed = false;
if (isset($_GET['id']) && !isset($_GET['path_id'])) {
// Coming from version page - this is the TO version
$from_version_page = true;
$to_version_fixed = $_GET['id'];
$path['to_version_id'] = $to_version_fixed;
}
// If editing, fetch existing data
if (isset($_GET['id']) && $_GET['id'] != '') {
$api_url = '/v2/products_software_upgrade_paths/rowID=' . $_GET['id'];
// If editing an existing path, load it
if (isset($_GET['path_id']) && $_GET['path_id'] != '') {
$api_url = '/v2/products_software_upgrade_paths/rowID=' . $_GET['path_id'];
$response = ioServer($api_url, '');
if (!empty($response)) {
@@ -53,6 +60,9 @@ if (isset($_GET['id']) && $_GET['id'] != '') {
}
}
// Determine filter version id from URL (for hw_version filtering)
$filter_version_id = $_GET['from_version_id'] ?? $_GET['to_version_id'] ?? $_GET['id'] ?? '';
// Fetch software versions for selects
$api_url = '/v2/products_software_versions/list';
$versions_response = ioServer($api_url, '');
@@ -163,10 +173,10 @@ $view ='
<a href="' . $url . '" class="btn alt mar-right-2">' . $button_cancel . '</a>
';
if ($delete_allowed === 1 && isset($_GET['id'])){
if ($delete_allowed === 1 && isset($_GET['path_id']) && $_GET['path_id'] != ''){
$view .= '<input type="submit" name="delete" value="X" class="btn red mar-right-2" onclick="return confirm(\'Are you sure you want to delete this upgrade path?\')">';
}
if (($update_allowed === 1 && isset($_GET['id'])) || ($create_allowed === 1 && !isset($_GET['id']))){
if (($update_allowed === 1 && isset($_GET['path_id'])) || ($create_allowed === 1 && !isset($_GET['path_id']))){
$view .= '<input type="submit" name="submit" value="💾+" class="btn">';
}
@@ -179,21 +189,48 @@ $view .= '<div class="content-block">
<option value="">Select From Version</option>';
if (!empty($versions)) {
foreach ($versions as $ver) {
// Skip the TO version from FROM dropdown to prevent FROM = TO
if ($path['to_version_id'] && $ver->rowID == $path['to_version_id']) {
continue;
}
$selected = ($path['from_version_id'] == $ver->rowID) ? ' selected' : '';
$view .= '<option value="' . $ver->rowID . '"' . $selected . '>' . htmlspecialchars($ver->name . ' (' . $ver->version . ')') . '</option>';
}
}
$view .= ' </select>
$view .= ' </select>';
// If TO version is fixed (coming from version page), show it as read-only text
if ($from_version_page && $to_version_fixed) {
$to_version_name = '';
foreach ($versions as $ver) {
if ($ver->rowID == $to_version_fixed) {
$to_version_name = htmlspecialchars($ver->name . ' (' . $ver->version . ')');
break;
}
}
$view .= '
<label for="to_version_display">To Version</label>
<input type="text" id="to_version_display" value="' . $to_version_name . '" disabled>
<input type="hidden" id="to_version_id" name="to_version_id" value="' . $to_version_fixed . '">';
} else {
// Show dropdown for TO version when editing
$view .= '
<label for="to_version_id"><i class="required">*</i>To Version</label>
<select id="to_version_id" name="to_version_id" required>
<option value="">Select To Version</option>';
if (!empty($versions)) {
foreach ($versions as $ver) {
// Skip the FROM version from TO dropdown to prevent FROM = TO
if ($path['from_version_id'] && $ver->rowID == $path['from_version_id']) {
continue;
}
$selected = ($path['to_version_id'] == $ver->rowID) ? ' selected' : '';
$view .= '<option value="' . $ver->rowID . '"' . $selected . '>' . htmlspecialchars($ver->name . ' (' . $ver->version . ')') . '</option>';
}
}
$view .= ' </select>
$view .= ' </select>';
}
$view .= '
<label for="price">Price</label>
<input id="price" type="number" step="0.01" name="price" placeholder="Price" value="' . htmlspecialchars($path['price']) . '">
<label for="currency">Currency</label>
@@ -207,6 +244,30 @@ $view .= ' </select>
<input type="hidden" name="rowID" value="' . htmlspecialchars($path['rowID']) . '">
</div>
</div>
<script>
// Validate that FROM and TO versions are different
document.querySelector("form").addEventListener("submit", function(e) {
const fromVersion = document.getElementById("from_version_id").value;
const toVersion = document.getElementById("to_version_id").value;
if (fromVersion && toVersion && fromVersion === toVersion) {
e.preventDefault();
alert("Error: FROM version cannot be the same as TO version");
return false;
}
});
// Dynamic filtering: Update dropdowns when selection changes
const fromSelect = document.getElementById("from_version_id");
const toSelect = document.getElementById("to_version_id");
if (fromSelect && toSelect && toSelect.tagName === "SELECT") {
fromSelect.addEventListener("change", function() {
// No need to dynamically filter since PHP already handles it
});
}
</script>
';
//OUTPUT

View File

@@ -154,7 +154,7 @@ $view = '
} else {
foreach ($all_paths as $path){
$view .= '
<tr onclick="window.location.href=\'index.php?page=products_software_upgrade_paths_manage&id='.$path->rowID.'\'" style="cursor: pointer;">
<tr onclick="window.location.href=\'index.php?page=products_software_upgrade_paths_manage&path_id='.$path->rowID.'\'" style="cursor: pointer;">
<td>' . ($version_map[$path->from_version_id] ?? $path->from_version_id) . '</td>
<td>' . ($version_map[$path->to_version_id] ?? $path->to_version_id) . '</td>
<td>'.$path->price.'</td>

View File

@@ -110,7 +110,6 @@ $view .= '</div>';
$view .= '<div class="tabs">
<a href="#" class="active">'.$tab1.'</a>
<a href="#">'.$tab3.'</a>
</div>
';
@@ -185,6 +184,11 @@ $view .= '
</div>
</div>';
$view .= '<div class="tabs">
<a href="#">'.$tab3.'</a>
</div>
';
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">
<label for="productcode">'.$general_created.'</label>

View File

@@ -93,9 +93,7 @@ $view .= '</div>';
$view .= '<div class="tabs">
<a href="#" class="active">'.$tab1 .'</a>
<a href="#">'.$tab3.'</a>
</div>
';
</div>';
//Define Service and User enabled
$view .= '<div class="content-block tab-content active">
@@ -130,6 +128,10 @@ $view .=' </select>
$view .= '</div>
</div>';
$view .= '<div class="tabs">
<a href="#">'.$tab3.'</a>
</div>';
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">
<label for="">'.$general_created.'</label>

View File

@@ -59,38 +59,33 @@ function format_var_html($key, $value) {
$html .= '<input type="' . $type . '" name="' . $key . '" id="' . $key . '" value="' . $value . '" placeholder="' . format_key($key) . '"' . $checked . '>';
return $html;
}
// Format tabs
function format_tabs($contents) {
// Format tabs and content together (interleaved for collapsible functionality)
function format_tabs_and_content($contents) {
$rows = explode("\n", $contents);
$tab = '<div class="tabs">';
$tab .= '<a href="#" class="active">General</a>';
for ($i = 0; $i < count($rows); $i++) {
preg_match('/\/\*(.*?)\*\//', $rows[$i], $match);
if ($match) {
$tab .= '<a href="#">' . $match[1] . '</a>';
}
}
$tab .= '</div>';
return $tab;
}
// Format form
function format_form($contents) {
$rows = explode("\n", $contents);
$form = '<div class="tab-content active">';
for ($i = 0; $i < count($rows); $i++) {
preg_match('/\/\*(.*?)\*\//', $rows[$i], $match);
if ($match) {
$form .= '</div><div class="tab-content">';
}
preg_match('/define\(\'(.*?)\', ?(.*?)\)/', $rows[$i], $match);
if ($match) {
$form .= format_var_html($match[1], $match[2]);
}
}
$form .= '</div>';
$output = '';
return $form;
// Start with General tab and its content
$output .= '<div class="tabs"><a href="#" class="active">General</a></div>';
$output .= '<div class="content-block tab-content active"><div class="form responsive-width-100">';
for ($i = 0; $i < count($rows); $i++) {
preg_match('/\/\*(.*?)\*\//', $rows[$i], $match);
if ($match) {
// Close previous content and start new tab
$output .= '</div></div>';
$output .= '<div class="tabs"><a href="#">' . $match[1] . '</a></div>';
$output .= '<div class="content-block tab-content"><div class="form responsive-width-100">';
}
preg_match('/define\(\'(.*?)\', ?(.*?)\)/', $rows[$i], $define_match);
if ($define_match) {
$output .= format_var_html($define_match[1], $define_match[2]);
}
}
$output .= '</div></div>';
return $output;
}
if (isset($_POST['submit']) && !empty($_POST)) {
// Update the configuration file with the new keys and values
foreach ($_POST as $k => $v) {
@@ -99,8 +94,6 @@ if (isset($_POST['submit']) && !empty($_POST)) {
}
file_put_contents($file, $contents);
//Return succesmessage
header('Location: index.php?page=settings&success_msg=1');
exit;
@@ -144,18 +137,9 @@ if (isset($success_msg)){
</div>';
}
$view .= format_tabs($contents);
$view .= '<div class="content-block">
<div class="form responsive-width-100">
';
$view .= format_form($contents);
$view .= format_tabs_and_content($contents);
$view .= '
</div>
</div>
</form>
<script>
document.querySelectorAll("input[type=\'checkbox\']").forEach(checkbox => {
checkbox.onclick = () => checkbox.value = checkbox.checked ? "true" : "false";

View File

@@ -123,6 +123,12 @@ $main_menu = [
"icon" => "fas fa-tachometer-alt",
"name" => "menu_firmwaretool"
] ,
"softwaretool" => [
"url" => "softwaretool",
"selected" => "softwaretool",
"icon" => "fas fa-download",
"name" => "menu_softwaretool"
] ,
"equipments_mass_update" => [
"url" => "equipments_mass_update",
"selected" => "equipments_mass_update",

View File

@@ -4,6 +4,7 @@ $menu_assets = 'Assets';
$menu_service_reports = 'Service Reports';
$menu_history = 'History';
$menu_firmwaretool = 'Firmwaretool';
$menu_softwaretool = 'Softwaretool';
$menu_equipments_mass_update = 'Mass updates';
$menu_products = 'Products';
$menu_sales = 'Sales';
@@ -327,6 +328,21 @@ $firmwaretool_step_5 = 'When firmwware available: The status bar will show "<i>F
$firmwaretool_step_6 = 'When firmwware available: Ensure SN and HW are read from the device and confirm this by selecting the checkbox "I confirm SN and HW are read from device"';
$firmwaretool_step_7 = 'Press <i>"Update firmware"</i> button to start the firmware update dialog and follow the onscreen instructions';
$firmwaretool_step_8 = '<b>Be aware: This process cannot be stopped and needs to finish.</b>';
$softwaretool_h2 = 'Softwaretool';
$softwaretool_p = 'Software upgrade options.';
$softwaretool_step = 'Instructions';
$softwaretool_step_1 = 'Connect the device to the computer by USB.(found under battery cover)';
$softwaretool_step_2 = 'Press "<i>connect</i>" button"';
$softwaretool_step_3 = 'A popup will appear asking to select a device. Select the device by clicking on it and the press the connect button.';
$softwaretool_step_4 = 'After pop-up disappears the device will be read, status bar will show progress';
$softwaretool_step_5 = 'Available software upgrades will be displayed with Name, Description and Price';
$softwaretool_step_6 = 'Select a free upgrade (price = 0) to download and install';
$softwaretool_step_7 = 'For paid upgrades, please contact support';
$softwaretool_step_8 = '<b>Be aware: This process cannot be stopped and needs to finish.</b>';
$softwaretool_no_updates = 'No software updates found';
$softwaretool_checking = 'Checking for software updates...';
$softwaretool_available = 'Software updates available';
$softwaretool_select_upgrade = 'Select an upgrade option:';
$newuser_subject = 'CustomerPortal user created';
$newuser_header = 'Dear CustomerPortal user';
$newuser_text = 'Your CustomerPortal administrator has provided access to the CustomerPortal. To complete your account you need to update your password via the link below.';

162
softwaretool.php Normal file
View File

@@ -0,0 +1,162 @@
<?php
defined(page_security_key) or exit;
if (debug && debug_id == $_SESSION['id']){
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
}
$page = 'softwaretool';
//Check if allowed
if (isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'R') === 0){
header('location: index.php');
exit;
}
$bearertoken = createCommunicationToken($_SESSION['userkey']);
template_header('Softwaretool', 'softwaretool','view');
$view = '
<div class="content-title">
<div class="title">
<i class="fa-solid fa-box-open"></i>
<div class="txt">
<h2>'.$softwaretool_h2 .'</h2>
<p>'.$softwaretool_p.'</p>
</div>
</div>';
if (isset($_GET['equipmentID'])){$returnpage = 'equipment&equipmentID='.$_GET['equipmentID']; } else {$returnpage = 'dashboard';}
//SHOW BACK BUTTON ONLY FOR PORTAL USERS
if (isAllowed('dashboard',$_SESSION['profile'],$_SESSION['permission'],'R') != 0){
$view .= '
<div class="title-actions">
<a href="index.php?page='.$returnpage.'" class="btn alt mar-right-2"><i class="fa-solid fa-arrow-left"></i></a>
<button class="btn" onclick="showInstructions()" style="">
<i class="fa-solid fa-circle-question"></i>
</button>
</div>
';
}
$view .= '
</div>';
$view .= '<div class="content-block">
<p id="servicetoken" value="" hidden>'.$bearertoken.'</p>
<div id="connectdevice" style="display:flex; gap: 10px; margin-bottom: 20px;">
<button class="btn" id="connectButton" onClick="connectDeviceForSoftware()" style="min-width: 150px;">
<i class="fa-solid fa-plug"></i>Connect
</button>
<div id="readStatus" style="flex: 1; background-color: #f1f1f1; border-radius: 4px; overflow: hidden;">
<div id="readBar" style="height: 100%; transition: width 0.3s ease;"></div>
</div>
</div>
<div id="Device_output" style="display:none;">
<div id="serialResults" style="font-family: monospace;white-space: pre;padding: 10px;background-color:#f1f1f1;display:none;"></div>
<div id="softwareCheckStatus" style="margin: 20px 0; padding: 20px; background-color:#f8f9fa; border-radius: 8px; text-align: center; display:none;">
<i class="fa-solid fa-spinner fa-spin" style="font-size: 24px; color: #04AA6D; margin-bottom: 10px;"></i>
<p id="checkingMessage" style="margin: 0; font-size: 16px;">'.$softwaretool_checking.'</p>
</div>
<div id="softwareOptions" style="margin-top: 20px; display:none;">
<h3 style="margin-bottom: 20px; color: #333;">'.$softwaretool_select_upgrade.'</h3>
<div id="softwareOptionsGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;">
</div>
</div>
<div id="noUpdatesMessage" style="margin: 20px 0; padding: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px; text-align: center; display:none;">
<i class="fa-solid fa-check-circle" style="font-size: 48px; margin-bottom: 15px;"></i>
<p style="margin: 0; font-size: 18px; font-weight: 500;"><strong>'.$softwaretool_no_updates.'</strong></p>
<p style="margin: 10px 0 0 0; opacity: 0.9;">Your device is up to date</p>
</div>
<div id="uploadSection" style="margin-top: 10px; display:none;">
<button class="btn" id="uploadSoftware" style="display:none;"
emergencyPlug-update verify
board="emergencyPlug"
port-filters= "[{usbVendorId: 1027, usbProductId: 24597}];"
disabled>Install Software
<span class="upload-progress"></span>
</button>
</div>
</div>
</div>
<!-- Help Modal -->
<div id="helpModal" style="display:none; 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;">
<div style="background: white; border-radius: 12px; max-width: 600px; max-height: 80vh; overflow-y: auto; margin: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.3);">
<div style="padding: 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0; color: #333;"><i class="fa-solid fa-circle-question"></i> '.$softwaretool_step.'</h3>
<button onclick="closeInstructions()" style="background: transparent; border: none; font-size: 20px; cursor: pointer; color: #666;"><i class="fa-solid fa-times"></i></button>
</div>
<div style="padding: 25px;">
<ol style="line-height: 1.8; color: #555;">
<li style="margin-bottom: 15px;">'.$softwaretool_step_1.'</li>
<li style="margin-bottom: 15px;">'.$softwaretool_step_2.'</li>
<li style="margin-bottom: 15px;">'.$softwaretool_step_3.'</li>
<li style="margin-bottom: 15px;">'.$softwaretool_step_4.'</li>
<li style="margin-bottom: 15px;">'.$softwaretool_step_5.'</li>
<li style="margin-bottom: 15px;">'.$softwaretool_step_6.'</li>
<li style="margin-bottom: 15px;">'.$softwaretool_step_7.'</li>
<li style="margin-bottom: 15px;">'.$softwaretool_step_8.'</li>
</ol>
</div>
</div>
</div>
';
$view .= '
</div>
</div>
';
$view .= '</div>';
//OUTPUT
echo $view;
echo '
<script src="assets/upload.js?'.script_version.'"></script>
<script src="assets/softwaretool.js?'.script_version.'"></script>
<script>
var link = "'.$baseurl.'";
var DEBUG = '.(debug ? 'true' : 'false').';
var port, textEncoder, writableStreamClosed, writer, historyIndex = -1;
const lineHistory = [];
// Modal functions - defined globally for inline onclick
window.showInstructions = function() {
const modal = document.getElementById("helpModal");
if (modal) {
modal.style.display = "flex";
}
};
window.closeInstructions = function() {
const modal = document.getElementById("helpModal");
if (modal) {
modal.style.display = "none";
}
};
// Close modal on background click
document.addEventListener("click", function(e) {
const modal = document.getElementById("helpModal");
if (modal && e.target === modal) {
closeInstructions();
}
});
</script>';
template_footer();
?>

View File

@@ -1002,8 +1002,7 @@ main .manage-order-table .delete-item:hover {
.table {
overflow-x: auto;
padding: 0;
border-radius: 8px;
overflow: hidden;
border-radius: 4px;
}
.table table {

View File

@@ -185,11 +185,7 @@ $view .= '</div>';
$view .= '<div class="tabs">
<a href="#" class="active">'.$tab1 .'</a>
<a href="#">'.$tab2.'</a>
<a href="#">'.$tab3.'</a>
'.(($update_allowed === 1 && $user_ID !='')? '<a href="#">'.$general_actions.'</a>':"").'
</div>
';
</div>';
//Define Service and User enabled
$view .= '<div class="content-block tab-content active">
@@ -263,6 +259,10 @@ $view .=' </select>
$view .= '</div>
</div>';
$view .= '<div class="tabs">
<a href="#">'.$tab2.'</a>
</div>';
//GET PARTNERDATA
$partner_data = json_decode($user['partnerhierarchy'])?? json_decode($_SESSION['partnerhierarchy']) ;
//BUID UP DROPDOWNS
@@ -289,6 +289,10 @@ $view .= '
</div>
</div>';
$view .= '<div class="tabs">
<a href="#">'.$tab3.'</a>
</div>';
//SUPERUSERS AND ADMINS CAN RESET BLOCKED USERS
if ($_SESSION['permission'] == 3 || $_SESSION['permission'] == 4){
@@ -313,6 +317,9 @@ $view .= '<div class="content-block tab-content">
</div>';
if ($update_allowed === 1 && $user_ID !=''){
$view .= '<div class="tabs">
<a href="#">'.$general_actions.'</a>
</div>';
$view .= '<div class="content-block tab-content">
<div class="form responsive-width-100">
<label for="service">'.$User_pw_reset .'</label>