- Implemented the software tool page with user interface for connecting devices. - Added functionality to display connection status and software upgrade options. - Included a help modal with step-by-step instructions for users. - Integrated error handling and user permission checks. - Enhanced user experience with dynamic content updates and visual feedback.
826 lines
34 KiB
Markdown
826 lines
34 KiB
Markdown
# 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
|
|
|