diff --git a/api/place_order.php b/api/place_order.php
index 0e2e05f..60f9e71 100644
--- a/api/place_order.php
+++ b/api/place_order.php
@@ -17,6 +17,15 @@ $name = trim($input['name'] ?? '');
$phoneInput = trim($input['phone'] ?? '');
$phone = normalize_oman_phone($phoneInput);
$address = trim($input['address'] ?? '');
+$paymentMethod = trim((string) ($input['payment_method'] ?? 'pay_later'));
+if (!in_array($paymentMethod, ['pay_later', 'pay_online'], true)) {
+ echo json_encode(['success' => false, 'error' => 'Invalid payment method']);
+ exit;
+}
+if ($paymentMethod === 'pay_online' && !thawani_is_configured()) {
+ echo json_encode(['success' => false, 'error' => 'Thawani payment is not configured']);
+ exit;
+}
if ($name === '' || $phoneInput === '' || $address === '') {
echo json_encode(['success' => false, 'error' => 'Missing customer details']);
@@ -75,7 +84,10 @@ $totalAmount = $subtotal + $totalVat;
try {
$db->beginTransaction();
- $stmt = $db->prepare("INSERT INTO online_orders (customer_name, customer_phone, customer_address, items_json, subtotal, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)");
+ $paymentGateway = $paymentMethod === 'pay_online' ? 'thawani' : null;
+ $paymentStatus = $paymentMethod === 'pay_online' ? 'pending' : 'unpaid';
+
+ $stmt = $db->prepare("INSERT INTO online_orders (customer_name, customer_phone, customer_address, items_json, subtotal, vat_amount, total_amount, payment_method, payment_gateway, payment_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$name,
$phone,
@@ -83,28 +95,37 @@ try {
json_encode($processedItems, JSON_UNESCAPED_UNICODE),
$subtotal,
$totalVat,
- $totalAmount
+ $totalAmount,
+ $paymentMethod,
+ $paymentGateway,
+ $paymentStatus
]);
$orderId = (int) $db->lastInsertId();
sync_online_order_stock_reservation([], 'rejected', $processedItems, 'pending');
- $db->commit();
-
- // Optional: send telegram and WhatsApp notifications if configured
- try {
- $orderData = [
- 'id' => $orderId,
+
+ $checkoutUrl = null;
+ if ($paymentMethod === 'pay_online') {
+ $thawaniResult = thawani_create_checkout_session($orderId, [
'customer_name' => $name,
'customer_phone' => $phone,
'customer_address' => $address,
'items' => $processedItems,
- 'subtotal' => $subtotal,
- 'vat_amount' => $totalVat,
- 'total_amount' => $totalAmount,
- 'status' => 'pending',
- 'created_at' => date('Y-m-d H:i:s'),
- ];
+ ]);
+ if (empty($thawaniResult['success'])) {
+ throw new RuntimeException((string) ($thawaniResult['error'] ?? 'Unable to create Thawani session'));
+ }
+ $checkoutUrl = (string) ($thawaniResult['checkout_url'] ?? '');
+ $sessionId = (string) ($thawaniResult['session_id'] ?? '');
+ $updateStmt = $db->prepare("UPDATE online_orders SET gateway_session_id = ? WHERE id = ?");
+ $updateStmt->execute([$sessionId, $orderId]);
+ }
+
+ $db->commit();
+
+ // Optional: send Telegram admin notice and customer WhatsApp notification.
+ try {
$msg = "🛒 *New Online Order #{$orderId}*
";
@@ -136,15 +157,19 @@ try {
$context = stream_context_create($options);
@file_get_contents($url, false, $context);
}
-
- if (wablas_is_configured()) {
- wablas_notify_online_order($orderData, 'created');
- }
- } catch (Exception $e) {
- // ignore notification errors
+ } catch (Throwable $e) {
+ error_log('Telegram notify failed for online order #' . $orderId . ': ' . $e->getMessage());
}
- echo json_encode(['success' => true]);
+ if ($paymentMethod === 'pay_later' && wablas_is_configured()) {
+ try {
+ wablas_notify_online_order_by_id($orderId, 'created');
+ } catch (Throwable $e) {
+ error_log('Customer WhatsApp notify failed for online order #' . $orderId . ': ' . $e->getMessage());
+ }
+ }
+
+ echo json_encode(['success' => true, 'redirect_url' => $checkoutUrl]);
} catch (Throwable $e) {
if ($db->inTransaction()) {
$db->rollBack();
diff --git a/api/settings.php b/api/settings.php
index e881ae1..7d63f7f 100644
--- a/api/settings.php
+++ b/api/settings.php
@@ -19,7 +19,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'wablas_daily_auto_send', 'wablas_daily_auto_time', 'wablas_daily_auto_last_date',
'wablas_template_invoice', 'wablas_template_daily_report',
'wablas_template_created', 'wablas_template_pending', 'wablas_template_accepted', 'wablas_template_completed', 'wablas_template_rejected',
- 'smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_secure', 'mail_from', 'mail_from_name'
+ 'smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_secure', 'mail_from', 'mail_from_name',
+ 'thawani_enabled', 'thawani_mode', 'thawani_publishable_key', 'thawani_secret_key', 'thawani_success_url', 'thawani_cancel_url'
];
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
@@ -51,6 +52,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['wablas_daily_auto_send'])) {
$_POST['wablas_daily_auto_send'] = '0';
}
+ if (!isset($_POST['thawani_enabled'])) {
+ $_POST['thawani_enabled'] = '0';
+ }
+ $thawaniMode = strtolower(trim((string) ($_POST['thawani_mode'] ?? 'sandbox')));
+ $_POST['thawani_mode'] = in_array($thawaniMode, ['sandbox', 'live'], true) ? $thawaniMode : 'sandbox';
unset($_POST['wablas_daily_auto_last_date']);
foreach ($keys as $key) {
diff --git a/edit_online_order.php b/edit_online_order.php
index 2543036..3ea5dc0 100644
--- a/edit_online_order.php
+++ b/edit_online_order.php
@@ -385,6 +385,14 @@ require __DIR__ . '/includes/header.php';
>= h(tr('مرفوض', 'Rejected')) ?>
+
+
= h(tr('الدفع', 'Payment')) ?>
+
= h(online_payment_method_label((string) ($editOrder['payment_method'] ?? 'pay_later'))) ?>
+
= h(online_payment_status_label((string) ($editOrder['payment_status'] ?? 'unpaid'))) ?>
+
+
= h(tr('جلسة ثواني:', 'Thawani session:')) ?> = h($editOrder['gateway_session_id']) ?>
+
+
diff --git a/includes/app.php b/includes/app.php
index 3858ee8..19735fb 100644
--- a/includes/app.php
+++ b/includes/app.php
@@ -121,6 +121,41 @@ try {
@file_put_contents($flagFileV7, '1');
}
+
+ $flagFileV8 = sys_get_temp_dir() . '/.schema_migrated_v8_' . md5(__DIR__);
+ if (!file_exists($flagFileV8)) {
+ $pdo = db();
+ $hasOnlineOrdersTable = (bool) $pdo->query("SHOW TABLES LIKE 'online_orders'")->fetchColumn();
+ if ($hasOnlineOrdersTable) {
+ $requiredColumns = [
+ 'payment_method' => "ALTER TABLE online_orders ADD COLUMN payment_method varchar(30) NOT NULL DEFAULT 'pay_later' AFTER total_amount",
+ 'payment_gateway' => "ALTER TABLE online_orders ADD COLUMN payment_gateway varchar(30) DEFAULT NULL AFTER payment_method",
+ 'payment_status' => "ALTER TABLE online_orders ADD COLUMN payment_status varchar(20) NOT NULL DEFAULT 'unpaid' AFTER payment_gateway",
+ 'gateway_session_id' => "ALTER TABLE online_orders ADD COLUMN gateway_session_id varchar(120) DEFAULT NULL AFTER payment_status",
+ 'gateway_transaction_id' => "ALTER TABLE online_orders ADD COLUMN gateway_transaction_id varchar(120) DEFAULT NULL AFTER gateway_session_id",
+ 'paid_at' => "ALTER TABLE online_orders ADD COLUMN paid_at datetime DEFAULT NULL AFTER gateway_transaction_id",
+ ];
+ foreach ($requiredColumns as $column => $sql) {
+ $exists = $pdo->query("SHOW COLUMNS FROM online_orders LIKE " . $pdo->quote($column))->fetchColumn();
+ if (!$exists) {
+ $pdo->exec($sql);
+ }
+ }
+
+ $pdo->exec("UPDATE online_orders SET payment_method = 'pay_later' WHERE payment_method IS NULL OR payment_method = ''");
+ $pdo->exec("UPDATE online_orders SET payment_status = CASE WHEN payment_method = 'pay_online' THEN 'pending' ELSE 'unpaid' END WHERE payment_status IS NULL OR payment_status = ''");
+ }
+
+ $pdo->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
+ ('thawani_enabled', '0'),
+ ('thawani_mode', 'sandbox'),
+ ('thawani_publishable_key', ''),
+ ('thawani_secret_key', ''),
+ ('thawani_success_url', ''),
+ ('thawani_cancel_url', '')");
+
+ @file_put_contents($flagFileV8, '1');
+ }
} catch (\Throwable $e) {}
@@ -308,6 +343,73 @@ function url_for(string $path, array $params = []): string
return $path . ($query ? ('?' . $query) : '');
}
+function request_scheme(): string
+{
+ $https = (string) ($_SERVER['HTTPS'] ?? '');
+ if ($https !== '' && strtolower($https) !== 'off') {
+ return 'https';
+ }
+
+ $forwardedProto = trim((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
+ if ($forwardedProto !== '') {
+ return strtolower(explode(',', $forwardedProto)[0]);
+ }
+
+ return 'http';
+}
+
+function app_base_url(): string
+{
+ $host = trim((string) ($_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'] ?? ''));
+ if ($host === '') {
+ $host = '127.0.0.1';
+ }
+
+ return request_scheme() . '://' . $host;
+}
+
+function append_query_params(string $url, array $params): string
+{
+ $parts = parse_url($url);
+ $queryParams = [];
+ if (!empty($parts['query'])) {
+ parse_str($parts['query'], $queryParams);
+ }
+ foreach ($params as $key => $value) {
+ if ($value === null || $value === '') {
+ continue;
+ }
+ $queryParams[$key] = $value;
+ }
+
+ $rebuilt = '';
+ if (!empty($parts['scheme'])) {
+ $rebuilt .= $parts['scheme'] . '://';
+ }
+ if (!empty($parts['user'])) {
+ $rebuilt .= $parts['user'];
+ if (!empty($parts['pass'])) {
+ $rebuilt .= ':' . $parts['pass'];
+ }
+ $rebuilt .= '@';
+ }
+ if (!empty($parts['host'])) {
+ $rebuilt .= $parts['host'];
+ }
+ if (!empty($parts['port'])) {
+ $rebuilt .= ':' . $parts['port'];
+ }
+ $rebuilt .= $parts['path'] ?? '';
+ if ($queryParams !== []) {
+ $rebuilt .= '?' . http_build_query($queryParams);
+ }
+ if (!empty($parts['fragment'])) {
+ $rebuilt .= '#' . $parts['fragment'];
+ }
+
+ return $rebuilt;
+}
+
function redirect_to(string $path, array $params = []): void
{
header('Location: ' . url_for($path, $params));
@@ -597,6 +699,236 @@ function payment_status_badge_class(string $status): string
};
}
+function online_payment_method_label(string $method): string
+{
+ return match ($method) {
+ 'pay_online' => tr('ادفع أونلاين', 'Pay Online'),
+ default => tr('ادفع لاحقاً', 'Pay Later'),
+ };
+}
+
+function online_payment_status_label(string $status): string
+{
+ return match ($status) {
+ 'paid' => tr('مدفوع', 'Paid'),
+ 'pending' => tr('بانتظار الدفع', 'Awaiting Payment'),
+ 'failed' => tr('فشل الدفع', 'Payment Failed'),
+ 'cancelled' => tr('تم الإلغاء', 'Cancelled'),
+ default => tr('غير مدفوع', 'Unpaid'),
+ };
+}
+
+function online_payment_status_badge_class(string $status): string
+{
+ return match ($status) {
+ 'paid' => 'bg-success text-white',
+ 'pending' => 'bg-warning text-dark',
+ 'failed' => 'bg-danger text-white',
+ 'cancelled' => 'bg-secondary text-white',
+ default => 'bg-danger-subtle text-danger-emphasis',
+ };
+}
+
+function thawani_is_enabled(): bool
+{
+ return (string) get_setting('thawani_enabled', '0') === '1';
+}
+
+function thawani_mode(): string
+{
+ $mode = strtolower(trim((string) get_setting('thawani_mode', 'sandbox')));
+ return in_array($mode, ['sandbox', 'live'], true) ? $mode : 'sandbox';
+}
+
+function thawani_checkout_base_url(): string
+{
+ return thawani_mode() === 'live' ? 'https://checkout.thawani.om' : 'https://uatcheckout.thawani.om';
+}
+
+function thawani_api_key(): string
+{
+ return trim((string) get_setting('thawani_secret_key', ''));
+}
+
+function thawani_publishable_key(): string
+{
+ return trim((string) get_setting('thawani_publishable_key', ''));
+}
+
+function thawani_is_configured(): bool
+{
+ return thawani_is_enabled() && thawani_api_key() !== '' && thawani_publishable_key() !== '';
+}
+
+function thawani_default_return_url(string $result): string
+{
+ return app_base_url() . '/thawani_return.php?result=' . rawurlencode($result);
+}
+
+function thawani_success_url(): string
+{
+ $custom = trim((string) get_setting('thawani_success_url', ''));
+ return $custom !== '' ? $custom : thawani_default_return_url('success');
+}
+
+function thawani_cancel_url(): string
+{
+ $custom = trim((string) get_setting('thawani_cancel_url', ''));
+ return $custom !== '' ? $custom : thawani_default_return_url('cancel');
+}
+
+function thawani_build_products(array $items): array
+{
+ $products = [];
+ foreach ($items as $item) {
+ $qty = max(1, (int) ($item['qty'] ?? 0));
+ $name = trim((string) ($item['name'] ?? $item['name_ar'] ?? $item['sku'] ?? 'Item'));
+ $lineTotal = (float) ($item['line_total'] ?? 0);
+ $vatAmount = (float) ($item['vat_amount'] ?? 0);
+ $unitAmount = (int) round((($lineTotal + $vatAmount) / $qty) * 1000);
+ $products[] = [
+ 'name' => $name,
+ 'quantity' => $qty,
+ 'unit_amount' => max(1, $unitAmount),
+ ];
+ }
+
+ return $products;
+}
+
+function thawani_call(string $method, string $path): array
+{
+ $url = rtrim(thawani_checkout_base_url(), '/') . $path;
+ $headers = [
+ 'Content-Type: application/json',
+ 'Accept: application/json',
+ 'thawani-api-key: ' . thawani_api_key(),
+ ];
+
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CUSTOMREQUEST => strtoupper($method),
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_TIMEOUT => 30,
+ ]);
+ $raw = curl_exec($ch);
+ $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $error = curl_error($ch);
+ curl_close($ch);
+
+ if ($raw === false) {
+ return ['success' => false, 'status' => $status, 'error' => $error !== '' ? $error : 'Unable to contact Thawani'];
+ }
+
+ $decoded = json_decode($raw, true);
+ return [
+ 'success' => $status >= 200 && $status < 300,
+ 'status' => $status,
+ 'data' => is_array($decoded) ? $decoded : null,
+ 'raw' => $raw,
+ 'error' => $status >= 200 && $status < 300 ? '' : ('HTTP ' . $status),
+ ];
+}
+
+function thawani_create_checkout_session(int $orderId, array $order): array
+{
+ if (!thawani_is_configured()) {
+ return ['success' => false, 'error' => 'Thawani is not configured'];
+ }
+
+ $payload = [
+ 'client_reference_id' => 'online-order-' . $orderId,
+ 'mode' => 'payment',
+ 'products' => thawani_build_products($order['items'] ?? []),
+ 'success_url' => append_query_params(thawani_success_url(), [
+ 'order_id' => $orderId,
+ ]),
+ 'cancel_url' => append_query_params(thawani_cancel_url(), [
+ 'order_id' => $orderId,
+ ]),
+ 'metadata' => [
+ 'order_id' => (string) $orderId,
+ 'customer_name' => (string) ($order['customer_name'] ?? ''),
+ 'customer_phone' => (string) ($order['customer_phone'] ?? ''),
+ ],
+ ];
+
+ $url = rtrim(thawani_checkout_base_url(), '/') . '/api/v1/checkout/session';
+ $headers = [
+ 'Content-Type: application/json',
+ 'Accept: application/json',
+ 'thawani-api-key: ' . thawani_api_key(),
+ ];
+
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_TIMEOUT => 30,
+ ]);
+ $raw = curl_exec($ch);
+ $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $error = curl_error($ch);
+ curl_close($ch);
+
+ if ($raw === false) {
+ return ['success' => false, 'status' => $status, 'error' => $error !== '' ? $error : 'Unable to contact Thawani'];
+ }
+
+ $decoded = json_decode($raw, true);
+ $sessionId = (string) ($decoded['data']['session_id'] ?? $decoded['session_id'] ?? '');
+ if ($status < 200 || $status >= 300 || $sessionId === '') {
+ $message = (string) ($decoded['message'] ?? $decoded['description'] ?? ('HTTP ' . $status));
+ return ['success' => false, 'status' => $status, 'error' => $message, 'data' => $decoded, 'raw' => $raw];
+ }
+
+ return [
+ 'success' => true,
+ 'status' => $status,
+ 'session_id' => $sessionId,
+ 'checkout_url' => rtrim(thawani_checkout_base_url(), '/') . '/pay/' . rawurlencode($sessionId) . '?key=' . rawurlencode(thawani_publishable_key()),
+ 'data' => $decoded,
+ 'raw' => $raw,
+ ];
+}
+
+function thawani_retrieve_session(string $sessionId): array
+{
+ if ($sessionId === '') {
+ return ['success' => false, 'error' => 'Missing session id'];
+ }
+
+ return thawani_call('GET', '/api/v1/checkout/session/' . rawurlencode($sessionId));
+}
+
+function thawani_session_paid(array $response): bool
+{
+ $data = (array) ($response['data']['data'] ?? $response['data'] ?? []);
+ $candidates = [
+ strtolower((string) ($data['payment_status'] ?? '')),
+ strtolower((string) ($data['status'] ?? '')),
+ strtolower((string) ($data['paymentStatus'] ?? '')),
+ strtolower((string) ($data['paymentStatusDescription'] ?? '')),
+ ];
+ foreach ($candidates as $candidate) {
+ if (in_array($candidate, ['paid', 'successful', 'success'], true)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function thawani_session_transaction_id(array $response): string
+{
+ $data = (array) ($response['data']['data'] ?? $response['data'] ?? []);
+ return (string) ($data['invoice'] ?? $data['payment_reference'] ?? $data['transaction_id'] ?? $data['reference'] ?? '');
+}
+
function apply_sale_payment(int $saleId, float $paymentAmount, bool $completeOrderWhenPaid = false): array
{
$sale = fetch_sale($saleId);
@@ -730,19 +1062,33 @@ function wablas_default_order_template(string $event): string
return match ($event) {
'created' => "مرحباً {customer_name}، تم استلام طلبك رقم #{order_id}.
الحالة: {status_label}
+طريقة الدفع: {payment_method_label}
+حالة الدفع: {payment_status_label}
+
+الأصناف:
+{items_summary}
+
الإجمالي: {total_amount}
العنوان: {customer_address}
شكراً لتسوقك معنا.",
'pending' => "مرحباً {customer_name}، طلبك رقم #{order_id} ما زال {status_label}.
+طريقة الدفع: {payment_method_label}
+حالة الدفع: {payment_status_label}
الإجمالي: {total_amount}
سنوافيك بأي تحديث جديد.",
'accepted' => "مرحباً {customer_name}، تم قبول طلبك رقم #{order_id}.
+طريقة الدفع: {payment_method_label}
+حالة الدفع: {payment_status_label}
الإجمالي: {total_amount}
سنبدأ التجهيز الآن.",
'completed' => "مرحباً {customer_name}، طلبك رقم #{order_id} أصبح {status_label}.
+طريقة الدفع: {payment_method_label}
+حالة الدفع: {payment_status_label}
الإجمالي: {total_amount}
شكراً لك.",
'rejected' => "مرحباً {customer_name}، نعتذر، تم تحديث طلبك رقم #{order_id} إلى {status_label}.
+طريقة الدفع: {payment_method_label}
+حالة الدفع: {payment_status_label}
إذا رغبت بالمساعدة تواصل معنا.",
default => "مرحباً {customer_name}، تم تحديث طلبك رقم #{order_id} إلى {status_label}.",
};
@@ -788,6 +1134,13 @@ function wablas_order_items_summary(array $order): string
function wablas_order_template_vars(array $order): array
{
$status = (string) ($order['status'] ?? 'pending');
+ $paymentMethod = (string) ($order['payment_method'] ?? 'pay_later');
+ $paymentStatus = (string) ($order['payment_status'] ?? ($paymentMethod === 'pay_online' ? 'pending' : 'unpaid'));
+ $itemsSummary = wablas_order_items_summary($order);
+ if (trim($itemsSummary) === '') {
+ $itemsSummary = '-';
+ }
+
return [
'order_id' => (string) ($order['id'] ?? ''),
'customer_name' => (string) ($order['customer_name'] ?? ''),
@@ -795,11 +1148,15 @@ function wablas_order_template_vars(array $order): array
'customer_address' => (string) ($order['customer_address'] ?? ''),
'status' => $status,
'status_label' => wablas_order_status_label($status),
+ 'payment_method' => $paymentMethod,
+ 'payment_method_label' => wablas_payment_method_label($paymentMethod),
+ 'payment_status' => $paymentStatus,
+ 'payment_status_label' => wablas_payment_status_label($paymentStatus),
'subtotal' => currency((float) ($order['subtotal'] ?? 0)),
'vat_amount' => currency((float) ($order['vat_amount'] ?? 0)),
'total_amount' => currency((float) ($order['total_amount'] ?? 0)),
'created_at' => (string) ($order['created_at'] ?? ''),
- 'items_summary' => wablas_order_items_summary($order),
+ 'items_summary' => $itemsSummary,
];
}
@@ -842,7 +1199,8 @@ function wablas_payment_method_label(string $method): string
'cash' => tr('كاش', 'Cash'),
'card' => tr('بطاقة', 'Card'),
'transfer', 'bank' => tr('تحويل', 'Transfer'),
- 'pay_later' => tr('آجل', 'Pay later'),
+ 'pay_later' => tr('الدفع لاحقاً', 'Pay later'),
+ 'pay_online' => tr('الدفع أونلاين', 'Pay online'),
default => $method,
};
}
@@ -853,6 +1211,9 @@ function wablas_payment_status_label(string $status): string
'paid' => tr('مدفوع', 'Paid'),
'partial' => tr('مدفوع جزئياً', 'Partial'),
'unpaid' => tr('غير مدفوع', 'Unpaid'),
+ 'pending' => tr('بانتظار الدفع', 'Pending payment'),
+ 'failed' => tr('فشل الدفع', 'Payment failed'),
+ 'cancelled' => tr('تم إلغاء الدفع', 'Payment cancelled'),
default => $status,
};
}
@@ -1228,6 +1589,34 @@ function wablas_notify_online_order(array $order, string $event): array
return $result;
}
+function fetch_online_order(int $orderId): ?array
+{
+ if ($orderId <= 0) {
+ return null;
+ }
+
+ try {
+ $stmt = db()->prepare('SELECT * FROM online_orders WHERE id = :id LIMIT 1');
+ $stmt->bindValue(':id', $orderId, PDO::PARAM_INT);
+ $stmt->execute();
+ $order = $stmt->fetch(PDO::FETCH_ASSOC);
+ return $order ?: null;
+ } catch (Throwable $e) {
+ error_log('Failed to fetch online order #' . $orderId . ': ' . $e->getMessage());
+ return null;
+ }
+}
+
+function wablas_notify_online_order_by_id(int $orderId, string $event): array
+{
+ $order = fetch_online_order($orderId);
+ if (!$order) {
+ return ['success' => false, 'error' => 'Online order not found'];
+ }
+
+ return wablas_notify_online_order($order, $event);
+}
+
function ensure_sales_table(): void
{
$sql = "CREATE TABLE IF NOT EXISTS sales_orders (
diff --git a/includes/footer_settings.php b/includes/footer_settings.php
index 47f1851..1b8c136 100644
--- a/includes/footer_settings.php
+++ b/includes/footer_settings.php
@@ -39,6 +39,11 @@
= h(tr('واتساب', 'WhatsApp')) ?>
+
+
+ = h(tr('الدفع الإلكتروني', 'Payments')) ?>
+
+
= h(tr('البريد الإلكتروني', 'Email')) ?>
@@ -146,6 +151,51 @@
+
+
+
+
+
+ >
+ = h(tr('تفعيل بوابة ثواني', 'Enable Thawani Gateway')) ?>
+
+
= h(tr('فعّلها ليظهر خيارا الدفع أونلاين أو الدفع لاحقاً في طلبات المتجر.', 'Enable it so the online store can offer Pay Online or Pay Later.')) ?>
+
+
+
+ = h(tr('البيئة', 'Mode')) ?>
+
+ >= h(tr('تجريبية (Sandbox)', 'Sandbox')) ?>
+ >= h(tr('حقيقية (Live)', 'Live')) ?>
+
+
+
+ = h(tr('المفتاح العام (Publishable Key)', 'Publishable Key')) ?>
+
+
+
+ = h(tr('المفتاح السري (Secret Key)', 'Secret Key')) ?>
+
+
+
+
= h(tr('رابط نجاح الدفع', 'Success URL')) ?>
+
+
= h(tr('اختياري. إذا تركته فارغاً سيستخدم النظام رابط الرجوع الداخلي تلقائياً.', 'Optional. Leave empty to use the built-in return URL automatically.')) ?>
+
+
+
= h(tr('رابط إلغاء الدفع', 'Cancel URL')) ?>
+
+
= h(tr('اختياري. يستخدم عندما يُلغى الدفع أو يعود العميل بدون إتمام العملية.', 'Optional. Used when payment is cancelled or the customer returns without completing checkout.')) ?>
+
+
+
+
= h(tr('معلومة', 'Note')) ?>
+
= h(tr('عند اختيار الدفع أونلاين سيُنشئ النظام جلسة Thawani ويرسل العميل مباشرةً لصفحة الدفع.', 'When Pay Online is selected, the system creates a Thawani session and redirects the customer to the hosted checkout page.')) ?>
+
+
+
+
+
diff --git a/online_orders.php b/online_orders.php
index 713c860..43932b4 100644
--- a/online_orders.php
+++ b/online_orders.php
@@ -152,6 +152,7 @@ require __DIR__ . '/includes/header.php';
= h(tr('العميل', 'Customer')) ?>
= h(tr('الهاتف', 'Telephone')) ?>
= h(tr('العنوان', 'Address')) ?>
+
= h(tr('الدفع', 'Payment')) ?>
= h(tr('المبلغ', 'Amount')) ?>
= h(tr('الحالة', 'Status')) ?>
= h(tr('إجراءات', 'Actions')) ?>
@@ -159,7 +160,7 @@ require __DIR__ . '/includes/header.php';
- = h(tr('لا توجد طلبات', 'No orders found')) ?>
+ = h(tr('لا توجد طلبات', 'No orders found')) ?>
= h($o['customer_name']) ?>
= h(phone_display($o['customer_phone'])) ?>
= h($o['customer_address']) ?>
+
+ = h(online_payment_method_label((string) ($o['payment_method'] ?? 'pay_later'))) ?>
+ = h(online_payment_status_label((string) ($o['payment_status'] ?? 'unpaid'))) ?>
+
= h(currency($o['total_amount'])) ?>
= h($statusText) ?>
@@ -188,6 +193,8 @@ require __DIR__ . '/includes/header.php';
"name" => $o["customer_name"],
"phone" => $o["customer_phone"],
"address" => $o["customer_address"],
+ "payment_method" => online_payment_method_label((string) ($o["payment_method"] ?? 'pay_later')),
+ "payment_status" => online_payment_status_label((string) ($o["payment_status"] ?? 'unpaid')),
"subtotal" => $o["subtotal"] ?? 0, "vat" => $o["vat_amount"] ?? 0, "total" => $o["total_amount"],
"items" => $items
], JSON_UNESCAPED_UNICODE), ENT_QUOTES, "UTF-8") ?>)'>
@@ -218,7 +225,7 @@ require __DIR__ . '/includes/header.php';
- = h(tr('إجمالي المبالغ:', 'Total Amounts:')) ?>
+ = h(tr('إجمالي المبالغ:', 'Total Amounts:')) ?>
= h(currency($totalAmount)) ?>
@@ -241,6 +248,8 @@ require __DIR__ . '/includes/header.php';
= h(tr('العميل', 'Customer')) ?>
= h(tr('الهاتف', 'Phone')) ?>
= h(tr('العنوان', 'Address')) ?>
+
= h(tr('طريقة الدفع', 'Payment Method')) ?>
+
= h(tr('حالة الدفع', 'Payment Status')) ?>
@@ -276,6 +285,8 @@ function viewOrder(order) {
document.getElementById('vName').innerText = order.name;
document.getElementById('vPhone').innerText = order.phone ? ('968 ' + String(order.phone).replace(/^((00968)|(968))/, '')) : '';
document.getElementById('vAddress').innerText = order.address;
+ document.getElementById('vPaymentMethod').innerText = order.payment_method || '';
+ document.getElementById('vPaymentStatus').innerText = order.payment_status || '';
document.getElementById('vSubtotal').innerText = Number(order.subtotal).toFixed(2);
document.getElementById('vVat').innerText = Number(order.vat).toFixed(2);
document.getElementById('vTotal').innerText = Number(order.total).toFixed(2);
diff --git a/shop.php b/shop.php
index 730cf48..0c4b279 100644
--- a/shop.php
+++ b/shop.php
@@ -3,6 +3,10 @@ require_once __DIR__ . '/includes/app.php';
$forcePublic = true;
$pageTitle = tr('الطلب عبر الإنترنت', 'Online Ordering');
+$shopPaymentStatus = trim((string) ($_GET['payment_status'] ?? ''));
+$shopPaymentMessage = trim((string) ($_GET['message'] ?? ''));
+$shopCanPayOnline = thawani_is_configured();
+$shopShowPayOnline = thawani_is_enabled() || $shopCanPayOnline;
require __DIR__ . '/includes/header.php';
@@ -110,6 +114,26 @@ body { background-color: #f8f9fa; }
+
+ tr('تم الدفع بنجاح وتم استلام طلبك.', 'Payment completed successfully and your order was received.'),
+ 'cancelled' => tr('تم إلغاء عملية الدفع. يمكنك المحاولة مرة أخرى أو اختيار الدفع لاحقاً.', 'Payment was cancelled. You can try again or choose Pay Later.'),
+ 'failed' => tr('تعذر تأكيد الدفع. إذا تم الخصم يرجى مراجعة الإدارة.', 'We could not confirm the payment. If you were charged, please contact the store.'),
+ default => tr('تم تحديث حالة الطلب.', 'The order status has been updated.'),
+ };
+ ?>
+
+ = h($shopPaymentMessage !== '' ? $shopPaymentMessage : $defaultMessage) ?>
+
+
+
@@ -244,6 +268,30 @@ body { background-color: #f8f9fa; }
= h(tr('العنوان', 'Address')) ?> *
+
+
= h(tr('طريقة الدفع', 'Payment Method')) ?> *
+
+
+
= h(tr('خيار الدفع أونلاين ظاهر لكن مفاتيح ثواني غير مكتملة في الإعدادات بعد.', 'Pay Online is visible, but the Thawani keys are not fully configured in Settings yet.')) ?>
+
+