Fix billing provisioning and admin defaults
Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/1e47877f-c80e-4514-bdff-2bd022c84f13 Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
parent
3e70455179
commit
439e57b333
15 changed files with 772 additions and 185 deletions
4
.github/module-map.md
vendored
4
.github/module-map.md
vendored
|
|
@ -17,7 +17,7 @@ This file captures how the control panel, storefront, agents, and helper scripts
|
||||||
|
|
||||||
1. **Auth/session** – Driven by `index.php` (panel) and `modules/billing/login.php` (storefront). Both set `$_SESSION['user_id']`, `users_login`, `users_group`, and `website_user_id`. The shared session cookie `opengamepanel_web` means logging into either surface immediately authenticates the other.
|
1. **Auth/session** – Driven by `index.php` (panel) and `modules/billing/login.php` (storefront). Both set `$_SESSION['user_id']`, `users_login`, `users_group`, and `website_user_id`. The shared session cookie `opengamepanel_web` means logging into either surface immediately authenticates the other.
|
||||||
2. **Catalog** – `modules/config_games` hosts XML definitions. Panel modules (`gamemanager`, `config_games`) and storefront pages (`serverlist.php`, `order.php`, documentation pages, and the XML-notes mirror) parse these files for display and provisioning metadata.
|
2. **Catalog** – `modules/config_games` hosts XML definitions. Panel modules (`gamemanager`, `config_games`) and storefront pages (`serverlist.php`, `order.php`, documentation pages, and the XML-notes mirror) parse these files for display and provisioning metadata.
|
||||||
3. **Provisioning** – Orders land in `gsp_billing_orders`. `modules/billing/includes/provisioner.php` reuses `modules/billing/create_servers.php` logic to allocate homes, assign nodes/IPs, configure mods, and kick off SteamCMD/rsync/manual installers. The same provisioner is invoked by:
|
3. **Provisioning** – Orders land in `gsp_billing_orders`. `modules/billing/create_servers.php` allocates homes, assigns nodes/IPs, configures mods, kicks off SteamCMD/rsync/manual installers, and then syncs the resulting `home_id` back into `billing_orders`, `billing_invoices`, and `billing_transactions` so paid services never stay orphaned. The same provisioner is invoked by:
|
||||||
- PayPal capture endpoint (`modules/billing/api/capture_order.php`).
|
- PayPal capture endpoint (`modules/billing/api/capture_order.php`).
|
||||||
- Panel module page `home.php?m=billing&p=provision_servers`.
|
- Panel module page `home.php?m=billing&p=provision_servers`.
|
||||||
- Cron/repair actions in `modules/billing/cron-shop.php`.
|
- Cron/repair actions in `modules/billing/cron-shop.php`.
|
||||||
|
|
@ -48,7 +48,7 @@ This file captures how the control panel, storefront, agents, and helper scripts
|
||||||
| Public pages | `index.php`, `serverlist.php`, `order.php`, `cart.php`, `payment_success.php`, `docs.php` | All include `bootstrap.php`, header/footer, shared CSS. Links remain root-relative. |
|
| Public pages | `index.php`, `serverlist.php`, `order.php`, `cart.php`, `payment_success.php`, `docs.php` | All include `bootstrap.php`, header/footer, shared CSS. Links remain root-relative. |
|
||||||
| Auth | `login.php`, `register.php`, `reset_password.php`, `forgot_password.php`, `includes/login_required.php`, `includes/admin_auth.php` | Share `opengamepanel_web` session, call into panel DB to validate roles. |
|
| Auth | `login.php`, `register.php`, `reset_password.php`, `forgot_password.php`, `includes/login_required.php`, `includes/admin_auth.php` | Share `opengamepanel_web` session, call into panel DB to validate roles. |
|
||||||
| Admin | `admin.php`, `adminserverlist.php`, `admin_orders.php`, `admin_coupons.php`, `admin_config.php`, `my_orders_panel.php` | Manage services, coupons, prices, and provisioning. `adminserverlist.php` controls service availability per node. |
|
| Admin | `admin.php`, `adminserverlist.php`, `admin_orders.php`, `admin_coupons.php`, `admin_config.php`, `my_orders_panel.php` | Manage services, coupons, prices, and provisioning. `adminserverlist.php` controls service availability per node. |
|
||||||
| PayPal API | `api/create_order.php`, `api/capture_order.php`, `webhook.php`, `logs/payment_capture.log` | Implements REST checkout. Once capture is confirmed, writes invoices/orders, updates coupons, and kicks `BillingProvisioner`. |
|
| PayPal API | `api/create_order.php`, `api/capture_order.php`, `webhook.php`, `logs/payment_capture.log` | Implements REST checkout. The cart stamps PayPal `custom_id` with the exact invoice IDs being purchased; capture/webhook handlers use that to mark the correct invoices paid, create/extend orders, and kick provisioning idempotently. |
|
||||||
| Provisioning bridge | `create_servers.php`, `includes/provisioner.php`, `includes/panel_bridge.php` | Shared between panel module and storefront backend. Encapsulates whole server creation/renewal pipeline. |
|
| Provisioning bridge | `create_servers.php`, `includes/provisioner.php`, `includes/panel_bridge.php` | Shared between panel module and storefront backend. Encapsulates whole server creation/renewal pipeline. |
|
||||||
| Cron helpers | `cron-shop.php`, `diag_remote.php` | Automations for renewals, diagnostics, health checks. |
|
| Cron helpers | `cron-shop.php`, `diag_remote.php` | Automations for renewals, diagnostics, health checks. |
|
||||||
| Documentation | `docs.php`, `docs/*`, `docs/admin_xml_notes.php` (PHP mirror of XML wiki) | Provide guidance for editing XML and game configs directly inside repo. |
|
| Documentation | `docs.php`, `docs/*`, `docs/admin_xml_notes.php` (PHP mirror of XML wiki) | Provide guidance for editing XML and game configs directly inside repo. |
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-05-06
|
||||||
|
- **Billing/admin provisioning hardening:** Styled the panel Migrate action like the other server action buttons, switched admin-created billing rows to the canonical monthly/31-day default, and made paid checkout fulfillment sync `billing_orders.home_id`, `billing_invoices.home_id`, and `billing_transactions.home_id` after provisioning so paid orders no longer stay at `home_id = 0`.
|
||||||
|
- **Billing cart data correctness:** `add_to_cart.php` now calculates invoice amounts from the selected slot count and duration, stores `subtotal`/`total_due` metadata, and replaces `ChangeMe` placeholders with securely generated passwords before anything is written to billing tables.
|
||||||
|
- **PayPal/coupon idempotency:** Cart checkout now stamps PayPal `custom_id` with the exact invoice IDs being purchased, capture/free/webhook handlers normalize month=31-day renewals, avoid duplicate transaction logs, and queue provisioning only for orders that still lack a home.
|
||||||
|
|
||||||
## 2026-05-05
|
## 2026-05-05
|
||||||
- **Billing checkout — automatic server provisioning after payment:** Fixed the core provisioning gap where `capture_order.php` never populated `$newOrderIds`, so the auto-provisioner was always skipped. After a successful PayPal capture (or zero-dollar checkout), a `billing_orders` row is now created for each paid invoice and passed to `billing_invoke_provision()` so the game server is created/installed immediately without manual admin action.
|
- **Billing checkout — automatic server provisioning after payment:** Fixed the core provisioning gap where `capture_order.php` never populated `$newOrderIds`, so the auto-provisioner was always skipped. After a successful PayPal capture (or zero-dollar checkout), a `billing_orders` row is now created for each paid invoice and passed to `billing_invoke_provision()` so the game server is created/installed immediately without manual admin action.
|
||||||
- **Billing checkout — duplicate provisioning prevention:** Invoice→Order linkage is written atomically (`billing_invoices.order_id` updated after order creation). Because `getUnpaidInvoicesForUser()` filters on `payment_status NOT IN ('paid',…)`, a retried PayPal capture will find no invoices and skip all processing — preventing duplicate servers.
|
- **Billing checkout — duplicate provisioning prevention:** Invoice→Order linkage is written atomically (`billing_invoices.order_id` updated after order creation). Because `getUnpaidInvoicesForUser()` filters on `payment_status NOT IN ('paid',…)`, a retried PayPal capture will find no invoices and skip all processing — preventing duplicate servers.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,36 @@ require_once(__DIR__ . '/includes/log.php');
|
||||||
/** @var string $table_prefix Table prefix for database tables */
|
/** @var string $table_prefix Table prefix for database tables */
|
||||||
|
|
||||||
// Start session if not already
|
// Start session if not already
|
||||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_name('opengamepanel_web');
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function billing_generate_password(int $bytes = 12): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return substr(bin2hex(random_bytes($bytes)), 0, $bytes * 2);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return substr(hash('sha256', uniqid('gsp', true) . microtime(true)), 0, $bytes * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function billing_normalize_duration(string $duration): array
|
||||||
|
{
|
||||||
|
$duration = strtolower(trim($duration));
|
||||||
|
switch ($duration) {
|
||||||
|
case 'day':
|
||||||
|
case 'daily':
|
||||||
|
return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1];
|
||||||
|
case 'year':
|
||||||
|
case 'yearly':
|
||||||
|
return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365];
|
||||||
|
case 'month':
|
||||||
|
case 'monthly':
|
||||||
|
default:
|
||||||
|
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Immediate request tracing log (helps confirm the script is hit)
|
// Immediate request tracing log (helps confirm the script is hit)
|
||||||
@mkdir(__DIR__ . '/logs', 0775, true);
|
@mkdir(__DIR__ . '/logs', 0775, true);
|
||||||
|
|
@ -51,11 +80,11 @@ $ip_id = isset($_POST['ip_id']) ? intval($_POST['ip_id']) : 0;
|
||||||
$max_players = isset($_POST['max_players']) ? intval($_POST['max_players']) : 0;
|
$max_players = isset($_POST['max_players']) ? intval($_POST['max_players']) : 0;
|
||||||
$qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1;
|
$qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1;
|
||||||
$invoice_duration = isset($_POST['invoice_duration']) ? $_POST['invoice_duration'] : 'month';
|
$invoice_duration = isset($_POST['invoice_duration']) ? $_POST['invoice_duration'] : 'month';
|
||||||
$remote_control_password = isset($_POST['remote_control_password']) ? $_POST['remote_control_password'] : '';
|
$remote_control_password = isset($_POST['remote_control_password']) ? trim((string)$_POST['remote_control_password']) : '';
|
||||||
$ftp_password = isset($_POST['ftp_password']) ? $_POST['ftp_password'] : '';
|
$ftp_password = isset($_POST['ftp_password']) ? trim((string)$_POST['ftp_password']) : '';
|
||||||
|
|
||||||
// Price lookup: try to find service price_monthly
|
// Price lookup: try to find service price_monthly
|
||||||
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
|
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
|
||||||
if (!$db) {
|
if (!$db) {
|
||||||
// Log connection error and exit
|
// Log connection error and exit
|
||||||
@mkdir(__DIR__ . '/logs', 0775, true);
|
@mkdir(__DIR__ . '/logs', 0775, true);
|
||||||
|
|
@ -94,15 +123,25 @@ if (!empty($resolve_username_for_user_id) && $db) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$price = 0.0;
|
$service_name = '';
|
||||||
|
$base_rate = 0.0;
|
||||||
|
$slot_min_qty = 1;
|
||||||
|
$slot_max_qty = 1;
|
||||||
|
$durationInfo = billing_normalize_duration($invoice_duration);
|
||||||
if ($service_id > 0) {
|
if ($service_id > 0) {
|
||||||
$stmt = $db->prepare("SELECT price_monthly, slot_min_qty, slot_max_qty FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
|
$stmt = $db->prepare("SELECT service_name, price_daily, price_monthly, price_year, slot_min_qty, slot_max_qty FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
|
||||||
if ($stmt) {
|
if ($stmt) {
|
||||||
$stmt->bind_param('i', $service_id);
|
$stmt->bind_param('i', $service_id);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$stmt->bind_result($price_monthly, $slot_min_qty, $slot_max_qty);
|
$stmt->bind_result($service_name, $price_daily, $price_monthly, $price_year, $slot_min_qty, $slot_max_qty);
|
||||||
if ($stmt->fetch()) {
|
if ($stmt->fetch()) {
|
||||||
$price = floatval($price_monthly);
|
if ($durationInfo['rate_type'] === 'daily') {
|
||||||
|
$base_rate = floatval($price_daily);
|
||||||
|
} elseif ($durationInfo['rate_type'] === 'yearly') {
|
||||||
|
$base_rate = floatval($price_year);
|
||||||
|
} else {
|
||||||
|
$base_rate = floatval($price_monthly);
|
||||||
|
}
|
||||||
// constrain slots
|
// constrain slots
|
||||||
if ($max_players < $slot_min_qty) $max_players = $slot_min_qty;
|
if ($max_players < $slot_min_qty) $max_players = $slot_min_qty;
|
||||||
if ($max_players > $slot_max_qty) $max_players = $slot_max_qty;
|
if ($max_players > $slot_max_qty) $max_players = $slot_max_qty;
|
||||||
|
|
@ -111,14 +150,27 @@ if ($service_id > 0) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($remote_control_password === '' || strcasecmp($remote_control_password, 'ChangeMe') === 0) {
|
||||||
|
$remote_control_password = billing_generate_password();
|
||||||
|
}
|
||||||
|
if ($ftp_password === '' || strcasecmp($ftp_password, 'ChangeMe') === 0) {
|
||||||
|
$ftp_password = billing_generate_password();
|
||||||
|
}
|
||||||
|
|
||||||
// Insert into {table_prefix}billing_invoices (NOT orders - invoice created first)
|
// Insert into {table_prefix}billing_invoices (NOT orders - invoice created first)
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
$status = 'due'; // Invoice status: due (unpaid), paid
|
$status = 'due'; // Invoice status: due (unpaid), paid
|
||||||
|
$payment_status = 'unpaid';
|
||||||
|
$qty = max(1, $qty);
|
||||||
|
$max_players = max(1, $max_players);
|
||||||
|
$subtotal = round($base_rate * $max_players * $qty, 2);
|
||||||
|
$amount = $subtotal;
|
||||||
|
$period_end = date('Y-m-d H:i:s', strtotime('+' . ($durationInfo['days'] * $qty) . ' days'));
|
||||||
|
|
||||||
// Normal flow: process POST immediately. If debug=1 is passed, we'll still log SQL and show results in logs.
|
// Normal flow: process POST immediately. If debug=1 is passed, we'll still log SQL and show results in logs.
|
||||||
$debug = (isset($_GET['debug']) && $_GET['debug'] == '1') || (isset($_POST['debug']) && $_POST['debug'] == '1');
|
$debug = (isset($_GET['debug']) && $_GET['debug'] == '1') || (isset($_POST['debug']) && $_POST['debug'] == '1');
|
||||||
|
|
||||||
// Build and execute a simple INSERT using mysqli_query for debugging clarity
|
// Build and execute the INSERT with prepared statements
|
||||||
@mkdir(__DIR__ . '/logs', 0775, true);
|
@mkdir(__DIR__ . '/logs', 0775, true);
|
||||||
$logfile = __DIR__ . '/logs/add_to_cart.log';
|
$logfile = __DIR__ . '/logs/add_to_cart.log';
|
||||||
site_log_info('add_to_cart_invoked', ['user_id'=>$user_id, 'service_id'=>$service_id]);
|
site_log_info('add_to_cart_invoked', ['user_id'=>$user_id, 'service_id'=>$service_id]);
|
||||||
|
|
@ -145,35 +197,71 @@ $esc_home_name = mysqli_real_escape_string($db, $home_name);
|
||||||
$esc_ip_id = intval($ip_id);
|
$esc_ip_id = intval($ip_id);
|
||||||
$esc_max_players = intval($max_players);
|
$esc_max_players = intval($max_players);
|
||||||
$esc_qty = intval($qty);
|
$esc_qty = intval($qty);
|
||||||
$esc_invoice_duration = mysqli_real_escape_string($db, $invoice_duration);
|
$description = trim(($service_name !== '' ? $service_name : 'Game Server') . ': ' . $home_name);
|
||||||
$esc_price = number_format((float)$price, 2, '.', '');
|
|
||||||
$esc_remote_control_password = mysqli_real_escape_string($db, $remote_control_password);
|
|
||||||
$esc_ftp_password = mysqli_real_escape_string($db, $ftp_password);
|
|
||||||
$esc_status = mysqli_real_escape_string($db, $status);
|
|
||||||
$esc_customer_name = mysqli_real_escape_string($db, $customer_name);
|
|
||||||
$esc_customer_email = mysqli_real_escape_string($db, $customer_email);
|
|
||||||
$esc_due_date = mysqli_real_escape_string($db, $due_date);
|
|
||||||
$esc_description = mysqli_real_escape_string($db, "New server: {$home_name}");
|
|
||||||
|
|
||||||
$sql = "INSERT INTO {$table_prefix}billing_invoices (
|
$sql = "INSERT INTO {$table_prefix}billing_invoices (
|
||||||
user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
|
order_id, user_id, service_id, home_id, home_name, ip, max_players, remote_control_password,
|
||||||
amount, remote_control_password, ftp_password, status, customer_name,
|
ftp_password, customer_name, customer_email, amount, discount_amount, currency, status,
|
||||||
customer_email, due_date, description, currency, order_id
|
billing_status, invoice_date, due_date, description, invoice_duration, rate_type, rate_per_player,
|
||||||
|
players, period_start, period_end, subtotal, total_due, payment_status, qty, coupon_id
|
||||||
) VALUES (
|
) VALUES (
|
||||||
{$esc_user_id}, {$esc_service_id}, '{$esc_home_name}', {$esc_ip_id},
|
0, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, 0.00, 'USD', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0
|
||||||
{$esc_max_players}, {$esc_qty}, '{$esc_invoice_duration}', {$esc_price},
|
|
||||||
'{$esc_remote_control_password}', '{$esc_ftp_password}', '{$esc_status}',
|
|
||||||
'{$esc_customer_name}', '{$esc_customer_email}', '{$esc_due_date}',
|
|
||||||
'{$esc_description}', 'USD', 0
|
|
||||||
)";
|
)";
|
||||||
|
|
||||||
site_log_info('add_to_cart_sql', ['sql'=>$sql]);
|
$stmt = $db->prepare($sql);
|
||||||
file_put_contents($logfile, date('c') . " - Creating invoice (not order): status=due\n", FILE_APPEND);
|
$res = false;
|
||||||
file_put_contents($logfile, date('c') . " - SQL: " . $sql . "\n", FILE_APPEND);
|
$err_no = 0;
|
||||||
|
$err = '';
|
||||||
|
if ($stmt) {
|
||||||
|
$invoice_duration = $durationInfo['invoice_duration'];
|
||||||
|
$rate_type = $durationInfo['rate_type'];
|
||||||
|
$stmt->bind_param(
|
||||||
|
'iisisssssdsssssdissssddsi',
|
||||||
|
$esc_user_id,
|
||||||
|
$esc_service_id,
|
||||||
|
$home_name,
|
||||||
|
$esc_ip_id,
|
||||||
|
$esc_max_players,
|
||||||
|
$remote_control_password,
|
||||||
|
$ftp_password,
|
||||||
|
$customer_name,
|
||||||
|
$customer_email,
|
||||||
|
$amount,
|
||||||
|
$status,
|
||||||
|
$status,
|
||||||
|
$now,
|
||||||
|
$due_date,
|
||||||
|
$description,
|
||||||
|
$invoice_duration,
|
||||||
|
$rate_type,
|
||||||
|
$base_rate,
|
||||||
|
$max_players,
|
||||||
|
$now,
|
||||||
|
$period_end,
|
||||||
|
$subtotal,
|
||||||
|
$amount,
|
||||||
|
$payment_status,
|
||||||
|
$esc_qty
|
||||||
|
);
|
||||||
|
$res = @$stmt->execute();
|
||||||
|
$err_no = mysqli_errno($db);
|
||||||
|
$err = mysqli_error($db);
|
||||||
|
} else {
|
||||||
|
$err_no = mysqli_errno($db);
|
||||||
|
$err = mysqli_error($db);
|
||||||
|
}
|
||||||
|
|
||||||
$res = @mysqli_query($db, $sql);
|
site_log_info('add_to_cart_invoice', [
|
||||||
$err_no = mysqli_errno($db);
|
'user_id' => $user_id,
|
||||||
$err = mysqli_error($db);
|
'service_id' => $service_id,
|
||||||
|
'home_name' => $home_name,
|
||||||
|
'remote_server_id' => $ip_id,
|
||||||
|
'players' => $max_players,
|
||||||
|
'qty' => $qty,
|
||||||
|
'invoice_duration' => $invoice_duration,
|
||||||
|
'subtotal' => $subtotal,
|
||||||
|
'total_due' => $amount,
|
||||||
|
]);
|
||||||
|
file_put_contents($logfile, date('c') . " - Creating invoice (not order): status=due total_due={$amount}\n", FILE_APPEND);
|
||||||
|
|
||||||
if (!$res || $err_no > 0) {
|
if (!$res || $err_no > 0) {
|
||||||
site_log_error('mysqli_query_failed', ['errno'=>$err_no, 'error'=>$err, 'sql'=>$sql]);
|
site_log_error('mysqli_query_failed', ['errno'=>$err_no, 'error'=>$err, 'sql'=>$sql]);
|
||||||
|
|
@ -193,6 +281,10 @@ if (!$res || $err_no > 0) {
|
||||||
file_put_contents($logfile, date('c') . " - Invoice created: invoice_id={$insert_id}\n", FILE_APPEND);
|
file_put_contents($logfile, date('c') . " - Invoice created: invoice_id={$insert_id}\n", FILE_APPEND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($stmt instanceof mysqli_stmt) {
|
||||||
|
$stmt->close();
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect to cart page
|
// Redirect to cart page
|
||||||
header('Location: cart.php');
|
header('Location: cart.php');
|
||||||
exit;
|
exit;
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,87 @@ mysqli_set_charset($mysqli, 'utf8mb4');
|
||||||
|
|
||||||
$prefix = $table_prefix ?? 'gsp_';
|
$prefix = $table_prefix ?? 'gsp_';
|
||||||
$repo = new BillingRepository($mysqli, $prefix);
|
$repo = new BillingRepository($mysqli, $prefix);
|
||||||
$svc = new BillingService($repo);
|
|
||||||
|
function cap_invoice_ids_from_custom_id(?string $customId): array {
|
||||||
|
if (!is_string($customId) || $customId === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (ctype_digit($customId)) {
|
||||||
|
return [intval($customId)];
|
||||||
|
}
|
||||||
|
if (stripos($customId, 'cart:') !== 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$parts = explode(',', substr($customId, 5));
|
||||||
|
$invoiceIds = [];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$part = trim($part);
|
||||||
|
if ($part !== '' && ctype_digit($part)) {
|
||||||
|
$invoiceIds[] = intval($part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_values(array_unique($invoiceIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cap_duration_metadata(array $invoice): array {
|
||||||
|
$duration = strtolower((string)($invoice['invoice_duration'] ?? $invoice['rate_type'] ?? 'month'));
|
||||||
|
switch ($duration) {
|
||||||
|
case 'day':
|
||||||
|
case 'daily':
|
||||||
|
return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1];
|
||||||
|
case 'year':
|
||||||
|
case 'yearly':
|
||||||
|
return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365];
|
||||||
|
case 'month':
|
||||||
|
case 'monthly':
|
||||||
|
default:
|
||||||
|
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cap_end_date(array $invoice, ?string $fromDate = null): string {
|
||||||
|
$meta = cap_duration_metadata($invoice);
|
||||||
|
$qty = max(1, intval($invoice['qty'] ?? 1));
|
||||||
|
$baseTs = time();
|
||||||
|
if (!empty($fromDate)) {
|
||||||
|
$fromTs = strtotime($fromDate);
|
||||||
|
if ($fromTs !== false && $fromTs > time()) {
|
||||||
|
$baseTs = $fromTs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return date('Y-m-d H:i:s', $baseTs + ($meta['days'] * $qty * 86400));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cap_discount_map(array $invoices, float $paidAmount): array {
|
||||||
|
$baseTotals = [];
|
||||||
|
$baseAmount = 0.0;
|
||||||
|
foreach ($invoices as $invoice) {
|
||||||
|
$invoiceId = intval($invoice['invoice_id'] ?? 0);
|
||||||
|
$lineBase = round((float)($invoice['subtotal'] ?? $invoice['total_due'] ?? $invoice['amount'] ?? 0), 2);
|
||||||
|
$baseTotals[$invoiceId] = $lineBase;
|
||||||
|
$baseAmount += $lineBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
$discountTotal = round(max(0, $baseAmount - $paidAmount), 2);
|
||||||
|
if ($discountTotal <= 0 || $baseAmount <= 0) {
|
||||||
|
return array_fill_keys(array_keys($baseTotals), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$discounts = [];
|
||||||
|
$remaining = $discountTotal;
|
||||||
|
$lastInvoiceId = array_key_last($baseTotals);
|
||||||
|
foreach ($baseTotals as $invoiceId => $lineBase) {
|
||||||
|
if ($invoiceId === $lastInvoiceId) {
|
||||||
|
$lineDiscount = $remaining;
|
||||||
|
} else {
|
||||||
|
$lineDiscount = round($discountTotal * ($lineBase / $baseAmount), 2);
|
||||||
|
$remaining = round($remaining - $lineDiscount, 2);
|
||||||
|
}
|
||||||
|
$discounts[$invoiceId] = min($lineBase, max(0, $lineDiscount));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $discounts;
|
||||||
|
}
|
||||||
|
|
||||||
// Capture payment via PayPal gateway
|
// Capture payment via PayPal gateway
|
||||||
try {
|
try {
|
||||||
|
|
@ -160,104 +240,157 @@ if (!$capture['success']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$txid = $capture['transaction_id'] ?? '';
|
$txid = $capture['transaction_id'] ?? '';
|
||||||
|
$paidAmount = round((float)($capture['amount'] ?? 0), 2);
|
||||||
$capture['payment_method'] = 'paypal';
|
$capture['payment_method'] = 'paypal';
|
||||||
|
$invoiceIds = cap_invoice_ids_from_custom_id($capture['custom_id'] ?? null);
|
||||||
|
$invoices = !empty($invoiceIds)
|
||||||
|
? $repo->getInvoicesForUserByIds($userId, $invoiceIds, true)
|
||||||
|
: $repo->getUnpaidInvoicesForUser($userId);
|
||||||
|
$invoicesPaid = 0;
|
||||||
|
$ordersCreated = 0;
|
||||||
|
$newOrderIds = [];
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$couponId = intval($_SESSION['cart_coupon_id'] ?? 0);
|
||||||
|
$discountMap = cap_discount_map($invoices, $paidAmount);
|
||||||
|
$couponCode = trim((string)($_SESSION['cart_coupon_code'] ?? ''));
|
||||||
|
|
||||||
// Process each unpaid invoice for this user
|
if ($couponId <= 0 && $couponCode !== '') {
|
||||||
$invoices = $repo->getUnpaidInvoicesForUser($userId);
|
$coupon = $repo->getCouponByCode($couponCode);
|
||||||
$invoicesPaid = 0;
|
$couponId = intval($coupon['coupon_id'] ?? 0);
|
||||||
$ordersCreated = 0;
|
}
|
||||||
$newOrderIds = [];
|
|
||||||
$now = date('Y-m-d H:i:s');
|
|
||||||
|
|
||||||
if (empty($invoices)) {
|
if (empty($invoices)) {
|
||||||
cap_log('NO_INVOICES', ['user_id' => $userId]);
|
cap_log('NO_INVOICES', ['user_id' => $userId, 'custom_id' => $capture['custom_id'] ?? null]);
|
||||||
|
ob_clean();
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error_code' => 'no_matching_invoices',
|
||||||
|
'message' => 'No matching unpaid invoices were found for this payment.',
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'request_id' => $requestId,
|
||||||
|
]);
|
||||||
|
mysqli_close($mysqli);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($invoices as $inv) {
|
foreach ($invoices as $inv) {
|
||||||
$invoiceId = intval($inv['invoice_id']);
|
$invoiceId = intval($inv['invoice_id']);
|
||||||
$homeId = intval($inv['home_id'] ?? 0);
|
$homeId = intval($inv['home_id'] ?? 0);
|
||||||
|
$invoiceBase = round((float)($inv['subtotal'] ?? $inv['total_due'] ?? $inv['amount'] ?? 0), 2);
|
||||||
|
$lineDiscount = round((float)($discountMap[$invoiceId] ?? 0), 2);
|
||||||
|
$lineTotal = round(max(0, $invoiceBase - $lineDiscount), 2);
|
||||||
|
$durationMeta = cap_duration_metadata($inv);
|
||||||
|
|
||||||
$result = $svc->processPaymentSuccess($capture, $invoiceId, $userId, $homeId, $inv);
|
$invoiceUpdate = [
|
||||||
if (!$result['success']) {
|
'coupon_id' => $couponId,
|
||||||
cap_log('INVOICE_PAY_FAILED', ['invoice_id' => $invoiceId, 'error' => $result['error'] ?? '']);
|
'discount_amount' => $lineDiscount,
|
||||||
|
'subtotal' => $invoiceBase,
|
||||||
|
'amount' => $lineTotal,
|
||||||
|
'total_due' => $lineTotal,
|
||||||
|
'status' => 'paid',
|
||||||
|
'billing_status' => 'Active',
|
||||||
|
'payment_status' => 'paid',
|
||||||
|
'payment_txid' => $txid,
|
||||||
|
'payment_method' => 'paypal',
|
||||||
|
'paid_date' => $now,
|
||||||
|
'invoice_duration' => $durationMeta['invoice_duration'],
|
||||||
|
'rate_type' => $durationMeta['rate_type'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$repo->updateInvoiceFields($invoiceId, $invoiceUpdate)) {
|
||||||
|
cap_log('INVOICE_PAY_FAILED', ['invoice_id' => $invoiceId, 'db_error' => $mysqli->error]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$invoicesPaid++;
|
$invoicesPaid++;
|
||||||
cap_log('INVOICE_PAID', ['invoice_id' => $invoiceId, 'txid' => $txid]);
|
cap_log('INVOICE_PAID', ['invoice_id' => $invoiceId, 'txid' => $txid, 'amount' => $lineTotal]);
|
||||||
|
|
||||||
// Record transaction in billing_transactions (idempotent — skip on duplicate external ID)
|
|
||||||
$rawCapture = $capture['raw_response'] ?? [];
|
$rawCapture = $capture['raw_response'] ?? [];
|
||||||
if (is_array($rawCapture)) {
|
if (is_array($rawCapture)) {
|
||||||
unset($rawCapture['client_secret'], $rawCapture['access_token']); // never log secrets
|
unset($rawCapture['client_secret'], $rawCapture['access_token']); // never log secrets
|
||||||
}
|
}
|
||||||
$repo->logTransaction([
|
|
||||||
'invoice_id' => $invoiceId,
|
|
||||||
'user_id' => $userId,
|
|
||||||
'home_id' => $homeId,
|
|
||||||
'payment_method' => 'paypal',
|
|
||||||
'transaction_external_id' => $txid,
|
|
||||||
'amount' => (float)($inv['amount'] ?? $inv['total_due'] ?? 0),
|
|
||||||
'currency' => (string)($inv['currency'] ?? 'USD'),
|
|
||||||
'status' => 'completed',
|
|
||||||
'raw_response' => $rawCapture,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Resolve (or create) the billing_orders row for this invoice so the provisioner can run.
|
// Resolve (or create) the billing_orders row for this invoice so the provisioner can run.
|
||||||
// billing_orders.status='Active' is what create_servers.php queries.
|
// billing_orders.status='Active' is what create_servers.php queries.
|
||||||
$orderId = intval($inv['order_id'] ?? 0);
|
$orderId = intval($inv['order_id'] ?? 0);
|
||||||
|
$currentHomeId = $homeId;
|
||||||
$durMap = [
|
|
||||||
'daily' => '+1 day', 'monthly' => '+1 month', 'yearly' => '+1 year',
|
|
||||||
'day' => '+1 day', 'month' => '+1 month', 'year' => '+1 year',
|
|
||||||
];
|
|
||||||
$dur = strtolower($inv['rate_type'] ?? $inv['invoice_duration'] ?? 'month');
|
|
||||||
$newEnd = date('Y-m-d H:i:s', strtotime($durMap[$dur] ?? '+1 month'));
|
|
||||||
|
|
||||||
if ($orderId > 0) {
|
if ($orderId > 0) {
|
||||||
// Existing order linked to this invoice — extend it and mark Active.
|
// Existing order linked to this invoice — extend it and mark Active.
|
||||||
$order = $repo->getOrder($orderId);
|
$order = $repo->getOrder($orderId);
|
||||||
if ($order) {
|
if ($order) {
|
||||||
$fromTs = (strtotime($order['end_date'] ?? '') > time()) ? strtotime($order['end_date']) : time();
|
$newEnd = cap_end_date($inv, $order['end_date'] ?? null);
|
||||||
$newEnd = date('Y-m-d H:i:s', strtotime($durMap[$dur] ?? '+1 month', $fromTs));
|
$currentHomeId = intval($order['home_id'] ?? 0);
|
||||||
$repo->extendOrder($orderId, $newEnd, $txid, $now);
|
$repo->updateOrderFields($orderId, [
|
||||||
|
'status' => 'Active',
|
||||||
|
'end_date' => $newEnd,
|
||||||
|
'payment_txid' => $txid,
|
||||||
|
'paid_ts' => $now,
|
||||||
|
'price' => $lineTotal,
|
||||||
|
'discount_amount' => $lineDiscount,
|
||||||
|
'coupon_id' => $couponId,
|
||||||
|
]);
|
||||||
|
if ($currentHomeId > 0) {
|
||||||
|
$repo->updateInvoiceFields($invoiceId, ['home_id' => $currentHomeId]);
|
||||||
|
}
|
||||||
$ordersCreated++;
|
$ordersCreated++;
|
||||||
// Queue for provisioning only if not yet provisioned (home_id still '0' / empty).
|
// Queue for provisioning only if not yet provisioned (home_id still '0' / empty).
|
||||||
$currentHomeId = (string)($order['home_id'] ?? '0');
|
if ($currentHomeId <= 0) {
|
||||||
if ($currentHomeId === '' || $currentHomeId === '0') {
|
|
||||||
$newOrderIds[] = $orderId;
|
$newOrderIds[] = $orderId;
|
||||||
cap_log('ORDER_QUEUED_PROVISION', ['order_id' => $orderId]);
|
cap_log('ORDER_QUEUED_PROVISION', ['order_id' => $orderId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No billing_orders row yet — create one now so the provisioner can run.
|
// No billing_orders row yet — create one now so the provisioner can run.
|
||||||
|
$newEnd = cap_end_date($inv, null);
|
||||||
$newOrderId = $repo->createOrder([
|
$newOrderId = $repo->createOrder([
|
||||||
'user_id' => intval($inv['user_id']),
|
'user_id' => intval($inv['user_id']),
|
||||||
'service_id' => intval($inv['service_id']),
|
'service_id' => intval($inv['service_id']),
|
||||||
'home_name' => $inv['home_name'] ?? '',
|
'home_name' => $inv['home_name'] ?? '',
|
||||||
'ip' => (string)($inv['ip'] ?? '0'),
|
'ip' => (string)($inv['ip'] ?? '0'),
|
||||||
'qty' => intval($inv['qty'] ?? 1),
|
'qty' => intval($inv['qty'] ?? 1),
|
||||||
'invoice_duration' => $inv['invoice_duration'] ?? 'month',
|
'invoice_duration' => $durationMeta['invoice_duration'],
|
||||||
'max_players' => intval($inv['max_players'] ?? 0),
|
'max_players' => intval($inv['max_players'] ?? 0),
|
||||||
'price' => (float)($inv['amount'] ?? $inv['total_due'] ?? 0),
|
'price' => $lineTotal,
|
||||||
|
'discount_amount' => $lineDiscount,
|
||||||
'remote_control_password' => $inv['remote_control_password'] ?? '',
|
'remote_control_password' => $inv['remote_control_password'] ?? '',
|
||||||
'ftp_password' => $inv['ftp_password'] ?? '',
|
'ftp_password' => $inv['ftp_password'] ?? '',
|
||||||
'status' => 'Active',
|
'status' => 'Active',
|
||||||
'end_date' => $newEnd,
|
'end_date' => $newEnd,
|
||||||
'payment_txid' => $txid,
|
'payment_txid' => $txid,
|
||||||
'paid_ts' => $now,
|
'paid_ts' => $now,
|
||||||
'coupon_id' => intval($inv['coupon_id'] ?? 0),
|
'coupon_id' => $couponId,
|
||||||
]);
|
]);
|
||||||
if ($newOrderId > 0) {
|
if ($newOrderId > 0) {
|
||||||
// Link invoice → order so retried captures are idempotent.
|
// Link invoice → order so retried captures are idempotent.
|
||||||
$repo->updateInvoiceOrderId($invoiceId, $newOrderId);
|
$repo->updateInvoiceOrderId($invoiceId, $newOrderId);
|
||||||
|
$repo->updateInvoiceFields($invoiceId, ['order_id' => $newOrderId]);
|
||||||
$newOrderIds[] = $newOrderId;
|
$newOrderIds[] = $newOrderId;
|
||||||
$ordersCreated++;
|
$ordersCreated++;
|
||||||
cap_log('ORDER_CREATED', ['invoice_id' => $invoiceId, 'order_id' => $newOrderId]);
|
cap_log('ORDER_CREATED', ['invoice_id' => $invoiceId, 'order_id' => $newOrderId]);
|
||||||
} else {
|
} else {
|
||||||
cap_log('ORDER_CREATE_FAILED', ['invoice_id' => $invoiceId, 'db_error' => $mysqli->error]);
|
cap_log('ORDER_CREATE_FAILED', ['invoice_id' => $invoiceId, 'db_error' => $mysqli->error]);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$repo->logTransaction([
|
||||||
|
'invoice_id' => $invoiceId,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'home_id' => $currentHomeId,
|
||||||
|
'payment_method' => 'paypal',
|
||||||
|
'transaction_external_id' => $txid,
|
||||||
|
'amount' => $lineTotal,
|
||||||
|
'currency' => (string)($inv['currency'] ?? 'USD'),
|
||||||
|
'status' => 'completed',
|
||||||
|
'raw_response' => $rawCapture,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($couponId > 0 && $invoicesPaid > 0) {
|
||||||
|
$mysqli->query("UPDATE `{$prefix}billing_coupons`
|
||||||
|
SET current_uses = current_uses + 1
|
||||||
|
WHERE coupon_id = " . intval($couponId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-provision new servers (orders without a home_id)
|
// Auto-provision new servers (orders without a home_id)
|
||||||
|
|
@ -278,12 +411,15 @@ if (!empty($newOrderIds)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unset($_SESSION['cart_coupon_code'], $_SESSION['cart_coupon_id']);
|
||||||
|
|
||||||
mysqli_close($mysqli);
|
mysqli_close($mysqli);
|
||||||
|
|
||||||
cap_log('COMPLETE', ['invoices_paid' => $invoicesPaid, 'txid' => $txid]);
|
cap_log('COMPLETE', ['invoices_paid' => $invoicesPaid, 'txid' => $txid, 'orders' => $newOrderIds]);
|
||||||
|
|
||||||
ob_clean();
|
ob_clean();
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
'status' => 'COMPLETED',
|
'status' => 'COMPLETED',
|
||||||
'txid' => $txid,
|
'txid' => $txid,
|
||||||
'invoices_paid' => $invoicesPaid,
|
'invoices_paid' => $invoicesPaid,
|
||||||
|
|
|
||||||
|
|
@ -258,19 +258,23 @@ $sandbox = function_exists('gsp_paypal_is_sandbox') ? gsp_paypal_is_sandbox
|
||||||
|
|
||||||
// Prepare PayPal items
|
// Prepare PayPal items
|
||||||
$paypal_items = [];
|
$paypal_items = [];
|
||||||
|
$paypal_invoice_ids = [];
|
||||||
foreach ((array)$invoices as $inv) {
|
foreach ((array)$invoices as $inv) {
|
||||||
$game_display = !empty($inv['game_name']) ? $inv['game_name'] : 'Game Server';
|
$game_display = !empty($inv['game_name']) ? $inv['game_name'] : 'Game Server';
|
||||||
$qty = max(1, intval($inv['qty']));
|
$qty = max(1, intval($inv['qty']));
|
||||||
|
$paypal_invoice_ids[] = intval($inv['invoice_id']);
|
||||||
|
$lineAmount = (float)($inv['total_due'] ?? $inv['amount'] ?? 0);
|
||||||
$paypal_items[] = [
|
$paypal_items[] = [
|
||||||
'name' => $inv['home_name'] . ' (' . $game_display . ')',
|
'name' => $inv['home_name'] . ' (' . $game_display . ')',
|
||||||
'description' => $inv['description'] ?? '',
|
'description' => $inv['description'] ?? '',
|
||||||
'quantity' => $qty,
|
'quantity' => $qty,
|
||||||
'unit_amount' => [
|
'unit_amount' => [
|
||||||
'currency_code' => 'USD',
|
'currency_code' => 'USD',
|
||||||
'value' => number_format(floatval($inv['amount']) / $qty, 2, '.', '')
|
'value' => number_format($lineAmount / $qty, 2, '.', '')
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
$paypal_custom_id = 'cart:' . implode(',', $paypal_invoice_ids);
|
||||||
|
|
||||||
// Get site base URL
|
// Get site base URL
|
||||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
|
||||||
|
|
@ -721,6 +725,7 @@ $siteBase = $protocol . $host;
|
||||||
setStatus('Creating order...');
|
setStatus('Creating order...');
|
||||||
return actions.order.create({
|
return actions.order.create({
|
||||||
purchase_units: [{
|
purchase_units: [{
|
||||||
|
custom_id: '<?php echo htmlspecialchars($paypal_custom_id, ENT_QUOTES, 'UTF-8'); ?>',
|
||||||
amount: {
|
amount: {
|
||||||
currency_code: 'USD',
|
currency_code: 'USD',
|
||||||
value: '<?php echo number_format($final_amount, 2, '.', ''); ?>',
|
value: '<?php echo number_format($final_amount, 2, '.', ''); ?>',
|
||||||
|
|
@ -845,4 +850,3 @@ $siteBase = $protocol . $host;
|
||||||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,17 +97,47 @@ $txid = 'free-' . time() . '-' . $userId;
|
||||||
require_once __DIR__ . '/classes/BillingRepository.php';
|
require_once __DIR__ . '/classes/BillingRepository.php';
|
||||||
require_once __DIR__ . '/classes/BillingService.php';
|
require_once __DIR__ . '/classes/BillingService.php';
|
||||||
|
|
||||||
$repo = new BillingRepository($db, $table_prefix);
|
$repo = new BillingRepository($db, $table_prefix);
|
||||||
$svc = new BillingService($repo);
|
|
||||||
$newOrderIds = [];
|
$newOrderIds = [];
|
||||||
|
$durationMeta = static function (array $invoice): array {
|
||||||
|
$duration = strtolower((string)($invoice['invoice_duration'] ?? $invoice['rate_type'] ?? 'month'));
|
||||||
|
switch ($duration) {
|
||||||
|
case 'day':
|
||||||
|
case 'daily':
|
||||||
|
return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1];
|
||||||
|
case 'year':
|
||||||
|
case 'yearly':
|
||||||
|
return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365];
|
||||||
|
case 'month':
|
||||||
|
case 'monthly':
|
||||||
|
default:
|
||||||
|
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
foreach ($invoices as $inv) {
|
foreach ($invoices as $inv) {
|
||||||
$invoiceId = intval($inv['invoice_id']);
|
$invoiceId = intval($inv['invoice_id']);
|
||||||
|
$invoiceBase = round((float)($inv['subtotal'] ?? $inv['total_due'] ?? $inv['amount'] ?? 0), 2);
|
||||||
|
$orderId = intval($inv['order_id'] ?? 0);
|
||||||
|
$meta = $durationMeta($inv);
|
||||||
|
|
||||||
// Mark invoice paid (zero-dollar, method=coupon)
|
$repo->updateInvoiceFields($invoiceId, [
|
||||||
$repo->markInvoicePaid($invoiceId, $txid, 'coupon', $now);
|
'order_id' => $orderId,
|
||||||
|
'coupon_id' => $couponId,
|
||||||
|
'discount_amount' => $invoiceBase,
|
||||||
|
'subtotal' => $invoiceBase,
|
||||||
|
'amount' => 0.00,
|
||||||
|
'total_due' => 0.00,
|
||||||
|
'status' => 'paid',
|
||||||
|
'billing_status' => 'Active',
|
||||||
|
'payment_status' => 'paid',
|
||||||
|
'payment_txid' => $txid,
|
||||||
|
'payment_method' => 'coupon',
|
||||||
|
'paid_date' => $now,
|
||||||
|
'invoice_duration' => $meta['invoice_duration'],
|
||||||
|
'rate_type' => $meta['rate_type'],
|
||||||
|
]);
|
||||||
|
|
||||||
// Log a $0 transaction for the audit trail
|
|
||||||
$repo->logTransaction([
|
$repo->logTransaction([
|
||||||
'invoice_id' => $invoiceId,
|
'invoice_id' => $invoiceId,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
|
|
@ -120,40 +150,72 @@ foreach ($invoices as $inv) {
|
||||||
'raw_response' => ['coupon_id' => $couponId, 'discount_pct' => $discountPct, 'original_amount' => (float)($inv['amount'] ?? 0)],
|
'raw_response' => ['coupon_id' => $couponId, 'discount_pct' => $discountPct, 'original_amount' => (float)($inv['amount'] ?? 0)],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Increment coupon use counter
|
$currentHomeId = 0;
|
||||||
if ($couponId > 0) {
|
$extendFrom = null;
|
||||||
mysqli_query($db, "UPDATE {$table_prefix}billing_coupons
|
if ($orderId > 0) {
|
||||||
SET current_uses = current_uses + 1
|
$order = $repo->getOrder($orderId);
|
||||||
WHERE coupon_id = " . intval($couponId));
|
if ($order) {
|
||||||
|
$currentHomeId = intval($order['home_id'] ?? 0);
|
||||||
|
$extendFrom = $order['end_date'] ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create billing_orders row so the provisioner can run
|
$baseTs = time();
|
||||||
$durMap = ['daily'=>'+1 day','monthly'=>'+1 month','yearly'=>'+1 year','day'=>'+1 day','month'=>'+1 month','year'=>'+1 year'];
|
if (!empty($extendFrom)) {
|
||||||
$dur = strtolower($inv['invoice_duration'] ?? 'month');
|
$extendTs = strtotime($extendFrom);
|
||||||
$endDate = date('Y-m-d H:i:s', strtotime($durMap[$dur] ?? '+1 month'));
|
if ($extendTs !== false && $extendTs > time()) {
|
||||||
|
$baseTs = $extendTs;
|
||||||
$newOrderId = $repo->createOrder([
|
}
|
||||||
'user_id' => intval($inv['user_id']),
|
|
||||||
'service_id' => intval($inv['service_id']),
|
|
||||||
'home_name' => $inv['home_name'] ?? '',
|
|
||||||
'ip' => (string)($inv['ip'] ?? '0'),
|
|
||||||
'qty' => intval($inv['qty'] ?? 1),
|
|
||||||
'invoice_duration' => $inv['invoice_duration'] ?? 'month',
|
|
||||||
'max_players' => intval($inv['max_players'] ?? 0),
|
|
||||||
'price' => 0.00,
|
|
||||||
'remote_control_password' => $inv['remote_control_password'] ?? '',
|
|
||||||
'ftp_password' => $inv['ftp_password'] ?? '',
|
|
||||||
'status' => 'Active',
|
|
||||||
'end_date' => $endDate,
|
|
||||||
'payment_txid' => $txid,
|
|
||||||
'paid_ts' => $now,
|
|
||||||
'coupon_id' => $couponId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($newOrderId > 0) {
|
|
||||||
$repo->updateInvoiceOrderId($invoiceId, $newOrderId);
|
|
||||||
$newOrderIds[] = $newOrderId;
|
|
||||||
}
|
}
|
||||||
|
$endDate = date('Y-m-d H:i:s', $baseTs + ($meta['days'] * max(1, intval($inv['qty'] ?? 1)) * 86400));
|
||||||
|
|
||||||
|
if ($orderId > 0) {
|
||||||
|
$repo->updateOrderFields($orderId, [
|
||||||
|
'status' => 'Active',
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'payment_txid' => $txid,
|
||||||
|
'paid_ts' => $now,
|
||||||
|
'price' => 0.00,
|
||||||
|
'discount_amount' => $invoiceBase,
|
||||||
|
'coupon_id' => $couponId,
|
||||||
|
]);
|
||||||
|
if ($currentHomeId > 0) {
|
||||||
|
$repo->updateInvoiceFields($invoiceId, ['home_id' => $currentHomeId]);
|
||||||
|
} else {
|
||||||
|
$newOrderIds[] = $orderId;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$newOrderId = $repo->createOrder([
|
||||||
|
'user_id' => intval($inv['user_id']),
|
||||||
|
'service_id' => intval($inv['service_id']),
|
||||||
|
'home_name' => $inv['home_name'] ?? '',
|
||||||
|
'ip' => (string)($inv['ip'] ?? '0'),
|
||||||
|
'qty' => intval($inv['qty'] ?? 1),
|
||||||
|
'invoice_duration' => $meta['invoice_duration'],
|
||||||
|
'max_players' => intval($inv['max_players'] ?? 0),
|
||||||
|
'price' => 0.00,
|
||||||
|
'discount_amount' => $invoiceBase,
|
||||||
|
'remote_control_password' => $inv['remote_control_password'] ?? '',
|
||||||
|
'ftp_password' => $inv['ftp_password'] ?? '',
|
||||||
|
'status' => 'Active',
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'payment_txid' => $txid,
|
||||||
|
'paid_ts' => $now,
|
||||||
|
'coupon_id' => $couponId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($newOrderId > 0) {
|
||||||
|
$repo->updateInvoiceOrderId($invoiceId, $newOrderId);
|
||||||
|
$repo->updateInvoiceFields($invoiceId, ['order_id' => $newOrderId]);
|
||||||
|
$newOrderIds[] = $newOrderId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($couponId > 0 && !empty($invoices)) {
|
||||||
|
mysqli_query($db, "UPDATE {$table_prefix}billing_coupons
|
||||||
|
SET current_uses = current_uses + 1
|
||||||
|
WHERE coupon_id = " . intval($couponId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear coupon from session
|
// Clear coupon from session
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ class BillingRepository
|
||||||
{
|
{
|
||||||
private mysqli $db;
|
private mysqli $db;
|
||||||
private string $prefix;
|
private string $prefix;
|
||||||
|
private array $columnCache = [];
|
||||||
|
|
||||||
public function __construct(mysqli $db, string $prefix = 'gsp_')
|
public function __construct(mysqli $db, string $prefix = 'gsp_')
|
||||||
{
|
{
|
||||||
|
|
@ -47,6 +48,32 @@ class BillingRepository
|
||||||
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get invoice rows for a specific user and invoice id list. */
|
||||||
|
public function getInvoicesForUserByIds(int $userId, array $invoiceIds, bool $onlyUnpaid = true): array
|
||||||
|
{
|
||||||
|
$invoiceIds = array_values(array_unique(array_filter(array_map('intval', $invoiceIds), static fn($id) => $id > 0)));
|
||||||
|
if (empty($invoiceIds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$placeholders = implode(',', array_fill(0, count($invoiceIds), '?'));
|
||||||
|
$types = str_repeat('i', count($invoiceIds) + 1);
|
||||||
|
$params = array_merge([$userId], $invoiceIds);
|
||||||
|
$where = $onlyUnpaid ? " AND payment_status IN ('unpaid','due')" : '';
|
||||||
|
$sql = "SELECT * FROM `{$this->prefix}billing_invoices`
|
||||||
|
WHERE user_id = ? AND invoice_id IN ({$placeholders}){$where}
|
||||||
|
ORDER BY invoice_id ASC";
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
if (!$stmt) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$stmt->bind_param($types, ...$params);
|
||||||
|
$stmt->execute();
|
||||||
|
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||||
|
$stmt->close();
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
/** Mark an invoice as paid. Also sets status='paid' so it disappears from cart queries. */
|
/** Mark an invoice as paid. Also sets status='paid' so it disappears from cart queries. */
|
||||||
public function markInvoicePaid(int $invoiceId, string $txid, string $method, string $paidAt): bool
|
public function markInvoicePaid(int $invoiceId, string $txid, string $method, string $paidAt): bool
|
||||||
{
|
{
|
||||||
|
|
@ -78,6 +105,7 @@ class BillingRepository
|
||||||
$txid = (string)($data['payment_txid'] ?? '');
|
$txid = (string)($data['payment_txid'] ?? '');
|
||||||
$paidTs = (string)($data['paid_ts'] ?? $now);
|
$paidTs = (string)($data['paid_ts'] ?? $now);
|
||||||
$couponId = intval($data['coupon_id'] ?? 0);
|
$couponId = intval($data['coupon_id'] ?? 0);
|
||||||
|
$discount = (float)($data['discount_amount'] ?? 0);
|
||||||
$ip = (string)($data['ip'] ?? '0');
|
$ip = (string)($data['ip'] ?? '0');
|
||||||
$qty = intval($data['qty'] ?? 1);
|
$qty = intval($data['qty'] ?? 1);
|
||||||
$maxPl = intval($data['max_players'] ?? 0);
|
$maxPl = intval($data['max_players'] ?? 0);
|
||||||
|
|
@ -88,25 +116,32 @@ class BillingRepository
|
||||||
$invDur = (string)($data['invoice_duration'] ?? 'month');
|
$invDur = (string)($data['invoice_duration'] ?? 'month');
|
||||||
$rcp = (string)($data['remote_control_password'] ?? '');
|
$rcp = (string)($data['remote_control_password'] ?? '');
|
||||||
$ftp = (string)($data['ftp_password'] ?? '');
|
$ftp = (string)($data['ftp_password'] ?? '');
|
||||||
|
$fields = [
|
||||||
$stmt = $this->db->prepare(
|
'user_id' => $userId,
|
||||||
"INSERT INTO `{$this->prefix}billing_orders`
|
'service_id' => $svcId,
|
||||||
(user_id, service_id, home_name, ip, qty, invoice_duration, max_players,
|
'home_name' => $homeName,
|
||||||
price, remote_control_password, ftp_password, home_id, status,
|
'ip' => $ip,
|
||||||
order_date, end_date, payment_txid, paid_ts, coupon_id)
|
'qty' => $qty,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '0', ?, ?, ?, ?, ?, ?)"
|
'invoice_duration' => $invDur,
|
||||||
);
|
'max_players' => $maxPl,
|
||||||
if (!$stmt) return 0;
|
'price' => $price,
|
||||||
$stmt->bind_param(
|
'discount_amount' => $discount,
|
||||||
'iissiisdsssssssi',
|
'remote_control_password' => $rcp,
|
||||||
$userId, $svcId, $homeName, $ip, $qty, $invDur, $maxPl,
|
'ftp_password' => $ftp,
|
||||||
$price, $rcp, $ftp,
|
'home_id' => '0',
|
||||||
$status, $now, $endDate, $txid, $paidTs, $couponId
|
'status' => $status,
|
||||||
);
|
'order_date' => $now,
|
||||||
if (!$stmt->execute()) { $stmt->close(); return 0; }
|
'end_date' => $endDate,
|
||||||
$id = (int)$stmt->insert_id;
|
'payment_txid' => $txid,
|
||||||
$stmt->close();
|
'paid_ts' => $paidTs,
|
||||||
return $id;
|
'coupon_id' => $couponId,
|
||||||
|
];
|
||||||
|
if ($this->hasColumn('billing_orders', 'paypal_data')) {
|
||||||
|
$fields['paypal_data'] = isset($data['paypal_data'])
|
||||||
|
? (is_array($data['paypal_data']) ? json_encode($data['paypal_data']) : (string)$data['paypal_data'])
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
return $this->insertAssoc('billing_orders', $fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -240,6 +275,24 @@ class BillingRepository
|
||||||
public function logTransaction(array $data): int
|
public function logTransaction(array $data): int
|
||||||
{
|
{
|
||||||
$this->ensureBillingTransactionsTable();
|
$this->ensureBillingTransactionsTable();
|
||||||
|
$invoiceId = intval($data['invoice_id'] ?? 0);
|
||||||
|
$extId = (string)($data['transaction_external_id'] ?? '');
|
||||||
|
if ($invoiceId > 0 && $extId !== '') {
|
||||||
|
$existing = $this->db->prepare(
|
||||||
|
"SELECT transaction_id FROM `{$this->prefix}billing_transactions`
|
||||||
|
WHERE invoice_id = ? AND transaction_external_id = ?
|
||||||
|
LIMIT 1"
|
||||||
|
);
|
||||||
|
if ($existing) {
|
||||||
|
$existing->bind_param('is', $invoiceId, $extId);
|
||||||
|
$existing->execute();
|
||||||
|
$row = $existing->get_result()->fetch_assoc();
|
||||||
|
$existing->close();
|
||||||
|
if (!empty($row['transaction_id'])) {
|
||||||
|
return (int)$row['transaction_id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
$stmt = $this->db->prepare(
|
$stmt = $this->db->prepare(
|
||||||
"INSERT INTO `{$this->prefix}billing_transactions`
|
"INSERT INTO `{$this->prefix}billing_transactions`
|
||||||
(invoice_id, user_id, home_id, payment_method, transaction_external_id,
|
(invoice_id, user_id, home_id, payment_method, transaction_external_id,
|
||||||
|
|
@ -248,11 +301,9 @@ class BillingRepository
|
||||||
);
|
);
|
||||||
if (!$stmt) return 0;
|
if (!$stmt) return 0;
|
||||||
$rawJson = is_array($data['raw_response']) ? json_encode($data['raw_response']) : (string)($data['raw_response'] ?? '');
|
$rawJson = is_array($data['raw_response']) ? json_encode($data['raw_response']) : (string)($data['raw_response'] ?? '');
|
||||||
$invoiceId = intval($data['invoice_id'] ?? 0);
|
|
||||||
$userId = intval($data['user_id'] ?? 0);
|
$userId = intval($data['user_id'] ?? 0);
|
||||||
$homeId = intval($data['home_id'] ?? 0);
|
$homeId = intval($data['home_id'] ?? 0);
|
||||||
$method = (string)($data['payment_method'] ?? 'paypal');
|
$method = (string)($data['payment_method'] ?? 'paypal');
|
||||||
$extId = (string)($data['transaction_external_id'] ?? '');
|
|
||||||
$amount = (float)($data['amount'] ?? 0);
|
$amount = (float)($data['amount'] ?? 0);
|
||||||
$currency = (string)($data['currency'] ?? 'USD');
|
$currency = (string)($data['currency'] ?? 'USD');
|
||||||
$status = (string)($data['status'] ?? 'completed');
|
$status = (string)($data['status'] ?? 'completed');
|
||||||
|
|
@ -494,4 +545,130 @@ class BillingRepository
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
return $ok;
|
return $ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCouponByCode(string $couponCode): ?array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"SELECT * FROM `{$this->prefix}billing_coupons`
|
||||||
|
WHERE code = ? AND is_active = 1
|
||||||
|
LIMIT 1"
|
||||||
|
);
|
||||||
|
if (!$stmt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$stmt->bind_param('s', $couponCode);
|
||||||
|
$stmt->execute();
|
||||||
|
$row = $stmt->get_result()->fetch_assoc();
|
||||||
|
$stmt->close();
|
||||||
|
return $row ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateInvoiceFields(int $invoiceId, array $data): bool
|
||||||
|
{
|
||||||
|
return $this->updateAssoc('billing_invoices', 'invoice_id', $invoiceId, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateOrderFields(int $orderId, array $data): bool
|
||||||
|
{
|
||||||
|
return $this->updateAssoc('billing_orders', 'order_id', $orderId, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasColumn(string $table, string $column): bool
|
||||||
|
{
|
||||||
|
$cacheKey = $table . '.' . $column;
|
||||||
|
if (array_key_exists($cacheKey, $this->columnCache)) {
|
||||||
|
return $this->columnCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableName = $this->db->real_escape_string($this->prefix . $table);
|
||||||
|
$columnName = $this->db->real_escape_string($column);
|
||||||
|
$res = $this->db->query(
|
||||||
|
"SELECT COUNT(*) AS cnt
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = '{$tableName}'
|
||||||
|
AND COLUMN_NAME = '{$columnName}'"
|
||||||
|
);
|
||||||
|
$exists = $res ? ((int)($res->fetch_assoc()['cnt'] ?? 0) > 0) : false;
|
||||||
|
$this->columnCache[$cacheKey] = $exists;
|
||||||
|
return $exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function insertAssoc(string $table, array $data): int
|
||||||
|
{
|
||||||
|
if (empty($data)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$columns = array_keys($data);
|
||||||
|
$placeholders = implode(',', array_fill(0, count($columns), '?'));
|
||||||
|
$sql = sprintf(
|
||||||
|
"INSERT INTO `%s%s` (%s) VALUES (%s)",
|
||||||
|
$this->prefix,
|
||||||
|
$table,
|
||||||
|
implode(',', array_map(static fn($field) => "`{$field}`", $columns)),
|
||||||
|
$placeholders
|
||||||
|
);
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
if (!$stmt) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
[$types, $values] = $this->prepareBindValues($data);
|
||||||
|
$stmt->bind_param($types, ...$values);
|
||||||
|
if (!$stmt->execute()) {
|
||||||
|
$stmt->close();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$id = (int)$stmt->insert_id;
|
||||||
|
$stmt->close();
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateAssoc(string $table, string $idColumn, int $idValue, array $data): bool
|
||||||
|
{
|
||||||
|
$data = array_filter($data, static fn($value) => $value !== null);
|
||||||
|
if (empty($data)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$set = [];
|
||||||
|
foreach (array_keys($data) as $field) {
|
||||||
|
$set[] = "`{$field}` = ?";
|
||||||
|
}
|
||||||
|
$sql = sprintf(
|
||||||
|
"UPDATE `%s%s` SET %s WHERE `%s` = ? LIMIT 1",
|
||||||
|
$this->prefix,
|
||||||
|
$table,
|
||||||
|
implode(', ', $set),
|
||||||
|
$idColumn
|
||||||
|
);
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
if (!$stmt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
[$types, $values] = $this->prepareBindValues($data);
|
||||||
|
$types .= 'i';
|
||||||
|
$values[] = $idValue;
|
||||||
|
$stmt->bind_param($types, ...$values);
|
||||||
|
$ok = $stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
return $ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepareBindValues(array $data): array
|
||||||
|
{
|
||||||
|
$types = '';
|
||||||
|
$values = [];
|
||||||
|
foreach ($data as $value) {
|
||||||
|
if (is_int($value)) {
|
||||||
|
$types .= 'i';
|
||||||
|
$values[] = $value;
|
||||||
|
} elseif (is_float($value)) {
|
||||||
|
$types .= 'd';
|
||||||
|
$values[] = $value;
|
||||||
|
} else {
|
||||||
|
$types .= 's';
|
||||||
|
$values[] = ($value === null) ? null : (string)$value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [$types, $values];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,17 @@
|
||||||
require_once __DIR__ . '/../../includes/lib_remote.php';
|
require_once __DIR__ . '/../../includes/lib_remote.php';
|
||||||
require_once __DIR__ . '/../config_games/server_config_parser.php';
|
require_once __DIR__ . '/../config_games/server_config_parser.php';
|
||||||
|
|
||||||
|
if (!function_exists('billing_generate_provision_password')) {
|
||||||
|
function billing_generate_provision_password(int $bytes = 12)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return substr(bin2hex(random_bytes($bytes)), 0, $bytes * 2);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return substr(hash('sha256', uniqid('gsp-provision', true) . microtime(true)), 0, $bytes * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!function_exists('billing_invoke_provision')) {
|
if (!function_exists('billing_invoke_provision')) {
|
||||||
function billing_invoke_provision(array $options = array())
|
function billing_invoke_provision(array $options = array())
|
||||||
{
|
{
|
||||||
|
|
@ -18,7 +29,8 @@ if (!function_exists('billing_invoke_provision')) {
|
||||||
|
|
||||||
function exec_ogp_module()
|
function exec_ogp_module()
|
||||||
{
|
{
|
||||||
global $db,$view,$settings;
|
global $db,$view,$settings,$table_prefix;
|
||||||
|
$db_prefix = isset($table_prefix) ? $table_prefix : '';
|
||||||
|
|
||||||
// $now is used in multiple branches below — define it once here so it is
|
// $now is used in multiple branches below — define it once here so it is
|
||||||
// always a string that date() / strtotime() can handle safely (PHP 8 fix).
|
// always a string that date() / strtotime() can handle safely (PHP 8 fix).
|
||||||
|
|
@ -48,9 +60,9 @@ function exec_ogp_module()
|
||||||
// Handle provision_all request - provision all Active (paid) orders for this user
|
// Handle provision_all request - provision all Active (paid) orders for this user
|
||||||
if ($provision_all) {
|
if ($provision_all) {
|
||||||
if ( $isAdmin ){
|
if ( $isAdmin ){
|
||||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE status='Active' AND (home_id='0' OR home_id='') ORDER BY order_id" );
|
$orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE status='Active' AND (home_id='0' OR home_id='') ORDER BY order_id" );
|
||||||
} else {
|
} else {
|
||||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE user_id=".$db->realEscapeSingle($user_id)." AND status='Active' AND (home_id='0' OR home_id='') ORDER BY order_id" );
|
$orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE user_id=".$db->realEscapeSingle($user_id)." AND status='Active' AND (home_id='0' OR home_id='') ORDER BY order_id" );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle provision_single or order_id parameter - provision specific order
|
// Handle provision_single or order_id parameter - provision specific order
|
||||||
|
|
@ -62,9 +74,9 @@ function exec_ogp_module()
|
||||||
}
|
}
|
||||||
$idList = implode(',', array_map('intval', $orderIds));
|
$idList = implode(',', array_map('intval', $orderIds));
|
||||||
if ( $isAdmin ){
|
if ( $isAdmin ){
|
||||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id IN ($idList) AND status='Active'" );
|
$orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE order_id IN ($idList) AND status='Active'" );
|
||||||
} else {
|
} else {
|
||||||
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id IN ($idList) AND user_id=".$db->realEscapeSingle($user_id)." AND status='Active'" );
|
$orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE order_id IN ($idList) AND user_id=".$db->realEscapeSingle($user_id)." AND status='Active'" );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$processed_orders = array();
|
$processed_orders = array();
|
||||||
|
|
@ -75,19 +87,28 @@ function exec_ogp_module()
|
||||||
|
|
||||||
foreach ((array)$orders as $order)
|
foreach ((array)$orders as $order)
|
||||||
{
|
{
|
||||||
|
$end_date = null;
|
||||||
|
$end_date_str = null;
|
||||||
$order_id = $order['order_id'];
|
$order_id = $order['order_id'];
|
||||||
$processed_orders[] = intval($order_id);
|
$processed_orders[] = intval($order_id);
|
||||||
$service_id = $order['service_id'];
|
$service_id = $order['service_id'];
|
||||||
$home_name = $order['home_name'];
|
$home_name = $order['home_name'];
|
||||||
$remote_control_password = $order['remote_control_password'];
|
$remote_control_password = $order['remote_control_password'];
|
||||||
$ftp_password = $order['ftp_password'];
|
$ftp_password = $order['ftp_password'];
|
||||||
|
if ($remote_control_password === '' || strcasecmp((string)$remote_control_password, 'ChangeMe') === 0) {
|
||||||
|
$remote_control_password = billing_generate_provision_password();
|
||||||
|
}
|
||||||
|
if ($ftp_password === '' || strcasecmp((string)$ftp_password, 'ChangeMe') === 0) {
|
||||||
|
$ftp_password = billing_generate_provision_password();
|
||||||
|
}
|
||||||
$ip = $order['ip'];
|
$ip = $order['ip'];
|
||||||
$max_players = $order['max_players'];
|
$max_players = $order['max_players'];
|
||||||
$user_id = $order['user_id'];
|
$user_id = $order['user_id'];
|
||||||
$extended = isset($order['extended']) && $order['extended'] == "1" ? TRUE : FALSE;
|
$extended = isset($order['extended']) && $order['extended'] == "1" ? TRUE : FALSE;
|
||||||
|
$alreadyProvisioned = !$extended && intval($order['home_id'] ?? 0) > 0;
|
||||||
//Query service info
|
//Query service info
|
||||||
$service = $db->resultQuery( "SELECT *
|
$service = $db->resultQuery( "SELECT *
|
||||||
FROM OGP_DB_PREFIXbilling_services
|
FROM `{$db_prefix}billing_services`
|
||||||
WHERE service_id=".$db->realEscapeSingle($service_id) );
|
WHERE service_id=".$db->realEscapeSingle($service_id) );
|
||||||
|
|
||||||
if( !empty( $service[0] ) )
|
if( !empty( $service[0] ) )
|
||||||
|
|
@ -106,7 +127,11 @@ function exec_ogp_module()
|
||||||
else
|
else
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if($extended)
|
if($alreadyProvisioned)
|
||||||
|
{
|
||||||
|
$home_id = intval($order['home_id']);
|
||||||
|
}
|
||||||
|
elseif($extended)
|
||||||
{
|
{
|
||||||
$home_id = $order['home_id'];
|
$home_id = $order['home_id'];
|
||||||
|
|
||||||
|
|
@ -167,7 +192,7 @@ function exec_ogp_module()
|
||||||
|
|
||||||
//Add IP:Port Pair to the Game Home
|
//Add IP:Port Pair to the Game Home
|
||||||
//need to get the IP_ID for this remote server.
|
//need to get the IP_ID for this remote server.
|
||||||
$result = $db->resultQuery("SELECT ip_id FROM OGP_DB_PREFIXremote_server_ips WHERE remote_server_id=".$ip);
|
$result = $db->resultQuery("SELECT ip_id FROM `{$db_prefix}remote_server_ips` WHERE remote_server_id=".$ip);
|
||||||
foreach ((array)$result as $rs)
|
foreach ((array)$result as $rs)
|
||||||
{
|
{
|
||||||
$ip_id = $rs['ip_id'];
|
$ip_id = $rs['ip_id'];
|
||||||
|
|
@ -290,7 +315,15 @@ function exec_ogp_module()
|
||||||
// Status values: Active (provisioned & current), Invoiced (renewal invoice open),
|
// Status values: Active (provisioned & current), Invoiced (renewal invoice open),
|
||||||
// Expired (past due and awaiting deletion)
|
// Expired (past due and awaiting deletion)
|
||||||
// end_date / next_invoice_date: when the next renewal invoice should be generated
|
// end_date / next_invoice_date: when the next renewal invoice should be generated
|
||||||
if ($order['invoice_duration'] == "day")
|
if ($alreadyProvisioned)
|
||||||
|
{
|
||||||
|
$existing_end = strtotime((string)($order['end_date'] ?? ''));
|
||||||
|
if ($existing_end === false || $existing_end <= 0) {
|
||||||
|
$existing_end = time();
|
||||||
|
}
|
||||||
|
$end_date_str = date('Y-m-d H:i:s', $existing_end);
|
||||||
|
}
|
||||||
|
elseif ($order['invoice_duration'] == "day")
|
||||||
{
|
{
|
||||||
|
|
||||||
if(empty($order['end_date']) || $order['end_date'] === NULL){
|
if(empty($order['end_date']) || $order['end_date'] === NULL){
|
||||||
|
|
@ -310,7 +343,7 @@ function exec_ogp_module()
|
||||||
{
|
{
|
||||||
// this is a new order
|
// this is a new order
|
||||||
if(empty($order['end_date']) || $order['end_date'] === NULL){
|
if(empty($order['end_date']) || $order['end_date'] === NULL){
|
||||||
$end_date = strtotime('+'.$order['qty'].' month');
|
$end_date = strtotime('+'.(intval($order['qty']) * 31).' day');
|
||||||
|
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
|
|
@ -319,7 +352,7 @@ function exec_ogp_module()
|
||||||
if ($current_end === false) {
|
if ($current_end === false) {
|
||||||
$current_end = time(); // fallback to now if date is invalid
|
$current_end = time(); // fallback to now if date is invalid
|
||||||
}
|
}
|
||||||
$end_date = strtotime('+'.$order['qty'].' month', $current_end);
|
$end_date = strtotime('+'.(intval($order['qty']) * 31).' day', $current_end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif ($order['invoice_duration'] == "year")
|
elseif ($order['invoice_duration'] == "year")
|
||||||
|
|
@ -339,24 +372,37 @@ function exec_ogp_module()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
$end_date_str = date('Y-m-d H:i:s', $end_date);
|
if (!isset($end_date_str)) {
|
||||||
|
$end_date_str = date('Y-m-d H:i:s', $end_date);
|
||||||
|
}
|
||||||
|
|
||||||
// Set order status to 'Active' (server provisioned and current)
|
// Set order status to 'Active' (server provisioned and current)
|
||||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders
|
$db->query("UPDATE `{$db_prefix}billing_orders`
|
||||||
SET status='Active'
|
SET status='Active'
|
||||||
WHERE order_id=".$db->realEscapeSingle($order_id));
|
WHERE order_id=".$db->realEscapeSingle($order_id));
|
||||||
|
|
||||||
// Set the order expiration / next renewal date
|
// Set the order expiration / next renewal date
|
||||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders
|
$db->query("UPDATE `{$db_prefix}billing_orders`
|
||||||
SET end_date='" . $db->realEscapeSingle($end_date_str) . "'
|
SET end_date='" . $db->realEscapeSingle($end_date_str) . "',
|
||||||
|
remote_control_password='" . $db->realEscapeSingle($remote_control_password) . "',
|
||||||
|
ftp_password='" . $db->realEscapeSingle($ftp_password) . "'
|
||||||
WHERE order_id=".$db->realEscapeSingle($order_id));
|
WHERE order_id=".$db->realEscapeSingle($order_id));
|
||||||
|
|
||||||
// Save home_id created by this order
|
// Save home_id created by this order
|
||||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders
|
$db->query("UPDATE `{$db_prefix}billing_orders`
|
||||||
SET home_id='" . $db->realEscapeSingle($home_id) . "' WHERE order_id=".$db->realEscapeSingle($order_id));
|
SET home_id='" . $db->realEscapeSingle($home_id) . "' WHERE order_id=".$db->realEscapeSingle($order_id));
|
||||||
|
|
||||||
|
$db->query("UPDATE `{$db_prefix}billing_invoices`
|
||||||
|
SET home_id=" . $db->realEscapeSingle($home_id) . ",
|
||||||
|
billing_status='Active'
|
||||||
|
WHERE order_id=" . $db->realEscapeSingle($order_id));
|
||||||
|
|
||||||
|
$db->query("UPDATE `{$db_prefix}billing_transactions`
|
||||||
|
SET home_id=" . $db->realEscapeSingle($home_id) . "
|
||||||
|
WHERE invoice_id IN (SELECT invoice_id FROM `{$db_prefix}billing_invoices` WHERE order_id=" . $db->realEscapeSingle($order_id) . ")");
|
||||||
|
|
||||||
// Set billing_status and next_invoice_date on server_homes
|
// Set billing_status and next_invoice_date on server_homes
|
||||||
$db->query("UPDATE OGP_DB_PREFIXserver_homes
|
$db->query("UPDATE `{$db_prefix}server_homes`
|
||||||
SET billing_status = 'Active',
|
SET billing_status = 'Active',
|
||||||
next_invoice_date = '" . $db->realEscapeSingle($end_date_str) . "',
|
next_invoice_date = '" . $db->realEscapeSingle($end_date_str) . "',
|
||||||
billing_enabled = 1
|
billing_enabled = 1
|
||||||
|
|
@ -366,7 +412,7 @@ function exec_ogp_module()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$db->query( "UPDATE OGP_DB_PREFIXgame_mods SET max_players= ".$order['max_players']." WHERE home_id=".$db->realEscapeSingle($home_id));
|
$db->query( "UPDATE `{$db_prefix}game_mods` SET max_players= ".$order['max_players']." WHERE home_id=".$db->realEscapeSingle($home_id));
|
||||||
|
|
||||||
// Show results and redirect
|
// Show results and redirect
|
||||||
if ($provisioned_count > 0) {
|
if ($provisioned_count > 0) {
|
||||||
|
|
@ -401,5 +447,3 @@ function exec_ogp_module()
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
// Module general information
|
// Module general information
|
||||||
$module_title = "billing";
|
$module_title = "billing";
|
||||||
$module_version = "3.4";
|
$module_version = "3.4";
|
||||||
$db_version = 4;
|
$db_version = 5;
|
||||||
$module_required = FALSE;
|
$module_required = FALSE;
|
||||||
// Module description
|
// Module description
|
||||||
$module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management.";
|
$module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management.";
|
||||||
|
|
@ -203,8 +203,8 @@ $install_queries[1] = array(
|
||||||
KEY `enabled` (`enabled`)
|
KEY `enabled` (`enabled`)
|
||||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;",
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;",
|
||||||
|
|
||||||
// Drop legacy mapping table if it still exists from older installs
|
// Legacy mapping table is handled by a later idempotent migration.
|
||||||
"DROP TABLE IF EXISTS `".OGP_DB_PREFIX."billing_service_remote_servers`"
|
"SELECT 1"
|
||||||
);
|
);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
@ -368,4 +368,22 @@ $install_queries[4] = array(
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// db_version 5 — Preserve the unused legacy service/node mapping table by
|
||||||
|
// renaming it to a *_deprecated_backup table instead of dropping it.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
$install_queries[5] = array(
|
||||||
|
function($db) {
|
||||||
|
$legacy = 'OGP_DB_PREFIXbilling_service_remote_servers';
|
||||||
|
$backup = 'OGP_DB_PREFIXbilling_service_remote_servers_deprecated_backup';
|
||||||
|
$legacyCheck = $db->resultQuery("SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{$legacy}'");
|
||||||
|
if (!$legacyCheck || empty($legacyCheck[0]['cnt']) || (int)$legacyCheck[0]['cnt'] === 0) return true;
|
||||||
|
|
||||||
|
$backupCheck = $db->resultQuery("SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{$backup}'");
|
||||||
|
if ($backupCheck && !empty($backupCheck[0]['cnt']) && (int)$backupCheck[0]['cnt'] > 0) return true;
|
||||||
|
|
||||||
|
return (bool)$db->query("RENAME TABLE `{$legacy}` TO `{$backup}`");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
|
||||||
|
|
@ -185,8 +185,8 @@ if ($row['price_monthly'] == 0.0) {
|
||||||
<table class="float-left">
|
<table class="float-left">
|
||||||
<form method="post" action="add_to_cart.php">
|
<form method="post" action="add_to_cart.php">
|
||||||
<input type="hidden" name="service_id" size="15" value="<?php echo intval($_REQUEST['service_id'] ?? $row['service_id'] ?? 0); ?>">
|
<input type="hidden" name="service_id" size="15" value="<?php echo intval($_REQUEST['service_id'] ?? $row['service_id'] ?? 0); ?>">
|
||||||
<input type="hidden" name="remote_control_password" size="15" value="ChangeMe">
|
<input type="hidden" name="remote_control_password" size="15" value="">
|
||||||
<input type="hidden" name="ftp_password" size="15" value="ChangeMe">
|
<input type="hidden" name="ftp_password" size="15" value="">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="right"><b>Game Server Name</b> </td>
|
<td align="right"><b>Game Server Name</b> </td>
|
||||||
<td align="left">
|
<td align="left">
|
||||||
|
|
@ -269,7 +269,7 @@ if ($row['price_monthly'] == 0.0) {
|
||||||
slider.oninput = function() {
|
slider.oninput = function() {
|
||||||
output.innerHTML = this.value;
|
output.innerHTML = this.value;
|
||||||
invoiceDuration.innerHTML = "Duration: "+invoiceslider.value+" months";
|
invoiceDuration.innerHTML = "Duration: "+invoiceslider.value+" months";
|
||||||
totalvalue = invoiceslider.value * <?php echo number_format($row['price_monthly'],2);?>;
|
totalvalue = slider.value * invoiceslider.value * <?php echo number_format($row['price_monthly'],2);?>;
|
||||||
price.innerHTML = "Total Price: $"+totalvalue.toFixed(2) ;
|
price.innerHTML = "Total Price: $"+totalvalue.toFixed(2) ;
|
||||||
}
|
}
|
||||||
invoiceslider.oninput = function() {
|
invoiceslider.oninput = function() {
|
||||||
|
|
|
||||||
|
|
@ -428,6 +428,27 @@ function wh_fetch_paypal_order(string $api_base, string $access_token, string $o
|
||||||
* Match the PayPal capture to a billing invoice, mark it paid, create/extend billing_orders,
|
* Match the PayPal capture to a billing invoice, mark it paid, create/extend billing_orders,
|
||||||
* and trigger server provisioning. Returns the billing_order_id or 0.
|
* and trigger server provisioning. Returns the billing_order_id or 0.
|
||||||
*/
|
*/
|
||||||
|
function wh_invoice_ids_from_custom_id($custom_id): array
|
||||||
|
{
|
||||||
|
if (!is_string($custom_id) || $custom_id === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (ctype_digit($custom_id)) {
|
||||||
|
return [intval($custom_id)];
|
||||||
|
}
|
||||||
|
if (stripos($custom_id, 'cart:') !== 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$invoice_ids = [];
|
||||||
|
foreach (explode(',', substr($custom_id, 5)) as $part) {
|
||||||
|
$part = trim($part);
|
||||||
|
if ($part !== '' && ctype_digit($part)) {
|
||||||
|
$invoice_ids[] = intval($part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_values(array_unique($invoice_ids));
|
||||||
|
}
|
||||||
|
|
||||||
function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $billing_dir = ''): int
|
function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $billing_dir = ''): int
|
||||||
{
|
{
|
||||||
$txid = $payment['capture_id'] ?? '';
|
$txid = $payment['capture_id'] ?? '';
|
||||||
|
|
@ -441,7 +462,17 @@ function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $bil
|
||||||
$invoices = [];
|
$invoices = [];
|
||||||
|
|
||||||
// 1) Match by numeric custom_id (which we set to invoice_id when creating the PayPal order)
|
// 1) Match by numeric custom_id (which we set to invoice_id when creating the PayPal order)
|
||||||
if (!empty($custom_id) && ctype_digit((string)$custom_id)) {
|
$custom_invoice_ids = wh_invoice_ids_from_custom_id($custom_id);
|
||||||
|
if (!empty($custom_invoice_ids)) {
|
||||||
|
$id_list = implode(',', array_map('intval', $custom_invoice_ids));
|
||||||
|
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_invoices` WHERE invoice_id IN ({$id_list}) AND status = 'due' ORDER BY invoice_id ASC");
|
||||||
|
if ($res) {
|
||||||
|
while ($row = mysqli_fetch_assoc($res)) {
|
||||||
|
$invoices[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (!empty($custom_id) && ctype_digit((string)$custom_id)) {
|
||||||
$inv_id = intval($custom_id);
|
$inv_id = intval($custom_id);
|
||||||
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_invoices` WHERE invoice_id = {$inv_id} AND status = 'due' LIMIT 1");
|
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_invoices` WHERE invoice_id = {$inv_id} AND status = 'due' LIMIT 1");
|
||||||
if ($res && $row = mysqli_fetch_assoc($res)) {
|
if ($res && $row = mysqli_fetch_assoc($res)) {
|
||||||
|
|
@ -477,6 +508,7 @@ function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $bil
|
||||||
}
|
}
|
||||||
|
|
||||||
$last_order_id = 0;
|
$last_order_id = 0;
|
||||||
|
$applied_coupon_id = 0;
|
||||||
|
|
||||||
foreach ($invoices as $inv) {
|
foreach ($invoices as $inv) {
|
||||||
$invoice_id = intval($inv['invoice_id']);
|
$invoice_id = intval($inv['invoice_id']);
|
||||||
|
|
@ -497,15 +529,14 @@ function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $bil
|
||||||
// Increment coupon usage if applicable
|
// Increment coupon usage if applicable
|
||||||
$coupon_id = intval($inv['coupon_id'] ?? 0);
|
$coupon_id = intval($inv['coupon_id'] ?? 0);
|
||||||
if ($coupon_id > 0) {
|
if ($coupon_id > 0) {
|
||||||
mysqli_query($db, "UPDATE `{$pfx}billing_coupons` SET current_uses = current_uses + 1 WHERE coupon_id = {$coupon_id}");
|
$applied_coupon_id = $coupon_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duration → months
|
$duration_days = 31 * $qty;
|
||||||
$months = 1;
|
if (stripos($duration, 'day') !== false) {
|
||||||
if (stripos($duration, 'year') !== false) {
|
$duration_days = $qty;
|
||||||
$months = $qty * 12;
|
} elseif (stripos($duration, 'year') !== false) {
|
||||||
} else {
|
$duration_days = 365 * $qty;
|
||||||
$months = $qty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($order_id > 0) {
|
if ($order_id > 0) {
|
||||||
|
|
@ -515,7 +546,7 @@ function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $bil
|
||||||
$current_end = $row['end_date'] ?? $now;
|
$current_end = $row['end_date'] ?? $now;
|
||||||
$extend_from = (strtotime($current_end) > time()) ? $current_end : $now;
|
$extend_from = (strtotime($current_end) > time()) ? $current_end : $now;
|
||||||
$dt = new DateTime($extend_from);
|
$dt = new DateTime($extend_from);
|
||||||
$dt->modify('+' . $months . ' months');
|
$dt->modify('+' . $duration_days . ' days');
|
||||||
$new_end = $dt->format('Y-m-d H:i:s');
|
$new_end = $dt->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
$stmt = mysqli_prepare($db, "UPDATE `{$pfx}billing_orders` SET end_date=?, status='Active', payment_txid=?, paid_ts=? WHERE order_id=? LIMIT 1");
|
$stmt = mysqli_prepare($db, "UPDATE `{$pfx}billing_orders` SET end_date=?, status='Active', payment_txid=?, paid_ts=? WHERE order_id=? LIMIT 1");
|
||||||
|
|
@ -530,7 +561,7 @@ function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $bil
|
||||||
} else {
|
} else {
|
||||||
// New order: create billing_orders row
|
// New order: create billing_orders row
|
||||||
$dt = new DateTime($now);
|
$dt = new DateTime($now);
|
||||||
$dt->modify('+' . $months . ' months');
|
$dt->modify('+' . $duration_days . ' days');
|
||||||
$end_date = $dt->format('Y-m-d H:i:s');
|
$end_date = $dt->format('Y-m-d H:i:s');
|
||||||
$invoice_amount = floatval($inv['amount'] ?? $inv['total_due'] ?? 0);
|
$invoice_amount = floatval($inv['amount'] ?? $inv['total_due'] ?? 0);
|
||||||
$price = number_format($invoice_amount, 2, '.', '');
|
$price = number_format($invoice_amount, 2, '.', '');
|
||||||
|
|
@ -572,6 +603,10 @@ function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $bil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($applied_coupon_id > 0) {
|
||||||
|
mysqli_query($db, "UPDATE `{$pfx}billing_coupons` SET current_uses = current_uses + 1 WHERE coupon_id = {$applied_coupon_id}");
|
||||||
|
}
|
||||||
|
|
||||||
return $last_order_id;
|
return $last_order_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,8 +116,8 @@ include(__DIR__ . '/includes/menu.php');
|
||||||
<!-- Order Form -->
|
<!-- Order Form -->
|
||||||
<form method="post" action="order_server.php">
|
<form method="post" action="order_server.php">
|
||||||
<input type="hidden" name="service_id" value="<?php echo $row['service_id']; ?>">
|
<input type="hidden" name="service_id" value="<?php echo $row['service_id']; ?>">
|
||||||
<input type="hidden" name="remote_control_password" value="ChangeMe">
|
<input type="hidden" name="remote_control_password" value="">
|
||||||
<input type="hidden" name="ftp_password" value="ChangeMe">
|
<input type="hidden" name="ftp_password" value="">
|
||||||
<table class="float-left">
|
<table class="float-left">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="right"><b>Game Server Name</b></td>
|
<td align="right"><b>Game Server Name</b></td>
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,9 @@ function exec_ogp_module()
|
||||||
0, // max_players — set later via edit_home
|
0, // max_players — set later via edit_home
|
||||||
$access_rights,
|
$access_rights,
|
||||||
$ftp,
|
$ftp,
|
||||||
$new_home_id
|
$new_home_id,
|
||||||
|
$control_password,
|
||||||
|
$ftppassword
|
||||||
);
|
);
|
||||||
|
|
||||||
$view->refresh("?m=user_games&p=edit&home_id=$new_home_id", 0);
|
$view->refresh("?m=user_games&p=edit&home_id=$new_home_id", 0);
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,9 @@ if (!function_exists('admin_register_server_in_billing')) {
|
||||||
$max_players,
|
$max_players,
|
||||||
$access_rights,
|
$access_rights,
|
||||||
$ftp,
|
$ftp,
|
||||||
$home_id
|
$home_id,
|
||||||
|
$remote_control_password = '',
|
||||||
|
$ftp_password = ''
|
||||||
) {
|
) {
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
// 1. Resolve service_id: find an existing billing_service matching //
|
// 1. Resolve service_id: find an existing billing_service matching //
|
||||||
|
|
@ -76,7 +78,7 @@ if (!function_exists('admin_register_server_in_billing')) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
$end_date = date('Y-m-d H:i:s', strtotime('+1 year'));
|
$end_date = date('Y-m-d H:i:s', strtotime('+31 days'));
|
||||||
$ftp_flag = $ftp ? 'enabled' : 'disabled';
|
$ftp_flag = $ftp ? 'enabled' : 'disabled';
|
||||||
|
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
|
|
@ -90,24 +92,34 @@ if (!function_exists('admin_register_server_in_billing')) {
|
||||||
'order_id' => 0,
|
'order_id' => 0,
|
||||||
'user_id' => intval($user_id),
|
'user_id' => intval($user_id),
|
||||||
'service_id' => $service_id,
|
'service_id' => $service_id,
|
||||||
|
'home_id' => intval($home_id),
|
||||||
'home_name' => $home_name,
|
'home_name' => $home_name,
|
||||||
'ip' => intval($rserver_id),
|
'ip' => intval($rserver_id),
|
||||||
'max_players' => intval($max_players),
|
'max_players' => intval($max_players),
|
||||||
'remote_control_password' => '',
|
'remote_control_password' => $remote_control_password,
|
||||||
'ftp_password' => '',
|
'ftp_password' => $ftp_password,
|
||||||
'customer_name' => $customer_name,
|
'customer_name' => $customer_name,
|
||||||
'customer_email' => $customer_email,
|
'customer_email' => $customer_email,
|
||||||
'amount' => '0.00',
|
'amount' => '0.00',
|
||||||
'discount_amount' => '0.00',
|
'discount_amount' => '0.00',
|
||||||
'currency' => 'USD',
|
'currency' => 'USD',
|
||||||
'status' => 'paid',
|
'status' => 'paid',
|
||||||
|
'billing_status' => 'Active',
|
||||||
'invoice_date' => $now,
|
'invoice_date' => $now,
|
||||||
'due_date' => $now,
|
'due_date' => $now,
|
||||||
'paid_date' => $now,
|
'paid_date' => $now,
|
||||||
'payment_txid' => 'admin-created',
|
'payment_txid' => 'admin-created',
|
||||||
'payment_method' => 'admin',
|
'payment_method' => 'admin',
|
||||||
'description' => 'Admin-created server: ' . $home_name,
|
'description' => 'Admin-created server: ' . $home_name,
|
||||||
'invoice_duration' => 'year',
|
'invoice_duration' => 'month',
|
||||||
|
'rate_type' => 'monthly',
|
||||||
|
'rate_per_player' => '0.0000',
|
||||||
|
'players' => intval($max_players),
|
||||||
|
'period_start' => $now,
|
||||||
|
'period_end' => $end_date,
|
||||||
|
'subtotal' => '0.00',
|
||||||
|
'total_due' => '0.00',
|
||||||
|
'payment_status' => 'paid',
|
||||||
'qty' => 1,
|
'qty' => 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -125,12 +137,12 @@ if (!function_exists('admin_register_server_in_billing')) {
|
||||||
'home_name' => $home_name,
|
'home_name' => $home_name,
|
||||||
'ip' => intval($rserver_id),
|
'ip' => intval($rserver_id),
|
||||||
'qty' => 1,
|
'qty' => 1,
|
||||||
'invoice_duration' => 'year',
|
'invoice_duration' => 'month',
|
||||||
'max_players' => intval($max_players),
|
'max_players' => intval($max_players),
|
||||||
'price' => '0.00',
|
'price' => '0.00',
|
||||||
'discount_amount' => '0.00',
|
'discount_amount' => '0.00',
|
||||||
'remote_control_password' => '',
|
'remote_control_password' => $remote_control_password,
|
||||||
'ftp_password' => '',
|
'ftp_password' => $ftp_password,
|
||||||
'home_id' => intval($home_id),
|
'home_id' => intval($home_id),
|
||||||
'status' => 'Active',
|
'status' => 'Active',
|
||||||
'order_date' => $now,
|
'order_date' => $now,
|
||||||
|
|
|
||||||
|
|
@ -90,9 +90,9 @@ function exec_ogp_module()
|
||||||
echo empty($row['home_name']) ? get_lang('not_available') : htmlentities($row['home_name']);
|
echo empty($row['home_name']) ? get_lang('not_available') : htmlentities($row['home_name']);
|
||||||
$expiration_date = $row['server_expiration_date'] == "X" ? "X" : date('d/m/Y H:i:s', is_numeric($row['server_expiration_date']) ? (int)$row['server_expiration_date'] : strtotime($row['server_expiration_date']));
|
$expiration_date = $row['server_expiration_date'] == "X" ? "X" : date('d/m/Y H:i:s', is_numeric($row['server_expiration_date']) ? (int)$row['server_expiration_date'] : strtotime($row['server_expiration_date']));
|
||||||
echo "</td><td>".$expiration_date."</td><td>
|
echo "</td><td>".$expiration_date."</td><td>
|
||||||
<a href='?m=user_games&p=del&home_id=$row[home_id]'>[".get_lang('delete')."]</a>
|
<a class='btn btn-danger btn-xs' href='?m=user_games&p=del&home_id=$row[home_id]'>".get_lang('delete')."</a>
|
||||||
<a href='?m=user_games&p=edit&home_id=$row[home_id]'>[".get_lang('edit')."]</a>
|
<a class='btn btn-primary btn-xs' href='?m=user_games&p=edit&home_id=$row[home_id]'>".get_lang('edit')."</a>
|
||||||
<a href='?m=user_games&p=migrate&home_id=$row[home_id]'>[".get_lang('migrate')."]</a>
|
<a class='btn btn-info btn-xs' href='?m=user_games&p=migrate&home_id=$row[home_id]'>".get_lang('migrate')."</a>
|
||||||
</td></tr>";
|
</td></tr>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue