From 543f0b3cac919fab07159862e1876d61d62a723f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CVeLiTi=E2=80=9D?= <“info@veliti.nl”> Date: Wed, 24 Dec 2025 14:07:28 +0100 Subject: [PATCH] feat: Add software licenses management page and update payment handling - Introduced a new licenses management page with functionality to create, update, and view software licenses. - Updated payment return handling in softwaretool.php to check payment status from the database and display appropriate modals for success, pending, and failure states. - Enhanced webhook_mollie.php to log webhook calls, handle payment status updates directly in the database, and generate invoices based on payment status. - Improved CSS styles for better alignment of buttons and modal components. - Added JavaScript for modal interactions and bulk license creation functionality. --- .gitignore | 3 + api/v2/get/equipments.php | 4 + api/v2/get/products_software_licenses.php | 22 +- api/v2/get/software_available.php | 8 +- api/v2/get/software_update.php | 8 +- api/v2/post/dealers.php | 36 ++ api/v2/post/payment.php | 166 +++--- api/v2/post/products_software_licenses.php | 120 +++- api/v2/post/products_software_versions.php | 19 +- assets/functions.php | 68 ++- assets/softwaretool.js | 173 ++++-- custom/bewellwell/style/bewellwell.css | 47 ++ custom/soveliti/style/soveliti.css | 47 ++ equipment.php | 3 +- licenses.php | 606 +++++++++++++++++++++ settings/settingsmenu.php | 6 + settings/settingsprofiles.php | 4 +- settings/settingsviews.php | 5 + softwaretool.php | 99 +++- style/admin.css | 44 ++ webhook_mollie.php | 150 ++++- 21 files changed, 1400 insertions(+), 238 deletions(-) create mode 100644 licenses.php diff --git a/.gitignore b/.gitignore index 689a894..3d559fe 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ settings/config.php variable_scan.php settings/soveliti/soveliti_config.php settings/soveliti/soveliti_settings.php +assets/database/dev_schema.sql +assets/database/migration.sql +assets/database/prod_schema.sql diff --git a/api/v2/get/equipments.php b/api/v2/get/equipments.php index ff797aa..d02c76d 100644 --- a/api/v2/get/equipments.php +++ b/api/v2/get/equipments.php @@ -49,6 +49,9 @@ if(isset($get_content) && $get_content!=''){ elseif ($v[0] == 'equipmentid') { //build up search $clause .= ' AND e.rowID = :'.$v[0]; + + //UPDATE VERSION STATUS + $sw_version_latest_update = 1; } elseif ($v[0] == 'servicedate') { //build up service coverage @@ -69,6 +72,7 @@ if(isset($get_content) && $get_content!=''){ elseif ($v[0] == 'h_equipmentid') { //build up search $clause .= ' AND h.equipmentid = :'.$v[0]; + } elseif ($v[0] == 'status') { //Update status based on status diff --git a/api/v2/get/products_software_licenses.php b/api/v2/get/products_software_licenses.php index 85e6e67..f47175d 100644 --- a/api/v2/get/products_software_licenses.php +++ b/api/v2/get/products_software_licenses.php @@ -1,6 +1,8 @@ soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';} //default whereclause -list($whereclause,$condition) = getWhereclauselvl2("software_licenses",$permission,$partner,'get'); +list($whereclause,$condition) = getWhereclauselvl2("products_software_licenses",$permission,$partner,'get'); //NEW ARRAY $criterias = []; @@ -52,12 +54,20 @@ if(isset($criterias['totals']) && $criterias['totals'] ==''){ $sql = 'SELECT count(*) as count FROM products_software_licenses '.$whereclause.''; } elseif (isset($criterias['list']) && $criterias['list'] =='') { - //SQL for list - $sql = 'SELECT l.*, u.username, v.name as version_name FROM products_software_licenses l LEFT JOIN users u ON l.user_id = u.id LEFT JOIN products_software_versions v ON l.version_id = v.rowID '.$whereclause.' ORDER BY l.created DESC'; + //SQL for list + $sql = 'SELECT l.*, v.name as version_name, v.version, e.serialnumber as assigned_serial + FROM products_software_licenses l + LEFT JOIN products_software_versions v ON l.version_id = v.rowID + LEFT JOIN equipment e ON l.license_key = e.sw_version_license + '.$whereclause.' ORDER BY l.created DESC'; } else { - //SQL for paged - $sql = 'SELECT l.*, u.username, v.name as version_name FROM products_software_licenses l LEFT JOIN users u ON l.user_id = u.id LEFT JOIN products_software_versions v ON l.version_id = v.rowID '.$whereclause.' ORDER BY l.created DESC LIMIT :page,:num_licenses'; + //SQL for paged + $sql = 'SELECT l.*, v.name as version_name, v.version, e.serialnumber as assigned_serial + FROM products_software_licenses l + LEFT JOIN products_software_versions v ON l.version_id = v.rowID + LEFT JOIN equipment e ON l.license_key = e.sw_version_license + '.$whereclause.' ORDER BY l.created DESC LIMIT :page,:num_licenses'; } $stmt = $pdo->prepare($sql); diff --git a/api/v2/get/software_available.php b/api/v2/get/software_available.php index 3cb21b4..3513575 100644 --- a/api/v2/get/software_available.php +++ b/api/v2/get/software_available.php @@ -245,16 +245,16 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ //Check if there's a valid license for this upgrade if ($final_price > 0 && $sw_version_license) { //Check if the license is valid - $sql = 'SELECT status, start_at, expires_at + $sql = 'SELECT status, starts_at, expires_at FROM products_software_licenses - WHERE license_key = ? AND equipment_id = ?'; + WHERE license_key = ?'; $stmt = $pdo->prepare($sql); - $stmt->execute([$sw_version_license, $equipment_rowid]); + $stmt->execute([$sw_version_license]); $license = $stmt->fetch(PDO::FETCH_ASSOC); if ($license && $license['status'] == 1) { $now = date('Y-m-d H:i:s'); - $start_at = $license['start_at']; + $start_at = $license['starts_at']; $expires_at = $license['expires_at']; //Check if license is within valid date range diff --git a/api/v2/get/software_update.php b/api/v2/get/software_update.php index 3a71457..2f4696b 100644 --- a/api/v2/get/software_update.php +++ b/api/v2/get/software_update.php @@ -281,16 +281,16 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ $license_applied = false; if ($final_price > 0 && $sw_version_license) { //Check if the license is valid - $sql = 'SELECT status, start_at, expires_at + $sql = 'SELECT status, starts_at, expires_at FROM products_software_licenses - WHERE license_key = ? AND equipment_id = ?'; + WHERE license_key = ?'; $stmt = $pdo->prepare($sql); - $stmt->execute([$sw_version_license, $equipment_rowid]); + $stmt->execute([$sw_version_license]); $license = $stmt->fetch(PDO::FETCH_ASSOC); if ($license && $license['status'] == 1) { $now = date('Y-m-d H:i:s'); - $start_at = $license['start_at']; + $start_at = $license['starts_at']; $expires_at = $license['expires_at']; //Check if license is within valid date range diff --git a/api/v2/post/dealers.php b/api/v2/post/dealers.php index 51f517e..d748cb0 100644 --- a/api/v2/post/dealers.php +++ b/api/v2/post/dealers.php @@ -354,6 +354,42 @@ elseif(isset($post_content['dealer_closeby'])){ echo json_encode(['error' => "Latitude or longitude not provided."]); } } +elseif(isset($post_content['action']) && $post_content['action']=='unsubscribe'){ + //++++++++++++++++++++++ + //Process DEALER UNSUBSCRIBE + //++++++++++++++++++++++ + + // Check if email is provided + if (isset($post_content['email']) && !empty($post_content['email'])) { + $email = $post_content['email']; + + try { + // Update dealer status to 0 (inactive) where email matches + $sql = 'UPDATE dealers SET status = 0 WHERE email = ?'; + $stmt = $pdo->prepare($sql); + + if ($stmt->execute([$email])) { + // Check if any rows were affected + if ($stmt->rowCount() > 0) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'success', 'message' => 'Dealer unsubscribed successfully']); + } else { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'No dealer found with this email']); + } + } else { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Database update failed']); + } + } catch (PDOException $e) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Database error occurred']); + } + } else { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Email not provided']); + } +} else { //++++++++++++++++++++++ diff --git a/api/v2/post/payment.php b/api/v2/post/payment.php index 1087f40..84b4c54 100644 --- a/api/v2/post/payment.php +++ b/api/v2/post/payment.php @@ -1,11 +1,12 @@ execute([$serial_number]); $equipment = $stmt->fetch(PDO::FETCH_ASSOC); if (!$equipment) { + http_response_code(404); echo json_encode(['error' => 'Device not found with serial number: ' . $serial_number], JSON_UNESCAPED_UNICODE); exit; @@ -46,15 +49,15 @@ $hw_version = $equipment['hw_version'] ?? ''; //+++++++++++++++++++++++++++++++++++++++++++++++++++++ // STEP 2: Get version data from version_id //+++++++++++++++++++++++++++++++++++++++++++++++++++++ -$sql = 'SELECT v.rowID as version_id, v.version, v.name, v.description, v.hw_version, p.productcode - FROM products_software_versions v - JOIN products_software p ON v.product_software_id = p.rowID - WHERE v.rowID = ? AND v.is_active = 1'; +$sql = 'SELECT rowID as version_id, version, name, description, hw_version + FROM products_software_versions + WHERE rowID = ? AND status = 1'; $stmt = $pdo->prepare($sql); $stmt->execute([$version_id]); $version = $stmt->fetch(PDO::FETCH_ASSOC); if (!$version) { + http_response_code(404); echo json_encode(['error' => 'Software version not found or inactive'], JSON_UNESCAPED_UNICODE); exit; @@ -93,6 +96,7 @@ if (!$has_upgrade_paths) { $final_currency = $upgrade_path['currency'] ?? 'EUR'; } else { // No upgrade path FROM current version + http_response_code(400); echo json_encode(['error' => 'No valid upgrade path from current version'], JSON_UNESCAPED_UNICODE); exit; @@ -103,20 +107,20 @@ if (!$has_upgrade_paths) { // STEP 4: Check license validity (lines 280-311 in software_update.php) //+++++++++++++++++++++++++++++++++++++++++++++++++++++ if ($final_price > 0 && $sw_version_license) { - $sql = 'SELECT status, start_at, expires_at + $sql = 'SELECT status, starts_at, expires_at FROM products_software_licenses - WHERE license_key = ? AND equipment_id = ?'; + WHERE license_key = ?'; $stmt = $pdo->prepare($sql); - $stmt->execute([$sw_version_license, $equipment_id]); + $stmt->execute([$sw_version_license]); $license = $stmt->fetch(PDO::FETCH_ASSOC); if ($license && $license['status'] == 1) { $now = date('Y-m-d H:i:s'); - $start_at = $license['start_at']; + $starts_at = $license['starts_at']; $expires_at = $license['expires_at']; // Check if license is within valid date range - if ((!$start_at || $start_at <= $now) && (!$expires_at || $expires_at >= $now)) { + if ((!$starts_at || $starts_at <= $now) && (!$expires_at || $expires_at >= $now)) { $final_price = '0.00'; } } @@ -126,68 +130,18 @@ if ($final_price > 0 && $sw_version_license) { // STEP 5: Verify price > 0 (free upgrades shouldn't reach payment API) //+++++++++++++++++++++++++++++++++++++++++++++++++++++ if ($final_price <= 0) { + http_response_code(400); echo json_encode(['error' => 'This upgrade is free. No payment required.'], JSON_UNESCAPED_UNICODE); exit; } //+++++++++++++++++++++++++++++++++++++++++++++++++++++ -// STEP 6: DEBUG MODE - Simulate payment without Mollie +// STEP 6: DEBUG MODE - Log but continue to real Mollie //+++++++++++++++++++++++++++++++++++++++++++++++++++++ if (debug) { - // Generate fake payment ID - $fake_payment_id = 'DEBUG_' . uniqid() . '_' . time(); - $checkout_url = 'https://'.$_SERVER['SERVER_NAME'].'/softwaretool.php?payment_return=1&payment_id=' . $fake_payment_id; - - // Store transaction in DB - $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, payment_method, created) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - $stmt = $pdo->prepare($sql); - $stmt->execute([ - $fake_payment_id, - $final_price, - 0, // 0 = pending - $user_data['email'] ?? '', - $user_data['first_name'] ?? '', - $user_data['last_name'] ?? '', - $user_data['address_street'] ?? '', - $user_data['address_city'] ?? '', - $user_data['address_state'] ?? '', - $user_data['address_zip'] ?? '', - $user_data['address_country'] ?? '', - $serial_number, - 0, // payment method - date('Y-m-d H:i:s') - ]); - - // Store transaction item with serial_number in item_options - $item_options = json_encode([ - 'serial_number' => $serial_number, - 'equipment_id' => $equipment_id, - 'hw_version' => $hw_version - ], JSON_UNESCAPED_UNICODE); - - $sql = 'INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, created) - VALUES (?, ?, ?, ?, ?, ?)'; - $stmt = $pdo->prepare($sql); - $stmt->execute([ - $fake_payment_id, - $version_id, - $final_price, - 1, - $item_options, - date('Y-m-d H:i:s') - ]); - - // Return fake checkout URL - $messages = json_encode([ - 'checkout_url' => $checkout_url, - 'payment_id' => $fake_payment_id, - 'debug_mode' => true - ], JSON_UNESCAPED_UNICODE); - echo $messages; - exit; + debuglog("DEBUG MODE: Creating real Mollie payment for testing"); + debuglog("DEBUG: Serial Number: $serial_number, Version ID: $version_id, Price: $final_price"); } //+++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -195,66 +149,110 @@ if (debug) { //+++++++++++++++++++++++++++++++++++++++++++++++++++++ try { // Initialize Mollie - require dirname(__FILE__, 3).'/initialize.php'; + require dirname(__FILE__, 4).'/initialize.php'; // Format price for Mollie (must be string with 2 decimals) $formatted_price = number_format((float)$final_price, 2, '.', ''); + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + // STEP 7A: Generate transaction ID BEFORE creating Mollie payment + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + // Generate unique transaction ID (same as placeorder.php) + $txn_id = strtoupper(uniqid('SC') . substr(md5(mt_rand()), 0, 5)); + + // Build webhook URL and redirect URL with actual transaction ID + $protocol = 'https'; + $hostname = $_SERVER['SERVER_NAME']; + $path = '/'; + $webhook_url = "{$protocol}://{$hostname}{$path}webhook_mollie.php"; + $redirect_url = "{$protocol}://{$hostname}{$path}?page=softwaretool&payment_return=1&order_id={$txn_id}"; + + if (debug) { + debuglog("DEBUG: Transaction ID: {$txn_id}"); + debuglog("DEBUG: redirectUrl being sent to Mollie: " . $redirect_url); + } + // Create payment with Mollie $payment = $mollie->payments->create([ 'amount' => [ 'currency' => $final_currency ?: 'EUR', - 'value' => $formatted_price + 'value' => "{$formatted_price}" ], - 'description' => 'Software upgrade to ' . $version['name'] . ' (v' . $version['version'] . ')', - 'redirectUrl' => 'https://'.$_SERVER['SERVER_NAME'].'/softwaretool.php?payment_return=1&payment_id={id}', - 'webhookUrl' => 'https://'.$_SERVER['SERVER_NAME'].'/webhook_mollie.php', + 'description' => "Software upgrade Order #{$txn_id}", + 'redirectUrl' => "{$redirect_url}", + 'webhookUrl' => "{$webhook_url}", 'metadata' => [ - 'order_id' => $payment->id // Store payment ID in metadata + 'order_id' => $txn_id, + 'serial_number' => $serial_number, + 'version_id' => $version_id, + 'equipment_id' => $equipment_id ] ]); $mollie_payment_id = $payment->id; $checkout_url = $payment->getCheckoutUrl(); + if (debug) { + debuglog("DEBUG: Mollie payment created successfully"); + debuglog("DEBUG: Payment ID: $mollie_payment_id"); + debuglog("DEBUG: Redirect URL sent: $redirect_url"); + debuglog("DEBUG: Redirect URL from Mollie object: " . $payment->redirectUrl); + debuglog("DEBUG: Full payment object: " . json_encode($payment)); + debuglog("DEBUG: Checkout URL: $checkout_url"); + } + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ - // STEP 8: Store transaction in DB + // STEP 8: Store transaction in DB using txn_id (order ID) //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + // Split name into first/last (simple split on first space) + $full_name = $user_data['name'] ?? ''; + $name_parts = explode(' ', $full_name, 2); + $first_name = $name_parts[0] ?? ''; + $last_name = $name_parts[1] ?? ''; + + // BUILD UP PARTNERHIERARCHY FROM USER + $partner_product = json_encode(array("salesid"=>$partner->salesid,"soldto"=>$partner->soldto), JSON_UNESCAPED_UNICODE); + $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, payment_method, created) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + address_street, address_city, address_state, address_zip, address_country, account_id, payment_method, accounthierarchy, created) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; $stmt = $pdo->prepare($sql); $stmt->execute([ - $mollie_payment_id, + $txn_id, // Use generated transaction ID, not Mollie payment ID $final_price, 0, // 0 = pending $user_data['email'] ?? '', - $user_data['first_name'] ?? '', - $user_data['last_name'] ?? '', - $user_data['address_street'] ?? '', - $user_data['address_city'] ?? '', - $user_data['address_state'] ?? '', - $user_data['address_zip'] ?? '', - $user_data['address_country'] ?? '', + $first_name, + $last_name, + $user_data['address'] ?? '', + $user_data['city'] ?? '', + '', // address_state (not collected) + $user_data['postal'] ?? '', + $user_data['country'] ?? '', $serial_number, 0, // payment method + $partner_product, date('Y-m-d H:i:s') ]); + // Get the database ID + $transaction_id = $pdo->lastInsertId(); + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ // STEP 9: Store transaction item with serial_number in item_options //+++++++++++++++++++++++++++++++++++++++++++++++++++++ $item_options = json_encode([ 'serial_number' => $serial_number, 'equipment_id' => $equipment_id, - 'hw_version' => $hw_version + 'hw_version' => $hw_version, + 'mollie_payment_id' => $mollie_payment_id // Store Mollie payment ID in options ], JSON_UNESCAPED_UNICODE); $sql = 'INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, created) VALUES (?, ?, ?, ?, ?, ?)'; $stmt = $pdo->prepare($sql); $stmt->execute([ - $mollie_payment_id, + $transaction_id, // Use database transaction ID (not txn_id string, not mollie_payment_id) $version_id, $final_price, 1, diff --git a/api/v2/post/products_software_licenses.php b/api/v2/post/products_software_licenses.php index faf6fd3..1b3a6fe 100644 --- a/api/v2/post/products_software_licenses.php +++ b/api/v2/post/products_software_licenses.php @@ -14,12 +14,16 @@ $post_content = json_decode($input,true); if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';} //default whereclause -list($whereclause,$condition) = getWhereclauselvl2("software_licenses",$permission,$partner,''); +list($whereclause,$condition) = getWhereclauselvl2("products_software_licenses",$permission,$partner,''); //SET PARAMETERS FOR QUERY $id = $post_content['rowID'] ?? ''; //check for rowID $command = ($id == '')? 'insert' : 'update'; //IF rowID = empty then INSERT if (isset($post_content['delete'])){$command = 'delete';} //change command to delete + +// Check for bulk creation +$is_bulk = isset($post_content['bulk']) && $post_content['bulk'] === true; + $date = date('Y-m-d H:i:s'); //CREATE EMPTY STRINGS @@ -27,12 +31,90 @@ $clause = ''; $clause_insert =''; $input_insert = ''; +//------------------------------------------ +// BULK LICENSE CREATION +//------------------------------------------ +if ($command == 'insert' && $is_bulk && isAllowed('products_software_licenses',$profile,$permission,'C') === 1){ + + $version_id = $post_content['version_id'] ?? ''; + $serials = $post_content['serials'] ?? []; + $transaction_id = $post_content['transaction_id'] ?? ''; + $license_type = $post_content['license_type'] ?? 0; + $status = $post_content['status'] ?? 0; + + if (empty($version_id) || empty($serials) || !is_array($serials)) { + http_response_code(400); + echo json_encode(['error' => 'Invalid parameters for bulk creation']); + exit; + } + + $accounthierarchy = json_encode(array("salesid"=>$partner->salesid,"soldto"=>$partner->soldto), JSON_UNESCAPED_UNICODE); + + // Prepare statement for bulk insert + $sql = 'INSERT INTO products_software_licenses (version_id, license_key, license_type, status, transaction_id, accounthierarchy, created, createdby) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)'; + $stmt = $pdo->prepare($sql); + + $created_count = 0; + foreach ($serials as $serial) { + if (empty($serial)) continue; + + // Generate UUID for license key + $license_key = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + + try { + $stmt->execute([ + $version_id, + $license_key, + $license_type, + $status, + $transaction_id, + $accounthierarchy, + $date, + $username + ]); + + // Assign license to equipment if serial number exists + $eq_sql = 'UPDATE equipment SET sw_version_license = ? WHERE serialnumber = ? AND accounthierarchy LIKE ?'; + $eq_stmt = $pdo->prepare($eq_sql); + $eq_stmt->execute([$license_key, $serial, '%'.$partner->soldto.'%']); + + $created_count++; + } catch (Exception $e) { + debuglog("Error creating license for serial $serial: " . $e->getMessage()); + } + } + + echo json_encode(['success' => true, 'created' => $created_count]); + exit; +} + +//------------------------------------------ +// SINGLE LICENSE CREATION OR UPDATE +//------------------------------------------ //ADD STANDARD PARAMETERS TO ARRAY BASED ON INSERT OR UPDATE if ($command == 'update'){ $post_content['updated'] = $date; $post_content['updatedby'] = $username; } elseif ($command == 'insert'){ + // Generate UUID for license key if not provided + if (empty($post_content['license_key'])) { + $post_content['license_key'] = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } + $post_content['created'] = $date; $post_content['createdby'] = $username; $post_content['accounthierarchy'] = json_encode(array("salesid"=>$partner->salesid,"soldto"=>$partner->soldto), JSON_UNESCAPED_UNICODE); @@ -42,10 +124,10 @@ else { } //CREATE NEW ARRAY AND MAP TO CLAUSE -if(isset($post_content) && $post_content!=''){ +if(isset($post_content) && $post_content!=''){ foreach ($post_content as $key => $var){ - if ($key == 'submit' || $key == 'rowID'){ - //do nothing + if ($key == 'submit' || $key == 'rowID' || $key == 'serial' || $key == 'bulk' || $key == 'serials'){ + //do nothing - skip these fields } else { $criterias[$key] = $var; @@ -64,27 +146,43 @@ $input_insert = substr($input_insert, 1); //Clean clause - remove first comma //QUERY AND VERIFY ALLOWED if ($command == 'update' && isAllowed('products_software_licenses',$profile,$permission,'U') === 1){ - + $sql = 'UPDATE products_software_licenses SET '.$clause.' WHERE rowID = ? '; $execute_input[] = $id; $stmt = $pdo->prepare($sql); $stmt->execute($execute_input); -} + + echo json_encode(['success' => true]); +} elseif ($command == 'insert' && isAllowed('products_software_licenses',$profile,$permission,'C') === 1){ - - //INSERT NEW ITEM + + //INSERT NEW ITEM $sql = 'INSERT INTO products_software_licenses ('.$clause_insert.') VALUES ('.$input_insert.')'; $stmt = $pdo->prepare($sql); $stmt->execute($execute_input); + + $new_license_id = $pdo->lastInsertId(); + $license_key = $post_content['license_key']; + + // Assign license to equipment if serial number provided + if (!empty($post_content['serial'])) { + $serial = $post_content['serial']; + $eq_sql = 'UPDATE equipment SET sw_version_license = ? WHERE serialnumber = ? AND accounthierarchy LIKE ?'; + $eq_stmt = $pdo->prepare($eq_sql); + $eq_stmt->execute([$license_key, $serial, '%'.$partner->soldto.'%']); + } + + echo json_encode(['success' => true, 'license_id' => $new_license_id, 'license_key' => $license_key]); } elseif ($command == 'delete' && isAllowed('products_software_licenses',$profile,$permission,'D') === 1){ - + $stmt = $pdo->prepare('DELETE FROM products_software_licenses WHERE rowID = ? '); - $stmt->execute([ $id ]); + $stmt->execute([ $id ]); //Add deletion to changelog changelog($dbname,'products_software_licenses',$id,'Delete','Delete',$username); - + + echo json_encode(['success' => true]); } else { //do nothing diff --git a/api/v2/post/products_software_versions.php b/api/v2/post/products_software_versions.php index 60417ab..18a3163 100644 --- a/api/v2/post/products_software_versions.php +++ b/api/v2/post/products_software_versions.php @@ -72,10 +72,9 @@ $hw_version = (isset($criterias['hw_version']))? $criterias['hw_version']:''; if ($command == 'update' && isAllowed('products_software_versions',$profile,$permission,'U') === 1){ //REMOVE LATEST FLAG FROM OTHER WHEN SEND + //Max 2 latest flags per hw_version: 1 with price (has upgrade path with price) and 1 without if (isset($criterias['latest']) && $criterias['latest'] == 1){ - $sql = 'UPDATE products_software_versions SET latest = 0 WHERE hw_version = ? AND rowID != ?'; - $stmt = $pdo->prepare($sql); - $stmt->execute([$hw_version, $id]); + updateSoftwareLatestFlags($pdo, $id, $hw_version); } $sql = 'UPDATE products_software_versions SET '.$clause.' WHERE rowID = ? '; @@ -84,18 +83,18 @@ if ($command == 'update' && isAllowed('products_software_versions',$profile,$per $stmt->execute($execute_input); } elseif ($command == 'insert' && isAllowed('products_software_versions',$profile,$permission,'C') === 1){ - - //REMOVE LATEST FLAG FROM OTHER IF SET - if (isset($criterias['latest']) && $criterias['latest'] == 1){ - $sql = 'UPDATE products_software_versions SET latest = 0 WHERE hw_version = ?'; - $stmt = $pdo->prepare($sql); - $stmt->execute([$hw_version]); - } //INSERT NEW ITEM $sql = 'INSERT INTO products_software_versions ('.$clause_insert.') VALUES ('.$input_insert.')'; $stmt = $pdo->prepare($sql); $stmt->execute($execute_input); + $new_id = $pdo->lastInsertId(); + + //REMOVE LATEST FLAG FROM OTHER IF SET + //Max 2 latest flags per hw_version: 1 with price (has upgrade path with price) and 1 without + if (isset($criterias['latest']) && $criterias['latest'] == 1){ + updateSoftwareLatestFlags($pdo, $new_id, $hw_version); + } } elseif ($command == 'delete' && isAllowed('products_software_versions',$profile,$permission,'D') === 1){ diff --git a/assets/functions.php b/assets/functions.php index a02c05f..6e50226 100644 --- a/assets/functions.php +++ b/assets/functions.php @@ -1418,7 +1418,8 @@ function getWhereclauselvl2($table_name,$permission,$partner,$method){ "software" => "p.accounthierarchy", "transactions" => "tx.accounthierarchy", "dealers" => "d.accounthierarchy", - "categories" => "c.accounthierarchy" + "categories" => "c.accounthierarchy", + "products_software_licenses" => "l.accounthierarchy" ]; $table = ($table_name != '') ? $table[$table_name] : 'accounthierarchy'; @@ -5154,23 +5155,7 @@ function updateSoftwareVersionStatus($pdo, $serialnumber = null) { $stmt->execute($bind_params); //------------------------------------------ - // STEP 3: Set sw_version_latest = 0 for equipment NOT matching latest version - //------------------------------------------ - $sql = 'UPDATE equipment e - JOIN products_software_assignment psa ON e.productrowid = psa.product_id AND psa.status = 1 - JOIN products_software_versions psv ON psa.software_version_id = psv.rowID - SET e.sw_version_latest = 0 - WHERE psv.latest = 1 - AND psv.status = 1 - AND lower(e.sw_version) <> lower(psv.version) - AND (psv.hw_version = e.hw_version OR psv.hw_version IS NULL OR psv.hw_version = "") - AND e.sw_version_latest = 1' . $sn_clause; - - $stmt = $pdo->prepare($sql); - $stmt->execute($bind_params); - - //------------------------------------------ - // STEP 4: Set sw_version_latest = 1 for equipment matching latest version + // STEP 3: Set sw_version_latest = 1 for equipment matching latest version //------------------------------------------ $sql = 'UPDATE equipment e JOIN products_software_assignment psa ON e.productrowid = psa.product_id AND psa.status = 1 @@ -5179,7 +5164,7 @@ function updateSoftwareVersionStatus($pdo, $serialnumber = null) { WHERE psv.latest = 1 AND psv.status = 1 AND lower(e.sw_version) = lower(psv.version) - AND (psv.hw_version = e.hw_version OR psv.hw_version IS NULL OR psv.hw_version = "") + AND (lower(psv.hw_version) = lower(e.hw_version) OR lower(psv.hw_version) IS NULL OR lower(psv.hw_version) = "") AND e.sw_version_latest = 0' . $sn_clause; $stmt = $pdo->prepare($sql); @@ -5542,4 +5527,49 @@ function generateSoftwareInvoice($invoice_data, $order_id, $language = 'US') { '; return [$html, $customer_email, $order_id]; +} + +/** + * Update latest flags for software versions + * Max 2 latest flags per hw_version: 1 with price (has upgrade path with price) and 1 without + * + * @param PDO $pdo - Database connection + * @param int $version_id - The version ID being set as latest + * @param string $hw_version - Hardware version + */ +function updateSoftwareLatestFlags($pdo, $version_id, $hw_version) { + //Check if current version has a priced upgrade path + $sql = 'SELECT COUNT(*) as has_price + FROM products_software_upgrade_paths + WHERE to_version_id = ? AND is_active = 1 AND price > 0'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$version_id]); + $current_has_price = $stmt->fetch(PDO::FETCH_ASSOC)['has_price'] > 0; + + //Remove latest flag only from versions in the same category (priced or free) + //Get all versions with same hw_version and check their pricing + $sql = 'SELECT psv.rowID, + CASE + WHEN EXISTS( + SELECT 1 FROM products_software_upgrade_paths pup + WHERE pup.to_version_id = psv.rowID + AND pup.is_active = 1 + AND pup.price > 0 + ) THEN 1 + ELSE 0 + END as has_price + FROM products_software_versions psv + WHERE psv.hw_version = ? AND psv.rowID != ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$hw_version, $version_id]); + $versions = $stmt->fetchAll(PDO::FETCH_ASSOC); + + //Update only versions in the same price category + foreach ($versions as $version) { + if ($version['has_price'] == ($current_has_price ? 1 : 0)) { + $sql = 'UPDATE products_software_versions SET latest = 0 WHERE rowID = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$version['rowID']]); + } + } } \ No newline at end of file diff --git a/assets/softwaretool.js b/assets/softwaretool.js index f4e9b4e..0194de5 100644 --- a/assets/softwaretool.js +++ b/assets/softwaretool.js @@ -459,132 +459,143 @@ function displaySoftwareOptions(options) { const isFree = price === 0; const isCurrent = option.is_current === true || option.is_current === 1; - // Create card + // Create card with gradient background const card = document.createElement("div"); card.style.cssText = ` - background: ${isCurrent ? '#f5f5f5' : 'white'}; - border: 2px solid ${isCurrent ? '#bbb' : (isFree ? '#e0e0e0' : '#e0e0e0')}; + background: ${isCurrent ? 'linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%)' : 'linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%)'}; border-radius: 4px; - padding: 15px; - transition: 0.3s; + padding: 25px 20px; + transition: all 0.3s ease; display: flex; flex-direction: column; position: relative; - overflow: hidden; + overflow: visible; transform: translateY(0px); - box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px; - opacity: ${isCurrent ? '0.6' : '1'}; + box-shadow: ${isCurrent ? '0 4px 12px rgba(0,0,0,0.08)' : '0 8px 20px rgba(0,0,0,0.12)'}; + opacity: ${isCurrent ? '0.7' : '1'}; pointer-events: ${isCurrent ? 'none' : 'auto'}; + min-height: 320px; `; if (!isCurrent) { card.onmouseenter = () => { - card.style.transform = 'translateY(-5px)'; - card.style.boxShadow = '0 8px 16px rgba(0,0,0,0.15)'; + card.style.transform = 'translateY(-8px) scale(1.02)'; + card.style.boxShadow = '0 12px 28px rgba(0,0,0,0.2)'; + card.style.borderColor = isFree ? '#038f5a' : '#FF4500'; }; card.onmouseleave = () => { - card.style.transform = 'translateY(0)'; - card.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)'; + card.style.transform = 'translateY(0) scale(1)'; + card.style.boxShadow = '0 8px 20px rgba(0,0,0,0.12)'; + card.style.borderColor = isFree ? '#04AA6D' : '#FF6B35'; }; } - // Badge for current/free/paid + // Badge for current/free/paid - VISIBLE const badge = document.createElement("div"); badge.style.cssText = ` position: absolute; - top: 15px; - right: 15px; - background: ${isCurrent ? '#6c757d' : '#04AA6D'}; + top: -10px; + right: 20px; + background: ${isCurrent ? '#6c757d' : (isFree ? 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)')}; color: white; - padding: 5px 12px; + padding: 8px 16px; border-radius: 20px; - font-size: 12px; - font-weight: bold; - display:none; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.5px; + text-transform: uppercase; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); `; if (isCurrent) { - badge.textContent = "CURRENT VERSION"; + badge.textContent = "INSTALLED"; } else if (isFree) { - badge.textContent = "Included"; + badge.textContent = "FREE"; + } else { + badge.textContent = "PREMIUM"; } - if (isCurrent || isFree) { - card.appendChild(badge); - } + card.appendChild(badge); - // Name + // Name with icon const name = document.createElement("h4"); name.style.cssText = ` - margin: 0 0 10px 0; + margin: 0 0 12px 0; color: #333; - font-size: 20px; - font-weight: 600; + font-size: 22px; + font-weight: 700; `; - name.textContent = option.name || "Software Update"; + name.innerHTML = `${option.name || "Software Update"}`; card.appendChild(name); - // Version + // Version with enhanced styling const version = document.createElement("div"); version.style.cssText = ` color: #666; font-size: 14px; margin-bottom: 15px; + display: flex; + align-items: center; + gap: 6px; `; - version.innerHTML = ` Version: ${option.version || "N/A"}`; + version.innerHTML = ` Version: ${option.version || "N/A"}`; card.appendChild(version); - // Description - const desc = document.createElement("p"); - desc.style.cssText = ` + // Description with preserved newlines + const descContainer = document.createElement("div"); + descContainer.style.cssText = ` color: #555; - font-size: 14px; - line-height: 1.6; + font-size: 13px; + line-height: 1.7; margin: 0 0 20px 0; flex-grow: 1; + white-space: pre-line; `; - desc.textContent = option.description || "No description available"; - card.appendChild(desc); + descContainer.textContent = option.description || "No description available"; + card.appendChild(descContainer); // Price section const priceSection = document.createElement("div"); priceSection.style.cssText = ` - border-top: 1px solid #e0e0e0; - padding-top: 15px; + border-top: 2px solid ${isFree ? '#04AA6D20' : '#FF6B3520'}; + padding-top: 20px; margin-top: auto; `; const priceText = document.createElement("div"); priceText.style.cssText = ` - font-size: 24px; - font-weight: bold; - color: ${isCurrent ? '#6c757d' : (isFree ? '#04AA6D' : '#333')}; + font-size: ${isCurrent ? '18px' : '28px'}; + font-weight: ${isCurrent ? '600' : '800'}; + color: ${isCurrent ? '#6c757d' : (isFree ? '#04AA6D' : '#FF6B35')}; margin-bottom: 15px; + text-align: center; + letter-spacing: 0.5px; `; if (isCurrent) { - priceText.textContent = "INSTALLED"; + priceText.innerHTML = ' INSTALLED'; } else { - priceText.textContent = isFree ? "Included" : `${option.currency || "€"} ${price.toFixed(2)}`; + priceText.innerHTML = isFree + ? 'Free' + : `${option.currency || "€"} ${price.toFixed(2)}`; } priceSection.appendChild(priceText); - // Action button + // Action button with gradient for paid const actionBtn = document.createElement("button"); actionBtn.className = "btn"; actionBtn.style.cssText = ` width: 100%; - background: ${isCurrent ? '#6c757d' : '#04AA6D'}; + background: ${isCurrent ? '#6c757d' : (isFree ? 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)')}; color: white; border: none; - padding: 12px; - border-radius: 6px; - font-size: 16px; - font-weight: 600; cursor: ${isCurrent ? 'not-allowed' : 'pointer'}; - transition: background 0.3s ease; + transition: all 0.3s ease; opacity: ${isCurrent ? '0.5' : '1'}; + box-shadow: ${isCurrent ? 'none' : '0 4px 12px rgba(0,0,0,0.15)'}; + letter-spacing: 0.5px; + text-transform: uppercase; `; if (isCurrent) { @@ -593,13 +604,29 @@ function displaySoftwareOptions(options) { } else if (isFree) { actionBtn.innerHTML = ''; actionBtn.onclick = () => selectUpgrade(option); - actionBtn.onmouseenter = () => actionBtn.style.background = '#038f5a'; - actionBtn.onmouseleave = () => actionBtn.style.background = '#04AA6D'; + actionBtn.onmouseenter = () => { + actionBtn.style.background = 'linear-gradient(135deg, #038f5a 0%, #026b43 100%)'; + actionBtn.style.transform = 'translateY(-2px)'; + actionBtn.style.boxShadow = '0 6px 16px rgba(0,0,0,0.2)'; + }; + actionBtn.onmouseleave = () => { + actionBtn.style.background = 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)'; + actionBtn.style.transform = 'translateY(0)'; + actionBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; + }; } else { actionBtn.innerHTML = ''; actionBtn.onclick = () => selectUpgrade(option); - actionBtn.onmouseenter = () => actionBtn.style.background = '#038f5a'; - actionBtn.onmouseleave = () => actionBtn.style.background = '#04AA6D'; + actionBtn.onmouseenter = () => { + actionBtn.style.background = 'linear-gradient(135deg, #FF4500 0%, #CC3700 100%)'; + actionBtn.style.transform = 'translateY(-2px)'; + actionBtn.style.boxShadow = '0 6px 16px rgba(255,107,53,0.4)'; + }; + actionBtn.onmouseleave = () => { + actionBtn.style.background = 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)'; + actionBtn.style.transform = 'translateY(0)'; + actionBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; + }; } priceSection.appendChild(actionBtn); @@ -980,10 +1007,19 @@ async function processPayment(paymentData, option, modal) { user_data: paymentData // name, email, address only }; + // Debug logging + if (typeof DEBUG !== 'undefined' && DEBUG) { + console.log("=== DEBUG: Payment Request ==="); + console.log("Serial Number:", deviceSerialNumber); + console.log("Version ID:", option.version_id); + console.log("User Data:", paymentData); + console.log("Request payload:", paymentRequest); + } + await logCommunication(`Payment initiated for version ${option.version_id}`, 'sent'); // Call payment API to create Mollie payment - const response = await fetch(link + "/v2/post/payment", { + const response = await fetch(link + "/v2/payment", { method: "POST", headers: { "Content-Type": "application/json", @@ -994,13 +1030,27 @@ async function processPayment(paymentData, option, modal) { if (!response.ok) { const errorData = await response.json(); + if (typeof DEBUG !== 'undefined' && DEBUG) { + console.error("DEBUG: Payment API error:", errorData); + } throw new Error(errorData.error || "Failed to create payment"); } const result = await response.json(); + if (typeof DEBUG !== 'undefined' && DEBUG) { + console.log("=== DEBUG: Payment Response ==="); + console.log("Result:", result); + console.log("Checkout URL:", result.checkout_url); + console.log("Payment ID:", result.payment_id); + } + if (result.checkout_url) { - await logCommunication(`Redirecting to payment provider`, 'sent'); + await logCommunication(`Redirecting to Mollie payment: ${result.payment_id}`, 'sent'); + + if (typeof DEBUG !== 'undefined' && DEBUG) { + console.log("DEBUG: Redirecting to Mollie checkout..."); + } // Close modal before redirect document.body.removeChild(modal); @@ -1012,6 +1062,9 @@ async function processPayment(paymentData, option, modal) { } } catch (error) { + if (typeof DEBUG !== 'undefined' && DEBUG) { + console.error("DEBUG: Payment processing error:", error); + } await logCommunication(`Payment error: ${error.message}`, 'error'); progressBar("0", "Payment failed: " + error.message, "#ff6666"); alert("Payment failed: " + error.message); @@ -1028,7 +1081,7 @@ async function downloadAndInstallSoftware(option, customerData = null) { if (paymentId) { try { // Verify serial number matches payment - const response = await fetch(link + `/v2/get/payment?payment_id=${paymentId}`, { + const response = await fetch(link + `/v2/payment?payment_id=${paymentId}`, { method: "GET", headers: { "Authorization": "Bearer " + document.getElementById("servicetoken").textContent diff --git a/custom/bewellwell/style/bewellwell.css b/custom/bewellwell/style/bewellwell.css index 23d0932..c08265f 100644 --- a/custom/bewellwell/style/bewellwell.css +++ b/custom/bewellwell/style/bewellwell.css @@ -2919,4 +2919,51 @@ main .products .product .price, main .products .products-wrapper .product .price width: 25px; height: 25px; margin: 1px; +} + +/* Button alignment styles */ +.form-actions, +.modal-actions, +.dialog-actions, +.button-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + margin-top: 20px; +} + +.title-actions { + display: flex; + gap: 10px; + align-items: center; + justify-content: flex-end; +} + +.filter-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + flex-wrap: wrap; +} + +main .form .button-container, +main .form .form-actions, +main .content-block .button-container { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + margin-top: 15px; +} + +.dialog .content .footer, +.modal .modal-footer { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + padding: 20px; + border-top: 1px solid #eee; } \ No newline at end of file diff --git a/custom/soveliti/style/soveliti.css b/custom/soveliti/style/soveliti.css index fcc5e9a..0ed2857 100644 --- a/custom/soveliti/style/soveliti.css +++ b/custom/soveliti/style/soveliti.css @@ -2921,4 +2921,51 @@ main .products .product .price, main .products .products-wrapper .product .price width: 25px; height: 25px; margin: 1px; +} + +/* Button alignment styles */ +.form-actions, +.modal-actions, +.dialog-actions, +.button-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + margin-top: 20px; +} + +.title-actions { + display: flex; + gap: 10px; + align-items: center; + justify-content: flex-end; +} + +.filter-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + flex-wrap: wrap; +} + +main .form .button-container, +main .form .form-actions, +main .content-block .button-container { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + margin-top: 15px; +} + +.dialog .content .footer, +.modal .modal-footer { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + padding: 20px; + border-top: 1px solid #eee; } \ No newline at end of file diff --git a/equipment.php b/equipment.php index 8944854..058404f 100644 --- a/equipment.php +++ b/equipment.php @@ -127,6 +127,7 @@ $view = '

'.$view_asset_h2.' - '.$responses->equipmentID.'

+ '; //------------------------------------ @@ -395,7 +396,7 @@ $view .= '
'.$view_asset_actions.'
- '.$button_history.' + '.$view_communication.' '.$view_users.' '; diff --git a/licenses.php b/licenses.php new file mode 100644 index 0000000..e95c97c --- /dev/null +++ b/licenses.php @@ -0,0 +1,606 @@ + +
+ +
+

Software Licenses ('.$query_total.')

+

Manage and create software licenses for devices

+
+
+
'; + +if ($create_allowed === 1){ + $view .= ''; +} + +$view .= ' +
+
'; + +if (isset($success_msg)){ +$view .= '
+ +

'.$success_msg.'

+ +
'; +} + +$view .= ' + +'; + +$view .= ' +
+
+ + + + + + + + + + + + + + '; + + if (empty($responses)){ + + $view .= ' + + + '; + } + else { + foreach ($responses as $response){ + // Check if license is expired based on timestamp + $actual_status = $response->status; + if (!empty($response->expires_at)) { + $expiry_time = strtotime($response->expires_at); + $current_time = time(); + if ($current_time > $expiry_time) { + // License is expired - override status + $actual_status = 2; + } + } + + // Status display based on actual status + $status_text = ''; + if ($actual_status == 0) { + $status_text = 'Inactive'; + } elseif ($actual_status == 1) { + $status_text = 'Assigned'; + } elseif ($actual_status == 2) { + $status_text = 'Expired'; + } + + // Format dates + $starts_display = '-'; + if (!empty($response->starts_at)) { + $starts_display = date('Y-m-d', strtotime($response->starts_at)); + } + + $expires_display = '-'; + if (!empty($response->expires_at)) { + $expires_display = date('Y-m-d', strtotime($response->expires_at)); + } + + $view .= ' + + + + + + + + + + '; + } + } +$view .= ' + +
License KeySoftware VersionStatusTransaction IDStarts AtExpiresAssigned To (Serial)
No licenses found
'.$response->license_key.''.$response->version_name.''.$status_text.''.($response->transaction_id ?? '-').''.$starts_display.''.$expires_display.''.($response->assigned_serial ?? '-').'
+
+
+'; + +$view.=''; + + + +// Bulk License Modal +$view .= ' +'; + +// License Details Modal +$view .= ' +'; + +//OUTPUT +echo $view; + +// Add JavaScript for modals and API calls +echo ' + + + +'; + +template_footer(); +?> diff --git a/settings/settingsmenu.php b/settings/settingsmenu.php index 89595f2..85cf303 100644 --- a/settings/settingsmenu.php +++ b/settings/settingsmenu.php @@ -47,6 +47,12 @@ $main_menu = [ "icon" => "fas fa-tachometer-alt", "name" => "menu_sales_orders" ], + "licenses" => [ + "url" => "licenses", + "selected" => "licenses", + "icon" => "fas fa-tachometer-alt", + "name" => "menu_sales_licenses" + ], "identity" => [ "url" => "identity", "selected" => "identity", diff --git a/settings/settingsprofiles.php b/settings/settingsprofiles.php index a819374..4ec0352 100644 --- a/settings/settingsprofiles.php +++ b/settings/settingsprofiles.php @@ -6,7 +6,7 @@ define('superuser_profile','admin,dashboard,profile,application,assets,firmwaret /*Admin*/ define('admin_profile','account,accounts,admin,dashboard,profile,application,assets,buildtool,buildtool,cartest,cartest_manage,cartests,changelog,communication,communication_send,communications,firmwaretool,histories,history,history_manage,marketing,partner,partners,sales,servicereport,servicereports,contract,contract_manage,contracts,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_software,products_versions,report_build,report_contracts_billing,report_healthindex,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_manage,users'); /*AdminPlus*/ -define('adminplus_profile','account,account_manage,accounts,admin,config,dashboard,profile,settings,api,application,appointment,assets,billing,buildtool,buildtool,cartest,cartest_manage,cartests,catalog,categories,category,changelog,checkout,com_log,communication,communication_send,communications,cronjob,debug,dev,discount,discounts,firmwaretool,generate_download_token,histories,history,history_manage,identity,identity_dealers,invoice,language,logfile,mailer,maintenance,marketing,media,media_manage,media_scanner,media_upload,order,orders,partner,partners,placeorder,pricelists,pricelists_items,pricelists_manage,profiles,register,render_service_report,reset,sales,security,servicereport,servicereports,shipping,shipping_manage,shopping_cart,software_available,software_download,software_update,tax,taxes,test,transactions,transactions_items,translation_manage,translations,translations_details,unscribe,upgrades,uploader,vin,contract,contract_manage,contracts,dealer,dealer_manage,dealers,dealers_media,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_attributes,products_attributes_items,products_attributes_manage,products_categories,products_configurations,products_media,products_software,products_software_assignment,products_software_assignments,products_software_assignments,products_software_licenses,products_software_upgrade_paths,products_software_upgrade_paths_manage,products_software_version,products_software_version_access_rules_manage,products_software_version_manage,products_software_versions,products_versions,report_build,report_contracts_billing,report_healthindex,report_usage,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_credentials,user_manage,users'); +define('adminplus_profile','account,account_manage,accounts,admin,config,dashboard,profile,settings,api,application,appointment,assets,billing,buildtool,buildtool,cartest,cartest_manage,cartests,catalog,categories,category,changelog,checkout,com_log,communication,communication_send,communications,cronjob,debug,dev,discount,discounts,firmwaretool,generate_download_token,histories,history,history_manage,identity,identity_dealers,language,licenses,logfile,mailer,maintenance,marketing,media,media_manage,media_scanner,media_upload,order,orders,partner,partners,payment,placeorder,pricelists,pricelists_items,pricelists_manage,profiles,register,render_service_report,reset,sales,security,servicereport,servicereports,shipping,shipping_manage,shopping_cart,software_available,software_download,software_update,softwaretool,tax,taxes,test,transactions,transactions_items,translation_manage,translations,translations_details,unscribe,upgrades,uploader,vin,contract,contract_manage,contracts,dealer,dealer_manage,dealers,dealers_media,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_attributes,products_attributes_items,products_attributes_manage,products_categories,products_configurations,products_media,products_software,products_software_assignment,products_software_assignments,products_software_assignments,products_software_licenses,products_software_upgrade_paths,products_software_upgrade_paths_manage,products_software_version,products_software_version_access_rules_manage,products_software_version_manage,products_software_versions,products_versions,report_build,report_contracts_billing,report_healthindex,report_usage,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_credentials,user_manage,users'); /*Build*/ define('build','dashboard,profile,application,buildtool,buildtool,firmwaretool,products_software'); /*Commerce*/ @@ -18,7 +18,7 @@ define('firmware','application,firmwaretool,products_software'); /*Garage*/ define('garage','dashboard,profile,application,cartest,cartest_manage,cartests,products_versions'); /*Interface*/ -define('interface','application,firmwaretool,contract,contracts,equipment_manage,equipments,products_software,products_versions,users'); +define('interface','application,firmwaretool,invoice,payment,transactions,transactions_items,contract,contracts,equipment_manage,equipments,products_software,products_versions,users'); /*Service*/ define('service','admin,dashboard,profile,application,assets,firmwaretool,histories,history,history_manage,marketing,partner,partners,servicereport,servicereports,equipment,equipment_manage,equipments,products_software,user,user_manage,users'); /*Other*/ diff --git a/settings/settingsviews.php b/settings/settingsviews.php index 014f3a9..0e7e408 100644 --- a/settings/settingsviews.php +++ b/settings/settingsviews.php @@ -55,8 +55,10 @@ $all_views = [ "history_manage", "identity", "identity_dealers", + "initialize", "invoice", "language", + "licenses", "logfile", "mailer", "maintenance", @@ -69,6 +71,7 @@ $all_views = [ "orders", "partner", "partners", + "payment", "placeorder", "pricelists", "pricelists_items", @@ -120,6 +123,7 @@ $all_views = [ "software_available", "software_download", "software_update", + "softwaretool", "tax", "taxes", "test", @@ -136,6 +140,7 @@ $all_views = [ "user_manage", "users", "vin", + "webhook_mollie", ]; ?> \ No newline at end of file diff --git a/softwaretool.php b/softwaretool.php index 66ea983..c419d56 100644 --- a/softwaretool.php +++ b/softwaretool.php @@ -18,24 +18,73 @@ $bearertoken = createCommunicationToken($_SESSION['userkey']); //+++++++++++++++++++++++++++++++++++++++++++++++++++++ // PAYMENT RETURN DETECTION //+++++++++++++++++++++++++++++++++++++++++++++++++++++ -$payment_return = isset($_GET['payment_id']) ? $_GET['payment_id'] : null; +$payment_return = isset($_GET['order_id']) ? $_GET['order_id'] : null; $payment_return_status = isset($_GET['payment_return']) ? $_GET['payment_return'] : null; template_header('Softwaretool', 'softwaretool','view'); // Show payment return message if returning from payment +$view = ''; +$payment_modal = ''; if ($payment_return && $payment_return_status) { - $view = ' -
-
- - Payment Successful! -

Your payment has been processed. Please reconnect your device to apply the software upgrade.

-

Payment ID: '.htmlspecialchars($payment_return).'

-
-
'; -} else { - $view = ''; + // Check actual payment status in database + $pdo = dbConnect($dbname); + $sql = 'SELECT payment_status FROM transactions WHERE txn_id = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$payment_return]); + $transaction = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($transaction) { + if ($transaction['payment_status'] == 1) { + // Payment confirmed as paid + $payment_modal = ' + '; + } else if ($transaction['payment_status'] == 0 || $transaction['payment_status'] == 101) { + // Payment pending + $payment_modal = ' + + '; + } else { + // Payment failed/cancelled + $payment_modal = ' + '; + } + } } $view .= ' @@ -90,7 +139,7 @@ $view .= '
'; //OUTPUT echo $view; +// Output payment modal if exists +echo $payment_modal; + echo ' @@ -169,12 +221,29 @@ echo ' } }; + // Payment modal functions + window.closePaymentModal = function() { + const modal = document.getElementById("paymentModal"); + if (modal) { + modal.style.display = "none"; + // Clean URL by removing payment_return and order_id parameters + const url = new URL(window.location); + url.searchParams.delete("payment_return"); + url.searchParams.delete("order_id"); + window.history.replaceState({}, document.title, url); + } + }; + // Close modal on background click document.addEventListener("click", function(e) { - const modal = document.getElementById("helpModal"); - if (modal && e.target === modal) { + const helpModal = document.getElementById("helpModal"); + if (helpModal && e.target === helpModal) { closeInstructions(); } + const paymentModal = document.getElementById("paymentModal"); + if (paymentModal && e.target === paymentModal) { + closePaymentModal(); + } }); '; diff --git a/style/admin.css b/style/admin.css index 8988e13..34cbd5e 100644 --- a/style/admin.css +++ b/style/admin.css @@ -2937,6 +2937,7 @@ main .products .product .price, main .products .products-wrapper .product .price display: flex; gap: 10px; align-items: center; + justify-content: flex-end; } .filter-panel { @@ -2979,6 +2980,7 @@ main .products .product .price, main .products .products-wrapper .product .price display: flex; gap: 10px; justify-content: flex-end; + align-items: center; flex-wrap: wrap; } @@ -3084,4 +3086,46 @@ main .products .product .price, main .products .products-wrapper .product .price font-size: 12px; color: #6c757d; font-style: italic; +} + +/* Button alignment styles */ +.form-actions, +.modal-actions, +.dialog-actions, +.button-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + margin-top: 20px; +} + +/* Ensure title-actions stay right-aligned */ +.title-actions { + display: flex; + gap: 10px; + align-items: center; + justify-content: flex-end; +} + +/* Form button containers should be right-aligned */ +main .form .button-container, +main .form .form-actions, +main .content-block .button-container { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + margin-top: 15px; +} + +/* Right-align buttons in dialog footers */ +.dialog .content .footer, +.modal .modal-footer { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + padding: 20px; + border-top: 1px solid #eee; } \ No newline at end of file diff --git a/webhook_mollie.php b/webhook_mollie.php index 07280da..8ab0333 100644 --- a/webhook_mollie.php +++ b/webhook_mollie.php @@ -5,14 +5,22 @@ require_once 'settings/config_redirector.php'; require_once 'assets/functions.php'; +include dirname(__FILE__).'/settings/settings_redirector.php'; + +// DEBUG: Log webhook call +debuglog("WEBHOOK CALLED - POST data: " . print_r($_POST, true)); //+++++++++++++++++++++++++++++++++++++++++++++++++++++ //LOGIN TO API (same as commerce webhook.php) //+++++++++++++++++++++++++++++++++++++++++++++++++++++ -$data = json_encode(array("clientID" => software_update_user, "clientsecret" => software_update_pw), JSON_UNESCAPED_UNICODE); +debuglog("WEBHOOK: Attempting API authorization..."); +debuglog("WEBHOOK: Interface user: " . interface_user); +$data = json_encode(array("clientID" => interface_user, "clientsecret" => interface_pw), JSON_UNESCAPED_UNICODE); $responses = ioAPIv2('/v2/authorization', $data,''); +debuglog("WEBHOOK: Authorization response: " . $responses); if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';} $clientsecret = $responses['token']; +debuglog("WEBHOOK: Token obtained: " . ($clientsecret ? 'YES' : 'NO')); //+++++++++++++++++++++++++++++++++++++++++++++++++++++ // BASEURL is required for invoice template @@ -64,7 +72,16 @@ try { // PRODUCTION MODE - Retrieve the payment's current state from Mollie //+++++++++++++++++++++++++++++++++++++++++++++++++++++ $payment = $mollie->payments->get($_POST["id"]); - $orderId = $payment->metadata->order_id; + // Get order ID from metadata (same as commerce product) + $orderId = $payment->metadata->order_id ?? null; + + if (!$orderId) { + debuglog("WEBHOOK ERROR: No order_id in payment metadata"); + http_response_code(400); + exit; + } + + debuglog("WEBHOOK: Payment ID: {$payment->id}, Order ID: {$orderId}"); $payment_status = null; if ($payment->isPaid() && !$payment->hasRefunds() && !$payment->hasChargebacks()) { @@ -81,18 +98,33 @@ try { } //+++++++++++++++++++++++++++++++++++++++++++++++++++++ - // Update transaction status via API + // Update transaction status directly in database //+++++++++++++++++++++++++++++++++++++++++++++++++++++ if ($payment_status !== null) { - $payload = json_encode(array("txn_id" => $orderId, "payment_status" => $payment_status), JSON_UNESCAPED_UNICODE); - $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret); - $transaction = json_decode($transaction,true); + debuglog("WEBHOOK: Order ID: $orderId, Payment Status: $payment_status"); + + $pdo = dbConnect($dbname); + + // Update transaction status + $sql = 'UPDATE transactions SET payment_status = ? WHERE txn_id = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$payment_status, $orderId]); + + debuglog("WEBHOOK: Transaction status updated in database"); + + // Fetch transaction data for license creation + $sql = 'SELECT * FROM transactions WHERE txn_id = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$orderId]); + $transaction = $stmt->fetch(PDO::FETCH_ASSOC); + + debuglog("WEBHOOK: Transaction data: " . print_r($transaction, true)); //+++++++++++++++++++++++++++++++++++++++++++++++++++++ // Only create license and invoice if payment is PAID //+++++++++++++++++++++++++++++++++++++++++++++++++++++ if ($payment_status == 1 && $transaction !== null && !empty($transaction)) { - if(count($transaction) > 0) { + debuglog("WEBHOOK: Payment is PAID, processing license..."); //+++++++++++++++++++++++++++++++++++++++++++++++++++++ // CREATE LICENSE for software upgrade @@ -100,9 +132,10 @@ try { $pdo = dbConnect($dbname); // Fetch transaction items to find software upgrade + // Note: transactions_items.txn_id is the database ID (transaction.id), not the txn_id string $sql = 'SELECT * FROM transactions_items WHERE txn_id = ?'; $stmt = $pdo->prepare($sql); - $stmt->execute([$orderId]); + $stmt->execute([$transaction['id']]); $items = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($items as $item) { @@ -124,14 +157,14 @@ try { // Create license $sql = 'INSERT INTO products_software_licenses - (license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby) + (version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, created, createdby) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'; $stmt = $pdo->prepare($sql); $stmt->execute([ + $item['item_id'], // version_id + 1, // license_type (1 = upgrade) $license_key, - $options['equipment_id'], - 'upgrade', - 1, // active + 1, // status = active date('Y-m-d H:i:s'), '2099-12-31 23:59:59', // effectively permanent $orderId, @@ -139,26 +172,79 @@ try { 'webhook' // created by webhook ]); + debuglog("WEBHOOK: License created: $license_key"); + // 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']]); + + debuglog("WEBHOOK: Equipment updated with license: {$options['equipment_id']}"); + } else { + debuglog("WEBHOOK: License already exists for order: $orderId"); } + } else { + debuglog("WEBHOOK: Not a software upgrade item (no serial_number/equipment_id)"); } + } else { + debuglog("WEBHOOK: No item_options found"); } } //+++++++++++++++++++++++++++++++++++++++++++++++++++++ - // Generate INVOICE via API + // Generate INVOICE directly in database //+++++++++++++++++++++++++++++++++++++++++++++++++++++ - $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)) { - // Fetch full invoice data with customer details - $invoice_cust = ioAPIv2('/v2/invoice/list=invoice&id='.$invoice['invoice_id'],'',$clientsecret); - $invoice_cust = json_decode($invoice_cust,true); + // Check if invoice already exists for this transaction + $sql = 'SELECT id FROM invoice WHERE txn_id = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$transaction['txn_id']]); + $existing_invoice = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$existing_invoice) { + // Create invoice + $sql = 'INSERT INTO invoice (txn_id, payment_status, payment_amount, shipping_amount, discount_amount, tax_amount, created) + VALUES (?, ?, ?, ?, ?, ?, ?)'; + $stmt = $pdo->prepare($sql); + $stmt->execute([ + $transaction['txn_id'], + $transaction['payment_status'], + $transaction['payment_amount'], + $transaction['shipping_amount'] ?? 0.00, + $transaction['discount_amount'] ?? 0.00, + $transaction['tax_amount'] ?? 0.00, + date('Y-m-d H:i:s') + ]); + $invoice_id = $pdo->lastInsertId(); + debuglog("WEBHOOK: Invoice created with ID: $invoice_id"); + } else { + $invoice_id = $existing_invoice['id']; + debuglog("WEBHOOK: Invoice already exists with ID: $invoice_id"); + } + + // Fetch full invoice data with customer details for email + // Note: invoice.txn_id = transactions.txn_id (VARCHAR order ID) + // transactions_items.txn_id = transactions.id (INT database ID) + $sql = 'SELECT tx.*, txi.item_id, txi.item_price, txi.item_quantity, txi.item_options, + p.productcode, p.productname, inv.id as invoice, inv.created as invoice_created, + i.language as user_language + FROM invoice inv + LEFT JOIN transactions tx ON tx.txn_id = inv.txn_id + LEFT JOIN transactions_items txi ON txi.txn_id = tx.id + LEFT JOIN products p ON p.rowID = txi.item_id + LEFT JOIN identity i ON i.userkey = tx.account_id + WHERE inv.id = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$invoice_id]); + $invoice_data = $stmt->fetchAll(PDO::FETCH_ASSOC); + + debuglog("WEBHOOK: Invoice data fetched: " . print_r($invoice_data, true)); + + if (!empty($invoice_data)) { + debuglog("WEBHOOK: Transforming invoice data..."); + // Transform the data (group items like the API does) + $invoice_cust = transformOrderData($invoice_data); + debuglog("WEBHOOK: Transformed invoice data: " . print_r($invoice_cust, true)); // Determine invoice language if (!empty($invoice_cust['customer']['language'])) { @@ -170,28 +256,44 @@ try { } // Generate invoice HTML (using custom template for software upgrades) + debuglog("WEBHOOK: Calling generateSoftwareInvoice with language: $invoice_language"); list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language); + debuglog("WEBHOOK: Invoice generated - Customer email: $customer_email, Order ID: $order_id"); + debuglog("WEBHOOK: Invoice HTML length: " . strlen($data)); //+++++++++++++++++++++++++++++++++++++++++++++++++++++ //CREATE PDF using DomPDF //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + debuglog("WEBHOOK: Creating PDF..."); $dompdf->loadHtml($data); $dompdf->setPaper('A4', 'portrait'); $dompdf->render(); $subject = 'Software Upgrade - Invoice: '.$order_id; $attachment = $dompdf->output(); + debuglog("WEBHOOK: PDF created, size: " . strlen($attachment) . " bytes"); //+++++++++++++++++++++++++++++++++++++++++++++++++++++ //Send email via PHPMailer //+++++++++++++++++++++++++++++++++++++++++++++++++++++ - send_mail($customer_email, $subject, $data, $attachment, $subject); + debuglog("WEBHOOK: Attempting to send email to: $customer_email"); + debuglog("WEBHOOK: Email subject: $subject"); + debuglog("WEBHOOK: Email config - Host: " . (defined('email_host_name') ? email_host_name : 'NOT DEFINED')); + debuglog("WEBHOOK: Email config - Port: " . (defined('email_outgoing_port') ? email_outgoing_port : 'NOT DEFINED')); + debuglog("WEBHOOK: Email config - Security: " . (defined('email_outgoing_security') ? email_outgoing_security : 'NOT DEFINED')); + debuglog("WEBHOOK: Email config - Username: " . (defined('email') ? email : 'NOT DEFINED')); + + // The send_mail function will exit on error and debuglog the error + $mail_result = send_mail($customer_email, $subject, $data, $attachment, $subject); + debuglog("WEBHOOK: Email sent successfully to: $customer_email"); // Send to bookkeeping if configured if(invoice_bookkeeping){ + debuglog("WEBHOOK: Sending to bookkeeping: " . email_bookkeeping); send_mail(email_bookkeeping, $subject, $data, $attachment, $subject); } + } else { + debuglog("WEBHOOK: No invoice data found for invoice_id: $invoice_id"); } - } } } @@ -200,9 +302,13 @@ try { echo "OK"; } catch (\Mollie\Api\Exceptions\ApiException $e) { + debuglog("WEBHOOK ERROR (Mollie API): " . $e->getMessage()); + debuglog("WEBHOOK ERROR TRACE: " . $e->getTraceAsString()); error_log("Webhook API call failed: " . htmlspecialchars($e->getMessage())); http_response_code(500); } catch (Exception $e) { + debuglog("WEBHOOK ERROR (General): " . $e->getMessage()); + debuglog("WEBHOOK ERROR TRACE: " . $e->getTraceAsString()); error_log("Webhook error: " . htmlspecialchars($e->getMessage())); http_response_code(500); }