Merge pull request #133 from GameServerPanel/copilot/gsp-fix-admin-migrate-button-formatting
Fix billing fulfillment/home linking and default admin server billing term
This commit is contained in:
commit
21c163a4b1
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.
|
||||
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`).
|
||||
- Panel module page `home.php?m=billing&p=provision_servers`.
|
||||
- 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
# 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
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,36 @@ require_once(__DIR__ . '/includes/log.php');
|
|||
/** @var string $table_prefix Table prefix for database tables */
|
||||
|
||||
// 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)
|
||||
@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;
|
||||
$qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1;
|
||||
$invoice_duration = isset($_POST['invoice_duration']) ? $_POST['invoice_duration'] : 'month';
|
||||
$remote_control_password = isset($_POST['remote_control_password']) ? $_POST['remote_control_password'] : '';
|
||||
$ftp_password = isset($_POST['ftp_password']) ? $_POST['ftp_password'] : '';
|
||||
$remote_control_password = isset($_POST['remote_control_password']) ? trim((string)$_POST['remote_control_password']) : '';
|
||||
$ftp_password = isset($_POST['ftp_password']) ? trim((string)$_POST['ftp_password']) : '';
|
||||
|
||||
// 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) {
|
||||
// Log connection error and exit
|
||||
@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) {
|
||||
$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) {
|
||||
$stmt->bind_param('i', $service_id);
|
||||
$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()) {
|
||||
$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
|
||||
if ($max_players < $slot_min_qty) $max_players = $slot_min_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)
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$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.
|
||||
$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);
|
||||
$logfile = __DIR__ . '/logs/add_to_cart.log';
|
||||
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_max_players = intval($max_players);
|
||||
$esc_qty = intval($qty);
|
||||
$esc_invoice_duration = mysqli_real_escape_string($db, $invoice_duration);
|
||||
$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}");
|
||||
|
||||
$description = trim(($service_name !== '' ? $service_name : 'Game Server') . ': ' . $home_name);
|
||||
$sql = "INSERT INTO {$table_prefix}billing_invoices (
|
||||
user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
|
||||
amount, remote_control_password, ftp_password, status, customer_name,
|
||||
customer_email, due_date, description, currency, order_id
|
||||
order_id, user_id, service_id, home_id, home_name, ip, max_players, remote_control_password,
|
||||
ftp_password, customer_name, customer_email, amount, discount_amount, currency, status,
|
||||
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 (
|
||||
{$esc_user_id}, {$esc_service_id}, '{$esc_home_name}', {$esc_ip_id},
|
||||
{$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
|
||||
0, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, 0.00, 'USD', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0
|
||||
)";
|
||||
|
||||
site_log_info('add_to_cart_sql', ['sql'=>$sql]);
|
||||
file_put_contents($logfile, date('c') . " - Creating invoice (not order): status=due\n", FILE_APPEND);
|
||||
file_put_contents($logfile, date('c') . " - SQL: " . $sql . "\n", FILE_APPEND);
|
||||
$stmt = $db->prepare($sql);
|
||||
$res = false;
|
||||
$err_no = 0;
|
||||
$err = '';
|
||||
if ($stmt) {
|
||||
$invoice_duration = $durationInfo['invoice_duration'];
|
||||
$rate_type = $durationInfo['rate_type'];
|
||||
$stmt->bind_param(
|
||||
'iisiissssdsssssssdissddsi',
|
||||
$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);
|
||||
$err_no = mysqli_errno($db);
|
||||
$err = mysqli_error($db);
|
||||
site_log_info('add_to_cart_invoice', [
|
||||
'user_id' => $user_id,
|
||||
'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) {
|
||||
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);
|
||||
}
|
||||
|
||||
if ($stmt instanceof mysqli_stmt) {
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
// Redirect to cart page
|
||||
header('Location: cart.php');
|
||||
exit;
|
||||
|
|
|
|||
|
|
@ -101,7 +101,87 @@ mysqli_set_charset($mysqli, 'utf8mb4');
|
|||
|
||||
$prefix = $table_prefix ?? 'gsp_';
|
||||
$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_get_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_get_end_date(array $invoice, ?string $fromDate = null): string {
|
||||
$meta = cap_get_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
|
||||
try {
|
||||
|
|
@ -160,104 +240,157 @@ if (!$capture['success']) {
|
|||
}
|
||||
|
||||
$txid = $capture['transaction_id'] ?? '';
|
||||
$paidAmount = round((float)($capture['amount'] ?? 0), 2);
|
||||
$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
|
||||
$invoices = $repo->getUnpaidInvoicesForUser($userId);
|
||||
$invoicesPaid = 0;
|
||||
$ordersCreated = 0;
|
||||
$newOrderIds = [];
|
||||
$now = date('Y-m-d H:i:s');
|
||||
if ($couponId <= 0 && $couponCode !== '') {
|
||||
$coupon = $repo->getCouponByCode($couponCode);
|
||||
$couponId = intval($coupon['coupon_id'] ?? 0);
|
||||
}
|
||||
|
||||
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) {
|
||||
$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_get_duration_metadata($inv);
|
||||
|
||||
$result = $svc->processPaymentSuccess($capture, $invoiceId, $userId, $homeId, $inv);
|
||||
if (!$result['success']) {
|
||||
cap_log('INVOICE_PAY_FAILED', ['invoice_id' => $invoiceId, 'error' => $result['error'] ?? '']);
|
||||
$invoiceUpdate = [
|
||||
'coupon_id' => $couponId,
|
||||
'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;
|
||||
}
|
||||
|
||||
$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'] ?? [];
|
||||
if (is_array($rawCapture)) {
|
||||
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.
|
||||
// billing_orders.status='Active' is what create_servers.php queries.
|
||||
$orderId = intval($inv['order_id'] ?? 0);
|
||||
|
||||
$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'));
|
||||
$currentHomeId = $homeId;
|
||||
|
||||
if ($orderId > 0) {
|
||||
// Existing order linked to this invoice — extend it and mark Active.
|
||||
$order = $repo->getOrder($orderId);
|
||||
if ($order) {
|
||||
$fromTs = (strtotime($order['end_date'] ?? '') > time()) ? strtotime($order['end_date']) : time();
|
||||
$newEnd = date('Y-m-d H:i:s', strtotime($durMap[$dur] ?? '+1 month', $fromTs));
|
||||
$repo->extendOrder($orderId, $newEnd, $txid, $now);
|
||||
$newEnd = cap_get_end_date($inv, $order['end_date'] ?? null);
|
||||
$currentHomeId = intval($order['home_id'] ?? 0);
|
||||
$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++;
|
||||
// Queue for provisioning only if not yet provisioned (home_id still '0' / empty).
|
||||
$currentHomeId = (string)($order['home_id'] ?? '0');
|
||||
if ($currentHomeId === '' || $currentHomeId === '0') {
|
||||
if ($currentHomeId <= 0) {
|
||||
$newOrderIds[] = $orderId;
|
||||
cap_log('ORDER_QUEUED_PROVISION', ['order_id' => $orderId]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No billing_orders row yet — create one now so the provisioner can run.
|
||||
$newEnd = cap_get_end_date($inv, null);
|
||||
$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',
|
||||
'invoice_duration' => $durationMeta['invoice_duration'],
|
||||
'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'] ?? '',
|
||||
'ftp_password' => $inv['ftp_password'] ?? '',
|
||||
'status' => 'Active',
|
||||
'end_date' => $newEnd,
|
||||
'payment_txid' => $txid,
|
||||
'paid_ts' => $now,
|
||||
'coupon_id' => intval($inv['coupon_id'] ?? 0),
|
||||
'coupon_id' => $couponId,
|
||||
]);
|
||||
if ($newOrderId > 0) {
|
||||
// Link invoice → order so retried captures are idempotent.
|
||||
$repo->updateInvoiceOrderId($invoiceId, $newOrderId);
|
||||
$repo->updateInvoiceFields($invoiceId, ['order_id' => $newOrderId]);
|
||||
$newOrderIds[] = $newOrderId;
|
||||
$ordersCreated++;
|
||||
cap_log('ORDER_CREATED', ['invoice_id' => $invoiceId, 'order_id' => $newOrderId]);
|
||||
} else {
|
||||
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)
|
||||
|
|
@ -278,12 +411,15 @@ if (!empty($newOrderIds)) {
|
|||
}
|
||||
}
|
||||
|
||||
unset($_SESSION['cart_coupon_code'], $_SESSION['cart_coupon_id']);
|
||||
|
||||
mysqli_close($mysqli);
|
||||
|
||||
cap_log('COMPLETE', ['invoices_paid' => $invoicesPaid, 'txid' => $txid]);
|
||||
cap_log('COMPLETE', ['invoices_paid' => $invoicesPaid, 'txid' => $txid, 'orders' => $newOrderIds]);
|
||||
|
||||
ob_clean();
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'COMPLETED',
|
||||
'txid' => $txid,
|
||||
'invoices_paid' => $invoicesPaid,
|
||||
|
|
|
|||
|
|
@ -258,19 +258,23 @@ $sandbox = function_exists('gsp_paypal_is_sandbox') ? gsp_paypal_is_sandbox
|
|||
|
||||
// Prepare PayPal items
|
||||
$paypal_items = [];
|
||||
$paypal_invoice_ids = [];
|
||||
foreach ((array)$invoices as $inv) {
|
||||
$game_display = !empty($inv['game_name']) ? $inv['game_name'] : 'Game Server';
|
||||
$qty = max(1, intval($inv['qty']));
|
||||
$paypal_invoice_ids[] = intval($inv['invoice_id']);
|
||||
$lineAmount = (float)($inv['total_due'] ?? $inv['amount'] ?? 0);
|
||||
$paypal_items[] = [
|
||||
'name' => $inv['home_name'] . ' (' . $game_display . ')',
|
||||
'description' => $inv['description'] ?? '',
|
||||
'quantity' => $qty,
|
||||
'unit_amount' => [
|
||||
'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
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
|
||||
|
|
@ -721,6 +725,7 @@ $siteBase = $protocol . $host;
|
|||
setStatus('Creating order...');
|
||||
return actions.order.create({
|
||||
purchase_units: [{
|
||||
custom_id: '<?php echo htmlspecialchars($paypal_custom_id, ENT_QUOTES, 'UTF-8'); ?>',
|
||||
amount: {
|
||||
currency_code: 'USD',
|
||||
value: '<?php echo number_format($final_amount, 2, '.', ''); ?>',
|
||||
|
|
@ -845,4 +850,3 @@ $siteBase = $protocol . $host;
|
|||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
|||
|
|
@ -97,17 +97,47 @@ $txid = 'free-' . time() . '-' . $userId;
|
|||
require_once __DIR__ . '/classes/BillingRepository.php';
|
||||
require_once __DIR__ . '/classes/BillingService.php';
|
||||
|
||||
$repo = new BillingRepository($db, $table_prefix);
|
||||
$svc = new BillingService($repo);
|
||||
$repo = new BillingRepository($db, $table_prefix);
|
||||
$newOrderIds = [];
|
||||
$duration_meta = 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) {
|
||||
$invoiceId = intval($inv['invoice_id']);
|
||||
$invoiceBase = round((float)($inv['subtotal'] ?? $inv['total_due'] ?? $inv['amount'] ?? 0), 2);
|
||||
$orderId = intval($inv['order_id'] ?? 0);
|
||||
$meta = $duration_meta($inv);
|
||||
|
||||
// Mark invoice paid (zero-dollar, method=coupon)
|
||||
$repo->markInvoicePaid($invoiceId, $txid, 'coupon', $now);
|
||||
$repo->updateInvoiceFields($invoiceId, [
|
||||
'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([
|
||||
'invoice_id' => $invoiceId,
|
||||
'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)],
|
||||
]);
|
||||
|
||||
// Increment coupon use counter
|
||||
if ($couponId > 0) {
|
||||
mysqli_query($db, "UPDATE {$table_prefix}billing_coupons
|
||||
SET current_uses = current_uses + 1
|
||||
WHERE coupon_id = " . intval($couponId));
|
||||
$currentHomeId = 0;
|
||||
$extendFrom = null;
|
||||
if ($orderId > 0) {
|
||||
$order = $repo->getOrder($orderId);
|
||||
if ($order) {
|
||||
$currentHomeId = intval($order['home_id'] ?? 0);
|
||||
$extendFrom = $order['end_date'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create billing_orders row so the provisioner can run
|
||||
$durMap = ['daily'=>'+1 day','monthly'=>'+1 month','yearly'=>'+1 year','day'=>'+1 day','month'=>'+1 month','year'=>'+1 year'];
|
||||
$dur = strtolower($inv['invoice_duration'] ?? 'month');
|
||||
$endDate = date('Y-m-d H:i:s', strtotime($durMap[$dur] ?? '+1 month'));
|
||||
|
||||
$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;
|
||||
$baseTs = time();
|
||||
if (!empty($extendFrom)) {
|
||||
$extendTs = strtotime($extendFrom);
|
||||
if ($extendTs !== false && $extendTs > time()) {
|
||||
$baseTs = $extendTs;
|
||||
}
|
||||
}
|
||||
$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
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class BillingRepository
|
|||
{
|
||||
private mysqli $db;
|
||||
private string $prefix;
|
||||
private array $columnCache = [];
|
||||
|
||||
public function __construct(mysqli $db, string $prefix = 'gsp_')
|
||||
{
|
||||
|
|
@ -47,6 +48,32 @@ class BillingRepository
|
|||
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. */
|
||||
public function markInvoicePaid(int $invoiceId, string $txid, string $method, string $paidAt): bool
|
||||
{
|
||||
|
|
@ -78,6 +105,7 @@ class BillingRepository
|
|||
$txid = (string)($data['payment_txid'] ?? '');
|
||||
$paidTs = (string)($data['paid_ts'] ?? $now);
|
||||
$couponId = intval($data['coupon_id'] ?? 0);
|
||||
$discount = (float)($data['discount_amount'] ?? 0);
|
||||
$ip = (string)($data['ip'] ?? '0');
|
||||
$qty = intval($data['qty'] ?? 1);
|
||||
$maxPl = intval($data['max_players'] ?? 0);
|
||||
|
|
@ -88,25 +116,32 @@ class BillingRepository
|
|||
$invDur = (string)($data['invoice_duration'] ?? 'month');
|
||||
$rcp = (string)($data['remote_control_password'] ?? '');
|
||||
$ftp = (string)($data['ftp_password'] ?? '');
|
||||
|
||||
$stmt = $this->db->prepare(
|
||||
"INSERT INTO `{$this->prefix}billing_orders`
|
||||
(user_id, service_id, home_name, ip, qty, invoice_duration, max_players,
|
||||
price, remote_control_password, ftp_password, home_id, status,
|
||||
order_date, end_date, payment_txid, paid_ts, coupon_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '0', ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
if (!$stmt) return 0;
|
||||
$stmt->bind_param(
|
||||
'iissiisdsssssssi',
|
||||
$userId, $svcId, $homeName, $ip, $qty, $invDur, $maxPl,
|
||||
$price, $rcp, $ftp,
|
||||
$status, $now, $endDate, $txid, $paidTs, $couponId
|
||||
);
|
||||
if (!$stmt->execute()) { $stmt->close(); return 0; }
|
||||
$id = (int)$stmt->insert_id;
|
||||
$stmt->close();
|
||||
return $id;
|
||||
$fields = [
|
||||
'user_id' => $userId,
|
||||
'service_id' => $svcId,
|
||||
'home_name' => $homeName,
|
||||
'ip' => $ip,
|
||||
'qty' => $qty,
|
||||
'invoice_duration' => $invDur,
|
||||
'max_players' => $maxPl,
|
||||
'price' => $price,
|
||||
'discount_amount' => $discount,
|
||||
'remote_control_password' => $rcp,
|
||||
'ftp_password' => $ftp,
|
||||
'home_id' => '0',
|
||||
'status' => $status,
|
||||
'order_date' => $now,
|
||||
'end_date' => $endDate,
|
||||
'payment_txid' => $txid,
|
||||
'paid_ts' => $paidTs,
|
||||
'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
|
||||
{
|
||||
$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(
|
||||
"INSERT INTO `{$this->prefix}billing_transactions`
|
||||
(invoice_id, user_id, home_id, payment_method, transaction_external_id,
|
||||
|
|
@ -248,11 +301,9 @@ class BillingRepository
|
|||
);
|
||||
if (!$stmt) return 0;
|
||||
$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);
|
||||
$homeId = intval($data['home_id'] ?? 0);
|
||||
$method = (string)($data['payment_method'] ?? 'paypal');
|
||||
$extId = (string)($data['transaction_external_id'] ?? '');
|
||||
$amount = (float)($data['amount'] ?? 0);
|
||||
$currency = (string)($data['currency'] ?? 'USD');
|
||||
$status = (string)($data['status'] ?? 'completed');
|
||||
|
|
@ -494,4 +545,130 @@ class BillingRepository
|
|||
$stmt->close();
|
||||
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__ . '/../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')) {
|
||||
function billing_invoke_provision(array $options = array())
|
||||
{
|
||||
|
|
@ -18,7 +29,8 @@ if (!function_exists('billing_invoke_provision')) {
|
|||
|
||||
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
|
||||
// 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
|
||||
if ($provision_all) {
|
||||
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 {
|
||||
$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
|
||||
|
|
@ -62,9 +74,9 @@ function exec_ogp_module()
|
|||
}
|
||||
$idList = implode(',', array_map('intval', $orderIds));
|
||||
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 {
|
||||
$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();
|
||||
|
|
@ -75,19 +87,28 @@ function exec_ogp_module()
|
|||
|
||||
foreach ((array)$orders as $order)
|
||||
{
|
||||
$end_date = null;
|
||||
$end_date_str = null;
|
||||
$order_id = $order['order_id'];
|
||||
$processed_orders[] = intval($order_id);
|
||||
$service_id = $order['service_id'];
|
||||
$home_name = $order['home_name'];
|
||||
$remote_control_password = $order['remote_control_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'];
|
||||
$max_players = $order['max_players'];
|
||||
$user_id = $order['user_id'];
|
||||
$extended = isset($order['extended']) && $order['extended'] == "1" ? TRUE : FALSE;
|
||||
$alreadyProvisioned = !$extended && intval($order['home_id'] ?? 0) > 0;
|
||||
//Query service info
|
||||
$service = $db->resultQuery( "SELECT *
|
||||
FROM OGP_DB_PREFIXbilling_services
|
||||
FROM `{$db_prefix}billing_services`
|
||||
WHERE service_id=".$db->realEscapeSingle($service_id) );
|
||||
|
||||
if( !empty( $service[0] ) )
|
||||
|
|
@ -106,7 +127,11 @@ function exec_ogp_module()
|
|||
else
|
||||
return;
|
||||
|
||||
if($extended)
|
||||
if($alreadyProvisioned)
|
||||
{
|
||||
$home_id = intval($order['home_id']);
|
||||
}
|
||||
elseif($extended)
|
||||
{
|
||||
$home_id = $order['home_id'];
|
||||
|
||||
|
|
@ -167,7 +192,7 @@ function exec_ogp_module()
|
|||
|
||||
//Add IP:Port Pair to the Game Home
|
||||
//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)
|
||||
{
|
||||
$ip_id = $rs['ip_id'];
|
||||
|
|
@ -290,7 +315,15 @@ function exec_ogp_module()
|
|||
// Status values: Active (provisioned & current), Invoiced (renewal invoice open),
|
||||
// Expired (past due and awaiting deletion)
|
||||
// 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){
|
||||
|
|
@ -310,7 +343,7 @@ function exec_ogp_module()
|
|||
{
|
||||
// this is a new order
|
||||
if(empty($order['end_date']) || $order['end_date'] === NULL){
|
||||
$end_date = strtotime('+'.$order['qty'].' month');
|
||||
$end_date = strtotime('+'.(intval($order['qty']) * 31).' day');
|
||||
|
||||
}
|
||||
else{
|
||||
|
|
@ -319,7 +352,7 @@ function exec_ogp_module()
|
|||
if ($current_end === false) {
|
||||
$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")
|
||||
|
|
@ -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)
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders
|
||||
$db->query("UPDATE `{$db_prefix}billing_orders`
|
||||
SET status='Active'
|
||||
WHERE order_id=".$db->realEscapeSingle($order_id));
|
||||
|
||||
// Set the order expiration / next renewal date
|
||||
$db->query("UPDATE OGP_DB_PREFIXbilling_orders
|
||||
SET end_date='" . $db->realEscapeSingle($end_date_str) . "'
|
||||
$db->query("UPDATE `{$db_prefix}billing_orders`
|
||||
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));
|
||||
|
||||
// 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));
|
||||
|
||||
$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
|
||||
$db->query("UPDATE OGP_DB_PREFIXserver_homes
|
||||
$db->query("UPDATE `{$db_prefix}server_homes`
|
||||
SET billing_status = 'Active',
|
||||
next_invoice_date = '" . $db->realEscapeSingle($end_date_str) . "',
|
||||
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
|
||||
if ($provisioned_count > 0) {
|
||||
|
|
@ -401,5 +447,3 @@ function exec_ogp_module()
|
|||
?>
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
// Module general information
|
||||
$module_title = "billing";
|
||||
$module_version = "3.4";
|
||||
$db_version = 4;
|
||||
$db_version = 5;
|
||||
$module_required = FALSE;
|
||||
// Module description
|
||||
$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`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;",
|
||||
|
||||
// Drop legacy mapping table if it still exists from older installs
|
||||
"DROP TABLE IF EXISTS `".OGP_DB_PREFIX."billing_service_remote_servers`"
|
||||
// Legacy mapping table is handled by a later idempotent migration.
|
||||
"SELECT 1"
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
@ -368,4 +368,22 @@ $install_queries[4] = array(
|
|||
) 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">
|
||||
<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="remote_control_password" size="15" value="ChangeMe">
|
||||
<input type="hidden" name="ftp_password" size="15" value="ChangeMe">
|
||||
<input type="hidden" name="remote_control_password" size="15" value="">
|
||||
<input type="hidden" name="ftp_password" size="15" value="">
|
||||
<tr>
|
||||
<td align="right"><b>Game Server Name</b> </td>
|
||||
<td align="left">
|
||||
|
|
@ -269,7 +269,7 @@ if ($row['price_monthly'] == 0.0) {
|
|||
slider.oninput = function() {
|
||||
output.innerHTML = this.value;
|
||||
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) ;
|
||||
}
|
||||
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,
|
||||
* 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
|
||||
{
|
||||
$txid = $payment['capture_id'] ?? '';
|
||||
|
|
@ -441,7 +462,17 @@ function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $bil
|
|||
$invoices = [];
|
||||
|
||||
// 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);
|
||||
$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)) {
|
||||
|
|
@ -477,6 +508,7 @@ function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $bil
|
|||
}
|
||||
|
||||
$last_order_id = 0;
|
||||
$applied_coupon_id = 0;
|
||||
|
||||
foreach ($invoices as $inv) {
|
||||
$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
|
||||
$coupon_id = intval($inv['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
|
||||
$months = 1;
|
||||
if (stripos($duration, 'year') !== false) {
|
||||
$months = $qty * 12;
|
||||
} else {
|
||||
$months = $qty;
|
||||
$duration_days = 31 * $qty;
|
||||
if (stripos($duration, 'day') !== false) {
|
||||
$duration_days = $qty;
|
||||
} elseif (stripos($duration, 'year') !== false) {
|
||||
$duration_days = 365 * $qty;
|
||||
}
|
||||
|
||||
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;
|
||||
$extend_from = (strtotime($current_end) > time()) ? $current_end : $now;
|
||||
$dt = new DateTime($extend_from);
|
||||
$dt->modify('+' . $months . ' months');
|
||||
$dt->modify('+' . $duration_days . ' days');
|
||||
$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");
|
||||
|
|
@ -530,7 +561,7 @@ function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $bil
|
|||
} else {
|
||||
// New order: create billing_orders row
|
||||
$dt = new DateTime($now);
|
||||
$dt->modify('+' . $months . ' months');
|
||||
$dt->modify('+' . $duration_days . ' days');
|
||||
$end_date = $dt->format('Y-m-d H:i:s');
|
||||
$invoice_amount = floatval($inv['amount'] ?? $inv['total_due'] ?? 0);
|
||||
$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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ include(__DIR__ . '/includes/menu.php');
|
|||
<!-- Order Form -->
|
||||
<form method="post" action="order_server.php">
|
||||
<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="ftp_password" value="ChangeMe">
|
||||
<input type="hidden" name="remote_control_password" value="">
|
||||
<input type="hidden" name="ftp_password" value="">
|
||||
<table class="float-left">
|
||||
<tr>
|
||||
<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
|
||||
$access_rights,
|
||||
$ftp,
|
||||
$new_home_id
|
||||
$new_home_id,
|
||||
$control_password,
|
||||
$ftppassword
|
||||
);
|
||||
|
||||
$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,
|
||||
$access_rights,
|
||||
$ftp,
|
||||
$home_id
|
||||
$home_id,
|
||||
$remote_control_password = '',
|
||||
$ftp_password = ''
|
||||
) {
|
||||
// ------------------------------------------------------------------ //
|
||||
// 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');
|
||||
$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';
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
|
|
@ -90,24 +92,34 @@ if (!function_exists('admin_register_server_in_billing')) {
|
|||
'order_id' => 0,
|
||||
'user_id' => intval($user_id),
|
||||
'service_id' => $service_id,
|
||||
'home_id' => intval($home_id),
|
||||
'home_name' => $home_name,
|
||||
'ip' => intval($rserver_id),
|
||||
'max_players' => intval($max_players),
|
||||
'remote_control_password' => '',
|
||||
'ftp_password' => '',
|
||||
'remote_control_password' => $remote_control_password,
|
||||
'ftp_password' => $ftp_password,
|
||||
'customer_name' => $customer_name,
|
||||
'customer_email' => $customer_email,
|
||||
'amount' => '0.00',
|
||||
'discount_amount' => '0.00',
|
||||
'currency' => 'USD',
|
||||
'status' => 'paid',
|
||||
'billing_status' => 'Active',
|
||||
'invoice_date' => $now,
|
||||
'due_date' => $now,
|
||||
'paid_date' => $now,
|
||||
'payment_txid' => 'admin-created',
|
||||
'payment_method' => 'admin',
|
||||
'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,
|
||||
);
|
||||
|
||||
|
|
@ -125,12 +137,12 @@ if (!function_exists('admin_register_server_in_billing')) {
|
|||
'home_name' => $home_name,
|
||||
'ip' => intval($rserver_id),
|
||||
'qty' => 1,
|
||||
'invoice_duration' => 'year',
|
||||
'invoice_duration' => 'month',
|
||||
'max_players' => intval($max_players),
|
||||
'price' => '0.00',
|
||||
'discount_amount' => '0.00',
|
||||
'remote_control_password' => '',
|
||||
'ftp_password' => '',
|
||||
'remote_control_password' => $remote_control_password,
|
||||
'ftp_password' => $ftp_password,
|
||||
'home_id' => intval($home_id),
|
||||
'status' => 'Active',
|
||||
'order_date' => $now,
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@ function exec_ogp_module()
|
|||
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']));
|
||||
echo "</td><td>".$expiration_date."</td><td>
|
||||
<a 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 href='?m=user_games&p=migrate&home_id=$row[home_id]'>[".get_lang('migrate')."]</a>
|
||||
<a class='btn btn-danger btn-xs' href='?m=user_games&p=del&home_id=$row[home_id]'>".get_lang('delete')."</a>
|
||||
<a class='btn btn-primary btn-xs' href='?m=user_games&p=edit&home_id=$row[home_id]'>".get_lang('edit')."</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>";
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue