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 .= '
';
} else {
$html .= '
';
$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 .= '
';
}
$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 .= '
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 .= '
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 .= '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 .= '
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 = '';
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 .= '
| Order | Product | Customer | Amount | Status | Updated |
';
foreach ($recentActivityOrders as $ra) {
$raStatus = (string) ($ra['status'] ?? 'pending_payment');
$html .= ''
. '| #' . 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 .= '
';
}
$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 .= '
'; // 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 .= '
';
$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 .= '- Create/update a CNAME from
' . dsa_h($domainStatusValue) . ' to ' . dsa_h($cnameTargetValue) . '. ';
} elseif ($expectedIpValue !== '') {
$html .= '- Create/update an A record for
' . dsa_h($domainStatusValue) . ' to ' . dsa_h($expectedIpValue) . '. ';
} else {
$html .= '- Create/update an A record for
' . dsa_h($domainStatusValue) . ' to your tenant server IP. ';
}
$html .= '- Wait for DNS propagation, then click Check Status or save again.
';
$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 .= '
| Username | Email | Role | Added |
';
foreach ($visibleAdmins as $adminRow) {
$html .= '| ' . 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'] ?? '')) . ' |
';
}
$html .= '
';
}
if (dsa_multiple_admins_tier_allowed($pdo)) {
$html .= '
';
} else {
$tierUpgradeUrl = dsa_tier_upgrade_url($pdo);
$html .= '
';
}
} 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 .= '';
}
// 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 .= '| Feature | Status | Expires | Activated |
';
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 .= '| ' . dsa_h((string) ($ek['feature'] ?? '')) . ' | '
. '' . $statusPill . ' | '
. '' . dsa_h((string) ($ek['expires_at'] ?? '')) . ' | '
. '' . dsa_h((string) ($ek['redeemed_at'] ?? '')) . ' |
';
}
$html .= '
';
}
$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 .= '- Customer Orders: Your customer places an order on your storefront and pays you directly.
';
$html .= '- 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 .= '
';
$html .= '- Order Delivered: Once the invoice is paid (automatically or manually), the products are delivered to your customer.
';
$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 .= '
';
// Append the separate Payouts Panel container outside the payments configuration form
$html .= '
';
$html .= dsa_admin_payouts_section_html($pdo);
$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 .= '
';
$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 .= '
';
$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 .= '
| Product | Category | Provisioning | Buy Cost | Sell Price | Status | Action |
';
$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 .= '' . dsa_h((string) $row['name']) . ' /' . dsa_h((string) $row['slug']) . ' ' . dsa_h(dsa_truncate_text((string) $row['description'], 120)) . ' | ';
$html .= '' . dsa_h((string) ($row['category'] ?? 'General')) . ' | ';
$html .= '' . dsa_h($provBadge['label']) . ' | ';
// 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 .= '' . $buyCostHtml . ' | ';
$marginColorStyle = $margin >= 0 ? 'color:#10b981;' : 'color:#f87171;';
$marginLabel = 'Profit: ' . dsa_money($margin, $storeCurrency);
$html .= ' | ';
$html .= ' | ';
$html .= ' |
';
}
if (count($catalog) === 0) {
$html .= '| No catalog products match the current filters. |
';
}
$html .= '
';
$html .= '
';
// Bulk Pricing section
$html .= '
'
. '
'
. '
Bulk Pricing
'
. 'One-Time Batch Run'
. ''
. '
Apply immediate price modifications to existing catalog items matching your selected Scope.
'
. '
';
$html .= '
';
// Card 1: Store Currency
$html .= '
';
$html .= '
Store Currency
';
$html .= '
Catalog prices sync in this currency. Re-syncs after changing.
';
$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 .= '
'; /* 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 .= '';
$html .= 'Enabled Products
';
$html .= '
| ID | Name | Source | Category | Buy Cost | Sell Price | Status |
';
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 .= '| ' . 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']) . ' |
';
}
if (count($products) === 0) {
$html .= '| No enabled products yet. Use Catalog or Custom Services. |
';
}
$html .= '
';
}
if ($section === 'orders') {
$html .= 'Orders
';
$html .= '
';
$html .= '
Showing ' . dsa_h((string) count($orders)) . ' of ' . dsa_h((string) $ordersTotal) . ' orders (page ' . dsa_h((string) $ordersPage) . ' of ' . dsa_h((string) $ordersPages) . ')
';
$html .= '
| ID | Product | Customer | Amount | Status | ' . dsa_h(dsa_ui_label('orders.lifecycle_dispatch')) . ' | ' . dsa_h(dsa_ui_label('orders.lifecycle_billing')) . ' | ' . dsa_h(dsa_ui_label('orders.lifecycle_provisioning')) . ' | Proof | Manage |
';
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 .= '| ' . 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 . ' |
';
}
if (count($orders) === 0) {
$html .= '| ' . dsa_empty_state('No orders found', 'Try a different filter or search term.') . ' |
';
}
$html .= '
';
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 .= '
'
. '
'
. '
'
. '
'
. '
'
. '
';
$html .= '
Showing ' . dsa_h((string) count($customers)) . ' customers.
';
$html .= '
| Customer | Orders | Lifetime | Last Order | Actions |
';
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 .= '' . 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'
. ''
. ''
. ''
. ''
. ''
. ' |
';
}
if (count($customers) === 0) {
$html .= '| No customers found for current filters. |
';
}
$html .= '
';
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 .= '
| Order | Product | Amount | Status | Payment | Created |
';
foreach ($focusCustomerOrders as $customerOrder) {
$html .= '| #' . 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']) . ' |
';
}
if (count($focusCustomerOrders) === 0) {
$html .= '| No orders yet. |
';
}
$html .= '
';
}
}
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
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 .= '
| Order | Product | Amount | Status | Actions |
';
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 .= '| #' . 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 . ' |
';
}
$html .= '
';
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 .= '
| Product | Order | Status | Last update | Actions |
';
foreach ($products as $row) {
$name = trim((string) ($row['product_name'] ?? ''));
if ($name === '') {
$name = 'Product';
}
$html .= '' . 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 .= '
';
}
$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 .= '
| Ticket | Subject | Order | Status | Last update | |
';
foreach ($ticketRows as $ticket) {
$ticketId = (int) ($ticket['id'] ?? 0);
$orderRef = (int) ($ticket['order_id'] ?? 0) > 0 ? '#' . (int) $ticket['order_id'] : '—';
$html .= '| #' . 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 .= '
';
$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 ($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 .= '
';
} 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 .= '
';
}
}
$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 .= '';
} 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 .= '
 . ')
';
}
$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) . '
';
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 = ''
. '';
$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) . '
';
$bodyHtml = ''
. '';
$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 = '';
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],
];
}