Item'])) { foreach ($response['CallbackMetadata']['Item'] as $item) { if (is_array($item) && isset($item['Name'])) { $meta[(string) $item['Name']] = $item['Value'] ?? ''; } } } $receipt = trim((string) ($meta['MpesaReceiptNumber'] ?? '')); $accountReference = trim((string) ($stkState['account_reference'] ?? (string) ($order['payment_ref'] ?? ''))); return dsa_apply_mpesa_payment_success($pdo, $orderId, $receipt, $accountReference); } function dsa_try_confirm_gateway_payment(PDO $pdo, int $orderId, bool $force = false): bool { if ($orderId <= 0) { return false; } $beforeStmt = $pdo->prepare("select status from orders where id = :id limit 1"); $beforeStmt->execute([':id' => $orderId]); $beforeStatus = (string) ($beforeStmt->fetchColumn() ?: ''); if (in_array($beforeStatus, ['paid', 'fulfilled'], true)) { return false; } dsa_try_sync_mpesa_payment_status($pdo, $orderId); dsa_sync_paid_signal_from_whmcs($pdo, $orderId, $force); $beforeStmt->execute([':id' => $orderId]); $afterStatus = (string) ($beforeStmt->fetchColumn() ?: ''); return $beforeStatus === 'pending_payment' && in_array($afterStatus, ['paid', 'fulfilled'], true); } function dsa_order_payment_status_payload(PDO $pdo, int $orderId): array { $order = dsa_get_order_with_product($pdo, $orderId); if (!$order) { return ['ok' => false, 'message' => 'Order not found.']; } $status = (string) ($order['status'] ?? 'pending_payment'); $clientStatus = dsa_customer_order_status_label((array) $order); $paid = in_array($status, ['paid', 'fulfilled'], true); return [ 'ok' => true, 'order_id' => $orderId, 'status' => $status, 'client_status' => $clientStatus, 'paid' => $paid, 'awaiting_confirmation' => ($status === 'pending_payment' && $clientStatus === 'payment_submitted'), 'redirect_url' => $paid ? ('/client-area/order/' . $orderId) : '', ]; } function dsa_payment_request_state_key(int $orderId): string { return 'dsa_stk_state_' . max(0, $orderId); } function dsa_payment_request_state_get(int $orderId): array { $key = dsa_payment_request_state_key($orderId); $raw = $_SESSION[$key] ?? null; if (!is_array($raw)) { return []; } return $raw; } function dsa_payment_request_state_set(int $orderId, array $state): void { $_SESSION[dsa_payment_request_state_key($orderId)] = $state; } function dsa_payment_flow_state_clear(int $orderId): void { if ($orderId <= 0) { return; } unset($_SESSION['dsa_payment_notice_' . $orderId]); unset($_SESSION[dsa_payment_request_state_key($orderId)]); } function dsa_build_paypal_checkout_url(PDO $pdo, array $order): string { $customLink = trim((string) dsa_setting($pdo, 'payment_paypal_link', '')); $amount = number_format(((int) $order['amount_cents']) / 100, 2, '.', ''); if ($customLink !== '') { $replacements = [ '{amount}' => $amount, '{currency}' => (string) $order['currency'], '{order_id}' => (string) $order['id'], '{order_ref}' => 'ORDER-' . (string) $order['id'], ]; return strtr($customLink, $replacements); } $businessEmail = trim((string) dsa_setting($pdo, 'payment_paypal_business_email', '')); if ($businessEmail === '') { return ''; } $base = dsa_setting($pdo, 'payment_paypal_sandbox', '0') === '1' ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr'; $returnUrl = dsa_app_base_url($pdo); if ($returnUrl === '') { return ''; } $query = http_build_query([ 'cmd' => '_xclick', 'business' => $businessEmail, 'item_name' => (string) $order['product_name'], 'amount' => $amount, 'currency_code' => (string) $order['currency'], 'custom' => 'ORDER-' . (string) $order['id'], 'invoice' => 'ORDER-' . (string) $order['id'], 'return' => $returnUrl . '/order/' . (int) $order['id'] . '/payment', 'cancel_return' => $returnUrl . '/order/' . (int) $order['id'] . '/payment', 'notify_url' => $returnUrl . '/payments/paypal/ipn', ]); return $base . '?' . $query; } function dsa_build_stripe_checkout_url(PDO $pdo, array $order): string { $customLink = trim((string) dsa_setting($pdo, 'payment_stripe_link', '')); $amount = number_format(((int) $order['amount_cents']) / 100, 2, '.', ''); if ($customLink !== '') { $replacements = [ '{amount}' => $amount, '{currency}' => (string) $order['currency'], '{order_id}' => (string) $order['id'], '{order_ref}' => 'ORDER-' . (string) $order['id'], ]; return strtr($customLink, $replacements); } $secretKey = trim((string) dsa_setting($pdo, 'payment_stripe_secret_key', '')); if ($secretKey === '') { return ''; } $baseUrl = dsa_app_base_url($pdo); if ($baseUrl === '') { return ''; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.stripe.com/v1/checkout/sessions'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_USERPWD, $secretKey . ':'); $successUrl = $baseUrl . '/order/' . (int) $order['id'] . '/payment?gateway=stripe&session_id={CHECKOUT_SESSION_ID}'; $cancelUrl = $baseUrl . '/order/' . (int) $order['id'] . '/payment'; $fields = [ 'mode' => 'payment', 'success_url' => $successUrl, 'cancel_url' => $cancelUrl, 'line_items[0][price_data][currency]' => strtolower((string) $order['currency']), 'line_items[0][price_data][product_data][name]' => (string) $order['product_name'], 'line_items[0][price_data][unit_amount]' => (int) $order['amount_cents'], 'line_items[0][quantity]' => 1, 'metadata[order_id]' => (string) $order['id'], ]; curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields)); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && $response) { $res = json_decode($response, true); if (isset($res['url'])) { return (string) $res['url']; } } return ''; } function dsa_build_paystack_checkout_url(PDO $pdo, array $order): string { $customLink = trim((string) dsa_setting($pdo, 'payment_paystack_link', '')); $amount = number_format(((int) $order['amount_cents']) / 100, 2, '.', ''); if ($customLink !== '') { $replacements = [ '{amount}' => $amount, '{currency}' => (string) $order['currency'], '{order_id}' => (string) $order['id'], '{order_ref}' => 'ORDER-' . (string) $order['id'], ]; return strtr($customLink, $replacements); } $secretKey = trim((string) dsa_setting($pdo, 'payment_paystack_secret_key', '')); if ($secretKey === '') { return ''; } $baseUrl = dsa_app_base_url($pdo); if ($baseUrl === '') { return ''; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.paystack.co/transaction/initialize'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . $secretKey, 'Content-Type: application/json', ]); $reference = 'PAYSTACK-ORD-' . (int) $order['id'] . '-' . time(); $callbackUrl = $baseUrl . '/order/' . (int) $order['id'] . '/payment?gateway=paystack'; $fields = [ 'amount' => (int) $order['amount_cents'], 'email' => 'customer-' . (int) $order['id'] . '@watu.com', 'reference' => $reference, 'callback_url' => $callbackUrl, 'metadata' => [ 'order_id' => (string) $order['id'], ], ]; curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields)); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && $response) { $res = json_decode($response, true); if (isset($res['status']) && $res['status'] === true && isset($res['data']['authorization_url'])) { return (string) $res['data']['authorization_url']; } } return ''; } function dsa_apply_gateway_payment_success(PDO $pdo, int $orderId, string $paymentRef): bool { if ($orderId <= 0) { return false; } $stmt = $pdo->prepare("select id, status from orders where id = :id limit 1"); $stmt->execute([':id' => $orderId]); $existing = $stmt->fetch(); if (!$existing) { return false; } if ((string) ($existing['status'] ?? '') !== 'pending_payment') { return (string) ($existing['status'] ?? '') === 'paid' || (string) ($existing['status'] ?? '') === 'fulfilled'; } $stmt = $pdo->prepare( "update orders set status = 'paid', payment_ref = :payment_ref, updated_at = datetime('now') where id = :id and status = 'pending_payment'" ); $stmt->execute([ ':id' => $orderId, ':payment_ref' => $paymentRef, ]); if ((int) $stmt->rowCount() <= 0) { return false; } dsa_payment_flow_state_clear($orderId); dsa_record_watupay_credit_for_order($pdo, $orderId); dsa_send_order_notification_email($pdo, $orderId, 'payment_received'); if (dsa_demo_mode_active($pdo)) { dsa_demo_mode_mark_paid_no_provision($pdo, $orderId); return true; } dsa_on_order_became_paid($pdo, $orderId); return true; } function dsa_verify_stripe_payment_and_confirm(PDO $pdo, int $orderId, string $sessionId): void { $secretKey = trim((string) dsa_setting($pdo, 'payment_stripe_secret_key', '')); if ($secretKey === '' || $sessionId === '') { return; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.stripe.com/v1/checkout/sessions/' . rawurlencode($sessionId)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_USERPWD, $secretKey . ':'); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && $response) { $res = json_decode($response, true); if (isset($res['payment_status']) && $res['payment_status'] === 'paid') { dsa_apply_gateway_payment_success($pdo, $orderId, 'Stripe Session: ' . $sessionId); } } } function dsa_verify_paystack_payment_and_confirm(PDO $pdo, int $orderId): void { $secretKey = trim((string) dsa_setting($pdo, 'payment_paystack_secret_key', '')); if ($secretKey === '') { return; } $reference = isset($_GET['reference']) ? (string) $_GET['reference'] : ''; if ($reference === '') { $reference = isset($_GET['trxref']) ? (string) $_GET['trxref'] : ''; } if ($reference === '') { return; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.paystack.co/transaction/verify/' . rawurlencode($reference)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . $secretKey, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && $response) { $res = json_decode($response, true); if (isset($res['status']) && $res['status'] === true && isset($res['data']['status']) && $res['data']['status'] === 'success') { $orderStmt = $pdo->prepare("select amount_cents from orders where id = :id limit 1"); $orderStmt->execute([':id' => $orderId]); $amountCents = (int) $orderStmt->fetchColumn(); $paystackAmount = (int) ($res['data']['amount'] ?? 0); if ($paystackAmount >= $amountCents) { dsa_apply_gateway_payment_success($pdo, $orderId, 'Paystack Ref: ' . $reference); } } } } function dsa_start_gateway_payment(PDO $pdo, int $orderId, string $inputPhone, bool $isResend = false): string { $stmt = $pdo->prepare( "select o.id, o.product_id, o.status, o.amount_cents, o.currency, o.customer_phone, o.payment_method, o.payment_ref, o.updated_at, p.slug as product_slug, p.catalog_product_id as source_product_id, p.name as product_name from orders o join products p on p.id = o.product_id where o.id = :id limit 1" ); $stmt->execute([':id' => $orderId]); $order = $stmt->fetch(); if (!$order) { return 'Order not found.'; } if ((string) $order['status'] === 'fulfilled') { return 'Order is already fulfilled.'; } $existingRef = trim((string) ($order['payment_ref'] ?? '')); $dispatchBufferSeconds = 4; $resendCooldownSeconds = 10; $requestWindowSeconds = 45; $paymentState = dsa_payment_request_state_get($orderId); $updatedAtTs = strtotime((string) ($order['updated_at'] ?? '')); $ageSeconds = null; if ($updatedAtTs !== false) { $ageSeconds = time() - (int) $updatedAtTs; } $resendAllowed = false; if ($isResend) { if (isset($paymentState['resend_available_at']) && time() >= (int) $paymentState['resend_available_at']) { $resendAllowed = true; } elseif ($ageSeconds !== null && $ageSeconds >= $resendCooldownSeconds) { $resendAllowed = true; } } if (!$resendAllowed && (string) $order['status'] === 'pending_payment' && $existingRef !== '' && stripos($existingRef, 'DSA-') === 0) { if ($ageSeconds !== null && $ageSeconds >= 0 && $ageSeconds < $dispatchBufferSeconds) { return 'Payment request is being dispatched. Please wait 4 seconds before trying again.'; } if ($ageSeconds !== null && $ageSeconds >= 0 && $ageSeconds < $resendCooldownSeconds) { $remaining = $resendCooldownSeconds - $ageSeconds; return 'A payment request is already in progress. You can resend in about ' . max(1, (int) $remaining) . ' seconds.'; } } $method = (string) ($order['payment_method'] ?? ''); if ($method === 'paypal' || $method === 'paypal_basic') { $url = dsa_build_paypal_checkout_url($pdo, $order); if ($url === '') { return 'PayPal is not fully configured yet. Add PayPal business email or direct payment link in settings.'; } header('Location: ' . $url); return ''; } if ($method === 'stripe') { $url = dsa_build_stripe_checkout_url($pdo, $order); if ($url === '') { return 'Stripe Checkout is not fully configured yet.'; } header('Location: ' . $url); return ''; } if ($method === 'paystack') { $url = dsa_build_paystack_checkout_url($pdo, $order); if ($url === '') { return 'Paystack is not fully configured yet.'; } header('Location: ' . $url); return ''; } if (!in_array($method, ['mpesa_direct', 'watupay_mpesa'], true)) { return 'This is an offline payment. Follow the instructions shown above and submit payment proof.'; } $config = dsa_mpesa_gateway_config($pdo, $method); if (!dsa_mpesa_gateway_ready($config)) { return ($method === 'watupay_mpesa' ? 'Watu Technology M-PESA' : 'Direct M-PESA') . ' gateway is not fully configured yet.'; } $phone = dsa_resolve_gateway_payment_phone($pdo, $orderId, (array) $order, $inputPhone); if ($phone === '') { return 'Enter a valid M-PESA phone number (e.g. 07XXXXXXXX or 2547XXXXXXXX).'; } $callbackUrl = dsa_gateway_callback_url($pdo, $method); if ($callbackUrl === '') { return 'Gateway callback is not configured correctly.'; } $sourceProductId = dsa_order_source_product_id_for_order($pdo, (int) $order['id'], (array) $order); if ($sourceProductId <= 0) { return 'Order product mapping is missing. Sync catalog/product mapping before starting M-PESA payment.'; } $order['source_product_id'] = $sourceProductId; $accountReference = dsa_order_account_reference_live($pdo, (array) $order); $result = dsa_mpesa_stk_push($config, $phone, (int) $order['amount_cents'], $accountReference, $callbackUrl); if (!empty($result['ok'])) { $now = time(); $save = $pdo->prepare( "update orders set payment_ref = :payment_ref, customer_phone = :customer_phone, updated_at = datetime('now') where id = :id" ); $save->execute([ ':id' => (int) $order['id'], ':payment_ref' => $accountReference, ':customer_phone' => $phone, ]); dsa_payment_request_state_set((int) $order['id'], [ 'sent_at' => $now, 'resend_available_at' => $now + $resendCooldownSeconds, 'expires_at' => $now + $requestWindowSeconds, 'checkout_request_id' => (string) ($result['checkout_request_id'] ?? ''), 'phone' => $phone, ]); dsa_order_mpesa_stk_state_set($pdo, (int) $order['id'], [ 'checkout_request_id' => (string) ($result['checkout_request_id'] ?? ''), 'merchant_request_id' => (string) ($result['merchant_request_id'] ?? ''), 'gateway' => $method, 'account_reference' => $accountReference, 'phone' => $phone, 'sent_at' => $now, ]); return 'M-PESA prompt sent. Enter PIN within 45 seconds.'; } return (string) ($result['message'] ?? 'Could not start payment.'); } function dsa_handle_mpesa_callback(PDO $pdo, array $payload): void { dsa_mpesa_log_callback('received', [ 'result_code' => (int) ($payload['Body']['stkCallback']['ResultCode'] ?? -1), 'checkout_request_id' => (string) ($payload['Body']['stkCallback']['CheckoutRequestID'] ?? ''), ]); if ((int) ($payload['Body']['stkCallback']['ResultCode'] ?? 1) !== 0) { return; } $meta = dsa_mpesa_extract_callback_metadata($payload); $accountReference = trim((string) ($meta['AccountReference'] ?? '')); if ($accountReference === '') { dsa_mpesa_log_callback('missing_account_reference', ['meta' => $meta]); return; } $orderId = dsa_extract_order_id_from_reference($accountReference); if ($orderId <= 0) { dsa_mpesa_log_callback('unknown_order_reference', ['account_reference' => $accountReference]); return; } $receipt = trim((string) ($meta['MpesaReceiptNumber'] ?? '')); $applied = dsa_apply_mpesa_payment_success($pdo, $orderId, $receipt, $accountReference); dsa_mpesa_log_callback($applied ? 'applied' : 'apply_failed', [ 'order_id' => $orderId, 'receipt' => $receipt, 'account_reference' => $accountReference, ]); } function dsa_handle_paypal_ipn(PDO $pdo, string $rawBody): void { if ($rawBody === '') { return; } parse_str($rawBody, $postData); if (!is_array($postData) || count($postData) === 0) { return; } $verifyParams = $postData; $verifyParams['cmd'] = '_notify-validate'; $verifyBody = http_build_query($verifyParams); $verifyUrl = dsa_setting($pdo, 'payment_paypal_sandbox', '0') === '1' ? 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr' : 'https://ipnpb.paypal.com/cgi-bin/webscr'; $ch = curl_init($verifyUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $verifyBody, CURLOPT_HTTPHEADER => ['Connection: Close', 'Content-Type: application/x-www-form-urlencoded'], CURLOPT_CONNECTTIMEOUT => 12, CURLOPT_TIMEOUT => 25, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); $verifyResponse = curl_exec($ch); curl_close($ch); if (!is_string($verifyResponse) || stripos($verifyResponse, 'VERIFIED') === false) { return; } $paymentStatus = strtolower(trim((string) ($postData['payment_status'] ?? ''))); if (!in_array($paymentStatus, ['completed', 'processed'], true)) { return; } $orderToken = trim((string) ($postData['custom'] ?? ($postData['invoice'] ?? ''))); if (!preg_match('/(\d+)/', $orderToken, $m)) { return; } $orderId = (int) $m[1]; if ($orderId <= 0) { return; } $txnId = trim((string) ($postData['txn_id'] ?? '')); $stmt = $pdo->prepare( "update orders set status = case when status = 'pending_payment' then 'paid' else status end, payment_ref = case when :payment_ref = '' then payment_ref else :payment_ref end, updated_at = datetime('now') where id = :id" ); $stmt->execute([ ':id' => $orderId, ':payment_ref' => $txnId, ]); dsa_record_watupay_credit_for_order($pdo, $orderId); if (dsa_demo_mode_active($pdo)) { dsa_demo_mode_mark_paid_no_provision($pdo, $orderId); return; } dsa_on_order_became_paid($pdo, $orderId); } function dsa_order_has_pending_stk_reference(string $paymentRef): bool { $paymentRef = trim($paymentRef); return $paymentRef !== '' && preg_match('/^DSA-\d+-\d+$/i', $paymentRef) === 1; } function dsa_watu_pay_txn_syncable_for_dsa(string $txnStatus, string $mpesaCode, string $orderRef): bool { return false; } function dsa_order_awaiting_watu_pay_reconciliation(array $order): bool { return false; } function dsa_reset_order_for_new_watu_pay_dispatch(PDO $pdo, int $orderId, string $mpesaCode): void { // Watu Pay reset logic has been retired. } function dsa_customer_order_status_label(array $order): string { $status = (string) ($order['status'] ?? 'pending_payment'); $paymentRef = trim((string) ($order['payment_ref'] ?? '')); $hasPendingStk = dsa_order_has_pending_stk_reference($paymentRef); $hasRealPaymentRef = ($paymentRef !== '' && !$hasPendingStk); $hasPaymentSignal = $hasRealPaymentRef || trim((string) ($order['payment_proof_path'] ?? '')) !== '' || $hasPendingStk; if ($status === 'pending_payment' && $hasPaymentSignal) { return 'payment_submitted'; } if ( in_array($status, ['paid', 'fulfilled'], true) && dsa_order_needs_whmcs_provisioning($order) && !dsa_order_whmcs_provisioning_complete($order) ) { return 'paid'; } return $status; } function dsa_admin_order_status_options(): array { return [ 'pending_payment' => 'Awaiting payment', 'paid' => 'Paid — setting up service', 'fulfilled' => 'Ready — service active', 'cancelled' => 'Cancelled', 'demo_simulated' => 'Demo simulated', ]; } function dsa_admin_order_status_label(string $status): string { $options = dsa_admin_order_status_options(); return $options[$status] ?? ucwords(str_replace('_', ' ', $status)); } function dsa_status_pill(string $status): string { static $labels = [ 'pending_payment' => 'Awaiting payment', 'payment_submitted' => 'Payment in progress', 'paid' => 'Paid — setting up your service', 'fulfilled' => 'Ready — service active', 'cancelled' => 'Cancelled', 'demo_simulated' => 'Demo Simulated', 'active' => 'Active', 'pending' => 'Pending', ]; $label = $labels[$status] ?? ucwords(str_replace('_', ' ', $status)); $cssClass = 'pill pill-' . preg_replace('/[^a-z0-9_-]/', '', strtolower($status)); return '' . dsa_h($label) . ''; } function dsa_customer_order_status_context(array $order): string { $status = (string) ($order['status'] ?? 'pending_payment'); $clientStatusLabel = dsa_customer_order_status_label($order); if ($clientStatusLabel === 'payment_submitted') { if (in_array($status, ['paid', 'fulfilled'], true) && dsa_order_needs_whmcs_provisioning($order)) { return 'Payment received — setting up your service on the platform.'; } return 'We received your payment request and are waiting for confirmation.'; } switch ($status) { case 'pending_payment': return 'Complete payment to activate your order.'; case 'paid': return 'Payment confirmed — we are setting up your service.'; case 'fulfilled': return 'Your service is active and ready to use.'; case 'cancelled': return 'This order was cancelled.'; default: return ''; } } function dsa_render_cart_account(PDO $pdo, string $notice, array $state): void { dsa_release_session_lock_if_safe(); $draft = dsa_checkout_draft_load(); if (!is_array($draft)) { header('Location: /cart'); return; } $sessionClientLoggedIn = !empty($_SESSION['dsa_client_logged_in']) && (int) ($_SESSION['dsa_client_id'] ?? 0) > 0; $otpState = (array) ($state['otp_state'] ?? []); $otpSent = !empty($otpState['otp_sent']); $accountMode = trim((string) ($state['account_mode'] ?? ($_POST['account_mode'] ?? 'login'))); if (!in_array($accountMode, ['login', 'register'], true)) { $accountMode = 'login'; } $customerName = trim((string) ($state['customer_name'] ?? ($_POST['customer_name'] ?? ''))); $customerEmail = trim((string) ($state['customer_email'] ?? ($_POST['customer_email'] ?? ''))); $totalCents = (int) ($draft['total_cents'] ?? 0); $currency = (string) ($draft['currency'] ?? DSA_DEFAULT_CURRENCY); $html = '
' . dsa_checkout_steps('account') . '

Account

'; $html .= '

Step 2: sign in or create your account. Email verification is required before payment.

'; if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } $html .= '

Order total: ' . dsa_h(dsa_money($totalCents, $currency)) . '

'; if ($sessionClientLoggedIn) { $clientEmail = (string) ($_SESSION['dsa_client_email'] ?? ''); $html .= '
Signed in as ' . dsa_h($clientEmail) . '.
'; $html .= '
'; $html .= ''; $html .= '
' . '' . 'Back to cart' . 'Sign out' . '
'; } else { $html .= '
'; $html .= ''; $html .= ''; $registerFieldsStyle = ($accountMode === 'register') ? '' : ' style="display:none;"'; $html .= '
'; $html .= ''; $html .= '
'; $html .= ''; if (!$otpSent) { $html .= ''; $html .= '
'; $html .= ''; $html .= '
'; } if ($otpSent) { $html .= dsa_checkout_render_otp_fields($otpState); if (function_exists('dsa_demo_mode_active') && dsa_demo_mode_active($pdo)) { $html .= '

Demo store: use verification code 000000.

'; } } $submitLabel = $otpSent ? 'Verify & continue to payment' : 'Send verification code'; $html .= '
' . '' . 'Back to cart' . '
'; $html .= ''; $html .= '

Already have an account elsewhere? Client area login

'; } $html .= '
'; dsa_render_layout('Checkout account', $html); } function dsa_checkout_steps(string $active): string { $steps = [ 'cart' => 'Cart / Config', 'account' => 'Account', 'payment' => 'Payment', 'confirmation' => 'Confirmation', ]; $html = '
'; $i = 0; foreach ($steps as $key => $label) { if ($i > 0) { $html .= ''; } $class = ($key === $active) ? 'step-active' : ''; $html .= '' . dsa_h($label) . ''; $i++; } $html .= '
'; return $html; } function dsa_empty_state(string $title, string $text, string $btnLabel = '', string $btnHref = ''): string { $html = '

' . dsa_h($title) . '

'; if ($text !== '') { $html .= '

' . dsa_h($text) . '

'; } if ($btnLabel !== '' && $btnHref !== '') { $html .= '
' . dsa_h($btnLabel) . '
'; } $html .= '
'; return $html; } function dsa_consume_payment_notice(int $orderId): string { if ($orderId <= 0) { return ''; } $key = 'dsa_payment_notice_' . $orderId; if (!isset($_SESSION[$key]) || !is_string($_SESSION[$key])) { return ''; } $notice = trim((string) $_SESSION[$key]); unset($_SESSION[$key]); return $notice; } function dsa_internal_perf_probe(PDO $pdo): void { if (!headers_sent()) { header('Content-Type: application/json; charset=utf-8'); } dsa_session_start_if_needed(true); $token = dsa_request_header('X-Release-Token'); if ($token === '') { $token = dsa_request_header('X-Watu-Key'); } if ($token === '') { $token = trim((string) ($_GET['token'] ?? $_POST['token'] ?? '')); } if (!dsa_release_token_is_valid($pdo, $token)) { http_response_code(403); echo json_encode(['ok' => false, 'message' => 'Invalid perf probe token.'], JSON_UNESCAPED_UNICODE); return; } $routesRaw = trim((string) ($_GET['routes'] ?? $_POST['routes'] ?? '')); $selected = []; if ($routesRaw !== '') { foreach (explode(',', $routesRaw) as $part) { $key = strtolower(trim((string) $part)); if ($key !== '') { $selected[$key] = true; } } } if (count($selected) === 0) { $selected = [ 'admin_dashboard' => true, 'admin_orders' => true, 'client_dashboard' => true, 'cart' => true, ]; } $results = []; $sessionBackup = isset($_SESSION) && is_array($_SESSION) ? $_SESSION : []; $requestUriBackup = (string) ($_SERVER['REQUEST_URI'] ?? ''); $requestMethodBackup = (string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'); $getBackup = $_GET ?? []; $postBackup = $_POST ?? []; foreach (array_keys($selected) as $routeKey) { $result = dsa_internal_perf_probe_route($pdo, $routeKey); if ($result !== null) { $results[$routeKey] = $result; } $_SESSION = $sessionBackup; $_GET = $getBackup; $_POST = $postBackup; $_SERVER['REQUEST_URI'] = $requestUriBackup; $_SERVER['REQUEST_METHOD'] = $requestMethodBackup; } echo json_encode([ 'ok' => true, 'generated_at' => date('c'), 'results' => $results, ], JSON_UNESCAPED_UNICODE); } function dsa_internal_perf_probe_route(PDO $pdo, string $routeKey): ?array { $routeKey = strtolower(trim($routeKey)); $started = microtime(true); $bytes = 0; $error = ''; $ok = true; try { ob_start(); if ($routeKey === 'admin_dashboard') { $admin = $pdo->query("select username from admin_users order by id asc limit 1")->fetch(); if (!$admin) { throw new RuntimeException('No admin user found.'); } $_SESSION['dsa_admin_logged_in'] = true; $_SESSION['dsa_admin_username'] = (string) ($admin['username'] ?? DSA_ADMIN_USERNAME); $_SERVER['REQUEST_URI'] = '/admin?section=dashboard'; $_SERVER['REQUEST_METHOD'] = 'GET'; dsa_render_admin($pdo, '', 'all', 'dashboard'); } elseif ($routeKey === 'admin_orders') { $admin = $pdo->query("select username from admin_users order by id asc limit 1")->fetch(); if (!$admin) { throw new RuntimeException('No admin user found.'); } $_SESSION['dsa_admin_logged_in'] = true; $_SESSION['dsa_admin_username'] = (string) ($admin['username'] ?? DSA_ADMIN_USERNAME); $_SERVER['REQUEST_URI'] = '/admin?section=orders&filter=all'; $_SERVER['REQUEST_METHOD'] = 'GET'; dsa_render_admin($pdo, '', 'all', 'orders'); } elseif ($routeKey === 'client_dashboard') { $clientStmt = $pdo->query( "select c.id, c.email from customers c left join orders o on o.customer_id = c.id group by c.id order by count(o.id) desc, c.id desc limit 1" ); $client = $clientStmt ? $clientStmt->fetch() : false; if (!$client) { throw new RuntimeException('No customer found.'); } $_SESSION['dsa_client_logged_in'] = true; $_SESSION['dsa_client_id'] = (int) ($client['id'] ?? 0); $_SESSION['dsa_client_email'] = (string) ($client['email'] ?? ''); $_SERVER['REQUEST_URI'] = '/client-area'; $_SERVER['REQUEST_METHOD'] = 'GET'; dsa_render_client_dashboard($pdo, ''); } elseif ($routeKey === 'cart') { $firstProduct = $pdo->query("select slug from products where status = 'active' order by id desc limit 1")->fetch(); $slug = trim((string) ($firstProduct['slug'] ?? '')); if ($slug !== '') { $_SESSION['dsa_cart'] = [$slug => 1]; } else { $_SESSION['dsa_cart'] = []; } $_SERVER['REQUEST_URI'] = '/cart'; $_SERVER['REQUEST_METHOD'] = 'GET'; dsa_render_cart($pdo, ''); } else { ob_end_clean(); return null; } $output = ob_get_clean(); $bytes = strlen((string) $output); } catch (Throwable $e) { $ok = false; $error = (string) $e->getMessage(); if (ob_get_level() > 0) { ob_end_clean(); } } $elapsedMs = (microtime(true) - $started) * 1000; return [ 'ok' => $ok, 'elapsed_ms' => round($elapsedMs, 2), 'bytes' => $bytes, 'error' => $error, ]; } function dsa_render_login(string $error): void { $html = '

Admin Login

'; if ($error !== '') { $html .= '

' . dsa_h($error) . '

'; } $html .= '
'; $html .= ''; $html .= ''; $html .= ''; $html .= '
'; $html .= '

Forgot password?

'; $html .= '
'; dsa_render_auth_layout('Admin login', $html); } function dsa_render_admin_forgot_password(string $error, string $notice): void { $html = '

Forgot Password

'; if ($error !== '') { $html .= '

' . dsa_h($error) . '

'; } if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } $html .= '

Enter your admin username to request a password reset link.

'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '

Back to login

'; $html .= '
'; dsa_render_auth_layout('Admin forgot password', $html); } function dsa_render_admin_reset_password(string $token, string $error, string $notice): void { $html = '

Reset Admin Password

'; if ($error !== '') { $html .= '

' . dsa_h($error) . '

'; } if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } if ($token === '') { $html .= '

Request a new reset link from the forgot password page.

'; $html .= '

Go to forgot password

'; $html .= '
'; dsa_render_auth_layout('Admin reset password', $html); return; } $html .= '
'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '
'; $html .= '

Back to login

'; $html .= ''; dsa_render_auth_layout('Admin reset password', $html); } function dsa_render_client_login(string $error): void { $returnTo = trim((string) ($_GET['return'] ?? '')); if ($returnTo === '' || $returnTo[0] !== '/' || strpos($returnTo, '//') !== false) { $returnTo = ''; } $html = '

Client Area Login

'; if ($error !== '') { $html .= '

' . dsa_h($error) . '

'; } $html .= '
'; $html .= ''; if ($returnTo !== '') { $html .= ''; } $html .= ''; $html .= ''; $html .= '
'; $html .= '

Forgot password?

'; $html .= '
'; dsa_render_layout('Client login', $html); } function dsa_whmcs_credits_default_summary(): array { return [ 'ok' => false, 'available_cents' => 0, 'queued_cents' => 0, 'paid_out_cents' => 0, 'currency' => DSA_DEFAULT_CURRENCY, 'payout_channel' => '', 'payout_target' => '', 'payout_timing' => '', 'payout_schedule' => '', 'message' => dsa_ui_label('credits.not_loaded'), ]; } function dsa_whmcs_credits_local(PDO $pdo): array { $summary = dsa_whmcs_credits_default_summary(); $cachedRaw = trim((string) dsa_setting($pdo, 'whmcs_credits_cache_json', '')); if ($cachedRaw === '') { return $summary; } $cached = json_decode($cachedRaw, true); if (!is_array($cached)) { return $summary; } return array_merge($summary, $cached); } function dsa_whmcs_credits_store_payload(PDO $pdo, array $credits, string $message = ''): array { $summary = dsa_whmcs_credits_default_summary(); $summary['ok'] = true; $summary['available_cents'] = (int) ($credits['available_cents'] ?? 0); $summary['queued_cents'] = (int) ($credits['queued_cents'] ?? 0); $summary['paid_out_cents'] = (int) ($credits['paid_out_cents'] ?? 0); $summary['currency'] = strtoupper(trim((string) ($credits['currency'] ?? DSA_DEFAULT_CURRENCY))); $summary['payout_channel'] = trim((string) ($credits['payout_channel'] ?? '')); $summary['payout_target'] = trim((string) ($credits['payout_target'] ?? '')); $summary['payout_timing'] = trim((string) ($credits['payout_timing'] ?? '')); $summary['payout_schedule'] = trim((string) ($credits['payout_schedule'] ?? '')); if ($summary['payout_channel'] === '') { $summary['payout_channel'] = trim((string) dsa_setting($pdo, 'watupay_payout_channel', '')); } if ($summary['payout_target'] === '') { $summary['payout_target'] = trim((string) dsa_setting($pdo, 'watupay_payout_target', '')); } if ($summary['payout_timing'] === '') { $summary['payout_timing'] = trim((string) dsa_setting($pdo, 'watupay_payout_timing', 'manual')); } if ($summary['payout_schedule'] === '' && $summary['payout_timing'] === 'scheduled') { $summary['payout_schedule'] = trim((string) dsa_setting($pdo, 'watupay_payout_schedule', '')); } $summary['message'] = $message !== '' ? $message : 'Stored locally.'; dsa_setting_set($pdo, 'whmcs_credits_cache_at', (string) time()); dsa_setting_set($pdo, 'whmcs_credits_cache_json', json_encode($summary, JSON_UNESCAPED_UNICODE)); return $summary; } function dsa_whmcs_credits_refresh_from_whmcs(PDO $pdo): array { $apiUrl = trim(dsa_setting($pdo, 'catalog_sync_whmcs_api_url', '')); $identifier = trim(dsa_setting($pdo, 'catalog_sync_whmcs_identifier', '')); $secret = trim(dsa_setting($pdo, 'catalog_sync_whmcs_secret', '')); if ($apiUrl === '' || $identifier === '' || $secret === '') { $summary = dsa_whmcs_credits_local($pdo); $summary['message'] = 'Source API credentials are not configured.'; return $summary; } if (stripos($apiUrl, 'client-catalog.php') === false) { $summary = dsa_whmcs_credits_local($pdo); $summary['message'] = 'Credits are only available from the Watu catalog endpoint.'; return $summary; } $key = $secret !== '' ? $secret : $identifier; $http = dsa_http_get_with_watu_key($apiUrl, $key); $raw = (string) ($http['body'] ?? ''); if (empty($http['ok']) || trim($raw) === '') { $summary = dsa_whmcs_credits_local($pdo); $summary['message'] = dsa_ui_label('credits.reach_failed'); return $summary; } $json = json_decode($raw, true); if (!is_array($json) || empty($json['ok'])) { $summary = dsa_whmcs_credits_local($pdo); $summary['message'] = dsa_ui_label('credits.invalid_data'); return $summary; } $credits = $json['credits'] ?? null; if (!is_array($credits)) { $summary = dsa_whmcs_credits_local($pdo); $summary['message'] = 'Credits are not available in source response.'; return $summary; } return dsa_whmcs_credits_store_payload($pdo, $credits, dsa_ui_label('credits.synced_from_account')); } function dsa_whmcs_credits_summary(PDO $pdo): array { return dsa_whmcs_credits_local($pdo); } function dsa_admin_payout_summary_card_html(PDO $pdo): string { $balance = dsa_payout_balance_summary($pdo); $currency = (string) ($balance['currency'] ?? DSA_DEFAULT_CURRENCY); $available = (int) ($balance['available_for_payout_cents'] ?? 0); $pending = (int) ($balance['pending_settlement_cents'] ?? 0); return '

' . dsa_h(dsa_ui_label('payouts.section_title')) . '

' . '

' . dsa_h(dsa_ui_label('payouts.available_for_payout')) . ': ' . dsa_h(dsa_money($available, $currency)) . '

' . '

' . dsa_h(dsa_ui_label('payouts.pending_settlement')) . ': ' . dsa_h(dsa_money($pending, $currency)) . '

' . '

' . dsa_h(dsa_ui_label('payouts.section_title')) . '

' . '
'; } function dsa_admin_credits_card_html(PDO $pdo, array $whmcsCredits, string $returnSection): string { $cacheAt = (int) dsa_setting($pdo, 'whmcs_credits_cache_at', '0'); $lastUpdated = $cacheAt > 0 ? date('Y-m-d H:i:s', $cacheAt) : 'never'; $refreshForm = '
' . '' . '' . '
'; if (!empty($whmcsCredits['ok'])) { $creditCurrency = (string) ($whmcsCredits['currency'] ?? DSA_DEFAULT_CURRENCY); return '

Watu Credits

' . '

Available Credit: ' . dsa_h(dsa_money((int) ($whmcsCredits['available_cents'] ?? 0), $creditCurrency)) . $refreshForm . '

' . '

Queued payouts: ' . dsa_h(dsa_money((int) ($whmcsCredits['queued_cents'] ?? 0), $creditCurrency)) . ' | Paid out: ' . dsa_h(dsa_money((int) ($whmcsCredits['paid_out_cents'] ?? 0), $creditCurrency)) . '

' . '

Payout destination: ' . dsa_h((string) (($whmcsCredits['payout_channel'] ?? '') !== '' ? $whmcsCredits['payout_channel'] : 'not set')) . ' - ' . dsa_h((string) (($whmcsCredits['payout_target'] ?? '') !== '' ? $whmcsCredits['payout_target'] : 'not set')) . '

' . '

Payout mode: ' . dsa_h((string) ($whmcsCredits['payout_timing'] ?? 'manual')) . (((string) ($whmcsCredits['payout_timing'] ?? '') === 'scheduled' && (string) ($whmcsCredits['payout_schedule'] ?? '') !== '') ? (' (' . dsa_h((string) $whmcsCredits['payout_schedule']) . ')') : '') . '

' . '

Stored locally | Last updated: ' . dsa_h($lastUpdated) . '

' . '
'; } return '

Watu Credits

' . '

' . dsa_h(dsa_ui_normalize_legacy_message((string) ($whmcsCredits['message'] ?? 'Credits not loaded yet.'))) . $refreshForm . '

' . '

Last updated: ' . dsa_h($lastUpdated) . '

'; } function dsa_order_dispatch_url(PDO $pdo): string { $apiUrl = trim(dsa_setting($pdo, 'catalog_sync_whmcs_api_url', '')); if ($apiUrl === '' || stripos($apiUrl, 'client-catalog.php') === false) { return ''; } return str_replace('client-catalog.php', 'client-order-intake.php', $apiUrl); } function dsa_order_payment_status_url(PDO $pdo): string { $apiUrl = trim(dsa_setting($pdo, 'catalog_sync_whmcs_api_url', '')); if ($apiUrl === '' || stripos($apiUrl, 'client-catalog.php') === false) { return ''; } return str_replace('client-catalog.php', 'client-payment-status.php', $apiUrl); } function dsa_order_dispatch_status_url(PDO $pdo): string { $apiUrl = trim(dsa_setting($pdo, 'catalog_sync_whmcs_api_url', '')); if ($apiUrl === '' || stripos($apiUrl, 'client-catalog.php') === false) { return ''; } return str_replace('client-catalog.php', 'client-order-status.php', $apiUrl); } function dsa_order_provision_url(PDO $pdo): string { $apiUrl = trim(dsa_setting($pdo, 'catalog_sync_whmcs_api_url', '')); if ($apiUrl === '' || stripos($apiUrl, 'client-catalog.php') === false) { return ''; } return str_replace('client-catalog.php', 'client-order-provision.php', $apiUrl); } function dsa_whitelabel_manage_url(PDO $pdo): string { $apiUrl = trim(dsa_setting($pdo, 'catalog_sync_whmcs_api_url', '')); if ($apiUrl === '' || stripos($apiUrl, 'client-catalog.php') === false) { return ''; } return str_replace('client-catalog.php', 'client-whitelabel-domain.php', $apiUrl); } function dsa_watu_clientarea_url(PDO $pdo, string $page = ''): string { $apiUrl = trim(dsa_setting($pdo, 'catalog_sync_whmcs_api_url', '')); if ($apiUrl === '') { return ''; } $pos = stripos($apiUrl, '/modules/'); if ($pos !== false) { $base = substr($apiUrl, 0, $pos); } else { $base = 'https://watutechnology.com'; } return rtrim($base, '/') . '/' . ltrim($page, '/'); } function dsa_manage_whitelabel_domain(PDO $pdo, string $domain, bool $enforce): array { $url = dsa_whitelabel_manage_url($pdo); $secret = trim(dsa_setting($pdo, 'catalog_sync_whmcs_secret', dsa_setting($pdo, 'catalog_sync_whmcs_identifier', ''))); if ($url === '' || $secret === '') { return [ 'ok' => false, 'message' => dsa_ui_label('domain_service.reach_failed'), 'status' => [], ]; } $payload = http_build_query([ 'watu_key' => $secret, 'domain' => strtolower(trim($domain)), 'enforce' => $enforce ? '1' : '0', ]); $http = dsa_http_post_form($url, $payload, 8, 3); $raw = (string) ($http['body'] ?? ''); if (empty($http['ok']) || trim($raw) === '') { return [ 'ok' => false, 'message' => dsa_ui_label('domain_service.request_failed_prefix', ['detail' => (string) ($http['error'] ?? 'unknown error')]), 'status' => [], ]; } $json = json_decode($raw, true); if (!is_array($json)) { return [ 'ok' => false, 'message' => dsa_ui_label('domain_service.invalid_json'), 'status' => [], ]; } return [ 'ok' => !empty($json['ok']), 'message' => trim((string) ($json['message'] ?? '')), 'status' => is_array($json['status'] ?? null) ? $json['status'] : [], ]; } function dsa_whitelabel_background_refresh(PDO $pdo, string $method, string $path, string $section = ''): void { if (strtoupper(trim($method)) !== 'GET') { return; } // Only run while viewing Branding — never block login or unrelated admin pages. if ($section !== 'branding') { return; } if (!dsa_custom_storefront_domain_enabled($pdo)) { return; } if (!($path === '/admin' || strpos($path, '/admin/') === 0)) { return; } if (empty($_SESSION['dsa_admin_logged_in'])) { return; } if ($path === '/internal' || strpos($path, '/internal/') === 0) { return; } if (in_array($path, ['/payments/mpesa/callback', '/payments/paypal/ipn', '/setup-internal.php'], true)) { return; } $pendingDomain = dsa_normalize_domain((string) dsa_setting($pdo, 'whitelabel_pending_domain', '')); $pendingEnforce = dsa_setting($pdo, 'whitelabel_pending_enforce', '') === '1'; if ($pendingDomain === '') { if (dsa_setting($pdo, 'force_whitelabel_domain', '0') !== '1') { return; } $domain = dsa_normalize_domain((string) dsa_setting($pdo, 'whitelabel_domain', '')); if ($domain === '' || !dsa_whitelabel_is_subdomain($domain)) { return; } $pendingDomain = $domain; $pendingEnforce = true; dsa_setting_set($pdo, 'whitelabel_pending_domain', $pendingDomain); dsa_setting_set($pdo, 'whitelabel_pending_enforce', '1'); dsa_setting_set($pdo, 'whitelabel_pending_requested_at', (string) time()); } if (!dsa_whitelabel_is_subdomain($pendingDomain)) { return; } $statusRaw = trim((string) dsa_setting($pdo, 'whitelabel_last_sync_json', '')); if ($statusRaw !== '') { $status = json_decode($statusRaw, true); if (is_array($status)) { $ready = !empty($status['status']['ready']); $statusDomain = dsa_normalize_domain((string) ($status['status']['domain'] ?? '')); if ($ready && $statusDomain === $pendingDomain) { dsa_setting_set($pdo, 'whitelabel_pending_domain', ''); dsa_setting_set($pdo, 'whitelabel_pending_enforce', ''); dsa_setting_set($pdo, 'whitelabel_pending_requested_at', '0'); return; } } } $now = time(); $lastCheckedAt = (int) dsa_setting($pdo, 'whitelabel_last_background_check_at', '0'); if ($lastCheckedAt > 0 && ($now - $lastCheckedAt) < 300) { return; } register_shutdown_function(static function () use ($pdo, $pendingDomain, $pendingEnforce, $now): void { if (function_exists('fastcgi_finish_request')) { @fastcgi_finish_request(); } dsa_setting_set($pdo, 'whitelabel_last_background_check_at', (string) $now); $sync = dsa_manage_whitelabel_domain($pdo, $pendingDomain, $pendingEnforce); dsa_setting_set($pdo, 'whitelabel_last_sync_message', (string) ($sync['message'] ?? '')); dsa_setting_set($pdo, 'whitelabel_last_sync_json', json_encode($sync, JSON_UNESCAPED_UNICODE)); if (!$pendingEnforce) { dsa_setting_set($pdo, 'whitelabel_pending_domain', ''); dsa_setting_set($pdo, 'whitelabel_pending_enforce', ''); dsa_setting_set($pdo, 'whitelabel_pending_requested_at', '0'); } else { $syncStatus = is_array($sync['status'] ?? null) ? (array) ($sync['status'] ?? []) : []; $ready = !empty($syncStatus['ready']); $statusDomain = dsa_normalize_domain((string) ($syncStatus['domain'] ?? '')); if ($ready && $statusDomain === $pendingDomain) { dsa_setting_set($pdo, 'whitelabel_pending_domain', ''); dsa_setting_set($pdo, 'whitelabel_pending_enforce', ''); dsa_setting_set($pdo, 'whitelabel_pending_requested_at', '0'); } } }); } function dsa_http_post_json_with_watu_key(string $url, string $watuKey, array $payload): array { $url = trim($url); $watuKey = trim($watuKey); if ($url === '' || $watuKey === '') { return ['ok' => false, 'body' => '', 'error' => 'Missing URL or key.', 'status' => 0]; } $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE); if ($body === false) { return ['ok' => false, 'body' => '', 'error' => 'Could not encode payload.', 'status' => 0]; } if (!function_exists('curl_init')) { return ['ok' => false, 'body' => '', 'error' => 'cURL is not available.', 'status' => 0]; } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Accept: application/json', 'X-Watu-Key: ' . $watuKey, ], CURLOPT_POSTFIELDS => $body, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, ]); $resp = curl_exec($ch); $err = $resp === false ? (string) curl_error($ch) : ''; $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return [ 'ok' => ($resp !== false && $status >= 200 && $status < 300), 'body' => $resp !== false ? (string) $resp : '', 'error' => $err !== '' ? $err : ($status > 0 ? ('HTTP ' . $status) : ''), 'status' => $status, ]; } function dsa_call_whmcs_extension_order(PDO $pdo, int $product_id): array { $empty = ['ok' => false, 'invoice_id' => 0, 'invoice_url' => '', 'error' => '']; if ($product_id <= 0) { return array_merge($empty, ['error' => 'Invalid product_id']); } $apiUrl = trim(dsa_setting($pdo, 'catalog_sync_whmcs_api_url', '')); $identifier = trim(dsa_setting($pdo, 'catalog_sync_whmcs_identifier', '')); $serviceId = (int) dsa_setting($pdo, 'tenant_whmcs_service_id', '0'); if ($apiUrl === '' || $identifier === '' || $serviceId <= 0) { return array_merge($empty, ['error' => dsa_ui_label('extension.account_not_linked')]); } $endpointUrl = str_replace('client-catalog.php', 'client-extension-order.php', $apiUrl); if (!function_exists('curl_init')) { return array_merge($empty, ['error' => 'cURL is not available']); } $payload = json_encode([ 'identifier' => $identifier, 'service_id' => $serviceId, 'product_id' => $product_id, ], JSON_UNESCAPED_UNICODE); if ($payload === false) { return array_merge($empty, ['error' => 'Could not encode request']); } $ch = curl_init($endpointUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'], CURLOPT_POSTFIELDS => $payload, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, ]); $resp = curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlErr = $resp === false ? (string) curl_error($ch) : ''; curl_close($ch); if ($resp === false || $curlErr !== '') { return array_merge($empty, ['error' => 'Network error: ' . $curlErr]); } $json = json_decode((string) $resp, true); if (!is_array($json)) { return array_merge($empty, ['error' => dsa_ui_label('extension.billing_unreachable')]); } if (!empty($json['ok'])) { return [ 'ok' => true, 'invoice_id' => (int) ($json['invoice_id'] ?? 0), 'invoice_url' => (string) ($json['invoice_url'] ?? ''), 'error' => '', ]; } return array_merge($empty, ['error' => (string) ($json['error'] ?? 'Unknown bridge error')]); } function dsa_dispatch_tenant_context(PDO $pdo): array { return [ 'tenant_domain' => trim((string) dsa_setting($pdo, 'system_origin_domain', '')), 'tenant_service_id' => (int) dsa_setting($pdo, 'tenant_whmcs_service_id', '0'), ]; } function dsa_order_item_missing_provisioning(array $product, array $provisioningInput): ?string { $profile = dsa_normalize_provisioning_profile((string) ($product['provisioning_profile'] ?? 'none')); $rules = dsa_parse_provisioning_rules($product); $fields = $rules['checkout_fields'] ?? []; if (!is_array($fields) || count($fields) === 0) { if ($profile === 'domain_registration') { $fields = [['key' => 'domain', 'required' => true, 'source' => 'domain']]; } elseif ($profile === 'hosting_custom_username') { $fields = [['key' => 'username', 'required' => true, 'source' => 'service_username', 'mode' => 'provided']]; } elseif ($profile === 'hosting_auto_username') { return null; } else { return null; } } foreach ($fields as $field) { if (!is_array($field)) { continue; } $key = trim((string) ($field['key'] ?? '')); if ($key === '') { continue; } $required = !empty($field['required']); $source = strtolower(trim((string) ($field['source'] ?? 'customfield'))); $mode = strtolower(trim((string) ($field['mode'] ?? ''))); if (($source === 'service_username' || $key === 'username') && $mode === 'auto') { continue; } if (!$required) { continue; } if ($source === 'domain' || $source === 'service_domain' || $key === 'domain') { $domain = dsa_domain_normalize_input((string) ($provisioningInput['domain'] ?? ''), (string) ($rules['default_tld'] ?? '')); if ($domain === '') { return 'Missing required domain for ' . trim((string) ($product['product_name'] ?? $product['name'] ?? 'product')) . '.'; } continue; } if ($source === 'service_username' || $key === 'username') { $username = strtolower(trim((string) ($provisioningInput['username'] ?? ''))); if ($username === '' || !preg_match('/^[a-z][a-z0-9]{2,15}$/', $username)) { return 'Missing required username for ' . trim((string) ($product['product_name'] ?? $product['name'] ?? 'product')) . '.'; } continue; } if (trim((string) ($provisioningInput[$key] ?? '')) === '') { return 'Missing required provisioning field "' . dsa_provisioning_field_display_label($field) . '" for ' . trim((string) ($product['product_name'] ?? $product['name'] ?? 'product')) . '.'; } } return null; } function dsa_validate_order_dispatch_provisioning(PDO $pdo, int $orderId): ?string { if ($orderId <= 0) { return 'Invalid order.'; } $items = dsa_order_dispatch_items($pdo, $orderId); if (count($items) === 0) { return null; } foreach ($items as $item) { if (!is_array($item)) { continue; } $productId = (int) ($item['product_id'] ?? 0); $product = [ 'provisioning_profile' => 'none', 'provisioning_rules_json' => '{}', 'product_name' => (string) ($item['product_name'] ?? ''), ]; if ($productId > 0) { $stmt = $pdo->prepare("select name, provisioning_profile, provisioning_rules_json from products where id = :id limit 1"); $stmt->execute([':id' => $productId]); $row = $stmt->fetch(); if ($row) { $product['provisioning_profile'] = (string) ($row['provisioning_profile'] ?? 'none'); $product['provisioning_rules_json'] = (string) ($row['provisioning_rules_json'] ?? '{}'); if (trim((string) ($product['product_name'] ?? '')) === '') { $product['product_name'] = (string) ($row['name'] ?? ''); } } } $missing = dsa_order_item_missing_provisioning($product, (array) ($item['provisioning_input'] ?? [])); if ($missing !== null) { return $missing; } } return null; } function dsa_order_dispatch_items(PDO $pdo, int $orderId): array { if ($orderId <= 0) { return []; } $stmt = $pdo->prepare( "select oi.id, oi.product_id, oi.qty, oi.unit_amount_cents, oi.line_amount_cents, oi.currency, oi.provisioning_input_json, coalesce(nullif(trim(oi.product_name), ''), p.name) as product_name, p.slug as product_slug from order_items oi left join products p on p.id = oi.product_id where oi.order_id = :order_id order by oi.id asc" ); $stmt->execute([':order_id' => $orderId]); $rows = $stmt->fetchAll(); if (!is_array($rows) || count($rows) === 0) { return []; } $out = []; foreach ($rows as $row) { $qty = max(1, (int) ($row['qty'] ?? 1)); $unit = max(0, (int) ($row['unit_amount_cents'] ?? 0)); if ($unit <= 0 && (int) ($row['line_amount_cents'] ?? 0) > 0) { $unit = (int) round(((int) ($row['line_amount_cents'] ?? 0)) / $qty); } $provisioningInput = json_decode((string) ($row['provisioning_input_json'] ?? '{}'), true); if (!is_array($provisioningInput)) { $provisioningInput = []; } $out[] = [ 'product_id' => (int) ($row['product_id'] ?? 0), 'product_slug' => (string) ($row['product_slug'] ?? ''), 'product_name' => (string) ($row['product_name'] ?? ''), 'qty' => $qty, 'unit_amount_cents' => $unit, 'currency' => (string) ($row['currency'] ?? DSA_DEFAULT_CURRENCY), 'provisioning_input' => $provisioningInput, ]; } return $out; } function dsa_dispatch_paid_order_to_whmcs(PDO $pdo, int $orderId): array { $result = ['ok' => false, 'message' => 'Dispatch skipped.']; $maybeOrder = dsa_get_order_with_product($pdo, $orderId); if ($maybeOrder && count(dsa_order_invoice_payment_target((array) $maybeOrder)) > 0) { dsa_apply_invoice_payment_to_whmcs($pdo, $orderId); $refreshed = dsa_get_order_with_product($pdo, $orderId) ?: $maybeOrder; return [ 'ok' => (string) ($refreshed['whmcs_dispatch_status'] ?? '') === 'provisioned', 'message' => (string) ($refreshed['whmcs_dispatch_message'] ?? 'Invoice payment processed.'), 'status' => (string) ($refreshed['whmcs_dispatch_status'] ?? ''), ]; } if (dsa_demo_mode_blocks_provisioning($pdo)) { $message = dsa_demo_mode_paid_notice(); $pdo->prepare( "update orders set whmcs_dispatch_status = 'demo_paid_no_provision', whmcs_dispatch_message = :message, whmcs_dispatched_at = datetime('now'), updated_at = datetime('now') where id = :id" )->execute([':id' => $orderId, ':message' => $message]); return ['ok' => true, 'message' => $message, 'status' => 'demo_paid_no_provision']; } if (dsa_setting($pdo, 'order_dispatch_enabled', '1') !== '1') { return ['ok' => false, 'message' => 'Order dispatch is disabled.']; } $provisioningError = dsa_validate_order_dispatch_provisioning($pdo, $orderId); if ($provisioningError !== null) { $stmt = $pdo->prepare( "update orders set whmcs_dispatch_status = 'failed', whmcs_dispatch_message = :message, whmcs_dispatched_at = datetime('now'), updated_at = datetime('now') where id = :id" ); $stmt->execute([':id' => $orderId, ':message' => $provisioningError]); return ['ok' => false, 'message' => $provisioningError]; } $order = dsa_get_order_with_product($pdo, $orderId); if (!$order || !in_array((string) ($order['status'] ?? ''), ['paid', 'fulfilled'], true)) { return ['ok' => false, 'message' => 'Order is not paid yet.']; } $paymentRef = trim((string) ($order['payment_ref'] ?? '')); if (dsa_order_has_real_mpesa_reference($paymentRef)) { dsa_reset_order_for_new_watu_pay_dispatch($pdo, $orderId, $paymentRef); $order = dsa_get_order_with_product($pdo, $orderId) ?: $order; } $dispatchStatus = (string) ($order['whmcs_dispatch_status'] ?? ''); if ( !dsa_order_has_pending_stk_reference($paymentRef) && (int) ($order['whmcs_invoice_id'] ?? 0) > 0 && in_array($dispatchStatus, ['invoice_created', 'invoice_paid', 'provisioned'], true) && dsa_order_dispatch_already_completed_for_payment($pdo, (array) $order) ) { return ['ok' => true, 'message' => dsa_ui_label('dispatch.already_dispatched')]; } if ($dispatchStatus === 'dispatching') { $dispatchedAtTs = strtotime((string) ($order['whmcs_dispatched_at'] ?? '')); $dispatchAgeSeconds = $dispatchedAtTs !== false ? (time() - (int) $dispatchedAtTs) : 9999; if ($dispatchAgeSeconds >= 0 && $dispatchAgeSeconds < 120) { return ['ok' => true, 'message' => 'Dispatch already in progress.']; } } if (dsa_order_has_pending_stk_reference($paymentRef)) { $pdo->prepare( "update orders set whmcs_order_id = 0, whmcs_invoice_id = 0, whmcs_service_id = 0, whmcs_dispatch_status = '', whmcs_dispatch_message = '', whmcs_dispatched_at = '', updated_at = datetime('now') where id = :id" )->execute([':id' => $orderId]); } $claimStmt = $pdo->prepare( "update orders set whmcs_dispatch_status = 'dispatching', whmcs_dispatched_at = datetime('now'), updated_at = datetime('now') where id = :id and coalesce(whmcs_dispatch_status, '') not in ('invoice_created', 'invoice_paid', 'provisioned', 'dispatching')" ); $claimStmt->execute([':id' => $orderId]); if ((int) $claimStmt->rowCount() <= 0) { $order = dsa_get_order_with_product($pdo, $orderId); if ( $order && !dsa_order_has_pending_stk_reference(trim((string) ($order['payment_ref'] ?? ''))) && (int) ($order['whmcs_invoice_id'] ?? 0) > 0 && dsa_order_dispatch_already_completed_for_payment($pdo, (array) $order) ) { return ['ok' => true, 'message' => dsa_ui_label('dispatch.already_dispatched')]; } return ['ok' => true, 'message' => 'Dispatch already in progress.']; } $dispatchUrl = dsa_order_dispatch_url($pdo); $watuKey = trim(dsa_setting($pdo, 'catalog_sync_whmcs_secret', dsa_setting($pdo, 'catalog_sync_whmcs_identifier', ''))); if ($dispatchUrl === '' || $watuKey === '') { $stmt = $pdo->prepare( "update orders set whmcs_dispatch_status = 'failed', whmcs_dispatch_message = :message, whmcs_dispatched_at = datetime('now'), updated_at = datetime('now') where id = :id" ); $msg = 'Dispatch endpoint/key not configured.'; $stmt->execute([':id' => $orderId, ':message' => $msg]); return ['ok' => false, 'message' => $msg]; } $items = dsa_order_dispatch_items($pdo, $orderId); if (count($items) === 0) { $items = [[ 'product_id' => (int) ($order['product_id'] ?? 0), 'product_slug' => (string) ($order['product_slug'] ?? ''), 'product_name' => (string) ($order['product_name'] ?? ''), 'qty' => 1, 'unit_amount_cents' => (int) ($order['amount_cents'] ?? 0), 'currency' => (string) ($order['currency'] ?? DSA_DEFAULT_CURRENCY), ]]; } $firstWhmcsOrderId = 0; $firstWhmcsInvoiceId = 0; $firstWhmcsServiceId = 0; $firstServiceDetails = []; $firstServiceAssets = []; $allOk = true; $messageParts = []; $finalStatus = 'failed'; $lineNo = 0; $dsaOrderRef = dsa_order_account_reference_live($pdo, (array) $order); if (strpos($dsaOrderRef, 'DSA-0-') === 0) { $dsaOrderRef = ''; } $tenantContext = dsa_dispatch_tenant_context($pdo); foreach ($items as $item) { $qty = max(1, (int) ($item['qty'] ?? 1)); for ($unitNo = 1; $unitNo <= $qty; $unitNo++) { $lineNo++; $externalRef = dsa_order_dispatch_external_ref($orderId, $lineNo, $paymentRef); $payload = [ 'order_id' => $orderId, 'external_order_ref' => $externalRef, 'amount_cents' => (int) ($item['unit_amount_cents'] ?? 0), 'currency' => (string) ($item['currency'] ?? DSA_DEFAULT_CURRENCY), 'tenant_payment_method' => (string) ($order['payment_method'] ?? ''), 'payment_ref' => (string) ($order['payment_ref'] ?? ''), 'dsa_order_ref' => $dsaOrderRef, 'customer_name' => (string) ($order['customer_name'] ?? ''), 'customer_email' => (string) ($order['customer_email'] ?? ''), 'customer_phone' => (string) ($order['customer_phone'] ?? ''), 'product_slug' => (string) ($item['product_slug'] ?? ''), 'product_name' => (string) ($item['product_name'] ?? ''), 'product_sku' => strpos((string) ($item['product_slug'] ?? ''), 'watu-sku-') === 0 ? substr((string) $item['product_slug'], strlen('watu-sku-')) : '', 'provision_input' => (array) ($item['provisioning_input'] ?? []), 'domain' => '', 'demo_mode' => dsa_demo_mode_active($pdo), 'tenant_domain' => (string) ($tenantContext['tenant_domain'] ?? ''), 'tenant_service_id' => (int) ($tenantContext['tenant_service_id'] ?? 0), ]; $http = dsa_http_post_json_with_watu_key($dispatchUrl, $watuKey, $payload); $bodyJson = json_decode((string) ($http['body'] ?? ''), true); $status = trim((string) ($bodyJson['status'] ?? 'failed')); $message = trim((string) ($bodyJson['message'] ?? ($http['error'] ?? 'Dispatch failed.'))); $whmcsOrderId = (int) ($bodyJson['whmcs_order_id'] ?? 0); $whmcsInvoiceId = (int) ($bodyJson['whmcs_invoice_id'] ?? 0); $whmcsServiceId = (int) ($bodyJson['whmcs_service_id'] ?? 0); $serviceDetails = is_array($bodyJson['service_details'] ?? null) ? (array) $bodyJson['service_details'] : []; $serviceAssets = is_array($bodyJson['service_assets'] ?? null) ? (array) $bodyJson['service_assets'] : []; if ($firstWhmcsOrderId <= 0 && $whmcsOrderId > 0) { $firstWhmcsOrderId = $whmcsOrderId; } if ($firstWhmcsInvoiceId <= 0 && $whmcsInvoiceId > 0) { $firstWhmcsInvoiceId = $whmcsInvoiceId; } if ($firstWhmcsServiceId <= 0 && $whmcsServiceId > 0) { $firstWhmcsServiceId = $whmcsServiceId; } if (count($firstServiceDetails) === 0 && count($serviceDetails) > 0) { $firstServiceDetails = $serviceDetails; } if (count($firstServiceAssets) === 0 && count($serviceAssets) > 0) { $firstServiceAssets = $serviceAssets; } if (empty($http['ok'])) { $allOk = false; } $messageParts[] = 'line ' . $lineNo . ': ' . ($message !== '' ? $message : 'dispatched'); if (in_array($status, ['provisioned', 'invoice_paid', 'invoice_created'], true)) { $finalStatus = $status; } } } $message = implode(' | ', $messageParts); if ($message === '') { $message = 'Dispatch finished with no response.'; } if (!$allOk && $finalStatus === 'failed') { $finalStatus = 'failed'; } $stmt = $pdo->prepare( "update orders set whmcs_order_id = :whmcs_order_id, whmcs_invoice_id = :whmcs_invoice_id, whmcs_service_id = :whmcs_service_id, whmcs_dispatch_status = :whmcs_dispatch_status, whmcs_dispatch_message = :whmcs_dispatch_message, service_details_json = :service_details_json, whmcs_dispatched_at = datetime('now'), updated_at = datetime('now') where id = :id" ); $stmt->execute([ ':id' => $orderId, ':whmcs_order_id' => $firstWhmcsOrderId, ':whmcs_invoice_id' => $firstWhmcsInvoiceId, ':whmcs_service_id' => $firstWhmcsServiceId, ':whmcs_dispatch_status' => $finalStatus, ':whmcs_dispatch_message' => $message, ':service_details_json' => json_encode($firstServiceDetails, JSON_UNESCAPED_UNICODE), ]); dsa_order_store_dispatch_assets($pdo, $orderId, [ 'service_assets' => $firstServiceAssets, 'service_details' => $firstServiceDetails, 'whmcs_domain_id' => (int) ($firstServiceAssets['whmcs_domain_id'] ?? 0), ]); if ( $allOk && in_array($finalStatus, ['invoice_created', 'invoice_paid', 'provisioned'], true) && dsa_order_has_real_mpesa_reference($paymentRef) ) { dsa_setting_set($pdo, dsa_order_dispatched_payment_ref_setting_key($orderId), $paymentRef); } return [ 'ok' => $allOk, 'message' => $message !== '' ? $message : 'Dispatch completed.', 'status' => $finalStatus, 'whmcs_invoice_id' => $firstWhmcsInvoiceId, ]; } function dsa_backfill_service_details(PDO $pdo, int $limit = 500): array { $limit = max(1, min(2000, $limit)); $dispatchUrl = dsa_order_dispatch_url($pdo); $watuKey = trim(dsa_setting($pdo, 'catalog_sync_whmcs_secret', dsa_setting($pdo, 'catalog_sync_whmcs_identifier', ''))); if ($dispatchUrl === '' || $watuKey === '') { return ['ok' => false, 'message' => 'Backfill failed: dispatch endpoint/key not configured.']; } $stmt = $pdo->prepare( "select o.id from orders o where o.whmcs_service_id > 0 and trim(coalesce(o.service_details_json, '')) in ('', '{}') order by o.id desc limit " . (int) $limit ); $stmt->execute([]); $rows = $stmt->fetchAll() ?: []; if (count($rows) === 0) { return ['ok' => true, 'message' => 'Service details backfill complete: no pending orders found.']; } $updated = 0; $skipped = 0; $failed = 0; foreach ($rows as $row) { $orderId = (int) ($row['id'] ?? 0); if ($orderId <= 0) { $skipped++; continue; } $order = dsa_get_order_with_product($pdo, $orderId); if (!$order) { $skipped++; continue; } $items = dsa_order_dispatch_items($pdo, $orderId); if (count($items) === 0) { $items = [[ 'product_slug' => (string) ($order['product_slug'] ?? ''), 'product_name' => (string) ($order['product_name'] ?? ''), 'unit_amount_cents' => (int) ($order['amount_cents'] ?? 0), 'currency' => (string) ($order['currency'] ?? DSA_DEFAULT_CURRENCY), 'provisioning_input' => [], ]]; } $item = $items[0]; $dsaOrderRef = dsa_order_account_reference_live($pdo, (array) $order); if (strpos($dsaOrderRef, 'DSA-0-') === 0) { $dsaOrderRef = ''; } $payload = array_merge([ 'order_id' => $orderId, 'external_order_ref' => 'tenant-order-' . $orderId . '-line-1', 'amount_cents' => (int) ($item['unit_amount_cents'] ?? (int) ($order['amount_cents'] ?? 0)), 'currency' => (string) ($item['currency'] ?? (string) ($order['currency'] ?? DSA_DEFAULT_CURRENCY)), 'tenant_payment_method' => (string) ($order['payment_method'] ?? ''), 'payment_ref' => (string) ($order['payment_ref'] ?? ''), 'dsa_order_ref' => $dsaOrderRef, 'customer_name' => (string) ($order['customer_name'] ?? ''), 'customer_email' => (string) ($order['customer_email'] ?? ''), 'customer_phone' => (string) ($order['customer_phone'] ?? ''), 'product_slug' => (string) ($item['product_slug'] ?? ''), 'product_name' => (string) ($item['product_name'] ?? ''), 'product_sku' => strpos((string) ($item['product_slug'] ?? ''), 'watu-sku-') === 0 ? substr((string) ($item['product_slug'] ?? ''), strlen('watu-sku-')) : '', 'provision_input' => (array) ($item['provisioning_input'] ?? []), 'domain' => '', 'demo_mode' => dsa_demo_mode_active($pdo), ], dsa_dispatch_tenant_context($pdo)); $http = dsa_http_post_json_with_watu_key($dispatchUrl, $watuKey, $payload); $bodyJson = json_decode((string) ($http['body'] ?? ''), true); $serviceDetails = is_array($bodyJson['service_details'] ?? null) ? (array) $bodyJson['service_details'] : []; if (count($serviceDetails) === 0) { $failed++; continue; } $upd = $pdo->prepare("update orders set service_details_json = :json, updated_at = datetime('now') where id = :id"); $upd->execute([ ':id' => $orderId, ':json' => json_encode($serviceDetails, JSON_UNESCAPED_UNICODE), ]); $updated++; } return [ 'ok' => true, 'message' => 'Service details backfill complete: updated ' . $updated . ', skipped ' . $skipped . ', failed ' . $failed . '.', ]; } function dsa_sync_paid_signal_from_whmcs(PDO $pdo, int $orderId, bool $force = false): void { if ($orderId <= 0) { return; } $stmt = $pdo->prepare( "select o.id, o.product_id, o.status, o.payment_method, o.payment_ref, p.slug as product_slug, p.catalog_product_id as source_product_id from orders o join products p on p.id = o.product_id where o.id = :id limit 1" ); $stmt->execute([':id' => $orderId]); $order = $stmt->fetch(); if (!$order) { return; } $orderStatus = (string) ($order['status'] ?? ''); if (in_array($orderStatus, ['paid', 'fulfilled'], true) && !dsa_order_awaiting_watu_pay_reconciliation($order)) { return; } $paymentMethod = (string) ($order['payment_method'] ?? ''); if (!in_array($paymentMethod, ['watupay_mpesa', 'mpesa_direct'], true)) { return; } if (!dsa_order_paid_sync_should_probe($pdo, $orderId, $force)) { return; } $statusUrl = dsa_order_payment_status_url($pdo); $watuKey = trim(dsa_setting($pdo, 'catalog_sync_whmcs_secret', dsa_setting($pdo, 'catalog_sync_whmcs_identifier', ''))); if ($statusUrl === '' || $watuKey === '') { return; } $orderRef = dsa_order_account_reference_live($pdo, (array) $order); if (strpos($orderRef, 'DSA-0-') === 0) { $sourceProductId = dsa_order_source_product_id_for_order($pdo, (int) ($order['id'] ?? 0), (array) $order); if ($sourceProductId > 0) { $order['source_product_id'] = $sourceProductId; $orderRef = dsa_order_account_reference($order); } } $existingRef = trim((string) ($order['payment_ref'] ?? '')); if ($existingRef !== '' && stripos($existingRef, 'DSA-') === 0) { $orderRef = $existingRef; } $sep = strpos($statusUrl, '?') === false ? '?' : '&'; $lookupUrl = $statusUrl . $sep . 'order_ref=' . rawurlencode($orderRef); $http = dsa_http_get_with_watu_key($lookupUrl, $watuKey); if (empty($http['ok']) || trim((string) ($http['body'] ?? '')) === '') { return; } $json = json_decode((string) $http['body'], true); if (!is_array($json) || empty($json['ok']) || empty($json['payment_found'])) { return; } $txnStatus = strtolower(trim((string) ($json['status'] ?? ''))); $mpesaCode = trim((string) ($json['mpesa_code'] ?? '')); if (!dsa_watu_pay_txn_syncable_for_dsa($txnStatus, $mpesaCode, $orderRef)) { return; } dsa_reset_order_for_new_watu_pay_dispatch($pdo, $orderId, $mpesaCode); $upd = $pdo->prepare( "update orders set status = 'paid', payment_ref = case when :payment_ref = '' then payment_ref else :payment_ref end, updated_at = datetime('now') where id = :id" ); $upd->execute([ ':id' => $orderId, ':payment_ref' => $mpesaCode, ]); dsa_record_watupay_credit_for_order($pdo, $orderId); dsa_send_order_notification_email($pdo, $orderId, 'payment_received'); dsa_payment_flow_state_clear($orderId); if (dsa_demo_mode_active($pdo)) { dsa_demo_mode_mark_paid_no_provision($pdo, $orderId); return; } dsa_on_order_became_paid($pdo, $orderId); } function dsa_order_paid_sync_should_probe(PDO $pdo, int $orderId, bool $force = false): bool { if ($orderId <= 0) { return false; } if ($force) { return true; } static $requestMemo = []; $memoKey = (string) spl_object_id($pdo) . '|' . (string) $orderId; $now = time(); if (isset($requestMemo[$memoKey]) && ($now - (int) $requestMemo[$memoKey]) < 15) { return false; } $requestMemo[$memoKey] = $now; $settingKey = 'order_paid_sync_checked_at_' . $orderId; $lastCheckedAt = (int) dsa_setting($pdo, $settingKey, '0'); if ($lastCheckedAt > 0 && ($now - $lastCheckedAt) < 30) { return false; } dsa_setting_set($pdo, $settingKey, (string) $now); return true; } /** @return array{success:bool,message:string} */ function dsa_admin_sync_order_watupay(PDO $pdo, int $orderId): array { if ($orderId <= 0) { return ['success' => false, 'message' => 'Invalid order ID.']; } $stmt = $pdo->prepare( "select o.id, o.status, o.payment_method, o.payment_ref, o.product_id, p.slug as product_slug, p.catalog_product_id as source_product_id from orders o join products p on p.id = o.product_id where o.id = :id limit 1" ); $stmt->execute([':id' => $orderId]); $order = $stmt->fetch(); if (!$order) { return ['success' => false, 'message' => 'Order not found.']; } $status = (string) ($order['status'] ?? ''); if (in_array($status, ['paid', 'fulfilled'], true)) { return ['success' => true, 'message' => 'Order #' . $orderId . ' is already ' . dsa_admin_order_status_label($status) . '.']; } $paymentMethod = (string) ($order['payment_method'] ?? ''); if (!in_array($paymentMethod, ['watupay_mpesa', 'mpesa_direct'], true)) { return ['success' => false, 'message' => 'Order #' . $orderId . ' is not a Watu Technology M-PESA / gateway order.']; } $orderRef = dsa_order_account_reference_live($pdo, (array) $order); if (strpos($orderRef, 'DSA-0-') === 0) { $sourceProductId = dsa_order_source_product_id_for_order($pdo, $orderId, (array) $order); if ($sourceProductId > 0) { $order['source_product_id'] = $sourceProductId; $orderRef = dsa_order_account_reference($order); } } $existingRef = trim((string) ($order['payment_ref'] ?? '')); if ($existingRef !== '' && stripos($existingRef, 'DSA-') === 0) { $orderRef = $existingRef; } $statusUrl = dsa_order_payment_status_url($pdo); $watuKey = trim(dsa_setting($pdo, 'catalog_sync_whmcs_secret', dsa_setting($pdo, 'catalog_sync_whmcs_identifier', ''))); if ($statusUrl === '' || $watuKey === '') { return ['success' => false, 'message' => 'Watu Technology M-PESA sync is not configured (missing bridge URL or Watu key).']; } $sep = strpos($statusUrl, '?') === false ? '?' : '&'; $lookupUrl = $statusUrl . $sep . 'order_ref=' . rawurlencode($orderRef); $http = dsa_http_get_with_watu_key($lookupUrl, $watuKey); if (empty($http['ok'])) { return ['success' => false, 'message' => 'Could not reach Watu Technology payment status API for ref ' . $orderRef . '.']; } $json = json_decode(trim((string) ($http['body'] ?? '')), true); if (!is_array($json) || empty($json['ok'])) { return ['success' => false, 'message' => 'Invalid response from Watu Technology payment gateway for ref ' . $orderRef . '.']; } if (empty($json['payment_found'])) { return [ 'success' => false, 'message' => 'No transaction found for ref ' . $orderRef . '. ' . 'Check gateway admin — use Corrected acc if the payer typed the wrong account, then Close (DSA sync).', ]; } $txnStatus = strtolower(trim((string) ($json['status'] ?? ''))); $mpesaCode = trim((string) ($json['mpesa_code'] ?? '')); if (!dsa_watu_pay_txn_syncable_for_dsa($txnStatus, $mpesaCode, $orderRef)) { return [ 'success' => false, 'message' => 'Watu Technology M-PESA has ref ' . $orderRef . ' but status is "' . $txnStatus . '" (needs open/closed with M-PESA code).', ]; } dsa_sync_paid_signal_from_whmcs($pdo, $orderId, true); $stmt->execute([':id' => $orderId]); $after = $stmt->fetch(); $afterStatus = (string) ($after['status'] ?? ''); if (in_array($afterStatus, ['paid', 'fulfilled'], true)) { return [ 'success' => true, 'message' => 'Order #' . $orderId . ' synced from Watu Technology M-PESA (' . $orderRef . ') → ' . dsa_admin_order_status_label($afterStatus) . '.', ]; } return [ 'success' => false, 'message' => 'Watu Technology M-PESA payment is closed for ' . $orderRef . ' but order #' . $orderId . ' did not update. Check dispatch logs.', ]; } function dsa_whmcs_extract_price_cents(array $remote, string &$currency): int { $pricing = $remote['pricing'] ?? []; if (!is_array($pricing) || count($pricing) === 0) { return 0; } foreach ($pricing as $currencyCode => $plans) { if (!is_array($plans)) { continue; } $currency = strtoupper((string) $currencyCode); foreach (['monthly', 'quarterly', 'semiannually', 'annually', 'biennially', 'triennially', 'onetime'] as $period) { $value = (string) ($plans[$period] ?? '-1.00'); $amount = dsa_parse_whmcs_money($value); if ($amount > 0) { return (int) round($amount * 100); } } } return 0; } function dsa_normalize_provisioning_profile(string $value): string { $profile = strtolower(trim($value)); $allowed = ['none', 'hosting_auto_username', 'hosting_custom_username', 'domain_registration']; return in_array($profile, $allowed, true) ? $profile : 'none'; } function dsa_parse_provisioning_rules(array $product): array { $raw = trim((string) ($product['provisioning_rules_json'] ?? '{}')); if ($raw === '') { $raw = '{}'; } $decoded = json_decode($raw, true); if (!is_array($decoded)) { $decoded = []; } $defaultTld = trim((string) ($decoded['default_tld'] ?? '')); if ($defaultTld !== '' && $defaultTld[0] !== '.') { $defaultTld = '.' . $defaultTld; } $decoded['default_tld'] = strtolower($defaultTld); $fields = $decoded['checkout_fields'] ?? []; if (!is_array($fields)) { $fields = []; } $decoded['checkout_fields'] = $fields; return $decoded; } function dsa_build_provisioning_rules_json_from_post(array $post): string { $defaultTld = strtolower(trim((string) ($post['provision_default_tld'] ?? ''))); if ($defaultTld !== '' && $defaultTld[0] !== '.') { $defaultTld = '.' . $defaultTld; } if ($defaultTld !== '' && !preg_match('/^\.[a-z0-9-]{2,20}$/', $defaultTld)) { $defaultTld = ''; } return json_encode(['default_tld' => $defaultTld], JSON_UNESCAPED_UNICODE); } function dsa_domain_normalize_input(string $raw, string $defaultTld = ''): string { $domain = strtolower(trim($raw)); if ($domain === '') { return ''; } $domain = preg_replace('/\s+/', '', $domain) ?? ''; if ($domain === '') { return ''; } if (!preg_match('/^[a-z0-9][a-z0-9-]*\.[a-z0-9.-]+$/', $domain)) { return ''; } return $domain; } function dsa_domain_is_reserved(string $domain): bool { $domain = strtolower(trim($domain)); if ($domain === '') { return false; } static $reserved = [ 'example.com', 'example.org', 'example.net', 'example.edu', 'example.gov', 'test.com', 'test.example', 'invalid', 'localhost', ]; if (in_array($domain, $reserved, true)) { return true; } if (preg_match('/\.(example|invalid|test|localhost)$/', $domain) === 1) { return true; } return false; } function dsa_domain_reserved_error(string $domain): string { return 'The domain "' . $domain . '" is reserved and cannot be used for provisioning. Enter your real domain name.'; } function dsa_provision_post_value(array $post, string $key, string $cartSlug = ''): string { if ($cartSlug !== '') { $bag = $post['provision_field'] ?? []; if (is_array($bag) && is_array($bag[$cartSlug] ?? null)) { return trim((string) (($bag[$cartSlug])[$key] ?? '')); } } else { $bag = $post['provision_field'] ?? []; if (is_array($bag)) { return trim((string) ($bag[$key] ?? '')); } } if ($key === 'domain') { if ($cartSlug !== '' && is_array($post['provision_domain'] ?? null)) { return trim((string) (($post['provision_domain'])[$cartSlug] ?? '')); } return trim((string) ($post['provision_domain'] ?? '')); } if ($key === 'username') { if ($cartSlug !== '' && is_array($post['provision_username'] ?? null)) { return trim((string) (($post['provision_username'])[$cartSlug] ?? '')); } return trim((string) ($post['provision_username'] ?? '')); } return ''; } function dsa_collect_provisioning_input(array $product, array $post, string $cartSlug = ''): array { $contract = dsa_provisioning_contract_from_product($product); if (empty($contract['sync_pulled'])) { return ['ok' => false, 'error' => 'Product configuration is incomplete. Please contact the store.', 'input' => []]; } $rules = $contract['rules']; $fields = $rules['checkout_fields'] ?? []; if (!is_array($fields)) { $fields = []; } $input = []; foreach ($fields as $field) { if (!is_array($field)) { continue; } $key = trim((string) ($field['key'] ?? '')); if ($key === '') { continue; } $required = !empty($field['required']); $source = strtolower(trim((string) ($field['source'] ?? 'customfield'))); $mode = strtolower(trim((string) ($field['mode'] ?? ''))); $value = dsa_provision_post_value($post, $key, $cartSlug); if ($source === 'domain' || $source === 'service_domain' || $key === 'domain') { $domain = dsa_domain_normalize_input($value, (string) ($rules['default_tld'] ?? '')); if ($domain === '' && $required) { return ['ok' => false, 'error' => 'Please enter a valid domain (e.g. example.com).', 'input' => []]; } if ($domain !== '' && dsa_domain_is_reserved($domain)) { return ['ok' => false, 'error' => dsa_domain_reserved_error($domain), 'input' => []]; } if ($domain !== '') { $input['domain'] = $domain; } continue; } if ($source === 'service_username' || $key === 'username') { if ($mode === 'auto') { $input['username_mode'] = 'auto'; continue; } $username = strtolower($value); if ($username === '' && !$required) { continue; } if (!preg_match('/^[a-z][a-z0-9]{2,15}$/', $username)) { return ['ok' => false, 'error' => 'Please enter a valid username (3-16 chars, letters/numbers, starts with a letter).', 'input' => []]; } $input['username'] = $username; $input['username_mode'] = 'provided'; continue; } if ($value === '') { if ($required) { return ['ok' => false, 'error' => 'Please fill all required provisioning fields.', 'input' => []]; } continue; } $input[$key] = $value; } return ['ok' => true, 'error' => '', 'input' => $input]; } function dsa_provisioning_field_display_label(array $field): string { $key = trim((string) ($field['key'] ?? '')); $label = trim((string) ($field['label'] ?? '')); if ($label !== '' && strpos($label, '|') !== false) { $parts = explode('|', $label, 2); $label = trim((string) ($parts[1] ?? $parts[0])); } if ($label === '' || $label === $key) { if (strpos($key, '|') !== false) { $parts = explode('|', $key, 2); return trim((string) ($parts[1] ?? $parts[0])); } return $key; } return $label; } function dsa_render_product_provisioning_fields(array $product, string $cartSlug = '', array $post = []): string { $contract = dsa_provisioning_contract_from_product($product); if (empty($contract['sync_pulled'])) { return '

Provisioning rules were not synced for this product. Run catalog sync in admin before checkout.

'; } $rules = $contract['rules']; $fields = $rules['checkout_fields'] ?? []; if (!is_array($fields) || count($fields) === 0) { return ''; } $html = '
'; foreach ($fields as $field) { if (!is_array($field)) { continue; } $key = trim((string) ($field['key'] ?? '')); if ($key === '') { continue; } $label = dsa_provisioning_field_display_label($field); $required = !empty($field['required']); $source = strtolower(trim((string) ($field['source'] ?? 'customfield'))); $mode = strtolower(trim((string) ($field['mode'] ?? ''))); if (($source === 'service_username' || $key === 'username') && $mode === 'auto') { $html .= '

Username will be auto-generated at provisioning time.

'; continue; } $inputType = strtolower(trim((string) ($field['input'] ?? 'text'))); $name = $cartSlug !== '' ? ('provision_field[' . $cartSlug . '][' . $key . ']') : ('provision_field[' . $key . ']'); $value = dsa_provision_post_value($post, $key, $cartSlug); $isTextarea = $inputType === 'textarea'; $placeholder = ''; if ($source === 'domain' || $source === 'service_domain' || $key === 'domain') { $placeholder = 'example.com'; } elseif ($source === 'service_username' || $key === 'username') { $placeholder = 'yourusername'; } if ($isTextarea) { $html .= ''; } else { $type = $inputType === 'password' ? 'password' : 'text'; $html .= ''; } $desc = trim((string) ($field['description'] ?? '')); if ($desc !== '') { $html .= '

' . dsa_h($desc) . '

'; } } $html .= '
'; return $html; } function dsa_guess_default_tld(string $name): string { if (preg_match('/(\.[a-z]{2,20})/i', $name, $m)) { return strtolower((string) ($m[1] ?? '.com')); } return '.com'; } function dsa_build_admin_url(array $params): string { $query = http_build_query($params); return '/admin' . ($query !== '' ? ('?' . $query) : ''); } function dsa_truncate_text(string $value, int $maxLength = 140): string { $text = trim($value); if ($maxLength < 4 || strlen($text) <= $maxLength) { return $text; } return rtrim(substr($text, 0, $maxLength - 3)) . '...'; } function dsa_render_admin(PDO $pdo, string $notice, string $filter = 'all', string $section = 'dashboard'): void { dsa_release_session_lock_if_safe(); if ($notice === '' && trim((string) ($_GET['notice'] ?? '')) !== '') { $notice = trim((string) $_GET['notice']); } $allowedSections = ['dashboard', 'catalog', 'custom-services', 'products', 'orders', 'customers', 'support', 'notifications', 'payments', 'payouts', 'branding', 'analytics', 'settings']; if (!in_array($section, $allowedSections, true)) { $section = 'dashboard'; } $forceWatuPayTab = false; if ($section === 'payouts') { $section = 'payments'; $forceWatuPayTab = true; } $catalogCategory = trim((string) ($_GET['catalog_category'] ?? 'all')); if ($catalogCategory === '') { $catalogCategory = 'all'; } $catalogState = trim((string) ($_GET['catalog_state'] ?? 'all')); if (!in_array($catalogState, ['all', 'enabled', 'not_enabled'], true)) { $catalogState = 'all'; } $catalogQuery = trim((string) ($_GET['catalog_q'] ?? '')); $catalogPage = (int) ($_GET['catalog_page'] ?? 1); if ($catalogPage < 1) { $catalogPage = 1; } $catalogPerPage = 5; $ordersQuery = trim((string) ($_GET['orders_q'] ?? '')); $ordersPage = max(1, (int) ($_GET['orders_page'] ?? 1)); $ordersPerPage = 40; $ordersTotal = 0; $ordersPages = 1; $customersQuery = trim((string) ($_GET['customers_q'] ?? '')); $customersStatus = trim((string) ($_GET['customers_status'] ?? 'all')); if (!in_array($customersStatus, ['all', 'with_orders', 'without_orders'], true)) { $customersStatus = 'all'; } $customersSort = trim((string) ($_GET['customers_sort'] ?? 'recent')); if (!in_array($customersSort, ['recent', 'name', 'email', 'orders'], true)) { $customersSort = 'recent'; } $focusCustomerId = (int) ($_GET['customer_id'] ?? 0); $catalog = []; $products = []; $productCategories = []; $productsCategory = trim((string) ($_GET['products_category'] ?? 'all')); if ($productsCategory === '') { $productsCategory = 'all'; } $orders = []; $customers = []; $dashboardCatalogCount = 0; $dashboardProductsCount = 0; $dashboardOrdersCount = 0; $catalogCategories = []; $catalogTotal = 0; $catalogPages = 1; $needsCatalog = in_array($section, ['catalog'], true); $needsProducts = in_array($section, ['products'], true); $needsOrders = in_array($section, ['orders'], true); $needsCustomers = in_array($section, ['customers'], true); $needsNotifications = in_array($section, ['notifications'], true); $needsDashboardCounts = ($section === 'dashboard'); $notificationTemplateCatalog = []; if ($needsCatalog) { $catalogCategories = $pdo->query("select distinct category from watu_catalog_products order by category asc")->fetchAll(PDO::FETCH_COLUMN) ?: []; $catalogCategories = array_values(array_filter(array_map('strval', $catalogCategories), static function ($value) { return trim($value) !== ''; })); $catalogBaseSql = "from watu_catalog_products c left join products p on p.catalog_product_id = c.id"; $catalogWhereParts = []; $catalogParams = []; if ($catalogCategory !== 'all') { $catalogWhereParts[] = "c.category = :category"; $catalogParams[':category'] = $catalogCategory; } if ($catalogState === 'enabled') { $catalogWhereParts[] = "p.id is not null"; } elseif ($catalogState === 'not_enabled') { $catalogWhereParts[] = "p.id is null"; } if ($catalogQuery !== '') { $catalogWhereParts[] = "(lower(c.name) like :search or lower(c.slug) like :search or lower(c.description) like :search)"; $catalogParams[':search'] = '%' . strtolower($catalogQuery) . '%'; } $catalogWhereSql = count($catalogWhereParts) > 0 ? (' where ' . implode(' and ', $catalogWhereParts)) : ''; $catalogTotalStmt = $pdo->prepare("select count(*) " . $catalogBaseSql . $catalogWhereSql); $catalogTotalStmt->execute($catalogParams); $catalogTotal = (int) $catalogTotalStmt->fetchColumn(); if ($needsCatalog) { $catalogPages = max(1, (int) ceil($catalogTotal / $catalogPerPage)); if ($catalogPage > $catalogPages) { $catalogPage = $catalogPages; } $catalogOffset = ($catalogPage - 1) * $catalogPerPage; $catalogSql = "select c.id, c.slug, c.name, c.category, c.description, c.cost_cents, c.currency, c.status, c.provisioning_profile, c.provisioning_rules_json, p.id as enabled_product_id, p.price_cents as sell_price_cents, p.status as sell_status " . $catalogBaseSql . $catalogWhereSql . " order by c.category asc, c.name asc limit " . (int) $catalogPerPage . " offset " . (int) $catalogOffset; $catalogStmt = $pdo->prepare($catalogSql); $catalogStmt->execute($catalogParams); $catalog = $catalogStmt->fetchAll(); } } if ($needsProducts) { $productCategories = $pdo->query("select distinct category from products order by category asc")->fetchAll(PDO::FETCH_COLUMN) ?: []; $productCategories = array_values(array_filter(array_map('strval', $productCategories), static function ($value) { return trim($value) !== ''; })); $productsWhere = ''; $productsParams = []; if ($productsCategory !== 'all') { $productsWhere = ' where category = :category'; $productsParams[':category'] = $productsCategory; } $productsStmt = $pdo->prepare( "select id, slug, name, category, description, cost_cents, price_cents, currency, status, product_source, price_monthly_cents, price_annual_cents, billing_cycles_json from products" . $productsWhere . " order by category asc, name asc limit 200" ); $productsStmt->execute($productsParams); $products = $productsStmt->fetchAll() ?: []; } if (!in_array($filter, ['all', 'pending_proof', 'paid', 'fulfilled', 'cancelled'], true)) { $filter = 'all'; } if ($needsOrders) { $ordersWhereParts = []; $ordersParams = []; if ($filter === 'pending_proof') { $ordersWhereParts[] = "o.status = 'pending_payment'"; } elseif ($filter === 'paid') { $ordersWhereParts[] = "o.status = 'paid'"; } elseif ($filter === 'fulfilled') { $ordersWhereParts[] = "o.status = 'fulfilled'"; } elseif ($filter === 'cancelled') { $ordersWhereParts[] = "o.status = 'cancelled'"; } if ($ordersQuery !== '') { $ordersWhereParts[] = "(cast(o.id as text) like :oq or lower(o.customer_email) like :oq or lower(o.customer_name) like :oq)"; $ordersParams[':oq'] = '%' . strtolower($ordersQuery) . '%'; } $ordersWhereSql = count($ordersWhereParts) > 0 ? ('where ' . implode(' and ', $ordersWhereParts)) : ''; $ordersTotalStmt = $pdo->prepare("select count(*) from orders o join products p on p.id = o.product_id " . $ordersWhereSql); $ordersTotalStmt->execute($ordersParams); $ordersTotal = (int) $ordersTotalStmt->fetchColumn(); $ordersPages = max(1, (int) ceil($ordersTotal / $ordersPerPage)); if ($ordersPage > $ordersPages) { $ordersPage = $ordersPages; } $ordersOffset = ($ordersPage - 1) * $ordersPerPage; $ordersStmt = $pdo->prepare( "select o.id, o.customer_id, o.customer_name, o.customer_email, o.customer_phone, o.amount_cents, o.currency, o.status, o.payment_ref, o.payment_method, o.payment_proof_path, o.delivery_value, o.created_at, o.whmcs_order_id, o.whmcs_invoice_id, o.whmcs_service_id, o.whmcs_dispatch_status, o.whmcs_dispatch_message, o.whmcs_dispatched_at, p.name as product_name, p.slug as product_slug, p.catalog_product_id, p.provisioning_profile, p.fulfillment_type as fulfillment_type from orders o join products p on p.id = o.product_id " . $ordersWhereSql . " order by o.id desc limit " . (int) $ordersPerPage . " offset " . (int) $ordersOffset ); $ordersStmt->execute($ordersParams); $orders = $ordersStmt->fetchAll(); foreach ($orders as $idx => $orderRow) { if (dsa_order_repair_premature_fulfillment($pdo, (int) ($orderRow['id'] ?? 0))) { $orders[$idx]['status'] = 'paid'; } } } if ($needsCustomers) { $customersWhereParts = []; $customersParams = []; if ($customersQuery !== '') { $customersWhereParts[] = "(lower(c.name) like :search or lower(c.email) like :search or lower(ifnull(c.phone,'')) like :search)"; $customersParams[':search'] = '%' . strtolower($customersQuery) . '%'; } $customersHavingSql = ''; if ($customersStatus === 'with_orders') { $customersHavingSql = ' having count(o.id) > 0'; } elseif ($customersStatus === 'without_orders') { $customersHavingSql = ' having count(o.id) = 0'; } $customersWhereSql = count($customersWhereParts) > 0 ? (' where ' . implode(' and ', $customersWhereParts)) : ''; $customersOrderSql = ' order by max(c.updated_at) desc, c.id desc'; if ($customersSort === 'name') { $customersOrderSql = ' order by lower(c.name) asc, c.id desc'; } elseif ($customersSort === 'email') { $customersOrderSql = ' order by lower(c.email) asc, c.id desc'; } elseif ($customersSort === 'orders') { $customersOrderSql = ' order by count(o.id) desc, max(c.updated_at) desc, c.id desc'; } $customersStmt = $pdo->prepare( "select c.id, c.name, c.email, c.phone, c.created_at, c.updated_at, count(o.id) as order_count, coalesce(sum(o.amount_cents), 0) as total_spend_cents, max(o.created_at) as last_order_at from customers c left join orders o on o.customer_id = c.id" . $customersWhereSql . " group by c.id" . $customersHavingSql . $customersOrderSql . " limit 120" ); $customersStmt->execute($customersParams); $customers = $customersStmt->fetchAll(); } if ($needsNotifications) { $notificationTemplateCatalog = dsa_notification_templates($pdo); } $recentActivityOrders = []; if ($needsDashboardCounts) { $catalogCountStmt = $pdo->query("select count(*) from watu_catalog_products"); $productsCountStmt = $pdo->query("select count(*) from products"); $ordersCountStmt = $pdo->query("select count(*) from orders"); $dashboardCatalogCount = (int) ($catalogCountStmt ? $catalogCountStmt->fetchColumn() : 0); $dashboardProductsCount = (int) ($productsCountStmt ? $productsCountStmt->fetchColumn() : 0); $dashboardOrdersCount = (int) ($ordersCountStmt ? $ordersCountStmt->fetchColumn() : 0); $recentActivityStmt = $pdo->query( "select o.id, o.status, o.amount_cents, o.currency, o.updated_at, o.customer_email, p.name as product_name from orders o join products p on p.id = o.product_id order by o.updated_at desc limit 8" ); $recentActivityOrders = $recentActivityStmt ? $recentActivityStmt->fetchAll() : []; } $focusCustomer = null; $focusCustomerOrders = []; if ($focusCustomerId > 0) { $focusStmt = $pdo->prepare( "select c.id, c.name, c.email, c.phone, c.created_at, c.updated_at, count(o.id) as order_count, coalesce(sum(o.amount_cents), 0) as total_spend_cents, max(o.created_at) as last_order_at from customers c left join orders o on o.customer_id = c.id where c.id = :id group by c.id limit 1" ); $focusStmt->execute([':id' => $focusCustomerId]); $focusCustomer = $focusStmt->fetch(); if ($focusCustomer) { $focusOrdersStmt = $pdo->prepare( "select o.id, o.status, o.amount_cents, o.currency, o.created_at, o.payment_method, p.name as product_name from orders o join products p on p.id = o.product_id where o.customer_id = :customer_id order by o.id desc limit 20" ); $focusOrdersStmt->execute([':customer_id' => $focusCustomerId]); $focusCustomerOrders = $focusOrdersStmt->fetchAll(); } } dsa_settings_prefetch($pdo); $needsBrandingData = ($section === 'branding'); $needsPaymentData = ($section === 'payments'); $needsPayoutData = in_array($section, ['payouts', 'dashboard'], true); $needsSyncData = ($section === 'catalog'); $payInstructions = ''; $brandTitle = DSA_APP_NAME; $brandTagline = DSA_DEFAULT_BRAND_TAGLINE; $heroTitle = DSA_DEFAULT_HERO_TITLE; $heroSubtitle = DSA_DEFAULT_HERO_SUBTITLE; $emailLogoUrl = ''; $emailPrimaryColor = '#2563eb'; $siteThemeSetting = 'dark'; $siteAccentColor = '#2563eb'; $sitePresetSetting = 'custom'; $presetDefs = dsa_theme_presets(); $siteUiTheme = 'store'; $themeExtStatus = ['elite' => false, 'rebel' => false, 'powerhouse' => false, 'warmth' => false]; $emailFooterText = ''; $whiteLabelDomain = ''; $systemOriginDomain = ''; $forceWhitelabelDomain = false; $whitelabelLastSyncMessage = ''; $whitelabelLastBackgroundCheckAt = 0; $whitelabelPendingDomain = ''; $whitelabelPendingEnforce = false; $whitelabelLastBackgroundText = ''; $whitelabelLastSync = []; $paymentMethodCashEnabled = true; $paymentMethodBankEnabled = false; $paymentMethodCustomEnabled = false; $paymentMethodPaypalEnabled = false; $paymentMethodWatuPayEnabled = false; $paymentMethodMpesaDirectEnabled = false; $paymentBankInstructions = ''; $paymentCustomLabel = 'Custom Manual'; $paymentCustomInstructions = ''; $paymentPaypalLink = ''; $paymentPaypalBusinessEmail = ''; $paymentPaypalSandbox = false; $paymentWatuPayConsumerKey = ''; $paymentWatuPayConsumerSecret = ''; $paymentWatuPayShortcode = ''; $paymentWatuPayPasskey = ''; $paymentWatuPayTillNumber = ''; $paymentWatuPayUseTill = false; $paymentWatuPaySandbox = false; $watupayPayoutChannel = 'phone'; $watupayPayoutTarget = ''; $watupayPayoutTiming = 'manual'; $watupayPayoutSchedule = ''; $paymentWatuPayInstructions = ''; $paymentMpesaDirectLabel = 'Lipa na M-PESA'; $paymentMpesaDirectInstructions = ''; $paymentMpesaConsumerKey = ''; $paymentMpesaConsumerSecret = ''; $paymentMpesaShortcode = ''; $paymentMpesaPasskey = ''; $paymentMpesaTillNumber = ''; $paymentMpesaUseTill = false; $paymentMpesaSandbox = false; $paymentCustomMethodsText = ''; $catalogSyncLastAt = ''; $catalogSyncApiUrl = ''; $catalogSyncIdentifier = ''; $catalogSyncSecret = ''; $catalogSyncDefaultMarkupPercent = '30'; $catalogSyncAutoApplyMarkup = true; $catalogSyncPullPaymentOptions = true; $catalogSyncPreserveStoreOverrides = true; $catalogSyncAutoEnabled = false; $catalogSyncIntervalMinutes = 60; $catalogSyncLastAutoAt = ''; $catalogSyncLastAutoResult = ''; $allowCustomProductCreate = true; $releaseUpdaterEnabled = false; $releaseManifestUrl = ''; $releaseCurrentVersion = ''; $releaseLastUpdateAt = ''; $releaseLastUpdateResult = ''; $releaseUpdaterToken = ''; if ($needsPayoutData) { // Loaded via dsa_payout_balance_summary in section renderers. } if ($needsBrandingData) { $brandTitle = dsa_setting($pdo, 'brand_title', DSA_APP_NAME); $brandTagline = dsa_setting($pdo, 'brand_tagline', DSA_DEFAULT_BRAND_TAGLINE); $heroTitle = dsa_setting($pdo, 'hero_title', DSA_DEFAULT_HERO_TITLE); $heroSubtitle = dsa_setting($pdo, 'hero_subtitle', DSA_DEFAULT_HERO_SUBTITLE); $emailLogoUrl = dsa_brand_logo_url($pdo); $emailPrimaryColor = dsa_setting($pdo, 'email_primary_color', '#2563eb'); if (!preg_match('/^#[0-9a-fA-F]{6}$/', (string) $emailPrimaryColor)) { $emailPrimaryColor = '#2563eb'; } $siteThemeSetting = dsa_setting($pdo, 'site_theme', 'dark'); if (!in_array($siteThemeSetting, ['dark', 'light'], true)) { $siteThemeSetting = 'dark'; } $siteAccentColor = dsa_setting($pdo, 'site_accent_color', '#2563eb'); if (!preg_match('/^#[0-9a-fA-F]{6}$/', (string) $siteAccentColor)) { $siteAccentColor = '#2563eb'; } $sitePresetSetting = dsa_setting($pdo, 'site_preset', 'custom'); if (!isset($presetDefs[$sitePresetSetting])) { $sitePresetSetting = 'custom'; } $siteUiTheme = dsa_setting($pdo, 'site_ui_theme', 'store'); if (!in_array($siteUiTheme, ['store', 'elite', 'rebel', 'powerhouse', 'warmth'], true)) { $siteUiTheme = 'store'; } $themeExtStatus = [ 'elite' => dsa_extension_active($pdo, 'theme_elite'), 'rebel' => dsa_extension_active($pdo, 'theme_rebel'), 'powerhouse' => dsa_extension_active($pdo, 'theme_powerhouse'), 'warmth' => dsa_extension_active($pdo, 'theme_warmth'), ]; if ($siteUiTheme !== 'store' && !($themeExtStatus[$siteUiTheme] ?? false)) { $siteUiTheme = 'store'; } $emailFooterText = dsa_setting($pdo, 'email_footer_text', ''); $whiteLabelDomain = dsa_setting($pdo, 'whitelabel_domain', ''); $systemOriginDomain = dsa_setting($pdo, 'system_origin_domain', ''); $forceWhitelabelDomain = dsa_setting($pdo, 'force_whitelabel_domain', '0') === '1'; $whitelabelLastSyncMessage = dsa_setting($pdo, 'whitelabel_last_sync_message', ''); if (stripos((string) $whitelabelLastSyncMessage, 'cpanel') !== false) { $whitelabelLastSyncMessage = 'Domain mapping active. Save or check status to refresh readiness.'; } $whitelabelLastSyncMessage = str_ireplace( ['Click Save again shortly to refresh readiness.', 'Click Save again to refresh readiness.'], 'Readiness checks are auto-refreshing on this page.', (string) $whitelabelLastSyncMessage ); $whitelabelLastBackgroundCheckAt = (int) dsa_setting($pdo, 'whitelabel_last_background_check_at', '0'); $whitelabelPendingDomain = dsa_normalize_domain((string) dsa_setting($pdo, 'whitelabel_pending_domain', '')); $whitelabelPendingEnforce = dsa_setting($pdo, 'whitelabel_pending_enforce', '') === '1'; if ($whitelabelLastBackgroundCheckAt > 0) { $ago = max(0, time() - $whitelabelLastBackgroundCheckAt); $whitelabelLastBackgroundText = $ago < 60 ? ($ago . 's ago') : (floor($ago / 60) . 'm ago'); } $whitelabelLastSyncRaw = dsa_setting($pdo, 'whitelabel_last_sync_json', ''); $whitelabelLastSync = json_decode((string) $whitelabelLastSyncRaw, true); if (!is_array($whitelabelLastSync)) { $whitelabelLastSync = []; } $syncStatus = is_array($whitelabelLastSync['status'] ?? null) ? (array) ($whitelabelLastSync['status'] ?? []) : []; } if ($needsPaymentData) { $whmcsCredits = dsa_whmcs_credits_local($pdo); $creditCurrency = (string) ($whmcsCredits['currency'] ?? 'KES'); $creditAmountCents = (int) ($whmcsCredits['available_cents'] ?? 0); $creditFormatted = dsa_money($creditAmountCents, $creditCurrency); $addCreditsUrl = dsa_watu_clientarea_url($pdo, 'clientarea.php?action=addfunds'); if ($addCreditsUrl === '') { $addCreditsUrl = 'https://watutechnology.com/clientarea.php?action=addfunds'; } $storeCurrency = dsa_store_currency($pdo); $payInstructions = dsa_setting($pdo, 'pay_instructions', ''); $paymentMethodOfflinePlatformAllowed = dsa_offline_payment_platform_allowed($pdo); $paymentMethodPaypalPlatformAllowed = dsa_platform_payment_method_allowed($pdo, 'paypal'); $paymentMethodPaypalBasicPlatformAllowed = dsa_platform_payment_method_allowed($pdo, 'paypal_basic'); $paymentMethodPaypalCheckoutPlatformAllowed = dsa_platform_payment_method_allowed($pdo, 'paypal_checkout'); $paymentMethodWatuPayPlatformAllowed = dsa_platform_payment_method_allowed($pdo, 'watupay_mpesa'); $paymentMethodMpesaDirectPlatformAllowed = dsa_platform_payment_method_allowed($pdo, 'mpesa_direct'); $paymentMethodOfflineEnabled = $paymentMethodOfflinePlatformAllowed && dsa_offline_payment_tenant_enabled($pdo); $paymentMethodPaypalEnabled = $paymentMethodPaypalPlatformAllowed && dsa_setting($pdo, 'payment_method_paypal_enabled', '0') === '1'; $rawBasic = dsa_setting($pdo, 'payment_method_paypal_basic_enabled', ''); if ($rawBasic === '') { $rawBasic = dsa_setting($pdo, 'payment_method_paypal_enabled', '0'); } $rawCheckout = dsa_setting($pdo, 'payment_method_paypal_checkout_enabled', ''); if ($rawCheckout === '') { $rawCheckout = dsa_setting($pdo, 'payment_method_paypal_enabled', '0'); } $paymentMethodPaypalBasicEnabled = $paymentMethodPaypalBasicPlatformAllowed && $rawBasic === '1'; $paymentMethodPaypalCheckoutEnabled = $paymentMethodPaypalCheckoutPlatformAllowed && $rawCheckout === '1'; $paymentMethodWatuPayEnabled = $paymentMethodWatuPayPlatformAllowed && dsa_setting($pdo, 'payment_method_watupay_mpesa_enabled', '0') === '1'; $paymentMethodMpesaDirectEnabled = $paymentMethodMpesaDirectPlatformAllowed && dsa_setting($pdo, 'payment_method_mpesa_direct_enabled', '0') === '1'; $paymentBankInstructions = dsa_setting($pdo, 'payment_bank_instructions', ''); $paymentCustomLabel = dsa_setting($pdo, 'payment_custom_label', 'Custom Manual'); $paymentCustomInstructions = dsa_setting($pdo, 'payment_custom_instructions', ''); $paymentPaypalLink = dsa_setting($pdo, 'payment_paypal_link', ''); $paymentPaypalBusinessEmail = dsa_setting($pdo, 'payment_paypal_business_email', ''); $paymentPaypalClientId = dsa_setting($pdo, 'payment_paypal_client_id', ''); $paymentPaypalClientSecret = dsa_setting($pdo, 'payment_paypal_client_secret', ''); $paymentPaypalSandbox = dsa_setting($pdo, 'payment_paypal_sandbox', '0') === '1'; $paymentMethodStripePlatformAllowed = dsa_platform_payment_method_allowed($pdo, 'stripe'); $paymentMethodPaystackPlatformAllowed = dsa_platform_payment_method_allowed($pdo, 'paystack'); $paymentMethodStripeEnabled = $paymentMethodStripePlatformAllowed && dsa_setting($pdo, 'payment_method_stripe_enabled', '0') === '1'; $paymentMethodPaystackEnabled = $paymentMethodPaystackPlatformAllowed && dsa_setting($pdo, 'payment_method_paystack_enabled', '0') === '1'; $paymentStripeLink = dsa_setting($pdo, 'payment_stripe_link', ''); $paymentStripePublishableKey = dsa_setting($pdo, 'payment_stripe_publishable_key', ''); $paymentStripeSecretKey = dsa_setting($pdo, 'payment_stripe_secret_key', ''); $paymentStripeWebhookSecret = dsa_setting($pdo, 'payment_stripe_webhook_secret', ''); $paymentStripeSandbox = dsa_setting($pdo, 'payment_stripe_sandbox', '0') === '1'; $paymentPaystackLink = dsa_setting($pdo, 'payment_paystack_link', ''); $paymentPaystackPublicKey = dsa_setting($pdo, 'payment_paystack_public_key', ''); $paymentPaystackSecretKey = dsa_setting($pdo, 'payment_paystack_secret_key', ''); $paymentPaystackSandbox = dsa_setting($pdo, 'payment_paystack_sandbox', '0') === '1'; $paymentWatuPayConsumerKey = dsa_setting($pdo, 'payment_watupay_consumer_key', ''); $paymentWatuPayConsumerSecret = dsa_setting($pdo, 'payment_watupay_consumer_secret', ''); $paymentWatuPayShortcode = dsa_setting($pdo, 'payment_watupay_shortcode', ''); $paymentWatuPayPasskey = dsa_setting($pdo, 'payment_watupay_passkey', ''); $paymentWatuPayTillNumber = dsa_setting($pdo, 'payment_watupay_till_number', ''); $paymentWatuPayUseTill = dsa_setting($pdo, 'payment_watupay_use_till', '0') === '1'; $paymentWatuPaySandbox = dsa_setting($pdo, 'payment_watupay_sandbox', '0') === '1'; $paymentWatuPayInstructions = dsa_setting($pdo, 'payment_watupay_instructions', ''); $paymentMpesaDirectLabel = dsa_setting($pdo, 'payment_mpesa_direct_label', 'Lipa na M-PESA'); $paymentMpesaDirectInstructions = dsa_setting($pdo, 'payment_mpesa_direct_instructions', ''); $paymentMpesaConsumerKey = dsa_setting($pdo, 'payment_mpesa_consumer_key', ''); $paymentMpesaConsumerSecret = dsa_setting($pdo, 'payment_mpesa_consumer_secret', ''); $paymentMpesaShortcode = dsa_setting($pdo, 'payment_mpesa_shortcode', ''); $paymentMpesaPasskey = dsa_setting($pdo, 'payment_mpesa_passkey', ''); $paymentMpesaTillNumber = dsa_setting($pdo, 'payment_mpesa_till_number', ''); $paymentMpesaUseTill = dsa_setting($pdo, 'payment_mpesa_use_till', '0') === '1'; $paymentMpesaSandbox = dsa_setting($pdo, 'payment_mpesa_sandbox', '0') === '1'; $rawCustomMethods = trim(dsa_setting($pdo, 'payment_custom_methods_json', '[]')); $parsedCustomMethods = json_decode($rawCustomMethods, true); if (is_array($parsedCustomMethods)) { $lines = []; foreach ($parsedCustomMethods as $row) { if (!is_array($row)) { continue; } $label = trim((string) ($row['label'] ?? '')); if ($label === '') { continue; } $instructions = trim((string) ($row['instructions'] ?? '')); $lines[] = $instructions !== '' ? ($label . '|' . $instructions) : $label; } $paymentCustomMethodsText = implode("\n", $lines); } } if ($needsSyncData) { $catalogSyncDefaultMarkupPercent = dsa_setting($pdo, 'catalog_sync_default_markup_percent', '30'); $allowCustomProductCreate = dsa_setting($pdo, 'allow_custom_product_create', '1') === '1'; } $html = '

Admin Dashboard

Signed in as ' . dsa_h((string) ($_SESSION['dsa_admin_username'] ?? '')) . ' | Logout

'; if ($notice !== '') { if (strpos($notice, '<') !== false && strpos($notice, '>') !== false) { $html .= '

' . strip_tags($notice, '

') . '

'; } else { $html .= '

' . dsa_h($notice) . '

'; } } $html .= '
'; $html .= '
'; if ($section === 'dashboard') { $html .= '

Overview

' . '
' . dsa_h((string) $dashboardCatalogCount) . 'Catalog products
' . '
' . dsa_h((string) $dashboardProductsCount) . 'Store products
' . '
' . dsa_h((string) $dashboardOrdersCount) . 'Total orders
' . '
'; if (count($recentActivityOrders) > 0) { $html .= '

Recent Activity

'; $html .= '
'; foreach ($recentActivityOrders as $ra) { $raStatus = (string) ($ra['status'] ?? 'pending_payment'); $html .= '' . '' . '' . '' . '' . '' . '' . ''; } $html .= '
OrderProductCustomerAmountStatusUpdated
#' . dsa_h((string) $ra['id']) . '' . dsa_h((string) ($ra['product_name'] ?? '')) . '' . dsa_h((string) ($ra['customer_email'] ?? '')) . '' . dsa_h(dsa_money((int) ($ra['amount_cents'] ?? 0), (string) ($ra['currency'] ?? DSA_DEFAULT_CURRENCY))) . '' . dsa_status_pill($raStatus) . '' . dsa_h((string) ($ra['updated_at'] ?? '')) . '
'; } $html .= dsa_admin_payout_summary_card_html($pdo); } if ($section === 'branding') { $html .= '

Branding & Domain

' . '

Customize your storefront identity, logos, and custom domain mapping below.

'; $html .= '
' . '' . '' . '' . '' . '
'; $html .= ''; $html .= '
'; // Pane 1: Theme & Copy $html .= '
'; $html .= '

Storefront Theme & Copy

'; $html .= '

Configure your store name, tagline copy, and pick your active storefront design theme system.

'; $html .= ''; $html .= '

Store name in the header and emails.

'; $html .= '

Homepage Hero Copy

'; $html .= '

Text above the Browse Products / View Services buttons. Lead with premium digital products and professional services — not hosting or domains first.

'; $html .= ''; $html .= ''; $html .= ''; // Store Theme picker $html .= '
'; $html .= '

Store Theme

'; $html .= '

Choose a complete visual identity for your store. Paid themes require an active extension — locked themes show a purchase link.

'; $uiThemeCards = [ 'store' => ['label' => 'The Store', 'desc' => 'Clean, modern, professional. Always free.', 'accent' => '#2563eb', 'bg' => '#0b1020', 'textc' => '#e5e7eb'], 'elite' => ['label' => 'The Elite', 'desc' => 'Luxury editorial. Playfair Display. Gold.', 'accent' => '#c9a84c', 'bg' => '#0a0a0a', 'textc' => '#f5f5f0'], 'rebel' => ['label' => 'The Rebel', 'desc' => 'Bold studio. Space Grotesk. Purple.', 'accent' => '#7c3aed', 'bg' => '#ffffff', 'textc' => '#111111'], 'powerhouse' => ['label' => 'The Powerhouse', 'desc' => 'Dev-first. JetBrains Mono. Cyan.', 'accent' => '#00d4aa', 'bg' => '#0d1117', 'textc' => '#e6edf3'], 'warmth' => ['label' => 'The Warmth', 'desc' => 'Boutique. Plus Jakarta Sans. Amber.', 'accent' => '#d97706', 'bg' => '#faf8f5', 'textc' => '#2c2416'], ]; $html .= '
'; foreach ($uiThemeCards as $thKey => $th) { $isFree = ($thKey === 'store'); $isUnlocked = $isFree || ($themeExtStatus[$thKey] ?? false); $isActive = ($siteUiTheme === $thKey); $borderCol = $isActive ? 'var(--accent)' : 'var(--border)'; $cardStyle = 'border:2px solid ' . $borderCol . ';border-radius:10px;overflow:hidden;' . ($isUnlocked ? 'cursor:pointer;' : 'cursor:default;opacity:.65;') . 'transition:border-color .2s;user-select:none;'; $lockedAttr = $isUnlocked ? '' : ' data-locked="1" data-theme-name="' . dsa_h($th['label']) . '"'; $html .= '
'; // Swatch $html .= '
'; $html .= '
'; $html .= '
'; $html .= '
Buy
'; if (!$isUnlocked) { $html .= '
🔒
'; } $html .= '
'; // Info $html .= '
'; $html .= '' . dsa_h($th['label']) . ''; $html .= '' . dsa_h($th['desc']) . ''; if (!$isFree && !$isUnlocked) { $html .= '
Purchase ' . dsa_h($th['label']) . ' to unlock
'; } $html .= '
'; if ($isUnlocked) { $html .= ''; } $html .= '
'; } $html .= '
'; // Theme picker JS $html .= ''; // Site appearance $appearDisplay = ($siteUiTheme === 'store') ? '' : 'display:none;'; $html .= '
'; $html .= '
'; $html .= '

Store Appearance

'; $html .= '

These controls apply to The Store theme only. Paid themes have fixed palettes.

'; $html .= ''; $html .= '
'; $darkChecked = $siteThemeSetting === 'dark' ? ' checked' : ''; $lightChecked = $siteThemeSetting === 'light' ? ' checked' : ''; $html .= ''; $html .= ''; $html .= '
'; $html .= '

Controls the background and text colors for all pages of your store.

'; $html .= ''; $html .= '
'; $html .= ''; $html .= 'Preview Button'; $html .= '3'; $html .= '
'; $html .= ''; $html .= '
'; // end #store-appearance-section $html .= '
'; $html .= '
'; // end Pane 1 // Pane 2: Logo & Email $html .= ''; // end Pane 2 $html .= '
'; // end the multipart form! $whiteLabelEnabled = dsa_white_label_enabled($pdo); $tierUpgradeUrl = dsa_tier_upgrade_url($pdo); // Pane 3: White Label $html .= '
'; $html .= '

White Label Settings

'; $html .= '

Remove the "Powered by Watu Technology" branding footer from your storefront customer portal and checkout pages.

'; if ($whiteLabelEnabled) { $html .= '
' . '🟢 Active (Branding Hidden)' . '
'; $html .= '

Your store is running with clean white-label templates. No Watu branding is shown to your shoppers.

'; } else { $html .= '
' . '🔒 Locked (Powered by Watu Active)' . '
'; $html .= '

Your storefront footer displays "Powered by Watu Technology". Upgrading your pricing plan unlocks seamless white-label custom themes.

'; $html .= '

Upgrade to Unlock White Label

'; } $html .= '
'; // end Pane 3 $customDomainEnabled = dsa_custom_storefront_domain_enabled($pdo); $demoWhitelabelLocked = dsa_demo_mode_active($pdo); $customDomainLocked = $demoWhitelabelLocked || !$customDomainEnabled; // Pane 4: Custom Domain $html .= '
'; $html .= '

Custom Domain Mapping

'; $html .= '

Brand your entire checkout URL by mapping your custom subdomain (e.g., store.yourbrand.com) directly to your tenant store instance.

'; if ($customDomainEnabled && !$demoWhitelabelLocked) { $html .= '
' . '🟢 Enabled on Your Plan' . '
'; } else { $html .= '
' . '🔒 Feature Locked' . '
'; if (!$customDomainEnabled) { $html .= '

Custom subdomain mapping is included on the Pro tier plan. Upgrade to map your own web address.

'; $html .= '

Upgrade Plan

'; } else if ($demoWhitelabelLocked) { $html .= '

Custom domain configuration is locked on this demo store environment.

'; } } $html .= '
'; $whitelabelDomainInputAttrs = $customDomainLocked ? ' readonly disabled' : ''; $forceWhitelabelInputAttrs = ($forceWhitelabelDomain ? ' checked' : '') . ($customDomainLocked ? ' disabled' : ''); $html .= ''; $html .= ''; $html .= '

Automatically redirects shoppers to your custom domain once the DNS records are validated and SSL certificate is issued.

'; $html .= ''; $html .= '
' . dsa_h($systemOriginDomain !== '' ? $systemOriginDomain : 'Not set') . '
'; $html .= '
'; if ($customDomainLocked) { $html .= ''; } else { $html .= ''; } $html .= ''; $html .= '
'; $html .= ''; if (count($syncStatus) > 0) { $mappingOk = !empty($syncStatus['mapping_ok']); $dnsOk = !empty($syncStatus['dns_ok']); $sslOk = !empty($syncStatus['ssl_ok']); $mappingState = $mappingOk ? 'OK' : 'Pending'; $dnsState = $dnsOk ? 'OK' : 'Pending'; $sslState = $sslOk ? 'OK' : 'Pending'; $domainStatusValue = trim((string) ($syncStatus['domain'] ?? $whiteLabelDomain)); $cnameTargetValue = trim((string) ($syncStatus['cname_target'] ?? '')); $expectedIpValue = trim((string) ($syncStatus['expected_ip'] ?? '')); $isEnforceMode = !empty($syncStatus['enforce']); $isReady = !empty($syncStatus['ready']); $redirectState = 'OFF'; $redirectNote = 'Redirect is disabled. Both domains can be used.'; if ($isEnforceMode && $isReady) { $redirectState = 'ACTIVE'; $redirectNote = 'Redirect is active. Browser traffic is forced to the white-label domain.'; } elseif ($isEnforceMode) { $redirectState = 'ARMED'; $redirectNote = 'Redirect is armed and waiting for readiness checks to complete.'; } $html .= '
'; $html .= '

White-label readiness status

'; $html .= '

Mapping: ' . dsa_h($mappingState) . ' | DNS: ' . dsa_h($dnsState) . ' | SSL: ' . dsa_h($sslState) . '

'; $html .= '

Redirect mode: ' . dsa_h($redirectState) . ' - ' . dsa_h($redirectNote) . '

'; if ($isEnforceMode && !$isReady) { $progressParts = [ $mappingOk ? 'Binding done' : 'Binding pending', $dnsOk ? 'DNS done' : 'DNS pending', $sslOk ? 'SSL done' : 'SSL pending', ]; $html .= '

Progress: ' . dsa_h(implode(' -> ', $progressParts)) . '

'; } if ($whitelabelLastSyncMessage !== '') { $html .= '

Latest check: ' . dsa_h(dsa_ui_normalize_legacy_message((string) $whitelabelLastSyncMessage)) . '

'; } if ($isEnforceMode && $domainStatusValue !== '') { $html .= '
'; $html .= '

How to complete setup

'; $html .= '
    '; if ($cnameTargetValue !== '') { $html .= '
  1. Create/update a CNAME from ' . dsa_h($domainStatusValue) . ' to ' . dsa_h($cnameTargetValue) . '.
  2. '; } elseif ($expectedIpValue !== '') { $html .= '
  3. Create/update an A record for ' . dsa_h($domainStatusValue) . ' to ' . dsa_h($expectedIpValue) . '.
  4. '; } else { $html .= '
  5. Create/update an A record for ' . dsa_h($domainStatusValue) . ' to your tenant server IP.
  6. '; } $html .= '
  7. Wait for DNS propagation, then click Check Status or save again.
  8. '; $html .= '
'; } $html .= '
'; } $html .= '
'; // end Pane 4 // Tabs javascript logic $html .= ''; $html .= '
'; // closes the outer card! } if ($section === 'settings') { $visibleAdmins = dsa_admin_users_visible($pdo); $isSuperadmin = dsa_admin_is_superadmin($pdo); $html .= '

Admin users

'; if ($isSuperadmin) { $html .= '

You are the superadmin. Demo admins can browse the store admin but cannot change catalog, orders, or settings. Other admins cannot see each other.

'; if (count($visibleAdmins) > 0) { $html .= '
'; foreach ($visibleAdmins as $adminRow) { $html .= '' . '' . '' . ''; } $html .= '
UsernameEmailRoleAdded
' . dsa_h((string) ($adminRow['username'] ?? '')) . '' . dsa_h((string) ($adminRow['email'] ?? '')) . '' . dsa_h((string) ($adminRow['role'] ?? DSA_ADMIN_ROLE_ADMIN)) . '' . dsa_h((string) ($adminRow['created_at'] ?? '')) . '
'; } if (dsa_multiple_admins_tier_allowed($pdo)) { $html .= '
' . '' . '' . '' . '

We generate a username and one-time password. In demo mode, catalog and settings stay read-only for all admins.

' . '
' . '
'; } else { $tierUpgradeUrl = dsa_tier_upgrade_url($pdo); $html .= '
' . '

🔒 Multiple Admin Accounts is a premium feature not available on ' . dsa_h(dsa_plan_tier_label(null, $pdo)) . '.

' . '

Upgrade your plan to unlock multiple admins

' . '
'; } } else { $self = $visibleAdmins[0] ?? dsa_admin_current($pdo); $html .= '

Your personal admin account. Other demo admins are hidden from this list.

'; if (is_array($self)) { $html .= '

Username: ' . dsa_h((string) ($self['username'] ?? '')) . '
' . 'Email: ' . dsa_h((string) ($self['email'] ?? '')) . '

'; } } $html .= '
'; if (dsa_demo_mode_active($pdo)) { $html .= '

Change Admin Password

'; $html .= '

Password changes are disabled on this demo store.

'; } else { $html .= '

Change Admin Password

'; $html .= '
'; $html .= ''; $html .= '
'; } // Extensions card $extPlanTier = dsa_normalize_plan_tier((string) dsa_setting($pdo, 'plan_tier', 'core')); $extTierLabel = dsa_plan_tier_label($extPlanTier); $extServiceId = (int) dsa_setting($pdo, 'tenant_whmcs_service_id', '0'); $extUpgradeUrl = $extServiceId > 0 ? 'https://watutechnology.com/web-portal/clientarea.php?action=upgrade&id=' . $extServiceId : 'https://watutechnology.com/web-portal/clientarea.php'; $extThemes = [ 201 => ['feature' => 'theme_elite', 'name' => 'Elite Theme', 'desc' => 'A bold, high-impact storefront theme for established brands.'], 208 => ['feature' => 'theme_rebel', 'name' => 'Rebel Theme', 'desc' => 'Edgy, modern design for stores that want to stand out.'], 209 => ['feature' => 'theme_powerhouse', 'name' => 'Powerhouse Theme', 'desc' => 'Clean, conversion-optimised layout for high-volume stores.'], 210 => ['feature' => 'theme_warmth', 'name' => 'Warmth Theme', 'desc' => 'Friendly, inviting design for community-focused businesses.'], ]; $extKeyRows = $pdo->query("SELECT feature, status, expires_at, redeemed_at FROM extension_keys ORDER BY redeemed_at DESC")->fetchAll(); $extActiveFeatures = []; foreach ($extKeyRows as $ek) { if ((string) ($ek['status'] ?? '') === 'active') { $extActiveFeatures[(string) ($ek['feature'] ?? '')] = true; } } $html .= '
'; $html .= '
'; $html .= '

Extensions

'; $html .= 'Your plan: ' . dsa_h($extTierLabel) . ''; $html .= '
'; $html .= '

Add premium theme extensions to your store. Each theme is $15/month billed through your Watu account.

'; $html .= '
'; foreach ($extThemes as $extPid => $extInfo) { $extFeature = $extInfo['feature']; $extIsActive = dsa_extension_active($pdo, $extFeature); $extPendingInvoiceId = (int) dsa_setting($pdo, 'pending_invoice_' . $extFeature, '0'); $extHasPending = $extPendingInvoiceId > 0; $html .= '
'; $html .= '
' . dsa_h($extInfo['name']) . '
'; $html .= '
' . dsa_h($extInfo['desc']) . '
'; $html .= '
$15/mo
'; if ($extIsActive) { $html .= 'Active'; } elseif ($extHasPending) { $extCatalogApiUrl = dsa_setting($pdo, 'catalog_sync_whmcs_api_url', ''); $extWhmcsBase = ''; if ($extCatalogApiUrl !== '') { $extWhmcsBase = preg_replace('#/modules/.*#', '', $extCatalogApiUrl); } $extInvoiceLink = $extWhmcsBase !== '' ? ($extWhmcsBase . '/viewinvoice.php?id=' . $extPendingInvoiceId) : ''; $html .= 'Invoice Pending'; if ($extInvoiceLink !== '') { $html .= ' Pay Now'; } } else { $html .= '
' . '' . '' . '' . '
'; } $html .= '
'; } $html .= '
'; if (!empty($extKeyRows)) { $html .= '
Active extension records'; $html .= '
'; foreach ($extKeyRows as $ek) { $ekStatus = (string) ($ek['status'] ?? 'unknown'); $pillStyle = match($ekStatus) { 'active' => 'background:#166534;color:#bbf7d0;', 'suspended' => 'background:#7c2d12;color:#fed7aa;', 'terminated' => 'background:#374151;color:#9ca3af;', 'grace' => 'background:#78350f;color:#fde68a;', default => 'background:#374151;color:#9ca3af;', }; $statusPill = '' . dsa_h($ekStatus) . ''; $html .= '' . '' . '' . ''; } $html .= '
FeatureStatusExpiresActivated
' . dsa_h((string) ($ek['feature'] ?? '')) . '' . $statusPill . '' . dsa_h((string) ($ek['expires_at'] ?? '')) . '' . dsa_h((string) ($ek['redeemed_at'] ?? '')) . '
'; } $html .= '

Want more features? Upgrade your plan

'; $html .= '
'; } if ($section === 'payments') { $html .= '

Payment methods & Payouts

Configure checkout payment gateways, customer pay instructions, and manage your custodian M-PESA payout withdrawals all in one unified console.

'; if (trim(dsa_setting($pdo, 'platform_payment_allowlist_synced_at', '')) !== '') { $html .= '

Methods disabled by Launchpad cannot be enabled here.

'; } if (!dsa_tier_allows_payment_method($pdo, 'offline')) { $html .= '

DSA plan stores only offer Watu Technology M-PESA at checkout. Upgrade to Pro for all payment methods.

'; } // Consolidated Pre-paid Credits Banner above the navigation menu $html .= '
'; $html .= '

⚠️ How Direct Checkout & Delivery Works

'; $html .= '
    '; $html .= '
  1. Customer Orders: Your customer places an order on your storefront and pays you directly.
  2. '; $html .= '
  3. Watu Invoice Generated: Your store automatically creates a purchase invoice on Watu for the order.'; $html .= '
      '; $html .= '
    • If pre-paid credit is present: The system uses it to pay the invoice instantly.
    • '; $html .= '
    • If no credit is present: You will need to pay the invoice manually.
    • '; $html .= '
  4. '; $html .= '
  5. Order Delivered: Once the invoice is paid (automatically or manually), the products are delivered to your customer.
  6. '; $html .= '
'; $html .= '
💡 Important: To ensure instant, automated delivery for your customers, please maintain a pre-paid credit balance.
'; $html .= '
'; $html .= 'Available Credit: ' . dsa_h($creditFormatted) . ''; $html .= '➕ Add Credits'; $html .= '
'; $html .= '
'; $html .= ' '; $html .= '
'; // Horizontal navigation (made Offline Payment active since General is moved to Catalog) $html .= '
'; $html .= ''; $html .= ''; if ($paymentMethodPaypalCheckoutPlatformAllowed) { $html .= ''; } if ($paymentMethodPaypalBasicPlatformAllowed) { $html .= ''; } if ($paymentMethodStripePlatformAllowed) { $html .= ''; } if ($paymentMethodPaystackPlatformAllowed) { $html .= ''; } $html .= '
'; // Tab 2: Offline Payment (Super Simplified & Cleaned Up) $html .= '
'; $html .= '
'; $html .= '

Offline payment

'; $html .= '📖 View Setup Guide'; $html .= '
'; $html .= ''; $html .= '
'; $html .= ''; $html .= '
'; // End pay-offline-fields $html .= '
'; // End paytab-offline // Tab 4: Direct M-PESA $html .= '
'; $html .= '
'; $html .= '

Direct M-PESA

'; $html .= '📖 View Setup Guide'; $html .= '
'; $html .= ''; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; // Maintain a hidden legacy unified field so we do not break any existing code that expects it $html .= ''; // Tab 5: PayPal Checkout if ($paymentMethodPaypalCheckoutPlatformAllowed) { $html .= ''; } // Tab 5b: PayPal Basic (Legacy) if ($paymentMethodPaypalBasicPlatformAllowed) { $html .= ''; } // Tab 6: Stripe Checkout if ($paymentMethodStripePlatformAllowed) { $html .= ''; } // Tab 7: Paystack if ($paymentMethodPaystackPlatformAllowed) { $html .= ''; } $html .= '
'; // Append the separate Payouts Panel container outside the payments configuration form $html .= ''; $html .= '
'; // Navigation script $html .= ' '; } if ($section === 'analytics') { $html .= dsa_admin_analytics_section_html($pdo); } if ($section === 'notifications') { $syncSource = trim((string) dsa_setting($pdo, 'notifications_source', 'whmcs_templates')); $syncAt = trim((string) dsa_setting($pdo, 'notifications_synced_at', '')); $html .= '

Email Notifications

' . dsa_h(dsa_ui_label('notifications.templates_synced')) . '

'; $html .= '

Source: ' . dsa_h($syncSource) . ($syncAt !== '' ? ' | Last sync: ' . dsa_h($syncAt) : '') . '

'; $groupedTemplates = []; foreach ($notificationTemplateCatalog as $templateKey => $tpl) { $categoryKey = (string) ($tpl['category_key'] ?? 'general'); $categoryLabel = (string) ($tpl['category_label'] ?? 'General Messages'); if (!isset($groupedTemplates[$categoryKey])) { $groupedTemplates[$categoryKey] = ['label' => $categoryLabel, 'items' => []]; } $groupedTemplates[$categoryKey]['items'][$templateKey] = $tpl; } $html .= '
' . ''; foreach ($groupedTemplates as $category) { $html .= '

' . dsa_h((string) ($category['label'] ?? 'Templates')) . '

'; foreach ((array) ($category['items'] ?? []) as $templateKey => $tpl) { $label = (string) ($tpl['template_label'] ?? $templateKey); $enabled = !empty($tpl['enabled']); $html .= '
' . '' . '
'; } $html .= '
'; } $html .= '

' . dsa_h(dsa_ui_label('notifications.templates_managed')) . '

'; $html .= '
'; $html .= '
'; } if ($section === 'catalog') { $storeCurrency = dsa_store_currency($pdo); $html .= ''; $html .= '
'; $html .= '

Watu Product Catalog

Choose products to sell, set your sell prices, and keep margins visible.

'; $html .= '
' . '
' . '
' . '
' . '
Reset
' . '
'; $showingFrom = $catalogTotal > 0 ? ($catalogOffset + 1) : 0; $showingTo = min($catalogOffset + count($catalog), $catalogTotal); $html .= '

Showing ' . dsa_h((string) $showingFrom) . '-' . dsa_h((string) $showingTo) . ' of ' . dsa_h((string) $catalogTotal) . ' products.

'; $html .= '
'; $ratesJson = dsa_setting($pdo, 'catalog_sync_currency_rates', '[]'); $rates = json_decode($ratesJson, true) ?: []; $storeCurrency = dsa_store_currency($pdo); foreach ($catalog as $row) { $costInStore = dsa_convert_currency((int) $row['cost_cents'], (string) $row['currency'], $storeCurrency, $rates); $sellPrice = (int) ($row['sell_price_cents'] ?? $costInStore); $sellStatus = (string) ($row['sell_status'] ?? 'active'); $margin = $sellPrice - $costInStore; $provBadge = dsa_provisioning_admin_badge((array) $row); $html .= ''; $html .= ''; $html .= ''; // Buy Cost formatting $buyCostHtml = '' . dsa_h(dsa_money((int) $row['cost_cents'], (string) $row['currency'])) . ''; if (strtoupper(trim((string) $row['currency'])) !== $storeCurrency) { $buyCostHtml .= '
(~ ' . dsa_h(dsa_money($costInStore, $storeCurrency)) . ')'; } $html .= ''; $marginColorStyle = $margin >= 0 ? 'color:#10b981;' : 'color:#f87171;'; $marginLabel = 'Profit: ' . dsa_money($margin, $storeCurrency); $html .= ''; $html .= ''; $html .= ''; } if (count($catalog) === 0) { $html .= ''; } $html .= '
ProductCategoryProvisioningBuy CostSell PriceStatusAction
' . dsa_h((string) $row['name']) . '
/' . dsa_h((string) $row['slug']) . '
' . dsa_h(dsa_truncate_text((string) $row['description'], 120)) . '
' . dsa_h((string) ($row['category'] ?? 'General')) . '' . dsa_h($provBadge['label']) . '' . $buyCostHtml . '
' . '' . '' . '' . '' . '' . '' . '
' . '' . dsa_h($storeCurrency) . '' . '' . '
' . '
' . dsa_h($marginLabel) . '
No catalog products match the current filters.
'; $html .= '
'; // Bulk Pricing section $html .= '
' . '
' . '

Bulk Pricing

' . 'One-Time Batch Run' . '
' . '

Apply immediate price modifications to existing catalog items matching your selected Scope.

' . '
'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '
' . '
' . '
' . '
'; $html .= '
' . '
' . '
'; $html .= '
'; $html .= '
'; // Card 1: Store Currency $html .= '
'; $html .= '

Store Currency

'; $html .= '

Catalog prices sync in this currency. Re-syncs after changing.

'; $html .= '
'; $html .= ''; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= '
'; // Card 2: Default Catalog Markup $html .= '
'; $html .= '
' . '

Default Catalog Markup

' . 'Persistent Config' . '
'; $html .= '

Profit margin automatically applied during automated daily syncs of newly imported catalog items.

'; $html .= '
'; $html .= ''; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= '
'; $html .= '
'; /* end of flex-settings-container */ $html .= '
'; /* end of catalog-section */ } if ($section === 'custom-services') { $html .= dsa_render_admin_custom_services_section($pdo, ''); } if ($section === 'products') { $html .= '

Products

All enabled storefront products — from Watu catalog sync and custom services.

Open Catalog Custom Services

'; $html .= '

Enabled Products

'; $html .= ''; foreach ($products as $p) { $marginCents = (int) $p['price_cents'] - (int) ($p['cost_cents'] ?? 0); $sourceBadge = dsa_product_source_badge_html((string) ($p['product_source'] ?? 'catalog')); $html .= ''; } if (count($products) === 0) { $html .= ''; } $html .= '
IDNameSourceCategoryBuy CostSell PriceStatus
' . dsa_h((string) $p['id']) . '' . dsa_h((string) $p['name']) . '
/' . dsa_h((string) $p['slug']) . '
' . $sourceBadge . '' . dsa_h((string) ($p['category'] ?? 'General')) . '' . dsa_h(dsa_money((int) ($p['cost_cents'] ?? 0), (string) $p['currency'])) . '' . dsa_h(dsa_money((int) $p['price_cents'], (string) $p['currency'])) . '
Margin: ' . dsa_h(dsa_money($marginCents, (string) $p['currency'])) . '
' . dsa_h((string) $p['status']) . '
No enabled products yet. Use Catalog or Custom Services.
'; } if ($section === 'orders') { $html .= '

Orders

'; $html .= '
' . '' . '
' . '
' . '
' . '
Reset
' . '
' . '' . '' . '' . '
' . '
'; $html .= '

Showing ' . dsa_h((string) count($orders)) . ' of ' . dsa_h((string) $ordersTotal) . ' orders (page ' . dsa_h((string) $ordersPage) . ' of ' . dsa_h((string) $ordersPages) . ')

'; $html .= '
'; foreach ($orders as $o) { $proofLink = trim((string) ($o['payment_proof_path'] ?? '')); $proofCell = $proofLink !== '' ? 'View proof' : 'none'; $adminOrderStatus = (string) $o['status']; $displayStatus = dsa_customer_order_status_label((array) $o); $statusContext = dsa_customer_order_status_context((array) $o); $statusLabel = dsa_status_pill($displayStatus); if ($displayStatus !== $adminOrderStatus) { $statusLabel .= '
Stored as: ' . dsa_h(dsa_admin_order_status_label($adminOrderStatus)) . ''; } if ($statusContext !== '') { $statusLabel .= '
' . dsa_h($statusContext) . ''; } if ((string) ($o['payment_method'] ?? '') !== '') { $statusLabel .= '
Method: ' . dsa_h((string) $o['payment_method']) . ''; } if ((string) ($o['delivery_value'] ?? '') !== '') { $statusLabel .= '
Delivery: ' . dsa_h((string) $o['delivery_value']) . ''; } $paymentMethod = (string) ($o['payment_method'] ?? ''); $isGateway = dsa_is_gateway_payment_method($paymentMethod); $canApprovePayment = dsa_admin_can_approve_order_payment((array) $o); $canEditPaymentRef = dsa_admin_can_edit_order_payment_ref((array) $o); $paymentRefRaw = (string) ($o['payment_ref'] ?? ''); $paymentRefValue = dsa_h($paymentRefRaw); $statusOptions = $isGateway ? dsa_admin_gateway_order_status_options($adminOrderStatus) : dsa_admin_order_status_options(); if ( $adminOrderStatus === 'paid' && dsa_order_needs_whmcs_provisioning((array) $o) && !dsa_order_whmcs_provisioning_complete((array) $o) ) { unset($statusOptions['fulfilled']); } $lifecycleCells = dsa_admin_lifecycle_status_cells((array) $o); $dispatchCell = $lifecycleCells['dispatch']; $billingCell = $lifecycleCells['billing']; $provisioningCell = $lifecycleCells['provisioning']; $canDispatch = dsa_admin_can_dispatch_order($pdo, (array) $o); $manage = '' . '' . ''; if ($canEditPaymentRef) { $manage .= '' . ''; } else { $manage .= '

Payment reference
' . ($paymentRefRaw !== '' ? dsa_h($paymentRefRaw) : 'Assigned when customer starts payment') . '

'; if ($isGateway) { $manage .= '

Watu Technology M-PESA / gateway order — payment confirms automatically when M-PESA is received. Reference cannot be edited here.

'; } } $manage .= '
' . ''; if ($isGateway && $adminOrderStatus === 'pending_payment') { $manage .= ''; } if ($canApprovePayment) { $manage .= ''; } $manage .= '
'; if ($canDispatch) { $manage .= '' . '' . '' . ''; } if (dsa_admin_can_refresh_watu_order_status((array) $o)) { $manage .= '' . '' . '' . ''; } if (dsa_admin_can_continue_order_on_watu((array) $o)) { $manage .= '' . '' . '' . ''; } if ( $adminOrderStatus === 'fulfilled' && ( !dsa_order_needs_whmcs_provisioning((array) $o) || dsa_order_whmcs_provisioning_complete((array) $o) ) ) { $manage .= '' . '' . '' . '' . ''; } $rowCls = 'row-' . preg_replace('/[^a-z0-9_-]/', '', strtolower($adminOrderStatus)); $html .= ''; } if (count($orders) === 0) { $html .= ''; } $html .= '
IDProductCustomerAmountStatus' . dsa_h(dsa_ui_label('orders.lifecycle_dispatch')) . '' . dsa_h(dsa_ui_label('orders.lifecycle_billing')) . '' . dsa_h(dsa_ui_label('orders.lifecycle_provisioning')) . 'ProofManage
' . dsa_h((string) $o['id']) . '' . dsa_h((string) $o['product_name']) . '' . dsa_h((string) $o['customer_name']) . '
' . dsa_h((string) $o['customer_email']) . '
' . dsa_h(dsa_money((int) $o['amount_cents'], (string) $o['currency'])) . '' . $statusLabel . '' . $dispatchCell . '' . $billingCell . '' . $provisioningCell . '' . $proofCell . '' . $manage . '
' . dsa_empty_state('No orders found', 'Try a different filter or search term.') . '
'; if ($ordersPages > 1) { $prevUrl = dsa_build_admin_url(['section' => 'orders', 'filter' => $filter, 'orders_q' => $ordersQuery, 'orders_page' => max(1, $ordersPage - 1)]); $nextUrl = dsa_build_admin_url(['section' => 'orders', 'filter' => $filter, 'orders_q' => $ordersQuery, 'orders_page' => min($ordersPages, $ordersPage + 1)]); $html .= ''; } $html .= '
'; } if ($section === 'customers') { $html .= '

Customers

Search customers, inspect order history, and login as customer for support.

'; $html .= '
' . '
' . '
' . '
' . '
Reset
' . '
'; $html .= '

Showing ' . dsa_h((string) count($customers)) . ' customers.

'; $html .= '
'; foreach ($customers as $customer) { $focusLink = dsa_build_admin_url([ 'section' => 'customers', 'customers_q' => $customersQuery, 'customers_status' => $customersStatus, 'customers_sort' => $customersSort, 'customer_id' => (int) ($customer['id'] ?? 0), ]); $html .= '' . '' . '' . ''; } if (count($customers) === 0) { $html .= ''; } $html .= '
CustomerOrdersLifetimeLast OrderActions
' . dsa_h((string) ($customer['name'] ?? '')) . '
' . dsa_h((string) ($customer['email'] ?? '')) . ''; if (trim((string) ($customer['phone'] ?? '')) !== '') { $html .= '
' . dsa_h((string) $customer['phone']) . ''; } $html .= '
' . dsa_h((string) ((int) ($customer['order_count'] ?? 0))) . '' . dsa_h(dsa_money((int) ($customer['total_spend_cents'] ?? 0), DSA_DEFAULT_CURRENCY)) . '' . dsa_h((string) (($customer['last_order_at'] ?? '') !== '' ? $customer['last_order_at'] : 'n/a')) . 'View' . '
' . '' . '' . '' . '' . '
No customers found for current filters.
'; if ($focusCustomer) { $html .= '

Customer Detail

'; $html .= '

' . dsa_h((string) ($focusCustomer['name'] ?? '')) . '
' . '' . dsa_h((string) ($focusCustomer['email'] ?? '')) . ''; if (trim((string) ($focusCustomer['phone'] ?? '')) !== '') { $html .= '
' . dsa_h((string) $focusCustomer['phone']) . ''; } $html .= '

'; $html .= '

Joined: ' . dsa_h((string) ($focusCustomer['created_at'] ?? 'n/a')) . ' | Orders: ' . dsa_h((string) ((int) ($focusCustomer['order_count'] ?? 0))) . ' | Lifetime spend: ' . dsa_h(dsa_money((int) ($focusCustomer['total_spend_cents'] ?? 0), DSA_DEFAULT_CURRENCY)) . '

'; $html .= '

Recent Orders

'; $html .= ''; foreach ($focusCustomerOrders as $customerOrder) { $html .= '' . '' . '' . '' . '' . ''; } if (count($focusCustomerOrders) === 0) { $html .= ''; } $html .= '
OrderProductAmountStatusPaymentCreated
#' . dsa_h((string) $customerOrder['id']) . '' . dsa_h((string) $customerOrder['product_name']) . '' . dsa_h(dsa_money((int) $customerOrder['amount_cents'], (string) $customerOrder['currency'])) . '' . dsa_status_pill((string) $customerOrder['status']) . '' . dsa_h((string) (($customerOrder['payment_method'] ?? '') !== '' ? $customerOrder['payment_method'] : 'n/a')) . '' . dsa_h((string) $customerOrder['created_at']) . '
No orders yet.
'; } } if ($section === 'support') { $html .= dsa_render_admin_support_section($pdo, $notice); } dsa_render_layout('Admin', $html); } function dsa_whmcs_portal_base_url(PDO $pdo): string { $apiUrl = trim(dsa_setting($pdo, 'catalog_sync_whmcs_api_url', '')); if ($apiUrl === '') { return ''; } $base = preg_replace('#/modules/.*$#', '', $apiUrl); return is_string($base) ? rtrim($base, '/') : ''; } function dsa_client_area_nav(string $active): string { $tabs = [ 'overview' => ['label' => 'Overview', 'path' => '/client-area'], 'orders' => ['label' => 'Orders', 'path' => '/client-area/orders'], 'products' => ['label' => 'My Products', 'path' => '/client-area/products'], 'billing' => ['label' => 'Billing', 'path' => '/client-area/billing'], 'support' => ['label' => 'Support', 'path' => '/client-area/support'], 'profile' => ['label' => 'Profile', 'path' => '/client-area/profile'], ]; $html = ''; return $html; } function dsa_client_support_subnav(string $active): string { $tabs = [ 'hub' => ['label' => 'Support Home', 'path' => '/client-area/support'], 'tickets' => ['label' => 'Tickets', 'path' => '/client-area/support/tickets'], 'knowledgebase' => ['label' => 'Knowledge Base', 'path' => '/client-area/support/knowledgebase'], ]; $html = ''; return $html; } function dsa_render_client_overview(PDO $pdo, string $notice): void { $clientId = (int) ($_SESSION['dsa_client_id'] ?? 0); dsa_release_session_lock_if_safe(); if ($clientId <= 0) { dsa_render_client_login('Please login.'); return; } $orderStmt = $pdo->prepare('select count(*) from orders where customer_id = :id'); $orderStmt->execute([':id' => $clientId]); $orderCount = (int) $orderStmt->fetchColumn(); $productStmt = $pdo->prepare("select count(*) from orders where customer_id = :id and status in ('paid', 'fulfilled')"); $productStmt->execute([':id' => $clientId]); $productCount = (int) $productStmt->fetchColumn(); $ticketStmt = $pdo->prepare("select count(*) from support_tickets where customer_id = :id and status in ('open', 'in_progress', 'awaiting_customer')"); $ticketStmt->execute([':id' => $clientId]); $openTickets = (int) $ticketStmt->fetchColumn(); $email = (string) ($_SESSION['dsa_client_email'] ?? ''); $html = '

Client Area

Signed in as ' . dsa_h($email) . '

'; if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } $html .= dsa_client_area_nav('overview') . '
'; $html .= '
'; $html .= '
' . $orderCount . 'Orders
'; $html .= '
' . $productCount . 'Products & services
'; $html .= '
' . $openTickets . 'Open tickets
'; $html .= '
'; $html .= '

Quick links

'; $html .= 'Orders & payments'; $html .= 'My products'; $html .= 'Support center'; $html .= 'Knowledge base'; $html .= 'My profile'; $html .= '

Orders show checkout and payment history. My Products is where you manage active services after provisioning.

'; dsa_render_layout('Client area', $html); } function dsa_render_client_orders_list(PDO $pdo, string $notice): void { $clientId = (int) ($_SESSION['dsa_client_id'] ?? 0); dsa_release_session_lock_if_safe(); if ($clientId <= 0) { dsa_render_client_login('Please login.'); return; } $clientOrdersPerPage = 20; $clientOrdersPage = max(1, (int) ($_GET['page'] ?? 1)); $clientOrdersTotalStmt = $pdo->prepare('select count(*) from orders where customer_id = :customer_id'); $clientOrdersTotalStmt->execute([':customer_id' => $clientId]); $clientOrdersTotal = (int) $clientOrdersTotalStmt->fetchColumn(); $clientOrdersPages = max(1, (int) ceil($clientOrdersTotal / $clientOrdersPerPage)); if ($clientOrdersPage > $clientOrdersPages) { $clientOrdersPage = $clientOrdersPages; } $clientOrdersOffset = ($clientOrdersPage - 1) * $clientOrdersPerPage; $stmt = $pdo->prepare( "select o.id, o.status, o.amount_cents, o.currency, o.payment_ref, o.delivery_value, o.created_at, p.name as product_name from orders o join products p on p.id = o.product_id where o.customer_id = :customer_id order by o.id desc limit " . (int) $clientOrdersPerPage . ' offset ' . (int) $clientOrdersOffset ); $stmt->execute([':customer_id' => $clientId]); $orders = $stmt->fetchAll(); $email = (string) ($_SESSION['dsa_client_email'] ?? ''); $html = '

Client Area

Signed in as ' . dsa_h($email) . '

'; if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } $html .= dsa_client_area_nav('orders') . '
'; $html .= '

Orders

Purchase records, payment status, and checkout history.

'; if ($clientOrdersTotal === 0) { $html .= dsa_empty_state('No orders yet', 'Products you purchase will appear here.', 'Browse Store', '/'); } else { $html .= '
'; foreach ($orders as $o) { $status = (string) $o['status']; $statusLabel = dsa_customer_order_status_label((array) $o); $orderLabel = trim((string) ($o['product_name'] ?? '')); if ($orderLabel === '') { $orderLabel = 'Order'; } $actions = '
Order details'; if (in_array($status, ['paid', 'fulfilled'], true)) { $actions .= 'Manage product'; } if ($status === 'pending_payment') { $payLabel = $statusLabel === 'payment_submitted' ? 'Continue payment' : 'Pay'; $actions .= '' . dsa_h($payLabel) . ''; } $actions .= '
'; $rowClass = 'row-' . $statusLabel; $html .= ''; } $html .= '
OrderProductAmountStatusActions
#' . dsa_h((string) $o['id']) . '' . dsa_h($orderLabel) . '' . dsa_h(dsa_money((int) $o['amount_cents'], (string) $o['currency'])) . '' . dsa_status_pill($statusLabel) . (($status === 'pending_payment' && $statusLabel === 'payment_submitted') ? '
Awaiting confirmation' : '') . '
' . $actions . '
'; if ($clientOrdersPages > 1) { $html .= ''; } } $html .= '
'; dsa_render_layout('Client orders', $html); } function dsa_render_client_products_list(PDO $pdo, string $notice): void { $clientId = (int) ($_SESSION['dsa_client_id'] ?? 0); dsa_release_session_lock_if_safe(); if ($clientId <= 0) { dsa_render_client_login('Please login.'); return; } $stmt = $pdo->prepare( "select o.id, o.status, o.amount_cents, o.currency, o.updated_at, o.delivery_value, p.name as product_name, p.description as product_description from orders o join products p on p.id = o.product_id where o.customer_id = :customer_id and o.status in ('paid', 'fulfilled') order by o.updated_at desc, o.id desc limit 100" ); $stmt->execute([':customer_id' => $clientId]); $products = $stmt->fetchAll(); $email = (string) ($_SESSION['dsa_client_email'] ?? ''); $html = '

Client Area

Signed in as ' . dsa_h($email) . '

'; if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } $html .= dsa_client_area_nav('products') . '
'; $html .= '

My Products

Active and provisioning services tied to your account. Manage delivery access, credentials, and service details here.

'; if (count($products) === 0) { $html .= dsa_empty_state('No active products yet', 'Fulfilled or provisioning purchases will appear here.', 'View orders', '/client-area/orders'); } else { $html .= '
'; foreach ($products as $row) { $name = trim((string) ($row['product_name'] ?? '')); if ($name === '') { $name = 'Product'; } $html .= '' . '' . '' . '' . ''; } $html .= '
ProductOrderStatusLast updateActions
' . dsa_h($name) . '
' . dsa_h(substr(trim((string) ($row['product_description'] ?? '')), 0, 80)) . '
#' . dsa_h((string) $row['id']) . '' . dsa_status_pill(dsa_customer_order_status_label((array) $row)) . '' . dsa_h((string) ($row['updated_at'] ?? '')) . '
'; } $html .= '
'; dsa_render_layout('My products', $html); } function dsa_render_client_support_hub(PDO $pdo, string $notice): void { $clientId = (int) ($_SESSION['dsa_client_id'] ?? 0); dsa_release_session_lock_if_safe(); if ($clientId <= 0) { dsa_render_client_login('Please login.'); return; } $email = (string) ($_SESSION['dsa_client_email'] ?? ''); $html = '

Client Area

Signed in as ' . dsa_h($email) . '

'; if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } $html .= dsa_client_area_nav('support') . '
'; $html .= dsa_client_support_subnav('hub'); $html .= '

Support Center

Get help with orders, billing, and active services.

'; $html .= '
'; $html .= '

Support tickets

Open a ticket for billing, provisioning, or technical questions.

Open tickets
'; $html .= '

Knowledge base

Self-service guides for payments, products, and common questions.

Browse articles
'; $html .= '
'; dsa_render_layout('Support', $html); } function dsa_render_client_tickets(PDO $pdo, string $error, string $notice): void { $clientId = (int) ($_SESSION['dsa_client_id'] ?? 0); dsa_release_session_lock_if_safe(); if ($clientId <= 0) { dsa_render_client_login('Please login.'); return; } $tickets = $pdo->prepare('select id, order_id, subject, status, created_at, updated_at, last_reply_at, last_reply_by from support_tickets where customer_id = :id order by coalesce(last_reply_at, updated_at, created_at) desc limit 50'); $tickets->execute([':id' => $clientId]); $ticketRows = $tickets->fetchAll(); $orderOptions = $pdo->prepare('select id from orders where customer_id = :id order by id desc limit 50'); $orderOptions->execute([':id' => $clientId]); $orders = $orderOptions->fetchAll(); $email = (string) ($_SESSION['dsa_client_email'] ?? ''); $html = '

Client Area

Signed in as ' . dsa_h($email) . '

'; if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } $html .= dsa_client_area_nav('tickets') . '
'; $html .= dsa_client_support_subnav('tickets'); $html .= '

Support tickets

'; if ($error !== '') { $html .= '

' . dsa_h($error) . '

'; } if (count($ticketRows) === 0) { $html .= '

No tickets yet.

'; } else { $html .= '
'; foreach ($ticketRows as $ticket) { $ticketId = (int) ($ticket['id'] ?? 0); $orderRef = (int) ($ticket['order_id'] ?? 0) > 0 ? '#' . (int) $ticket['order_id'] : '—'; $html .= ''; } $html .= '
TicketSubjectOrderStatusLast update
#' . dsa_h((string) $ticketId) . '' . dsa_h((string) $ticket['subject']) . '' . dsa_h($orderRef) . '' . dsa_ticket_status_pill((string) ($ticket['status'] ?? 'open')) . '' . dsa_h((string) ($ticket['last_reply_at'] ?? $ticket['updated_at'] ?? $ticket['created_at'] ?? '')) . 'Open
'; } $html .= '
'; $html .= '

New ticket

'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '

You will receive email updates and can reply by email or on this platform.

'; $html .= '
'; dsa_render_layout('Support tickets', $html); } function dsa_render_client_dashboard(PDO $pdo, string $notice): void { dsa_render_client_overview($pdo, $notice); } function dsa_render_client_order(PDO $pdo, int $orderId, string $notice): void { $clientId = (int) ($_SESSION['dsa_client_id'] ?? 0); dsa_release_session_lock_if_safe(); $stmt = $pdo->prepare( "select o.id, o.customer_email, o.status, o.amount_cents, o.currency, o.payment_ref, o.payment_method, o.payment_proof_path, o.payment_proof_note, o.delivery_value, o.service_details_json, o.whmcs_dispatch_status, o.whmcs_dispatch_message, o.created_at, o.updated_at, p.name as product_name, p.fulfillment_type from orders o join products p on p.id = o.product_id where o.id = :id and o.customer_id = :customer_id limit 1" ); $stmt->execute([':id' => $orderId, ':customer_id' => $clientId]); $order = $stmt->fetch(); if (!$order) { dsa_render_client_dashboard($pdo, 'Order not found.'); return; } dsa_sync_paid_signal_from_whmcs($pdo, $orderId); dsa_try_confirm_gateway_payment($pdo, $orderId, true); $stmt->execute([':id' => $orderId, ':customer_id' => $clientId]); $order = $stmt->fetch() ?: $order; if ((string) ($order['status'] ?? '') === 'paid') { dsa_on_order_became_paid($pdo, $orderId); $stmt->execute([':id' => $orderId, ':customer_id' => $clientId]); $order = $stmt->fetch() ?: $order; } $timeline = dsa_build_order_timeline($order); $orderLabel = trim((string) ($order['product_name'] ?? '')); if ($orderLabel === '') { $orderLabel = 'Order'; } $clientStatusLabel = dsa_customer_order_status_label((array) $order); // Build "next step" card content to display at top $nextStepHtml = ''; if ((string) $order['status'] === 'fulfilled') { if ((string) $order['delivery_value'] !== '' && (string) $order['fulfillment_type'] !== 'license_key') { $nextStepHtml = 'Open Delivery'; } else { $nextStepHtml = 'Manage product'; } } elseif ((string) $order['status'] === 'paid') { if (dsa_demo_mode_active($pdo) || dsa_order_is_demo_paid_no_provision((array) $order)) { $nextStepHtml = '

' . dsa_h(dsa_demo_mode_paid_notice()) . '

'; } else { $nextStepHtml = '

Payment confirmed. Provisioning is in progress — we\'ll notify you when ready. View product status

'; } } elseif ($clientStatusLabel === 'payment_submitted') { $paymentMethod = (string) ($order['payment_method'] ?? ''); if (dsa_is_gateway_payment_method($paymentMethod)) { $nextStepHtml = 'Continue payment' . '

Didn\'t get the M-PESA prompt? Open payment to resend or update your phone number.

'; } else { $nextStepHtml = '

We received your payment request and are waiting for confirmation.

'; } } else { $nextStepHtml = 'Pay Now'; } $html = '
'; $html .= '

Purchase record · All orders

'; $html .= '

Order #' . dsa_h((string) $order['id']) . '

'; $html .= '

Payment, billing, and checkout details for this purchase.

'; if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } // Prominent next-step card at top $html .= '
' . '

Next step

' . '
' . $nextStepHtml . '
' . '

Status: ' . dsa_status_pill($clientStatusLabel) . '

'; $statusContext = dsa_customer_order_status_context((array) $order); if ($statusContext !== '') { $html .= '

' . dsa_h($statusContext) . '

'; } $html .= '
'; $html .= '

Payment summary

'; $html .= '

Product purchased: ' . dsa_h($orderLabel) . '

'; $html .= '

Order total: ' . dsa_h(dsa_money((int) $order['amount_cents'], (string) $order['currency'])) . '

'; $html .= '

Ordered: ' . dsa_h((string) ($order['created_at'] ?? '')) . ' · Last update: ' . dsa_h((string) ($order['updated_at'] ?? '')) . '

'; if ((string) $order['status'] === 'pending_payment' && $clientStatusLabel === 'payment_submitted') { $html .= '

Payment request submitted. Waiting for successful payment confirmation.

'; } if (trim((string) $order['payment_ref']) !== '') { $html .= '

Payment Ref: ' . dsa_h((string) $order['payment_ref']) . '

'; } if ((string) ($order['payment_method'] ?? '') !== '') { $methodLabel = dsa_payment_method_display_label($pdo, (string) $order['payment_method']); $html .= '

Payment Option: ' . dsa_h($methodLabel) . '

'; } if ((string) $order['payment_proof_path'] !== '') { $html .= '

View uploaded proof

'; } $itemsTable = dsa_render_order_items_table($pdo, (int) $order['id']); if ($itemsTable !== '') { $html .= '

Line items

' . $itemsTable; } $html .= '

Order progress

'; foreach ($timeline as $item) { $html .= '

' . ($item['done'] ? '✅ ' : '⏳ ') . dsa_h($item['label']) . '

'; } if (in_array((string) $order['status'], ['paid', 'fulfilled'], true)) { $html .= '

Service credentials and delivery access are on the product management page.

'; } $html .= '
'; if ((string) $order['status'] === 'pending_payment') { $html .= '' . dsa_h($clientStatusLabel === 'payment_submitted' ? 'Continue payment' : 'Pay now') . ''; } $html .= 'Back to orders' . 'Client area' . '
'; if ($clientStatusLabel === 'payment_submitted' && dsa_is_gateway_payment_method((string) ($order['payment_method'] ?? ''))) { $html .= ''; } $html .= '
'; dsa_render_layout('Client order', $html); } function dsa_render_client_product_manage(PDO $pdo, int $orderId, string $notice): void { $clientId = (int) ($_SESSION['dsa_client_id'] ?? 0); dsa_release_session_lock_if_safe(); $stmt = $pdo->prepare( "select o.id, o.status, o.amount_cents, o.currency, o.payment_ref, o.payment_proof_path, o.delivery_value, o.service_details_json, o.service_asset_json, o.whmcs_order_id, o.whmcs_invoice_id, o.whmcs_service_id, o.whmcs_domain_id, o.whmcs_dispatch_status, o.customer_email, o.customer_id, o.updated_at, o.notify_opt_in, p.name as product_name, p.description as product_description, p.fulfillment_type, p.slug as product_slug, p.catalog_product_id, p.provisioning_profile from orders o join products p on p.id = o.product_id where o.id = :id and o.customer_id = :customer_id limit 1" ); $stmt->execute([':id' => $orderId, ':customer_id' => $clientId]); $order = $stmt->fetch(); if (!$order) { dsa_render_client_dashboard($pdo, 'Product not found for your account.'); return; } $productLabel = trim((string) ($order['product_name'] ?? '')); if ($productLabel === '') { $productLabel = 'Product'; } $html = '
'; $html .= '

Active service · All products · Order #' . dsa_h((string) $order['id']) . '

'; $html .= '

' . dsa_h($productLabel) . '

'; $html .= '

Manage your provisioned service, delivery access, and technical details.

'; if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } $productStatusLabel = dsa_customer_order_status_label((array) $order); $html .= '

Service status: ' . dsa_status_pill($productStatusLabel) . '

'; $productStatusContext = dsa_customer_order_status_context((array) $order); if ($productStatusContext !== '') { $html .= '

' . dsa_h($productStatusContext) . '

'; } $html .= '

' . dsa_h((string) $order['product_description']) . '

'; $html .= '

Last update: ' . dsa_h((string) $order['updated_at']) . '

'; $html .= '
'; $html .= dsa_render_service_details_card((array) $order); if (in_array((string) ($order['status'] ?? ''), ['paid', 'fulfilled'], true)) { $html .= dsa_enhance_client_product_manage($pdo, (array) $order, ''); } $sourceProductId = dsa_order_source_product_id((array) $order); if ($sourceProductId > 0) { $kbArticles = dsa_kb_client_articles($pdo, $clientId, $sourceProductId); if (count($kbArticles) > 0) { $html .= '
'; $html .= 'Help articles (' . count($kbArticles) . ')'; $html .= '
    '; foreach ($kbArticles as $kbArticle) { $kbId = (int) ($kbArticle['id'] ?? 0); $kbTitle = trim((string) ($kbArticle['title'] ?? '')); if ($kbId <= 0 || $kbTitle === '') { continue; } $html .= '
  • ' . dsa_h($kbTitle) . '
  • '; } $html .= '
'; $html .= '

All articles

'; $html .= '
'; } } $html .= '

Delivery & support

'; if ((string) $order['status'] === 'fulfilled' && (string) $order['delivery_value'] !== '') { if ((string) $order['fulfillment_type'] === 'license_key') { $html .= '

License key: ' . dsa_h((string) $order['delivery_value']) . '

'; } else { $html .= 'Open delivery'; } } if ((string) $order['status'] !== 'fulfilled') { $html .= 'Complete payment'; } $html .= 'View order & payment'; $html .= 'Get support'; $html .= '
'; if ((string) $order['payment_proof_path'] !== '') { $html .= '

View submitted payment proof

'; } $html .= '

Billing details and invoices are on the billing page.

'; $html .= '
'; dsa_render_layout('Manage product', $html); } function dsa_render_payment_proof(PDO $pdo, int $orderId, string $notice): void { dsa_release_session_lock_if_safe(); $stmt = $pdo->prepare( "select o.id, o.customer_email, o.customer_phone, o.payment_ref, o.payment_method, o.payment_proof_note, o.payment_proof_path, o.status, o.amount_cents, o.currency, o.service_details_json, o.whmcs_dispatch_status, o.updated_at, p.name as product_name from orders o join products p on p.id = o.product_id where o.id = :id limit 1" ); $stmt->execute([':id' => $orderId]); $order = $stmt->fetch(); if (!$order) { http_response_code(404); dsa_render_layout('Not found', '

Order not found

'); return; } if ((string) ($order['status'] ?? '') === 'demo_simulated') { header('Location: /client-area/order/' . $orderId); return; } dsa_try_confirm_gateway_payment($pdo, $orderId, true); $stmt->execute([':id' => $orderId]); $order = $stmt->fetch() ?: $order; if ((string) ($order['status'] ?? '') === 'paid') { dsa_on_order_became_paid($pdo, $orderId); $stmt->execute([':id' => $orderId]); $order = $stmt->fetch() ?: $order; } $orderLabel = trim((string) ($order['product_name'] ?? '')); if ($orderLabel === '') { $orderLabel = 'Order'; } if ($notice === '') { $notice = dsa_consume_payment_notice($orderId); } $methodKey = (string) ($order['payment_method'] ?? ''); $resolvedMethodKey = dsa_resolve_payment_method_key($pdo, $methodKey); $payStatusLabel = dsa_customer_order_status_label((array) $order); $orderStatus = (string) ($order['status'] ?? 'pending_payment'); if (in_array($orderStatus, ['paid', 'fulfilled'], true)) { dsa_payment_flow_state_clear($orderId); header('Location: /client-area/order/' . $orderId); exit; } $allowPaymentActions = ($orderStatus === 'pending_payment'); $isAwaitingConfirmation = ($orderStatus === 'pending_payment' && $payStatusLabel === 'payment_submitted'); $isGatewayMethod = dsa_is_gateway_payment_method((string) ($order['payment_method'] ?? '')); $stkInProgress = ($isAwaitingConfirmation && $isGatewayMethod); $paymentState = dsa_payment_request_state_get((int) $order['id']); $nowTs = time(); $countdownLeft = 0; $resendLeft = 0; if ($isAwaitingConfirmation) { $updatedAtTs = strtotime((string) ($order['updated_at'] ?? '')); if ($updatedAtTs !== false) { $countdownLeft = max(0, 45 - max(0, $nowTs - (int) $updatedAtTs)); $resendLeft = max(0, 10 - max(0, $nowTs - (int) $updatedAtTs)); } if (isset($paymentState['expires_at'])) { $countdownLeft = max($countdownLeft, max(0, (int) $paymentState['expires_at'] - $nowTs)); } if (isset($paymentState['resend_available_at'])) { $resendLeft = max($resendLeft, max(0, (int) $paymentState['resend_available_at'] - $nowTs)); } } $storedPhone = dsa_resolve_gateway_payment_phone($pdo, (int) $order['id'], (array) $order, ''); $displayPhone = dsa_format_msisdn_for_display($storedPhone); $inlineNotice = dsa_is_inline_payment_notice($notice) ? $notice : ''; $topNotice = ($inlineNotice !== '' || dsa_is_stk_success_notice($notice)) ? '' : $notice; if ($stkInProgress) { $methodLabel = dsa_payment_method_display_label($pdo, $methodKey); if ($methodLabel === '') { $methodLabel = 'M-PESA'; } $totalMoney = dsa_money((int) $order['amount_cents'], (string) $order['currency']); $statusMessage = $countdownLeft <= 0 ? 'Waiting for M-PESA confirmation. This usually takes a few seconds.' : 'Check your phone and enter your M-PESA PIN within ' . dsa_h((string) $countdownLeft) . ' seconds.'; $resendDisabled = $resendLeft > 0; $html = '
' . dsa_checkout_steps('payment') . '

Payment

'; $html .= '

Order #' . dsa_h((string) $order['id']) . ' · ' . dsa_h($orderLabel) . ' · ' . dsa_h($totalMoney) . '

'; $html .= '
'; $html .= '

' . dsa_h($methodLabel) . '

'; $html .= '

' . $statusMessage . '

'; $html .= '
'; $html .= ''; $html .= ''; if ($inlineNotice !== '') { $html .= '

' . dsa_h($inlineNotice) . '

'; } $html .= ''; $html .= ''; $html .= '
'; $html .= ''; $html .= '
'; if ($resendDisabled) { $html .= '

Resend available in ' . dsa_h((string) $resendLeft) . ' seconds.

'; } $html .= '
'; $paymentMethods = dsa_payment_methods($pdo); if (count($paymentMethods) > 0) { $html .= '
Use a different payment method'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; } $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; dsa_render_layout('Payment', $html); return; } $html = '
' . dsa_checkout_steps('payment') . '

Payment

'; if (dsa_demo_mode_active($pdo)) { $html .= '

Step 3: Demo checkout — skip payment to finish.

'; } else { $html .= '

Step 3: Choose how to pay and complete your order.

'; } if ($topNotice !== '') { $html .= '

' . dsa_h($topNotice) . '

'; } $html .= '

Order #' . dsa_h((string) $order['id']) . ' for ' . dsa_h($orderLabel) . '

'; $itemsTable = dsa_render_order_items_table($pdo, (int) $order['id']); if ($itemsTable !== '') { $html .= '

Items

' . $itemsTable; } $html .= '

Total: ' . dsa_h(dsa_money((int) $order['amount_cents'], (string) $order['currency'])) . '

'; $html .= '

Status: ' . dsa_status_pill($payStatusLabel) . '

'; if ($isAwaitingConfirmation) { $html .= '

Payment request submitted. Complete the prompt to finish payment.

'; } elseif ($orderStatus === 'fulfilled') { $html .= '
Your service is ready and active. ' . 'Manage your product · ' . 'View order details' . '
'; } elseif ($orderStatus === 'paid' || dsa_order_is_demo_paid_no_provision((array) $order)) { if (dsa_demo_mode_active($pdo) || dsa_order_is_demo_paid_no_provision((array) $order)) { $html .= '
' . dsa_h(dsa_demo_mode_paid_notice()) . '
'; } else { $html .= '
Payment confirmed successfully. Provisioning is in progress. ' . 'View product status
'; } } $paymentMethods = dsa_payment_methods($pdo); if ($allowPaymentActions && dsa_demo_mode_active($pdo) && count($paymentMethods) === 0) { $html .= dsa_demo_mode_render_payment_only_card((int) $order['id']); $html .= '
'; dsa_render_layout('Payment', $html); return; } if (count($paymentMethods) > 0 && $allowPaymentActions) { $gatewayReady = ($resolvedMethodKey !== '' && isset($paymentMethods[$resolvedMethodKey]) && dsa_is_gateway_payment_method($resolvedMethodKey) && !$isAwaitingConfirmation); if ($gatewayReady) { $method = $paymentMethods[$resolvedMethodKey]; $html .= '
'; $html .= '

' . dsa_h((string) $method['label']) . '

'; if (trim((string) ($method['instructions'] ?? '')) !== '') { $html .= '

' . nl2br(dsa_h(trim((string) $method['instructions']))) . '

'; } if (($resolvedMethodKey === 'paypal' || $resolvedMethodKey === 'paypal_checkout') && ($method['paypal_type'] ?? '') === 'checkout') { $clientId = dsa_h((string) $method['client_id']); $usdAmount = number_format(((int) $order['amount_cents']) / 100, 2, '.', ''); $orderId = (int) $order['id']; $html .= '
'; $html .= ''; $html .= ''; } else { $html .= '
'; $html .= ''; if ($inlineNotice !== '') { $html .= '

' . dsa_h($inlineNotice) . '

'; } if (in_array($resolvedMethodKey, ['mpesa_direct', 'watupay_mpesa'], true)) { $html .= ''; $html .= ''; } $startLabel = ($resolvedMethodKey === 'paypal' || $resolvedMethodKey === 'paypal_basic' || $resolvedMethodKey === 'paypal_checkout') ? 'Start PayPal payment' : 'Start M-PESA payment'; $html .= '
'; if (dsa_demo_mode_active($pdo)) { $html .= dsa_demo_mode_render_skip_payment_button((int) $order['id']); } $html .= '
'; } if (count($paymentMethods) > 1) { $html .= '
Use a different payment method'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; } $html .= '
'; } else { $paymentStepTitle = $methodKey === '' ? 'Choose payment option' : 'Change payment option'; $paymentStepButton = $methodKey === '' ? 'Continue' : 'Update payment option'; $html .= '
'; $html .= '

' . dsa_h($paymentStepTitle) . '

'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; if (dsa_demo_mode_active($pdo)) { $html .= dsa_demo_mode_render_skip_payment_button((int) $order['id']); } $html .= '
'; } } if ($allowPaymentActions && $resolvedMethodKey !== '' && isset($paymentMethods[$resolvedMethodKey]) && !dsa_is_gateway_payment_method($resolvedMethodKey)) { $method = $paymentMethods[$resolvedMethodKey]; $html .= '

Payment option: ' . dsa_h((string) $method['label']) . '

'; if (trim((string) ($method['instructions'] ?? '')) !== '') { $html .= '

' . nl2br(dsa_h((string) $method['instructions'])) . '

'; } if (trim((string) ($method['pay_url'] ?? '')) !== '') { $html .= '

Pay now

'; } } elseif ($allowPaymentActions && $methodKey !== '' && ($resolvedMethodKey === '' || !isset($paymentMethods[$resolvedMethodKey]))) { $html .= '

Complete your payment using the agreed method, then submit payment reference and proof below.

'; } if ((string) $order['payment_proof_path'] !== '') { $html .= '

Current proof: View uploaded proof

'; } $isCashOnlyMethod = ($methodKey === 'cash'); $showManualProofStep = $allowPaymentActions && dsa_is_manual_payment_proof_method($resolvedMethodKey !== '' ? $resolvedMethodKey : $methodKey); if ($isCashOnlyMethod && $allowPaymentActions) { $html .= '

Cash Payment Instructions

' . '

Follow the payment instructions above to complete your cash payment. No online proof submission is required for this method.

' . '
Back to Order'; if (dsa_demo_mode_active($pdo)) { $html .= dsa_demo_mode_render_skip_payment_button((int) $order['id']); } $html .= '
'; } elseif ($showManualProofStep) { $html .= '

Confirm Payment

After paying, submit transaction reference and proof so we can activate your order.

'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '
Back to Order'; if (dsa_demo_mode_active($pdo)) { $html .= dsa_demo_mode_render_skip_payment_button((int) $order['id']); } $html .= '
'; } else { $html .= '
Back to Order'; if ($allowPaymentActions && dsa_demo_mode_active($pdo) && $methodKey === '') { $html .= dsa_demo_mode_render_skip_payment_button((int) $order['id']); } $html .= '
'; } $html .= ''; dsa_render_layout('Payment', $html); } function dsa_normalize_license_pool(string $value): string { $lines = preg_split('/\r\n|\r|\n/', $value) ?: []; $clean = []; foreach ($lines as $line) { $line = trim((string) $line); if ($line !== '') { $clean[] = $line; } } return implode("\n", array_values(array_unique($clean))); } function dsa_get_order_with_product(PDO $pdo, int $orderId): ?array { $stmt = $pdo->prepare( "select o.id, o.product_id, o.customer_name, o.customer_email, o.customer_phone, o.status, o.payment_ref, o.payment_method, o.notify_opt_in, o.amount_cents, o.currency, o.delivery_value, o.service_details_json, o.whmcs_order_id, o.whmcs_invoice_id, o.whmcs_service_id, o.whmcs_dispatch_status, o.whmcs_dispatch_message, p.slug as product_slug, p.name as product_name, p.category, p.delivery_url, p.fulfillment_type, p.catalog_product_id, p.provisioning_profile from orders o join products p on p.id = o.product_id where o.id = :id limit 1" ); $stmt->execute([':id' => $orderId]); $row = $stmt->fetch(); return $row ?: null; } function dsa_assign_license_key(PDO $pdo, int $productId): string { $poolKey = 'license_pool_' . $productId; $usedKey = 'license_used_' . $productId; $poolRaw = dsa_setting($pdo, $poolKey, ''); $pool = preg_split('/\r\n|\r|\n/', $poolRaw) ?: []; $cleanPool = []; foreach ($pool as $item) { $item = trim((string) $item); if ($item !== '') { $cleanPool[] = $item; } } $usedRaw = dsa_setting($pdo, $usedKey, '[]'); $used = json_decode($usedRaw, true); if (!is_array($used)) { $used = []; } foreach ($cleanPool as $candidate) { if (!in_array($candidate, $used, true)) { $used[] = $candidate; dsa_setting_set($pdo, $usedKey, json_encode(array_values(array_unique($used)))); return $candidate; } } return ''; } function dsa_order_provisioning_already_started(array $order): bool { $dispatchStatus = (string) ($order['whmcs_dispatch_status'] ?? ''); if (in_array($dispatchStatus, ['dispatching', 'invoice_created', 'invoice_paid', 'provisioned', 'demo_paid_no_provision'], true)) { return true; } return (int) ($order['whmcs_invoice_id'] ?? 0) > 0 || (int) ($order['whmcs_order_id'] ?? 0) > 0; } function dsa_order_repair_premature_fulfillment(PDO $pdo, int $orderId): bool { if ($orderId <= 0) { return false; } $order = dsa_get_order_with_product($pdo, $orderId); if (!$order || (string) ($order['status'] ?? '') !== 'fulfilled') { return false; } if (dsa_order_awaiting_watu_pay_reconciliation($order)) { $stmt = $pdo->prepare( "update orders set status = 'paid', updated_at = datetime('now') where id = :id and status = 'fulfilled'" ); $stmt->execute([':id' => $orderId]); return $stmt->rowCount() > 0; } if (!dsa_order_needs_whmcs_provisioning($order)) { return false; } if (dsa_order_whmcs_provisioning_complete($order)) { return false; } $stmt = $pdo->prepare( "update orders set status = 'paid', updated_at = datetime('now') where id = :id and status = 'fulfilled'" ); $stmt->execute([':id' => $orderId]); return $stmt->rowCount() > 0; } function dsa_admin_lifecycle_dispatch_label(array $order): string { $status = (string) ($order['whmcs_dispatch_status'] ?? ''); static $labels = [ '' => 'Not started', 'dispatching' => 'In progress', 'invoice_created' => 'Invoice created', 'invoice_paid' => 'Invoice created', 'provisioned' => 'Invoice created', 'failed' => 'Failed', 'demo_paid_no_provision' => 'Demo — skipped', ]; return $labels[$status] ?? ucwords(str_replace('_', ' ', $status)); } function dsa_admin_lifecycle_billing_label(array $order): string { $status = (string) ($order['whmcs_dispatch_status'] ?? ''); if ($status === 'demo_paid_no_provision') { return 'N/A'; } if (in_array($status, ['invoice_paid', 'provisioned'], true)) { return 'Paid'; } if ($status === 'invoice_created' || (int) ($order['whmcs_invoice_id'] ?? 0) > 0) { return 'Awaiting payment'; } return 'Not started'; } function dsa_admin_lifecycle_provisioning_label(array $order): string { $status = (string) ($order['whmcs_dispatch_status'] ?? ''); if ($status === 'demo_paid_no_provision') { return 'N/A'; } if ($status === 'provisioned') { return 'Service active'; } if ($status === 'failed' && (int) ($order['whmcs_invoice_id'] ?? 0) > 0) { return 'Failed'; } if ($status === 'invoice_paid') { return 'In progress'; } return 'Not started'; } /** @return array{dispatch:string,billing:string,provisioning:string} */ function dsa_admin_lifecycle_status_cells(array $order): array { $na = 'N/A'; if (!dsa_order_needs_whmcs_provisioning($order)) { return ['dispatch' => $na, 'billing' => $na, 'provisioning' => $na]; } $dispatchMessage = trim((string) ($order['whmcs_dispatch_message'] ?? '')); $messageSuffix = $dispatchMessage !== '' ? '
' . dsa_h($dispatchMessage) . '' : ''; $dispatchCell = dsa_h(dsa_admin_lifecycle_dispatch_label($order)) . $messageSuffix; $billingCell = dsa_h(dsa_admin_lifecycle_billing_label($order)); $provisioningCell = dsa_h(dsa_admin_lifecycle_provisioning_label($order)); return [ 'dispatch' => $dispatchCell, 'billing' => $billingCell, 'provisioning' => $provisioningCell, ]; } function dsa_admin_can_dispatch_order(PDO $pdo, array $order): bool { if ((string) ($order['status'] ?? '') !== 'paid') { return false; } if (!dsa_order_needs_whmcs_provisioning($order)) { return false; } $dispatchStatus = (string) ($order['whmcs_dispatch_status'] ?? ''); $invoiceId = (int) ($order['whmcs_invoice_id'] ?? 0); if ( !dsa_order_has_pending_stk_reference(trim((string) ($order['payment_ref'] ?? ''))) && $invoiceId > 0 && in_array($dispatchStatus, ['invoice_created', 'invoice_paid', 'provisioned'], true) && dsa_order_dispatch_already_completed_for_payment($pdo, $order) ) { return false; } if ($dispatchStatus === 'dispatching') { $dispatchedAtTs = strtotime((string) ($order['whmcs_dispatched_at'] ?? '')); $dispatchAgeSeconds = $dispatchedAtTs !== false ? (time() - (int) $dispatchedAtTs) : 9999; if ($dispatchAgeSeconds >= 0 && $dispatchAgeSeconds < 120) { return false; } return true; } if (in_array($dispatchStatus, ['invoice_created', 'invoice_paid', 'provisioned', 'demo_paid_no_provision'], true)) { return false; } return true; } /** @return array{success:bool,message:string} */ function dsa_admin_dispatch_order(PDO $pdo, int $orderId): array { if ($orderId <= 0) { return ['success' => false, 'message' => 'Invalid order ID.']; } $order = dsa_get_order_with_product($pdo, $orderId); if (!$order) { return ['success' => false, 'message' => 'Order not found.']; } // Invoice-payment orders (e.g. renewals) do not provision anything; they only // settle an existing WHMCS invoice. Route them straight to the invoice handler // instead of the generic provisioning/fulfillment path. if (count(dsa_order_invoice_payment_target((array) $order)) > 0) { dsa_apply_invoice_payment_to_whmcs($pdo, $orderId); $order = dsa_get_order_with_product($pdo, $orderId) ?: $order; $paidOk = (string) ($order['whmcs_dispatch_status'] ?? '') === 'provisioned'; $detail = trim((string) ($order['whmcs_dispatch_message'] ?? '')); if ($paidOk) { return ['success' => true, 'message' => 'Renewal invoice paid. ' . $detail]; } return [ 'success' => false, 'message' => 'Invoice not settled yet. ' . ($detail !== '' ? $detail : 'Awaiting confirmed payment from a configured payment method.'), ]; } dsa_order_repair_premature_fulfillment($pdo, $orderId); $order = dsa_get_order_with_product($pdo, $orderId) ?: $order; if ((string) ($order['status'] ?? '') !== 'paid') { return ['success' => false, 'message' => 'Only paid orders can be dispatched to Watu.']; } if (!dsa_order_needs_whmcs_provisioning($order)) { return ['success' => false, 'message' => 'This product does not require Watu platform dispatch.']; } if (!dsa_admin_can_dispatch_order($pdo, $order)) { return ['success' => false, 'message' => dsa_ui_label('dispatch.already_dispatched')]; } $dispatch = dsa_dispatch_paid_order_to_whmcs($pdo, $orderId); $order = dsa_get_order_with_product($pdo, $orderId) ?: $order; $dispatchStatus = (string) ($order['whmcs_dispatch_status'] ?? ''); if (in_array($dispatchStatus, ['invoice_created', 'invoice_paid', 'provisioned'], true)) { dsa_apply_auto_fulfillment($pdo, $orderId); return [ 'success' => true, 'message' => 'Order #' . $orderId . ' dispatched to Watu — ' . dsa_admin_lifecycle_dispatch_label($order) . '.', ]; } $detail = trim((string) ($order['whmcs_dispatch_message'] ?? ($dispatch['message'] ?? 'Dispatch did not complete.'))); return ['success' => false, 'message' => 'Dispatch failed: ' . $detail]; } function dsa_on_order_became_paid(PDO $pdo, int $orderId): void { if (dsa_demo_mode_blocks_provisioning($pdo)) { return; } $order = dsa_get_order_with_product($pdo, $orderId); if ($order && count(dsa_order_invoice_payment_target((array) $order)) > 0) { dsa_apply_invoice_payment_to_whmcs($pdo, $orderId); return; } dsa_apply_auto_dispatch_on_paid($pdo, $orderId); dsa_sync_order_dispatch_status_from_watu($pdo, $orderId, false); dsa_apply_auto_fulfillment($pdo, $orderId); } /** * Handles orders that exist only to pay an already-issued WHMCS invoice (renewals). * Instead of creating a new WHMCS order, it applies the collected payment to the * target invoice via the bridge; WHMCS then renews the underlying service/domain. */ function dsa_apply_invoice_payment_to_whmcs(PDO $pdo, int $orderId): void { $payOrder = dsa_get_order_with_product($pdo, $orderId); if (!$payOrder) { return; } $target = dsa_order_invoice_payment_target((array) $payOrder); $invoiceId = (int) ($target['whmcs_invoice_id'] ?? 0); $sourceOrderId = (int) ($target['source_order_id'] ?? 0); if ($invoiceId <= 0 || $sourceOrderId <= 0) { return; } if (!in_array((string) ($payOrder['status'] ?? ''), ['paid', 'fulfilled'], true)) { return; } if ((string) ($payOrder['whmcs_dispatch_status'] ?? '') === 'provisioned') { return; } $stmt = $pdo->prepare("select * from orders where id = :id limit 1"); $stmt->execute([':id' => $sourceOrderId]); $sourceOrder = $stmt->fetch(); if (!$sourceOrder || strcasecmp((string) ($sourceOrder['customer_email'] ?? ''), (string) ($payOrder['customer_email'] ?? '')) !== 0) { return; } $accountRef = trim((string) ($payOrder['payment_ref'] ?? '')); $resp = dsa_service_manage_call($pdo, $sourceOrder, 'pay_invoice', [ 'invoice_id' => $invoiceId, 'account_ref' => $accountRef, 'tenant_payment_method' => (string) ($payOrder['payment_method'] ?? ''), 'amount_cents' => (int) ($payOrder['amount_cents'] ?? 0), ]); $message = trim((string) ($resp['message'] ?? '')); if (!empty($resp['ok']) && (string) ($resp['status'] ?? '') === 'paid') { $upd = $pdo->prepare( "update orders set status = 'fulfilled', whmcs_invoice_id = :inv, whmcs_dispatch_status = 'provisioned', whmcs_dispatch_message = :msg, whmcs_dispatched_at = datetime('now'), updated_at = datetime('now') where id = :id" ); $upd->execute([':inv' => $invoiceId, ':msg' => $message !== '' ? $message : 'Invoice paid.', ':id' => $orderId]); return; } $failMsg = $message !== '' ? $message : 'Invoice payment could not be applied yet.'; $upd = $pdo->prepare( "update orders set whmcs_invoice_id = :inv, whmcs_dispatch_status = 'invoice_created', whmcs_dispatch_message = :msg, whmcs_dispatched_at = datetime('now'), updated_at = datetime('now') where id = :id" ); $upd->execute([':inv' => $invoiceId, ':msg' => $failMsg, ':id' => $orderId]); } function dsa_watu_dispatch_status_rank(string $status): int { return match (strtolower(trim($status))) { 'provisioned' => 4, 'invoice_paid' => 3, 'invoice_created' => 2, 'dispatching', 'processing' => 1, default => 0, }; } function dsa_order_dispatch_status_poll_allowed(PDO $pdo, int $orderId, bool $force = false): bool { if ($force || $orderId <= 0) { return $force; } static $requestMemo = []; $memoKey = (string) spl_object_id($pdo) . '|dispatch|' . (string) $orderId; $now = time(); if (isset($requestMemo[$memoKey]) && ($now - (int) $requestMemo[$memoKey]) < 30) { return false; } $requestMemo[$memoKey] = $now; return true; } function dsa_order_dispatch_status_should_poll(array $order): bool { if (!dsa_order_needs_whmcs_provisioning($order)) { return false; } if (dsa_order_whmcs_provisioning_complete($order)) { return false; } $dispatchStatus = (string) ($order['whmcs_dispatch_status'] ?? ''); if ((int) ($order['whmcs_invoice_id'] ?? 0) > 0) { return true; } return in_array($dispatchStatus, ['dispatching', 'invoice_created', 'invoice_paid'], true); } function dsa_apply_watu_dispatch_status_payload(PDO $pdo, int $orderId, array $bodyJson): bool { if (empty($bodyJson['ok']) || empty($bodyJson['found'])) { return false; } $order = dsa_get_order_with_product($pdo, $orderId); if (!$order) { return false; } $newStatus = trim((string) ($bodyJson['status'] ?? '')); if ($newStatus === '') { return false; } $oldStatus = (string) ($order['whmcs_dispatch_status'] ?? ''); if (dsa_watu_dispatch_status_rank($newStatus) < dsa_watu_dispatch_status_rank($oldStatus)) { return false; } $serviceDetails = is_array($bodyJson['service_details'] ?? null) ? (array) $bodyJson['service_details'] : []; $serviceAssets = is_array($bodyJson['service_assets'] ?? null) ? (array) $bodyJson['service_assets'] : []; $message = trim((string) ($bodyJson['message'] ?? '')); $stmt = $pdo->prepare( "update orders set whmcs_order_id = :whmcs_order_id, whmcs_invoice_id = :whmcs_invoice_id, whmcs_service_id = :whmcs_service_id, whmcs_dispatch_status = :whmcs_dispatch_status, whmcs_dispatch_message = :whmcs_dispatch_message, service_details_json = :service_details_json, whmcs_dispatched_at = case when coalesce(whmcs_dispatched_at, '') = '' then datetime('now') else whmcs_dispatched_at end, updated_at = datetime('now') where id = :id" ); $stmt->execute([ ':id' => $orderId, ':whmcs_order_id' => max((int) ($order['whmcs_order_id'] ?? 0), (int) ($bodyJson['whmcs_order_id'] ?? 0)), ':whmcs_invoice_id' => max((int) ($order['whmcs_invoice_id'] ?? 0), (int) ($bodyJson['whmcs_invoice_id'] ?? 0)), ':whmcs_service_id' => max((int) ($order['whmcs_service_id'] ?? 0), (int) ($bodyJson['whmcs_service_id'] ?? 0)), ':whmcs_dispatch_status' => $newStatus, ':whmcs_dispatch_message' => $message !== '' ? $message : (string) ($order['whmcs_dispatch_message'] ?? ''), ':service_details_json' => count($serviceDetails) > 0 ? json_encode($serviceDetails, JSON_UNESCAPED_UNICODE) : (string) ($order['service_details_json'] ?? '{}'), ]); if ((int) $stmt->rowCount() <= 0) { return false; } if (count($serviceAssets) > 0 || count($serviceDetails) > 0) { dsa_order_store_dispatch_assets($pdo, $orderId, [ 'service_assets' => $serviceAssets, 'service_details' => $serviceDetails, 'whmcs_domain_id' => (int) ($bodyJson['whmcs_domain_id'] ?? ($serviceAssets['whmcs_domain_id'] ?? 0)), ]); } return true; } /** @return array{success:bool,message:string} */ function dsa_sync_order_dispatch_status_from_watu(PDO $pdo, int $orderId, bool $force = false): array { if ($orderId <= 0) { return ['success' => false, 'message' => 'Invalid order ID.']; } $order = dsa_get_order_with_product($pdo, $orderId); if (!$order) { return ['success' => false, 'message' => 'Order not found.']; } if (!dsa_order_needs_whmcs_provisioning($order)) { return ['success' => true, 'message' => 'No Watu dispatch tracking required for this product.']; } if (!dsa_order_dispatch_status_should_poll($order)) { return ['success' => true, 'message' => 'Order has not been dispatched to Watu yet.']; } if (!$force && !dsa_order_dispatch_status_poll_allowed($pdo, $orderId, false)) { return ['success' => true, 'message' => 'Watu status was checked recently.']; } $statusUrl = dsa_order_dispatch_status_url($pdo); $watuKey = trim(dsa_setting($pdo, 'catalog_sync_whmcs_secret', dsa_setting($pdo, 'catalog_sync_whmcs_identifier', ''))); if ($statusUrl === '' || $watuKey === '') { return ['success' => false, 'message' => 'Watu status endpoint is not configured.']; } $sep = strpos($statusUrl, '?') === false ? '?' : '&'; $lookupUrl = $statusUrl . $sep . 'order_id=' . rawurlencode((string) $orderId); $http = dsa_http_get_with_watu_key($lookupUrl, $watuKey); if (empty($http['ok']) || trim((string) ($http['body'] ?? '')) === '') { return ['success' => false, 'message' => 'Could not reach Watu order status: ' . trim((string) ($http['error'] ?? 'unknown error'))]; } $json = json_decode((string) $http['body'], true); if (!is_array($json) || empty($json['ok'])) { return ['success' => false, 'message' => 'Watu returned an invalid order status response.']; } if (empty($json['found'])) { return ['success' => true, 'message' => trim((string) ($json['message'] ?? 'No Watu dispatch record for this order yet.'))]; } $updated = dsa_apply_watu_dispatch_status_payload($pdo, $orderId, $json); if ($updated) { dsa_apply_auto_fulfillment($pdo, $orderId); } dsa_maybe_continue_order_on_watu($pdo, $orderId, false); $order = dsa_get_order_with_product($pdo, $orderId) ?: $order; $statusLabel = dsa_admin_lifecycle_provisioning_label($order); if ($updated) { return [ 'success' => true, 'message' => 'Watu status refreshed — provisioning: ' . $statusLabel . '.', ]; } return [ 'success' => true, 'message' => 'Watu status unchanged — provisioning: ' . $statusLabel . '.', ]; } function dsa_admin_can_refresh_watu_order_status(array $order): bool { if (!in_array((string) ($order['status'] ?? ''), ['paid', 'fulfilled'], true)) { return false; } if (!dsa_order_needs_whmcs_provisioning($order)) { return false; } if (dsa_order_whmcs_provisioning_complete($order)) { return false; } return dsa_order_dispatch_status_should_poll($order); } /** @return array{success:bool,message:string} */ function dsa_admin_refresh_order_watu_status(PDO $pdo, int $orderId): array { if ($orderId <= 0) { return ['success' => false, 'message' => 'Invalid order ID.']; } dsa_order_repair_premature_fulfillment($pdo, $orderId); return dsa_sync_order_dispatch_status_from_watu($pdo, $orderId, true); } function dsa_admin_can_continue_order_on_watu(array $order): bool { if (!in_array((string) ($order['status'] ?? ''), ['paid', 'fulfilled'], true)) { return false; } if (!dsa_order_needs_whmcs_provisioning($order)) { return false; } if (dsa_order_whmcs_provisioning_complete($order)) { return false; } $dispatchStatus = (string) ($order['whmcs_dispatch_status'] ?? ''); return in_array($dispatchStatus, ['invoice_created', 'invoice_paid'], true) || ((int) ($order['whmcs_invoice_id'] ?? 0) > 0 && $dispatchStatus !== 'provisioned'); } /** @return array{success:bool,message:string} */ function dsa_continue_order_on_watu(PDO $pdo, int $orderId, bool $force = false): array { if ($orderId <= 0) { return ['success' => false, 'message' => 'Invalid order ID.']; } $order = dsa_get_order_with_product($pdo, $orderId); if (!$order) { return ['success' => false, 'message' => 'Order not found.']; } if (!dsa_order_needs_whmcs_provisioning($order)) { return ['success' => true, 'message' => 'No Watu provisioning required for this product.']; } if (dsa_order_whmcs_provisioning_complete($order)) { return ['success' => true, 'message' => 'Service is already active on Watu.']; } if (!$force && !dsa_admin_can_continue_order_on_watu($order)) { return ['success' => false, 'message' => 'Order is not ready to continue on Watu yet.']; } if (!$force && !dsa_order_dispatch_status_poll_allowed($pdo, $orderId, false)) { return ['success' => true, 'message' => 'Watu continue was attempted recently.']; } $provisionUrl = dsa_order_provision_url($pdo); $watuKey = trim(dsa_setting($pdo, 'catalog_sync_whmcs_secret', dsa_setting($pdo, 'catalog_sync_whmcs_identifier', ''))); if ($provisionUrl === '' || $watuKey === '') { return ['success' => false, 'message' => 'Watu provision endpoint is not configured.']; } $http = dsa_http_post_json_with_watu_key($provisionUrl, $watuKey, [ 'order_id' => $orderId, 'payment_ref' => trim((string) ($order['payment_ref'] ?? '')), ]); if (empty($http['ok']) || trim((string) ($http['body'] ?? '')) === '') { return ['success' => false, 'message' => 'Could not reach Watu provision service: ' . trim((string) ($http['error'] ?? 'unknown error'))]; } $json = json_decode((string) $http['body'], true); if (!is_array($json) || empty($json['ok'])) { return ['success' => false, 'message' => trim((string) ($json['message'] ?? 'Watu provision request failed.'))]; } $json['found'] = true; dsa_apply_watu_dispatch_status_payload($pdo, $orderId, $json); dsa_apply_auto_fulfillment($pdo, $orderId); $order = dsa_get_order_with_product($pdo, $orderId) ?: $order; $status = (string) ($order['whmcs_dispatch_status'] ?? ''); if ($status === 'provisioned') { return ['success' => true, 'message' => 'Order #' . $orderId . ' is now active on Watu.']; } return [ 'success' => true, 'message' => 'Watu updated — dispatch: ' . dsa_admin_lifecycle_dispatch_label($order) . ', billing: ' . dsa_admin_lifecycle_billing_label($order) . ', provisioning: ' . dsa_admin_lifecycle_provisioning_label($order) . '. ' . trim((string) ($json['message'] ?? '')), ]; } function dsa_maybe_continue_order_on_watu(PDO $pdo, int $orderId, bool $force = false): void { if ($orderId <= 0) { return; } $order = dsa_get_order_with_product($pdo, $orderId); if (!$order || !dsa_admin_can_continue_order_on_watu($order)) { return; } $dispatchStatus = (string) ($order['whmcs_dispatch_status'] ?? ''); if (!$force && $dispatchStatus !== 'invoice_paid') { return; } static $requestMemo = []; $memoKey = (string) spl_object_id($pdo) . '|continue|' . (string) $orderId; $now = time(); if (!$force && isset($requestMemo[$memoKey]) && ($now - (int) $requestMemo[$memoKey]) < 60) { return; } $requestMemo[$memoKey] = $now; dsa_continue_order_on_watu($pdo, $orderId, true); } /** @return array{success:bool,message:string} */ function dsa_admin_continue_order_on_watu(PDO $pdo, int $orderId): array { if ($orderId <= 0) { return ['success' => false, 'message' => 'Invalid order ID.']; } dsa_order_repair_premature_fulfillment($pdo, $orderId); return dsa_continue_order_on_watu($pdo, $orderId, true); } function dsa_apply_auto_dispatch_on_paid(PDO $pdo, int $orderId): void { if (dsa_setting($pdo, 'order_dispatch_enabled', '1') !== '1') { return; } $order = dsa_get_order_with_product($pdo, $orderId); if (!$order || !dsa_admin_can_dispatch_order($pdo, $order)) { return; } dsa_dispatch_paid_order_to_whmcs($pdo, $orderId); } function dsa_order_needs_whmcs_provisioning(array $order): bool { $fulfillmentType = (string) ($order['fulfillment_type'] ?? 'manual'); if (in_array($fulfillmentType, ['instant_link', 'license_key'], true)) { return false; } $profile = dsa_normalize_provisioning_profile((string) ($order['provisioning_profile'] ?? 'none')); if ($profile !== 'none') { return true; } $slug = (string) ($order['product_slug'] ?? ''); if (preg_match('/^(?:watu-sku-)?(?:catalog|whmcs-product)-\d+$/', $slug) === 1) { return true; } if ((int) ($order['catalog_product_id'] ?? 0) > 0) { return true; } return dsa_order_source_product_id($order) > 0; } function dsa_order_whmcs_provisioning_complete(array $order): bool { return (string) ($order['whmcs_dispatch_status'] ?? '') === 'provisioned'; } function dsa_apply_auto_fulfillment(PDO $pdo, int $orderId): void { if (dsa_demo_mode_blocks_provisioning($pdo)) { return; } $order = dsa_get_order_with_product($pdo, $orderId); if (!$order || (string) $order['status'] !== 'paid') { return; } $needsWhmcs = dsa_order_needs_whmcs_provisioning($order); $dispatchStatus = (string) ($order['whmcs_dispatch_status'] ?? ''); $whmcsInvoiceId = (int) ($order['whmcs_invoice_id'] ?? 0); if ($whmcsInvoiceId > 0 && in_array($dispatchStatus, ['invoice_created', 'invoice_paid', 'provisioned'], true)) { if ($needsWhmcs && dsa_order_whmcs_provisioning_complete($order)) { dsa_mark_order_fulfilled($pdo, $orderId, false); } return; } if ($needsWhmcs) { if (dsa_order_whmcs_provisioning_complete($order)) { dsa_mark_order_fulfilled($pdo, $orderId, false); } return; } if (dsa_order_has_multiple_items($pdo, $orderId)) { return; } $fulfillmentType = (string) ($order['fulfillment_type'] ?? 'manual'); if (in_array($fulfillmentType, ['instant_link', 'license_key'], true)) { dsa_mark_order_fulfilled($pdo, $orderId, true); } } function dsa_mark_order_fulfilled(PDO $pdo, int $orderId, bool $sendEmail): array { $order = dsa_get_order_with_product($pdo, $orderId); if (!$order) { return ['ok' => false, 'message' => 'Order not found.']; } if (in_array((string) $order['status'], ['pending_payment', 'cancelled'], true)) { return ['ok' => false, 'message' => 'Order must be paid before fulfillment.']; } if (dsa_order_needs_whmcs_provisioning($order) && !dsa_order_whmcs_provisioning_complete($order)) { return ['ok' => false, 'message' => 'Service is not active on Watu yet. Fulfillment happens after provisioning completes.']; } $deliveryValue = trim((string) ($order['delivery_value'] ?? '')); $fulfillmentType = (string) ($order['fulfillment_type'] ?? 'manual'); if ($deliveryValue === '' && $fulfillmentType === 'instant_link') { $deliveryValue = trim((string) ($order['delivery_url'] ?? '')); } elseif ($deliveryValue === '' && $fulfillmentType === 'license_key') { $deliveryValue = dsa_assign_license_key($pdo, (int) $order['product_id']); if ($deliveryValue === '') { return ['ok' => false, 'message' => 'No available license key in pool for this product.']; } } $stmt = $pdo->prepare( "update orders set status = 'fulfilled', delivery_value = :delivery_value, updated_at = datetime('now') where id = :id" ); $stmt->execute([ ':id' => $orderId, ':delivery_value' => $deliveryValue, ]); $notified = dsa_send_order_notification_email($pdo, $orderId, 'order_fulfilled'); if ($sendEmail && !$notified) { dsa_send_fulfillment_email($pdo, $orderId); } return ['ok' => true, 'message' => 'Order fulfilled.']; } function dsa_send_fulfillment_email(PDO $pdo, int $orderId): bool { $order = dsa_get_order_with_product($pdo, $orderId); if (!$order || (string) $order['status'] !== 'fulfilled') { return false; } $brandTitle = dsa_setting($pdo, 'brand_title', DSA_APP_NAME); $emailLogoUrl = dsa_brand_logo_url($pdo); $emailPrimaryColor = trim(dsa_setting($pdo, 'email_primary_color', '#2563eb')); $emailFooterText = trim(dsa_setting($pdo, 'email_footer_text', 'Need help? Reply to this email and our team will assist.')); if (!preg_match('/^#[0-9a-fA-F]{6}$/', $emailPrimaryColor)) { $emailPrimaryColor = '#2563eb'; } $deliveryValue = trim((string) ($order['delivery_value'] ?? '')); $fulfillmentType = (string) ($order['fulfillment_type'] ?? 'manual'); $subject = 'Your digital order is fulfilled'; $bodyText = "Hi " . (string) $order['customer_name'] . ",\n\n" . "Your order #" . $orderId . " for " . (string) $order['product_name'] . " is fulfilled.\n"; if ($deliveryValue !== '') { if ($fulfillmentType === 'license_key') { $bodyText .= "Your license key: " . $deliveryValue . "\n"; } else { $bodyText .= "Delivery link: " . $deliveryValue . "\n"; } } else { $bodyText .= "Our team will share delivery details shortly.\n"; } $bodyText .= "\nRegards,\n" . $brandTitle; $bodyHtml = ''; $bodyHtml .= '
'; if ($emailLogoUrl !== '') { $bodyHtml .= '

' . dsa_h($brandTitle) . '

'; } $bodyHtml .= '

Your digital order is fulfilled

'; $bodyHtml .= '

Hi ' . dsa_h((string) $order['customer_name']) . ',

'; $bodyHtml .= '

Your order #' . dsa_h((string) $orderId) . ' for ' . dsa_h((string) $order['product_name']) . ' is fulfilled.

'; if ($deliveryValue !== '') { if ($fulfillmentType === 'license_key') { $bodyHtml .= '

Your license key: ' . dsa_h($deliveryValue) . '

'; } else { $bodyHtml .= '

Access your delivery

'; $bodyHtml .= '

If the button does not work, open this link: ' . dsa_h($deliveryValue) . '

'; } } else { $bodyHtml .= '

Our team will share delivery details shortly.

'; } if ($emailFooterText !== '') { $bodyHtml .= '
'; $bodyHtml .= '

' . nl2br(dsa_h($emailFooterText)) . '

'; } $bodyHtml .= '

Regards,
' . dsa_h($brandTitle) . '

'; $bodyHtml .= '
'; return dsa_send_mail((string) $order['customer_email'], $subject, $bodyText, $bodyHtml); } function dsa_notification_template_defaults(): array { return [ 'general.order_received' => [ 'category_key' => 'general', 'category_label' => 'General Messages', 'template_label' => 'Order Confirmation', 'trigger_key' => 'order_received', 'subject_template' => 'Order #{{order_id}} received - {{product_name}}', 'body_template' => "Hi {{customer_name}},\n\nWe've received your order #{{order_id}} for {{product_name}}.\nCurrent status: {{status}}.\n\nWe will notify you when payment and provisioning are completed.\n\nRegards,\n{{store_name}}", ], 'invoice.payment_received' => [ 'category_key' => 'invoice', 'category_label' => 'Invoice Messages', 'template_label' => 'Invoice Payment Confirmation', 'trigger_key' => 'payment_received', 'subject_template' => 'Payment received for order #{{order_id}}', 'body_template' => "Hi {{customer_name}},\n\nPayment has been received for order #{{order_id}} ({{product_name}}).\nPayment reference: {{payment_ref}}\nCurrent status: {{status}}.\n\nProvisioning is now in progress.\n\nRegards,\n{{store_name}}", ], 'invoice.renewal_due' => [ 'category_key' => 'invoice', 'category_label' => 'Invoice Messages', 'template_label' => 'Service Renewal Due', 'trigger_key' => 'renewal_due', 'subject_template' => 'Renewal due for {{product_name}} — {{renewal_amount}}', 'body_template' => "Hi {{customer_name}},\n\nYour {{product_name}} service is due for renewal on {{renewal_due_date}}.\n\nAmount due: {{renewal_amount}}\n\nPay now in your client area:\n{{renewal_pay_url}}\n\nRegards,\n{{store_name}}", ], 'product_service.fulfilled' => [ 'category_key' => 'product_service', 'category_label' => 'Product/Service Messages', 'template_label' => 'Hosting/Product Fulfilled', 'trigger_key' => 'order_fulfilled', 'subject_template' => 'Order #{{order_id}} fulfilled - {{product_name}}', 'body_template' => "Hi {{customer_name}},\n\nYour order #{{order_id}} for {{product_name}} is fulfilled.\nDetails: {{delivery_value}}\n\nRegards,\n{{store_name}}", ], 'domain.fulfilled' => [ 'category_key' => 'domain', 'category_label' => 'Domain Messages', 'template_label' => 'Domain Order Fulfilled', 'trigger_key' => 'order_fulfilled', 'subject_template' => 'Domain order #{{order_id}} fulfilled - {{product_name}}', 'body_template' => "Hi {{customer_name}},\n\nYour domain order #{{order_id}} for {{product_name}} is fulfilled.\nDetails: {{delivery_value}}\n\nRegards,\n{{store_name}}", ], ]; } function dsa_notification_templates(PDO $pdo): array { $defaults = dsa_notification_template_defaults(); $result = []; foreach ($defaults as $templateKey => $template) { $result[$templateKey] = array_merge($template, [ 'template_key' => $templateKey, 'enabled' => true, ]); } $syncedJson = trim((string) dsa_setting($pdo, 'notifications_synced_templates_json', '')); if ($syncedJson !== '') { $synced = json_decode($syncedJson, true); if (is_array($synced)) { foreach ($synced as $templateKey => $tpl) { if (!is_array($tpl) || !isset($result[$templateKey])) { continue; } if (trim((string) ($tpl['subject'] ?? '')) !== '') { $result[$templateKey]['subject_template'] = (string) $tpl['subject']; } if (trim((string) ($tpl['body'] ?? '')) !== '') { $result[$templateKey]['body_template'] = (string) $tpl['body']; } if (trim((string) ($tpl['template_label'] ?? '')) !== '') { $result[$templateKey]['template_label'] = (string) $tpl['template_label']; } if (trim((string) ($tpl['category_key'] ?? '')) !== '') { $result[$templateKey]['category_key'] = (string) $tpl['category_key']; } if (trim((string) ($tpl['category_label'] ?? '')) !== '') { $result[$templateKey]['category_label'] = (string) $tpl['category_label']; } $safeKey = preg_replace('/[^a-z0-9_\.]+/i', '_', (string) $templateKey) ?? ''; if ($safeKey === '') { continue; } $defaultEnabled = !empty($tpl['enabled']) ? '1' : '0'; $savedEnabled = trim((string) dsa_setting($pdo, 'notify_enabled_' . $safeKey, '')); if ($savedEnabled === '') { $savedEnabled = $defaultEnabled; dsa_setting_set($pdo, 'notify_enabled_' . $safeKey, $savedEnabled); } $result[$templateKey]['enabled'] = $savedEnabled === '1'; } } } return $result; } function dsa_save_notification_templates(PDO $pdo, array $post): void { $input = is_array($post['notifications'] ?? null) ? (array) $post['notifications'] : []; foreach (dsa_notification_template_defaults() as $templateKey => $_default) { $row = is_array($input[$templateKey] ?? null) ? (array) $input[$templateKey] : []; $safeKey = preg_replace('/[^a-z0-9_\.]+/i', '_', (string) $templateKey) ?? ''; if ($safeKey === '') { continue; } dsa_setting_set($pdo, 'notify_enabled_' . $safeKey, isset($row['enabled']) ? '1' : '0'); } } function dsa_notification_template_key_for_order(array $order, string $triggerKey): string { if ($triggerKey === 'order_received') { return 'general.order_received'; } if ($triggerKey === 'payment_received') { return 'invoice.payment_received'; } if ($triggerKey === 'renewal_due') { return 'invoice.renewal_due'; } if ($triggerKey === 'order_fulfilled') { $category = strtolower(trim((string) ($order['category'] ?? ''))); $name = strtolower(trim((string) ($order['product_name'] ?? ''))); if (strpos($category, 'domain') !== false || strpos($name, 'domain') !== false || preg_match('/\.[a-z]{2,20}/', $name)) { return 'domain.fulfilled'; } return 'product_service.fulfilled'; } return ''; } function dsa_notification_order_password_fallback(): string { return 'Use the password you set at checkout or reset via control panel'; } function dsa_notification_body_contains_html(string $body): bool { return preg_match('/<[a-z][\s\S]*>/i', $body) === 1; } function dsa_notification_plain_text_from_html(string $html): string { $text = preg_replace('/<(br|\/p|\/div|\/li|\/tr|\/h[1-6])\s*\/?>/i', "\n", $html) ?? $html; $text = strip_tags($text); $text = html_entity_decode($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $text = preg_replace("/[ \t]+\n/", "\n", $text) ?? $text; $text = preg_replace("/\n{3,}/", "\n\n", $text) ?? $text; return trim($text); } function dsa_notification_order_merge_replacements(PDO $pdo, array $order, array $service): array { $orderId = (int) ($order['id'] ?? 0); $storeName = trim((string) dsa_setting($pdo, 'brand_title', DSA_APP_NAME)); if ($storeName === '') { $storeName = DSA_APP_NAME; } $storeUrl = dsa_base_url($pdo); $signature = trim((string) dsa_setting($pdo, 'email_footer_text', '')); if ($signature === '') { $signature = "Regards,\n" . $storeName; } $customerName = (string) ($order['customer_name'] ?? 'Customer'); $customerEmail = (string) ($order['customer_email'] ?? ''); $productName = trim((string) ($order['product_name'] ?? '')); if ($productName === '') { $productName = trim((string) ($service['product_name'] ?? '')); } $password = trim((string) ($service['password'] ?? '')); if ($password === '') { $password = dsa_notification_order_password_fallback(); } $serverName = trim((string) ($service['server_name'] ?? '')); $serverHostname = trim((string) ($service['server_hostname'] ?? '')); $serverIp = trim((string) ($service['server_ip'] ?? '')); $serverDisplay = $serverName !== '' ? $serverName : $serverHostname; $whmcsOrderId = (int) ($order['whmcs_order_id'] ?? 0); $whmcsInvoiceId = (int) ($order['whmcs_invoice_id'] ?? 0); $whmcsServiceId = (int) ($order['whmcs_service_id'] ?? ($service['service_id'] ?? 0)); $deliveryValue = trim((string) ($order['delivery_value'] ?? '')); if ($deliveryValue === '' && count($service) > 0) { $parts = []; foreach ([ 'Domain' => (string) ($service['domain'] ?? ''), 'Username' => (string) ($service['username'] ?? ''), 'Server' => $serverDisplay, 'Server IP' => $serverIp !== '' ? $serverIp : (string) ($service['dedicated_ip'] ?? ''), 'Status' => (string) ($service['status'] ?? ''), 'Next Due Date' => (string) ($service['next_due_date'] ?? ''), ] as $label => $value) { $value = trim((string) $value); if ($value !== '') { $parts[] = $label . ': ' . $value; } } for ($i = 1; $i <= 4; $i++) { $ns = trim((string) ($service['ns' . $i] ?? ($service['nameserver' . $i] ?? ''))); if ($ns !== '') { $parts[] = 'NS' . $i . ': ' . $ns; } } if (count($parts) > 0) { $deliveryValue = implode("\n", $parts); } } $values = [ 'customer_name' => $customerName, 'client_name' => $customerName, 'clientname' => $customerName, 'customer_email' => $customerEmail, 'client_email' => $customerEmail, 'clientemail' => $customerEmail, 'product_name' => $productName, 'service_product_name' => trim((string) ($service['product_name'] ?? $productName)), 'order_id' => (string) $orderId, 'order_number' => (string) $orderId, 'whmcs_order_id' => $whmcsOrderId > 0 ? (string) $whmcsOrderId : '', 'invoice_id' => $whmcsInvoiceId > 0 ? (string) $whmcsInvoiceId : '', 'payment_ref' => (string) ($order['payment_ref'] ?? ''), 'status' => (string) ($order['status'] ?? ''), 'delivery_value' => $deliveryValue, 'service_domain' => (string) ($service['domain'] ?? ''), 'service_username' => (string) ($service['username'] ?? ''), 'service_password' => $password, 'service_server' => $serverDisplay, 'service_server_name' => $serverName, 'service_server_hostname' => $serverHostname, 'service_server_ip' => $serverIp !== '' ? $serverIp : trim((string) ($service['dedicated_ip'] ?? '')), 'service_dedicated_ip' => trim((string) ($service['dedicated_ip'] ?? '')), 'service_status' => (string) ($service['status'] ?? ''), 'service_next_due_date' => (string) ($service['next_due_date'] ?? ''), 'service_billing_cycle' => (string) ($service['billing_cycle'] ?? ''), 'service_recurring_amount' => (string) ($service['recurring_amount'] ?? ''), 'service_first_payment_amount' => (string) ($service['first_payment_amount'] ?? ''), 'service_payment_method' => (string) ($service['payment_method'] ?? ''), 'service_reg_date' => (string) ($service['registration_date'] ?? ''), 'service_registration_date' => (string) ($service['registration_date'] ?? ''), 'service_id' => $whmcsServiceId > 0 ? (string) $whmcsServiceId : trim((string) ($service['service_id'] ?? '')), 'store_name' => $storeName, 'company_name' => $storeName, 'companyname' => $storeName, 'company_link' => $storeUrl, 'companylink' => $storeUrl, 'signature' => $signature, ]; for ($i = 1; $i <= 4; $i++) { $ns = trim((string) ($service['ns' . $i] ?? ($service['nameserver' . $i] ?? ''))); $values['service_ns' . $i] = $ns; $values['nameserver' . $i] = $ns; } $replacements = []; foreach ($values as $key => $value) { $replacements['{{' . $key . '}}'] = $value; $replacements['{$' . $key . '}'] = $value; } return $replacements; } function dsa_send_order_notification_email(PDO $pdo, int $orderId, string $triggerKey): bool { $order = dsa_get_order_with_product($pdo, $orderId); if (!$order || !dsa_has_table($pdo, 'order_notification_events')) { return false; } if ((int) ($order['notify_opt_in'] ?? 1) !== 1) { return false; } $alreadyStmt = $pdo->prepare("select id from order_notification_events where order_id = :order_id and trigger_key = :trigger_key limit 1"); $alreadyStmt->execute([':order_id' => $orderId, ':trigger_key' => $triggerKey]); if ($alreadyStmt->fetch()) { return false; } $templateKey = dsa_notification_template_key_for_order($order, $triggerKey); if ($templateKey === '') { return false; } $templates = dsa_notification_templates($pdo); $tpl = $templates[$templateKey] ?? null; if (!is_array($tpl) || empty($tpl['enabled'])) { return false; } $service = dsa_order_service_details($order); $replacements = dsa_notification_order_merge_replacements($pdo, $order, $service); $storeName = (string) ($replacements['{{store_name}}'] ?? DSA_APP_NAME); $brandedSubject = dsa_notification_apply_branding($pdo, (string) ($tpl['subject_template'] ?? '')); $brandedBody = dsa_notification_apply_branding($pdo, (string) ($tpl['body_template'] ?? '')); $subject = strtr($brandedSubject, $replacements); $bodyText = strtr($brandedBody, $replacements); if (trim($subject) === '' || trim($bodyText) === '') { return false; } $logoUrl = dsa_brand_logo_url($pdo); $storeUrl = dsa_base_url($pdo); $headerLogo = $logoUrl !== '' ? '
' . dsa_h($storeName) . '
' : '

' . dsa_h($storeName) . '

'; if (dsa_notification_body_contains_html($bodyText)) { $htmlReplacements = []; foreach ($replacements as $token => $value) { $htmlReplacements[$token] = htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } $bodyInner = strtr($brandedBody, $htmlReplacements); $plainBody = dsa_notification_plain_text_from_html($bodyText); } else { $bodyInner = nl2br(dsa_h($bodyText)); $plainBody = $bodyText; } $bodyHtml = '' . '
' . $headerLogo . $bodyInner . '

' . dsa_h($storeUrl) . '

' . '
'; $sent = dsa_send_mail((string) ($order['customer_email'] ?? ''), $subject, $plainBody, $bodyHtml); if ($sent) { $mark = $pdo->prepare( "insert into order_notification_events (order_id, trigger_key, sent_at) values (:order_id, :trigger_key, datetime('now')) on conflict(order_id, trigger_key) do nothing" ); $mark->execute([':order_id' => $orderId, ':trigger_key' => $triggerKey]); } return $sent; } function dsa_send_renewal_invoice_notification(PDO $pdo, int $sourceOrderId, int $whmcsInvoiceId, string $dueDate, int $retailCents, string $currency): bool { if ($sourceOrderId <= 0 || $whmcsInvoiceId <= 0 || !dsa_has_table($pdo, 'order_notification_events')) { return false; } $triggerKey = 'renewal_invoice_' . $whmcsInvoiceId; $alreadyStmt = $pdo->prepare("select id from order_notification_events where order_id = :order_id and trigger_key = :trigger_key limit 1"); $alreadyStmt->execute([':order_id' => $sourceOrderId, ':trigger_key' => $triggerKey]); if ($alreadyStmt->fetch()) { return false; } $order = dsa_get_order_with_product($pdo, $sourceOrderId); if (!$order || (int) ($order['notify_opt_in'] ?? 1) !== 1) { return false; } $templates = dsa_notification_templates($pdo); $tpl = $templates['invoice.renewal_due'] ?? null; if (!is_array($tpl) || empty($tpl['enabled'])) { return false; } $service = dsa_order_service_details($order); $replacements = dsa_notification_order_merge_replacements($pdo, $order, $service); $storeUrl = rtrim((string) ($replacements['{{company_link}}'] ?? dsa_base_url($pdo)), '/'); $renewalAmount = dsa_money(max(0, $retailCents), $currency); $payUrl = $storeUrl . '/client-area/billing/invoice/' . $whmcsInvoiceId . '?order=' . $sourceOrderId; $replacements['{{renewal_amount}}'] = $renewalAmount; $replacements['{{renewal_due_date}}'] = $dueDate !== '' ? $dueDate : '-'; $replacements['{{renewal_pay_url}}'] = $payUrl; $replacements['{{whmcs_invoice_id}}'] = (string) $whmcsInvoiceId; $storeName = (string) ($replacements['{{store_name}}'] ?? DSA_APP_NAME); $brandedSubject = dsa_notification_apply_branding($pdo, (string) ($tpl['subject_template'] ?? '')); $brandedBody = dsa_notification_apply_branding($pdo, (string) ($tpl['body_template'] ?? '')); $subject = strtr($brandedSubject, $replacements); $bodyText = strtr($brandedBody, $replacements); if (trim($subject) === '' || trim($bodyText) === '') { return false; } $logoUrl = dsa_brand_logo_url($pdo); $headerLogo = $logoUrl !== '' ? '
' . dsa_h($storeName) . '
' : '

' . dsa_h($storeName) . '

'; $bodyHtml = '' . '
' . $headerLogo . nl2br(dsa_h($bodyText)) . '

' . dsa_h($storeUrl) . '

' . '
'; $sent = dsa_send_mail((string) ($order['customer_email'] ?? ''), $subject, $bodyText, $bodyHtml); if ($sent) { $mark = $pdo->prepare( "insert into order_notification_events (order_id, trigger_key, sent_at) values (:order_id, :trigger_key, datetime('now')) on conflict(order_id, trigger_key) do nothing" ); $mark->execute([':order_id' => $sourceOrderId, ':trigger_key' => $triggerKey]); } return $sent; } function dsa_notification_apply_branding(PDO $pdo, string $template): string { $storeName = trim((string) dsa_setting($pdo, 'brand_title', DSA_APP_NAME)); if ($storeName === '') { $storeName = DSA_APP_NAME; } $storeUrl = dsa_base_url($pdo); $logoUrl = dsa_brand_logo_url($pdo); $replacements = [ '{$company_name}' => $storeName, '{$companyname}' => $storeName, '{{company_name}}' => $storeName, '{$company_link}' => $storeUrl, '{$companylink}' => $storeUrl, '{{company_link}}' => $storeUrl, '{$logo}' => $logoUrl, '{{logo}}' => $logoUrl, ]; $text = strtr($template, $replacements); $text = preg_replace('#https?://(www\.)?watutechnology\.com[^\s"\']*#i', $storeUrl, $text) ?? $text; $text = preg_replace('#https?://[^/\s]+/(support|web-portal|clientarea)[^\s"\']*#i', $storeUrl, $text) ?? $text; return $text; } function dsa_render_client_profile(PDO $pdo, string $error, string $notice): void { $clientId = (int) ($_SESSION['dsa_client_id'] ?? 0); dsa_release_session_lock_if_safe(); if ($clientId <= 0) { dsa_render_client_login('Please login to view your profile.'); return; } $stmt = $pdo->prepare("select id, name, email, phone from customers where id = :id limit 1"); $stmt->execute([':id' => $clientId]); $customer = $stmt->fetch(); if (!$customer) { dsa_render_client_login('Session expired. Please login again.'); return; } $html = '

My Profile

'; if ($error !== '') { $html .= '

' . dsa_h($error) . '

'; } if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } $html .= '
'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '
'; $html .= '

Change Password

Leave blank to keep your current password.

'; $html .= ''; $html .= ''; $html .= ''; $html .= '
'; dsa_render_layout('My Profile', $html); } function dsa_render_client_forgot_password(string $error, string $notice): void { $html = '

Forgot Password

'; if ($error !== '') { $html .= '

' . dsa_h($error) . '

'; } if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } $html .= '

Enter your client area email to receive a password reset link.

'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '

Back to login

'; $html .= '
'; dsa_render_layout('Forgot password', $html); } function dsa_render_client_reset_password(string $token, string $error, string $notice): void { $html = '

Reset Password

'; if ($error !== '') { $html .= '

' . dsa_h($error) . '

'; } if ($notice !== '') { $html .= '

' . dsa_h($notice) . '

'; } if ($token === '') { $html .= '

Request a new reset link from the forgot password page.

'; $html .= '

Go to forgot password

'; $html .= '
'; dsa_render_layout('Reset password', $html); return; } $html .= '
'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '
'; $html .= ''; dsa_render_layout('Reset password', $html); } function dsa_base_url(?PDO $pdo = null): string { $configuredDomain = ''; if ($pdo instanceof PDO) { $configuredDomain = dsa_normalize_domain((string) dsa_setting($pdo, 'whitelabel_domain', '')); } if ($configuredDomain !== '') { return 'https://' . $configuredDomain; } $scheme = dsa_request_scheme(); $host = dsa_normalize_domain((string) ($_SERVER['HTTP_HOST'] ?? '')); if ($host === '') { return 'https://dsa-demo.launch-pad.biz'; } return $scheme . '://' . $host; } function dsa_issue_client_password_reset(PDO $pdo, string $email): void { if ($email === '') { return; } $stmt = $pdo->prepare("select id, email from customers where email = :email limit 1"); $stmt->execute([':email' => $email]); $customer = $stmt->fetch(); if (!$customer) { return; } $token = bin2hex(random_bytes(24)); $tokenHash = hash('sha256', $token); $ins = $pdo->prepare( "insert into customer_password_resets (customer_id, token_hash, expires_at, used_at, created_at) values (:customer_id, :token_hash, datetime('now', '+30 minutes'), null, datetime('now'))" ); $ins->execute([ ':customer_id' => (int) $customer['id'], ':token_hash' => $tokenHash, ]); $resetUrl = dsa_base_url($pdo) . '/client-area/reset-password?token=' . urlencode($token); $subject = 'Reset your client area password'; $text = "We received a request to reset your client area password.\n\n" . "Reset link (valid for 30 minutes): " . $resetUrl . "\n\n" . "If you did not request this, you can ignore this email."; $html = '' . '
' . '

Reset your client area password

' . '

Use the button below to reset your password. This link expires in 30 minutes.

' . '

Reset Password

' . '

If you did not request this, you can ignore this email.

' . '
'; dsa_send_mail((string) $customer['email'], $subject, $text, $html); } function dsa_is_valid_client_reset_token(PDO $pdo, string $token): bool { if ($token === '') { return false; } $tokenHash = hash('sha256', $token); $stmt = $pdo->prepare( "select id from customer_password_resets where token_hash = :token_hash and used_at is null and expires_at >= datetime('now') limit 1" ); $stmt->execute([':token_hash' => $tokenHash]); return (bool) $stmt->fetch(); } function dsa_apply_client_password_reset(PDO $pdo, string $token, string $newPassword): bool { if ($token === '') { return false; } $tokenHash = hash('sha256', $token); $stmt = $pdo->prepare( "select id, customer_id from customer_password_resets where token_hash = :token_hash and used_at is null and expires_at >= datetime('now') limit 1" ); $stmt->execute([':token_hash' => $tokenHash]); $row = $stmt->fetch(); if (!$row) { return false; } $updateCustomer = $pdo->prepare("update customers set password_hash = :hash, updated_at = datetime('now') where id = :id"); $updateCustomer->execute([ ':id' => (int) $row['customer_id'], ':hash' => password_hash($newPassword, PASSWORD_DEFAULT), ]); $markUsed = $pdo->prepare("update customer_password_resets set used_at = datetime('now') where id = :id"); $markUsed->execute([':id' => (int) $row['id']]); return true; } function dsa_issue_admin_password_reset(PDO $pdo, string $username): void { if ($username === '') { return; } $stmt = $pdo->prepare("select id, username, email from admin_users where username = :username limit 1"); $stmt->execute([':username' => $username]); $admin = $stmt->fetch(); if (!$admin) { return; } $token = bin2hex(random_bytes(24)); $tokenHash = hash('sha256', $token); $ins = $pdo->prepare( "insert into admin_password_resets (admin_user_id, token_hash, expires_at, used_at, created_at) values (:admin_user_id, :token_hash, datetime('now', '+30 minutes'), null, datetime('now'))" ); $ins->execute([ ':admin_user_id' => (int) $admin['id'], ':token_hash' => $tokenHash, ]); $resetUrl = dsa_base_url($pdo) . '/admin/reset-password?token=' . urlencode($token); $recipient = trim((string) ($admin['email'] ?? '')); if (!filter_var($recipient, FILTER_VALIDATE_EMAIL)) { $configuredRecipient = trim((string) dsa_setting($pdo, 'admin_password_reset_email', '')); $recipient = filter_var($configuredRecipient, FILTER_VALIDATE_EMAIL) ? $configuredRecipient : ''; } $sentByEmail = false; if ($recipient !== '') { $subject = 'Reset your admin password'; $text = "We received a request to reset an admin password.\n\n" . "Reset link (valid for 30 minutes): " . $resetUrl . "\n\n" . "If you did not request this, you can ignore this email."; $html = '' . '
' . '

Reset your admin password

' . '

Use the button below to reset your password. This link expires in 30 minutes.

' . '

Reset Password

' . '

If you did not request this, you can ignore this email.

' . '
'; $sentByEmail = dsa_send_mail($recipient, $subject, $text, $html); } if (!$sentByEmail) { $opsLogPath = __DIR__ . '/storage/admin_password_reset_requests.log'; $entry = [ 'time' => date('c'), 'username' => (string) ($admin['username'] ?? ''), 'expires_at' => date('c', time() + 1800), 'reset_url' => $resetUrl, ]; @file_put_contents($opsLogPath, json_encode($entry, JSON_UNESCAPED_SLASHES) . PHP_EOL, FILE_APPEND); } dsa_audit_log('admin_password_reset_requested', [ 'username' => (string) ($admin['username'] ?? ''), 'delivery' => $sentByEmail ? 'email' : 'ops_log', ]); } function dsa_is_valid_admin_reset_token(PDO $pdo, string $token): bool { if ($token === '') { return false; } $tokenHash = hash('sha256', $token); $stmt = $pdo->prepare( "select id from admin_password_resets where token_hash = :token_hash and used_at is null and expires_at >= datetime('now') limit 1" ); $stmt->execute([':token_hash' => $tokenHash]); return (bool) $stmt->fetch(); } function dsa_apply_admin_password_reset(PDO $pdo, string $token, string $newPassword): bool { if ($token === '') { return false; } $tokenHash = hash('sha256', $token); $stmt = $pdo->prepare( "select id, admin_user_id from admin_password_resets where token_hash = :token_hash and used_at is null and expires_at >= datetime('now') limit 1" ); $stmt->execute([':token_hash' => $tokenHash]); $row = $stmt->fetch(); if (!$row) { return false; } $pdo->beginTransaction(); try { $updateAdmin = $pdo->prepare("update admin_users set password_hash = :hash, updated_at = datetime('now') where id = :id"); $updateAdmin->execute([ ':id' => (int) $row['admin_user_id'], ':hash' => password_hash($newPassword, PASSWORD_DEFAULT), ]); $markUsed = $pdo->prepare("update admin_password_resets set used_at = datetime('now') where id = :id and used_at is null"); $markUsed->execute([':id' => (int) $row['id']]); if ($markUsed->rowCount() !== 1) { $pdo->rollBack(); return false; } $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } return false; } dsa_audit_log('admin_password_reset_completed', [ 'admin_user_id' => (int) $row['admin_user_id'], ]); return true; } function dsa_build_order_timeline(array $order): array { $status = (string) ($order['status'] ?? 'pending_payment'); $dispatchStatus = (string) ($order['whmcs_dispatch_status'] ?? ''); $clientStatusLabel = dsa_customer_order_status_label($order); $paymentConfirmed = in_array($status, ['paid', 'fulfilled'], true) && $clientStatusLabel !== 'payment_submitted'; $serviceReady = $status === 'fulfilled' && dsa_order_whmcs_provisioning_complete($order); if (!$serviceReady && $status === 'fulfilled' && !dsa_order_needs_whmcs_provisioning($order)) { $serviceReady = true; } if ($status === 'demo_simulated') { return [ ['label' => 'Order received', 'done' => true], ['label' => 'Legacy demo order (no payment)', 'done' => true], ]; } if ($dispatchStatus === 'demo_paid_no_provision') { return [ ['label' => 'Order received', 'done' => true], ['label' => 'Watu Technology M-PESA payment received', 'done' => true], ['label' => 'Demo store — no service provisioned (refund within 72 business hours if paid in error)', 'done' => true], ]; } return [ ['label' => 'Order received', 'done' => true], ['label' => 'Payment received', 'done' => $paymentConfirmed], ['label' => 'Setting up your service', 'done' => $serviceReady], ['label' => 'Service ready', 'done' => $serviceReady], ]; }