thawani payments
This commit is contained in:
parent
9b0dd7d971
commit
8cadf8aa40
@ -17,6 +17,15 @@ $name = trim($input['name'] ?? '');
|
|||||||
$phoneInput = trim($input['phone'] ?? '');
|
$phoneInput = trim($input['phone'] ?? '');
|
||||||
$phone = normalize_oman_phone($phoneInput);
|
$phone = normalize_oman_phone($phoneInput);
|
||||||
$address = trim($input['address'] ?? '');
|
$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 === '') {
|
if ($name === '' || $phoneInput === '' || $address === '') {
|
||||||
echo json_encode(['success' => false, 'error' => 'Missing customer details']);
|
echo json_encode(['success' => false, 'error' => 'Missing customer details']);
|
||||||
@ -75,7 +84,10 @@ $totalAmount = $subtotal + $totalVat;
|
|||||||
try {
|
try {
|
||||||
$db->beginTransaction();
|
$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([
|
$stmt->execute([
|
||||||
$name,
|
$name,
|
||||||
$phone,
|
$phone,
|
||||||
@ -83,28 +95,37 @@ try {
|
|||||||
json_encode($processedItems, JSON_UNESCAPED_UNICODE),
|
json_encode($processedItems, JSON_UNESCAPED_UNICODE),
|
||||||
$subtotal,
|
$subtotal,
|
||||||
$totalVat,
|
$totalVat,
|
||||||
$totalAmount
|
$totalAmount,
|
||||||
|
$paymentMethod,
|
||||||
|
$paymentGateway,
|
||||||
|
$paymentStatus
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$orderId = (int) $db->lastInsertId();
|
$orderId = (int) $db->lastInsertId();
|
||||||
sync_online_order_stock_reservation([], 'rejected', $processedItems, 'pending');
|
sync_online_order_stock_reservation([], 'rejected', $processedItems, 'pending');
|
||||||
$db->commit();
|
|
||||||
|
|
||||||
// Optional: send telegram and WhatsApp notifications if configured
|
$checkoutUrl = null;
|
||||||
try {
|
if ($paymentMethod === 'pay_online') {
|
||||||
$orderData = [
|
$thawaniResult = thawani_create_checkout_session($orderId, [
|
||||||
'id' => $orderId,
|
|
||||||
'customer_name' => $name,
|
'customer_name' => $name,
|
||||||
'customer_phone' => $phone,
|
'customer_phone' => $phone,
|
||||||
'customer_address' => $address,
|
'customer_address' => $address,
|
||||||
'items' => $processedItems,
|
'items' => $processedItems,
|
||||||
'subtotal' => $subtotal,
|
]);
|
||||||
'vat_amount' => $totalVat,
|
if (empty($thawaniResult['success'])) {
|
||||||
'total_amount' => $totalAmount,
|
throw new RuntimeException((string) ($thawaniResult['error'] ?? 'Unable to create Thawani session'));
|
||||||
'status' => 'pending',
|
}
|
||||||
'created_at' => date('Y-m-d H:i:s'),
|
|
||||||
];
|
|
||||||
|
|
||||||
|
$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}*
|
$msg = "🛒 *New Online Order #{$orderId}*
|
||||||
|
|
||||||
";
|
";
|
||||||
@ -136,15 +157,19 @@ try {
|
|||||||
$context = stream_context_create($options);
|
$context = stream_context_create($options);
|
||||||
@file_get_contents($url, false, $context);
|
@file_get_contents($url, false, $context);
|
||||||
}
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
if (wablas_is_configured()) {
|
error_log('Telegram notify failed for online order #' . $orderId . ': ' . $e->getMessage());
|
||||||
wablas_notify_online_order($orderData, 'created');
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
// ignore notification errors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
} catch (Throwable $e) {
|
||||||
if ($db->inTransaction()) {
|
if ($db->inTransaction()) {
|
||||||
$db->rollBack();
|
$db->rollBack();
|
||||||
|
|||||||
@ -19,7 +19,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
'wablas_daily_auto_send', 'wablas_daily_auto_time', 'wablas_daily_auto_last_date',
|
'wablas_daily_auto_send', 'wablas_daily_auto_time', 'wablas_daily_auto_last_date',
|
||||||
'wablas_template_invoice', 'wablas_template_daily_report',
|
'wablas_template_invoice', 'wablas_template_daily_report',
|
||||||
'wablas_template_created', 'wablas_template_pending', 'wablas_template_accepted', 'wablas_template_completed', 'wablas_template_rejected',
|
'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)");
|
$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'])) {
|
if (!isset($_POST['wablas_daily_auto_send'])) {
|
||||||
$_POST['wablas_daily_auto_send'] = '0';
|
$_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']);
|
unset($_POST['wablas_daily_auto_last_date']);
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
foreach ($keys as $key) {
|
||||||
|
|||||||
@ -385,6 +385,14 @@ require __DIR__ . '/includes/header.php';
|
|||||||
<option value="rejected" <?= $editOrder['status'] === 'rejected' ? 'selected' : '' ?>><?= h(tr('مرفوض', 'Rejected')) ?></option>
|
<option value="rejected" <?= $editOrder['status'] === 'rejected' ? 'selected' : '' ?>><?= h(tr('مرفوض', 'Rejected')) ?></option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="border rounded-3 p-3 bg-light mb-4">
|
||||||
|
<div class="small text-muted mb-2"><?= h(tr('الدفع', 'Payment')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= h(online_payment_method_label((string) ($editOrder['payment_method'] ?? 'pay_later'))) ?></div>
|
||||||
|
<div class="mt-2"><span class="badge <?= h(online_payment_status_badge_class((string) ($editOrder['payment_status'] ?? 'unpaid'))) ?>"><?= h(online_payment_status_label((string) ($editOrder['payment_status'] ?? 'unpaid'))) ?></span></div>
|
||||||
|
<?php if (!empty($editOrder['gateway_session_id'])): ?>
|
||||||
|
<div class="small text-muted mt-2"><?= h(tr('جلسة ثواني:', 'Thawani session:')) ?> <?= h($editOrder['gateway_session_id']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Summary -->
|
<!-- Summary -->
|
||||||
<div class="totals-box mb-4">
|
<div class="totals-box mb-4">
|
||||||
|
|||||||
393
includes/app.php
393
includes/app.php
@ -121,6 +121,41 @@ try {
|
|||||||
|
|
||||||
@file_put_contents($flagFileV7, '1');
|
@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) {}
|
} catch (\Throwable $e) {}
|
||||||
|
|
||||||
|
|
||||||
@ -308,6 +343,73 @@ function url_for(string $path, array $params = []): string
|
|||||||
return $path . ($query ? ('?' . $query) : '');
|
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
|
function redirect_to(string $path, array $params = []): void
|
||||||
{
|
{
|
||||||
header('Location: ' . url_for($path, $params));
|
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
|
function apply_sale_payment(int $saleId, float $paymentAmount, bool $completeOrderWhenPaid = false): array
|
||||||
{
|
{
|
||||||
$sale = fetch_sale($saleId);
|
$sale = fetch_sale($saleId);
|
||||||
@ -730,19 +1062,33 @@ function wablas_default_order_template(string $event): string
|
|||||||
return match ($event) {
|
return match ($event) {
|
||||||
'created' => "مرحباً {customer_name}، تم استلام طلبك رقم #{order_id}.
|
'created' => "مرحباً {customer_name}، تم استلام طلبك رقم #{order_id}.
|
||||||
الحالة: {status_label}
|
الحالة: {status_label}
|
||||||
|
طريقة الدفع: {payment_method_label}
|
||||||
|
حالة الدفع: {payment_status_label}
|
||||||
|
|
||||||
|
الأصناف:
|
||||||
|
{items_summary}
|
||||||
|
|
||||||
الإجمالي: {total_amount}
|
الإجمالي: {total_amount}
|
||||||
العنوان: {customer_address}
|
العنوان: {customer_address}
|
||||||
شكراً لتسوقك معنا.",
|
شكراً لتسوقك معنا.",
|
||||||
'pending' => "مرحباً {customer_name}، طلبك رقم #{order_id} ما زال {status_label}.
|
'pending' => "مرحباً {customer_name}، طلبك رقم #{order_id} ما زال {status_label}.
|
||||||
|
طريقة الدفع: {payment_method_label}
|
||||||
|
حالة الدفع: {payment_status_label}
|
||||||
الإجمالي: {total_amount}
|
الإجمالي: {total_amount}
|
||||||
سنوافيك بأي تحديث جديد.",
|
سنوافيك بأي تحديث جديد.",
|
||||||
'accepted' => "مرحباً {customer_name}، تم قبول طلبك رقم #{order_id}.
|
'accepted' => "مرحباً {customer_name}، تم قبول طلبك رقم #{order_id}.
|
||||||
|
طريقة الدفع: {payment_method_label}
|
||||||
|
حالة الدفع: {payment_status_label}
|
||||||
الإجمالي: {total_amount}
|
الإجمالي: {total_amount}
|
||||||
سنبدأ التجهيز الآن.",
|
سنبدأ التجهيز الآن.",
|
||||||
'completed' => "مرحباً {customer_name}، طلبك رقم #{order_id} أصبح {status_label}.
|
'completed' => "مرحباً {customer_name}، طلبك رقم #{order_id} أصبح {status_label}.
|
||||||
|
طريقة الدفع: {payment_method_label}
|
||||||
|
حالة الدفع: {payment_status_label}
|
||||||
الإجمالي: {total_amount}
|
الإجمالي: {total_amount}
|
||||||
شكراً لك.",
|
شكراً لك.",
|
||||||
'rejected' => "مرحباً {customer_name}، نعتذر، تم تحديث طلبك رقم #{order_id} إلى {status_label}.
|
'rejected' => "مرحباً {customer_name}، نعتذر، تم تحديث طلبك رقم #{order_id} إلى {status_label}.
|
||||||
|
طريقة الدفع: {payment_method_label}
|
||||||
|
حالة الدفع: {payment_status_label}
|
||||||
إذا رغبت بالمساعدة تواصل معنا.",
|
إذا رغبت بالمساعدة تواصل معنا.",
|
||||||
default => "مرحباً {customer_name}، تم تحديث طلبك رقم #{order_id} إلى {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
|
function wablas_order_template_vars(array $order): array
|
||||||
{
|
{
|
||||||
$status = (string) ($order['status'] ?? 'pending');
|
$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 [
|
return [
|
||||||
'order_id' => (string) ($order['id'] ?? ''),
|
'order_id' => (string) ($order['id'] ?? ''),
|
||||||
'customer_name' => (string) ($order['customer_name'] ?? ''),
|
'customer_name' => (string) ($order['customer_name'] ?? ''),
|
||||||
@ -795,11 +1148,15 @@ function wablas_order_template_vars(array $order): array
|
|||||||
'customer_address' => (string) ($order['customer_address'] ?? ''),
|
'customer_address' => (string) ($order['customer_address'] ?? ''),
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'status_label' => wablas_order_status_label($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)),
|
'subtotal' => currency((float) ($order['subtotal'] ?? 0)),
|
||||||
'vat_amount' => currency((float) ($order['vat_amount'] ?? 0)),
|
'vat_amount' => currency((float) ($order['vat_amount'] ?? 0)),
|
||||||
'total_amount' => currency((float) ($order['total_amount'] ?? 0)),
|
'total_amount' => currency((float) ($order['total_amount'] ?? 0)),
|
||||||
'created_at' => (string) ($order['created_at'] ?? ''),
|
'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'),
|
'cash' => tr('كاش', 'Cash'),
|
||||||
'card' => tr('بطاقة', 'Card'),
|
'card' => tr('بطاقة', 'Card'),
|
||||||
'transfer', 'bank' => tr('تحويل', 'Transfer'),
|
'transfer', 'bank' => tr('تحويل', 'Transfer'),
|
||||||
'pay_later' => tr('آجل', 'Pay later'),
|
'pay_later' => tr('الدفع لاحقاً', 'Pay later'),
|
||||||
|
'pay_online' => tr('الدفع أونلاين', 'Pay online'),
|
||||||
default => $method,
|
default => $method,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -853,6 +1211,9 @@ function wablas_payment_status_label(string $status): string
|
|||||||
'paid' => tr('مدفوع', 'Paid'),
|
'paid' => tr('مدفوع', 'Paid'),
|
||||||
'partial' => tr('مدفوع جزئياً', 'Partial'),
|
'partial' => tr('مدفوع جزئياً', 'Partial'),
|
||||||
'unpaid' => tr('غير مدفوع', 'Unpaid'),
|
'unpaid' => tr('غير مدفوع', 'Unpaid'),
|
||||||
|
'pending' => tr('بانتظار الدفع', 'Pending payment'),
|
||||||
|
'failed' => tr('فشل الدفع', 'Payment failed'),
|
||||||
|
'cancelled' => tr('تم إلغاء الدفع', 'Payment cancelled'),
|
||||||
default => $status,
|
default => $status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1228,6 +1589,34 @@ function wablas_notify_online_order(array $order, string $event): array
|
|||||||
return $result;
|
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
|
function ensure_sales_table(): void
|
||||||
{
|
{
|
||||||
$sql = "CREATE TABLE IF NOT EXISTS sales_orders (
|
$sql = "CREATE TABLE IF NOT EXISTS sales_orders (
|
||||||
|
|||||||
@ -39,6 +39,11 @@
|
|||||||
<?= h(tr('واتساب', 'WhatsApp')) ?>
|
<?= h(tr('واتساب', 'WhatsApp')) ?>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="settings-payment-tab" data-bs-toggle="tab" data-bs-target="#settings-payment-pane" type="button" role="tab" aria-controls="settings-payment-pane" aria-selected="false">
|
||||||
|
<?= h(tr('الدفع الإلكتروني', 'Payments')) ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="settings-email-tab" data-bs-toggle="tab" data-bs-target="#settings-email-pane" type="button" role="tab" aria-controls="settings-email-pane" aria-selected="false">
|
<button class="nav-link" id="settings-email-tab" data-bs-toggle="tab" data-bs-target="#settings-email-pane" type="button" role="tab" aria-controls="settings-email-pane" aria-selected="false">
|
||||||
<?= h(tr('البريد الإلكتروني', 'Email')) ?>
|
<?= h(tr('البريد الإلكتروني', 'Email')) ?>
|
||||||
@ -146,6 +151,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="settings-payment-pane" role="tabpanel" aria-labelledby="settings-payment-tab" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="border rounded-4 p-3 bg-body-tertiary">
|
||||||
|
<div class="form-check form-switch mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="thawaniEnabled" name="thawani_enabled" value="1" <?= thawani_is_enabled() ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label fw-semibold" for="thawaniEnabled"><?= h(tr('تفعيل بوابة ثواني', 'Enable Thawani Gateway')) ?></label>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted"><?= h(tr('فعّلها ليظهر خيارا الدفع أونلاين أو الدفع لاحقاً في طلبات المتجر.', 'Enable it so the online store can offer Pay Online or Pay Later.')) ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= h(tr('البيئة', 'Mode')) ?></label>
|
||||||
|
<select class="form-select" name="thawani_mode">
|
||||||
|
<option value="sandbox" <?= get_setting('thawani_mode', 'sandbox') === 'sandbox' ? 'selected' : '' ?>><?= h(tr('تجريبية (Sandbox)', 'Sandbox')) ?></option>
|
||||||
|
<option value="live" <?= get_setting('thawani_mode') === 'live' ? 'selected' : '' ?>><?= h(tr('حقيقية (Live)', 'Live')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label"><?= h(tr('المفتاح العام (Publishable Key)', 'Publishable Key')) ?></label>
|
||||||
|
<input type="text" class="form-control" name="thawani_publishable_key" value="<?= h(get_setting('thawani_publishable_key')) ?>" placeholder="pk_...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label"><?= h(tr('المفتاح السري (Secret Key)', 'Secret Key')) ?></label>
|
||||||
|
<input type="password" class="form-control" name="thawani_secret_key" value="<?= h(get_setting('thawani_secret_key')) ?>" placeholder="sk_...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= h(tr('رابط نجاح الدفع', 'Success URL')) ?></label>
|
||||||
|
<input type="url" class="form-control" name="thawani_success_url" value="<?= h(get_setting('thawani_success_url')) ?>" placeholder="<?= h(thawani_default_return_url('success')) ?>">
|
||||||
|
<div class="form-text"><?= h(tr('اختياري. إذا تركته فارغاً سيستخدم النظام رابط الرجوع الداخلي تلقائياً.', 'Optional. Leave empty to use the built-in return URL automatically.')) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= h(tr('رابط إلغاء الدفع', 'Cancel URL')) ?></label>
|
||||||
|
<input type="url" class="form-control" name="thawani_cancel_url" value="<?= h(get_setting('thawani_cancel_url')) ?>" placeholder="<?= h(thawani_default_return_url('cancel')) ?>">
|
||||||
|
<div class="form-text"><?= h(tr('اختياري. يستخدم عندما يُلغى الدفع أو يعود العميل بدون إتمام العملية.', 'Optional. Used when payment is cancelled or the customer returns without completing checkout.')) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info rounded-4 mb-0">
|
||||||
|
<div class="fw-semibold mb-1"><?= h(tr('معلومة', 'Note')) ?></div>
|
||||||
|
<div class="small mb-0"><?= h(tr('عند اختيار الدفع أونلاين سيُنشئ النظام جلسة Thawani ويرسل العميل مباشرةً لصفحة الدفع.', 'When Pay Online is selected, the system creates a Thawani session and redirects the customer to the hosted checkout page.')) ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="settings-email-pane" role="tabpanel" aria-labelledby="settings-email-tab" tabindex="0">
|
<div class="tab-pane fade" id="settings-email-pane" role="tabpanel" aria-labelledby="settings-email-tab" tabindex="0">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
|||||||
@ -152,6 +152,7 @@ require __DIR__ . '/includes/header.php';
|
|||||||
<th><?= h(tr('العميل', 'Customer')) ?></th>
|
<th><?= h(tr('العميل', 'Customer')) ?></th>
|
||||||
<th><?= h(tr('الهاتف', 'Telephone')) ?></th>
|
<th><?= h(tr('الهاتف', 'Telephone')) ?></th>
|
||||||
<th><?= h(tr('العنوان', 'Address')) ?></th>
|
<th><?= h(tr('العنوان', 'Address')) ?></th>
|
||||||
|
<th><?= h(tr('الدفع', 'Payment')) ?></th>
|
||||||
<th><?= h(tr('المبلغ', 'Amount')) ?></th>
|
<th><?= h(tr('المبلغ', 'Amount')) ?></th>
|
||||||
<th><?= h(tr('الحالة', 'Status')) ?></th>
|
<th><?= h(tr('الحالة', 'Status')) ?></th>
|
||||||
<th class="pe-4 text-end print-hide"><?= h(tr('إجراءات', 'Actions')) ?></th>
|
<th class="pe-4 text-end print-hide"><?= h(tr('إجراءات', 'Actions')) ?></th>
|
||||||
@ -159,7 +160,7 @@ require __DIR__ . '/includes/header.php';
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if(empty($orders)): ?>
|
<?php if(empty($orders)): ?>
|
||||||
<tr><td colspan="8" class="text-center py-5 text-muted"><?= h(tr('لا توجد طلبات', 'No orders found')) ?></td></tr>
|
<tr><td colspan="9" class="text-center py-5 text-muted"><?= h(tr('لا توجد طلبات', 'No orders found')) ?></td></tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php
|
<?php
|
||||||
$totalAmount = 0;
|
$totalAmount = 0;
|
||||||
@ -180,6 +181,10 @@ require __DIR__ . '/includes/header.php';
|
|||||||
<td class="fw-bold"><?= h($o['customer_name']) ?></td>
|
<td class="fw-bold"><?= h($o['customer_name']) ?></td>
|
||||||
<td dir="ltr"><?= h(phone_display($o['customer_phone'])) ?></td>
|
<td dir="ltr"><?= h(phone_display($o['customer_phone'])) ?></td>
|
||||||
<td style="max-width: 200px;" class="text-truncate" title="<?= h($o['customer_address']) ?>"><?= h($o['customer_address']) ?></td>
|
<td style="max-width: 200px;" class="text-truncate" title="<?= h($o['customer_address']) ?>"><?= h($o['customer_address']) ?></td>
|
||||||
|
<td>
|
||||||
|
<div class="small fw-semibold"><?= h(online_payment_method_label((string) ($o['payment_method'] ?? 'pay_later'))) ?></div>
|
||||||
|
<span class="badge <?= h(online_payment_status_badge_class((string) ($o['payment_status'] ?? 'unpaid'))) ?> mt-1"><?= h(online_payment_status_label((string) ($o['payment_status'] ?? 'unpaid'))) ?></span>
|
||||||
|
</td>
|
||||||
<td class="fw-bold text-primary"><?= h(currency($o['total_amount'])) ?></td>
|
<td class="fw-bold text-primary"><?= h(currency($o['total_amount'])) ?></td>
|
||||||
<td><span class="badge <?= $statusClass ?> px-2 py-1"><?= h($statusText) ?></span></td>
|
<td><span class="badge <?= $statusClass ?> px-2 py-1"><?= h($statusText) ?></span></td>
|
||||||
<td class="pe-4 text-end print-hide">
|
<td class="pe-4 text-end print-hide">
|
||||||
@ -188,6 +193,8 @@ require __DIR__ . '/includes/header.php';
|
|||||||
"name" => $o["customer_name"],
|
"name" => $o["customer_name"],
|
||||||
"phone" => $o["customer_phone"],
|
"phone" => $o["customer_phone"],
|
||||||
"address" => $o["customer_address"],
|
"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"],
|
"subtotal" => $o["subtotal"] ?? 0, "vat" => $o["vat_amount"] ?? 0, "total" => $o["total_amount"],
|
||||||
"items" => $items
|
"items" => $items
|
||||||
], JSON_UNESCAPED_UNICODE), ENT_QUOTES, "UTF-8") ?>)'>
|
], JSON_UNESCAPED_UNICODE), ENT_QUOTES, "UTF-8") ?>)'>
|
||||||
@ -218,7 +225,7 @@ require __DIR__ . '/includes/header.php';
|
|||||||
<?php if(!empty($orders)): ?>
|
<?php if(!empty($orders)): ?>
|
||||||
<tfoot class="table-light">
|
<tfoot class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="text-end fw-bold"><?= h(tr('إجمالي المبالغ:', 'Total Amounts:')) ?></td>
|
<td colspan="6" class="text-end fw-bold"><?= h(tr('إجمالي المبالغ:', 'Total Amounts:')) ?></td>
|
||||||
<td colspan="3" class="fw-bold text-primary"><?= h(currency($totalAmount)) ?></td>
|
<td colspan="3" class="fw-bold text-primary"><?= h(currency($totalAmount)) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
@ -241,6 +248,8 @@ require __DIR__ . '/includes/header.php';
|
|||||||
<div class="col-6 mb-2"><small class="text-muted d-block"><?= h(tr('العميل', 'Customer')) ?></small><strong id="vName"></strong></div>
|
<div class="col-6 mb-2"><small class="text-muted d-block"><?= h(tr('العميل', 'Customer')) ?></small><strong id="vName"></strong></div>
|
||||||
<div class="col-6 mb-2"><small class="text-muted d-block"><?= h(tr('الهاتف', 'Phone')) ?></small><strong id="vPhone"></strong></div>
|
<div class="col-6 mb-2"><small class="text-muted d-block"><?= h(tr('الهاتف', 'Phone')) ?></small><strong id="vPhone"></strong></div>
|
||||||
<div class="col-12"><small class="text-muted d-block"><?= h(tr('العنوان', 'Address')) ?></small><strong id="vAddress"></strong></div>
|
<div class="col-12"><small class="text-muted d-block"><?= h(tr('العنوان', 'Address')) ?></small><strong id="vAddress"></strong></div>
|
||||||
|
<div class="col-6 mt-2"><small class="text-muted d-block"><?= h(tr('طريقة الدفع', 'Payment Method')) ?></small><strong id="vPaymentMethod"></strong></div>
|
||||||
|
<div class="col-6 mt-2"><small class="text-muted d-block"><?= h(tr('حالة الدفع', 'Payment Status')) ?></small><strong id="vPaymentStatus"></strong></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -276,6 +285,8 @@ function viewOrder(order) {
|
|||||||
document.getElementById('vName').innerText = order.name;
|
document.getElementById('vName').innerText = order.name;
|
||||||
document.getElementById('vPhone').innerText = order.phone ? ('968 ' + String(order.phone).replace(/^((00968)|(968))/, '')) : '';
|
document.getElementById('vPhone').innerText = order.phone ? ('968 ' + String(order.phone).replace(/^((00968)|(968))/, '')) : '';
|
||||||
document.getElementById('vAddress').innerText = order.address;
|
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('vSubtotal').innerText = Number(order.subtotal).toFixed(2);
|
||||||
document.getElementById('vVat').innerText = Number(order.vat).toFixed(2);
|
document.getElementById('vVat').innerText = Number(order.vat).toFixed(2);
|
||||||
document.getElementById('vTotal').innerText = Number(order.total).toFixed(2);
|
document.getElementById('vTotal').innerText = Number(order.total).toFixed(2);
|
||||||
|
|||||||
63
shop.php
63
shop.php
@ -3,6 +3,10 @@ require_once __DIR__ . '/includes/app.php';
|
|||||||
|
|
||||||
$forcePublic = true;
|
$forcePublic = true;
|
||||||
$pageTitle = tr('الطلب عبر الإنترنت', 'Online Ordering');
|
$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';
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
@ -110,6 +114,26 @@ body { background-color: #f8f9fa; }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($shopPaymentStatus !== ''): ?>
|
||||||
|
<?php
|
||||||
|
$alertClass = 'alert-info';
|
||||||
|
if ($shopPaymentStatus === 'paid') {
|
||||||
|
$alertClass = 'alert-success';
|
||||||
|
} elseif (in_array($shopPaymentStatus, ['cancelled', 'failed'], true)) {
|
||||||
|
$alertClass = 'alert-warning';
|
||||||
|
}
|
||||||
|
$defaultMessage = match ($shopPaymentStatus) {
|
||||||
|
'paid' => 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.'),
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<div class="alert <?= h($alertClass) ?> rounded-4 shadow-sm mb-4">
|
||||||
|
<?= h($shopPaymentMessage !== '' ? $shopPaymentMessage : $defaultMessage) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!empty($catalog)): ?>
|
<?php if (!empty($catalog)): ?>
|
||||||
<!-- Search and Filter -->
|
<!-- Search and Filter -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
@ -244,6 +268,30 @@ body { background-color: #f8f9fa; }
|
|||||||
<label class="form-label fw-semibold"><?= h(tr('العنوان', 'Address')) ?> *</label>
|
<label class="form-label fw-semibold"><?= h(tr('العنوان', 'Address')) ?> *</label>
|
||||||
<textarea class="form-control form-control-lg rounded-3" id="customerAddress" rows="2" required></textarea>
|
<textarea class="form-control form-control-lg rounded-3" id="customerAddress" rows="2" required></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold d-block mb-3"><?= h(tr('طريقة الدفع', 'Payment Method')) ?> *</label>
|
||||||
|
<div class="row g-3">
|
||||||
|
<?php if ($shopShowPayOnline): ?>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="border rounded-4 p-3 w-100 h-100 bg-light-subtle <?= $shopCanPayOnline ? '' : 'opacity-75' ?>">
|
||||||
|
<input class="form-check-input me-2" type="radio" name="payment_method" value="pay_online" <?= $shopCanPayOnline ? '' : 'disabled' ?>>
|
||||||
|
<span class="fw-bold d-block"><?= h(tr('ادفع أونلاين', 'Pay Online')) ?></span>
|
||||||
|
<span class="small text-muted"><?= h(tr('سيتم تحويلك إلى بوابة ثواني لإتمام الدفع مباشرة.', 'You will be redirected to Thawani checkout to complete payment.')) ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="border rounded-4 p-3 w-100 h-100 bg-light-subtle">
|
||||||
|
<input class="form-check-input me-2" type="radio" name="payment_method" value="pay_later" checked>
|
||||||
|
<span class="fw-bold d-block"><?= h(tr('ادفع لاحقاً', 'Pay Later')) ?></span>
|
||||||
|
<span class="small text-muted"><?= h(tr('سيتم إنشاء الطلب الآن وسيتم تحصيل المبلغ لاحقاً.', 'Your order will be created now and payment will be collected later.')) ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php if (!$shopCanPayOnline && $shopShowPayOnline): ?>
|
||||||
|
<div class="form-text mt-2 text-warning"><?= h(tr('خيار الدفع أونلاين ظاهر لكن مفاتيح ثواني غير مكتملة في الإعدادات بعد.', 'Pay Online is visible, but the Thawani keys are not fully configured in Settings yet.')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-top-0 pt-0 pb-4 px-4 d-flex justify-content-between">
|
<div class="modal-footer border-top-0 pt-0 pb-4 px-4 d-flex justify-content-between">
|
||||||
@ -370,10 +418,19 @@ async function submitOrder() {
|
|||||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const paymentMethodField = document.querySelector('input[name="payment_method"]:checked');
|
||||||
|
if (!paymentMethodField) {
|
||||||
|
Swal.fire('<?= h(tr('تنبيه', 'Warning')) ?>', '<?= h(tr('اختر طريقة الدفع.', 'Please choose a payment method.')) ?>', 'warning');
|
||||||
|
btn.innerHTML = origText;
|
||||||
|
btn.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
name: document.getElementById('customerName').value,
|
name: document.getElementById('customerName').value,
|
||||||
phone: document.getElementById('customerPhone').value,
|
phone: document.getElementById('customerPhone').value,
|
||||||
address: document.getElementById('customerAddress').value,
|
address: document.getElementById('customerAddress').value,
|
||||||
|
payment_method: paymentMethodField.value,
|
||||||
items: cart
|
items: cart
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -389,6 +446,12 @@ async function submitOrder() {
|
|||||||
cart = {};
|
cart = {};
|
||||||
saveCart();
|
saveCart();
|
||||||
if (cartModalInstance) cartModalInstance.hide();
|
if (cartModalInstance) cartModalInstance.hide();
|
||||||
|
|
||||||
|
if (json.redirect_url) {
|
||||||
|
window.location.href = json.redirect_url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: '<?= h(tr('تم إرسال الطلب بنجاح!', 'Order submitted successfully!')) ?>',
|
title: '<?= h(tr('تم إرسال الطلب بنجاح!', 'Order submitted successfully!')) ?>',
|
||||||
text: '<?= h(tr('سنتواصل معك قريباً لتأكيد الطلب.', 'We will contact you shortly to confirm the order.')) ?>',
|
text: '<?= h(tr('سنتواصل معك قريباً لتأكيد الطلب.', 'We will contact you shortly to confirm the order.')) ?>',
|
||||||
|
|||||||
96
thawani_return.php
Normal file
96
thawani_return.php
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/app.php';
|
||||||
|
|
||||||
|
$result = trim((string) ($_GET['result'] ?? 'cancel'));
|
||||||
|
$orderId = (int) ($_GET['order_id'] ?? 0);
|
||||||
|
|
||||||
|
if ($orderId <= 0) {
|
||||||
|
redirect_to('shop.php', [
|
||||||
|
'payment_status' => 'failed',
|
||||||
|
'message' => tr('تعذر العثور على الطلب.', 'Could not find the order.'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = db()->prepare('SELECT * FROM online_orders WHERE id = ? LIMIT 1');
|
||||||
|
$stmt->execute([$orderId]);
|
||||||
|
$order = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$order) {
|
||||||
|
redirect_to('shop.php', [
|
||||||
|
'payment_status' => 'failed',
|
||||||
|
'message' => tr('الطلب غير موجود.', 'The order was not found.'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = json_decode((string) ($order['items_json'] ?? '[]'), true) ?: [];
|
||||||
|
$oldStatus = (string) ($order['status'] ?? 'pending');
|
||||||
|
$currentPaymentStatus = (string) ($order['payment_status'] ?? 'unpaid');
|
||||||
|
$sessionId = trim((string) ($order['gateway_session_id'] ?? $_GET['session_id'] ?? ''));
|
||||||
|
|
||||||
|
$redirectStatus = 'failed';
|
||||||
|
$redirectMessage = tr('تعذر تأكيد الدفع. حاول مرة أخرى أو اختر الدفع لاحقاً.', 'We could not confirm the payment. Please try again or choose Pay Later.');
|
||||||
|
$sendCustomerWhatsapp = false;
|
||||||
|
|
||||||
|
if ($result === 'success' && $sessionId !== '') {
|
||||||
|
$verification = thawani_retrieve_session($sessionId);
|
||||||
|
if (!empty($verification['success']) && thawani_session_paid($verification)) {
|
||||||
|
$transactionId = thawani_session_transaction_id($verification);
|
||||||
|
$updateStmt = db()->prepare('UPDATE online_orders SET payment_status = ?, gateway_session_id = ?, gateway_transaction_id = ?, paid_at = NOW() WHERE id = ?');
|
||||||
|
$updateStmt->execute(['paid', $sessionId, $transactionId !== '' ? $transactionId : null, $orderId]);
|
||||||
|
|
||||||
|
$sendCustomerWhatsapp = true;
|
||||||
|
$redirectStatus = 'paid';
|
||||||
|
$redirectMessage = tr('تم الدفع بنجاح وتم استلام طلبك.', 'Payment completed successfully and your order was received.');
|
||||||
|
} else {
|
||||||
|
if ($oldStatus === 'pending') {
|
||||||
|
db()->beginTransaction();
|
||||||
|
try {
|
||||||
|
sync_online_order_stock_reservation($items, $oldStatus, $items, 'rejected');
|
||||||
|
$updateStmt = db()->prepare('UPDATE online_orders SET status = ?, payment_status = ? WHERE id = ?');
|
||||||
|
$updateStmt->execute(['rejected', 'failed', $orderId]);
|
||||||
|
db()->commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if (db()->inTransaction()) {
|
||||||
|
db()->rollBack();
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
} elseif ($currentPaymentStatus !== 'paid') {
|
||||||
|
$updateStmt = db()->prepare('UPDATE online_orders SET payment_status = ? WHERE id = ?');
|
||||||
|
$updateStmt->execute(['failed', $orderId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($currentPaymentStatus !== 'paid' && $oldStatus === 'pending') {
|
||||||
|
db()->beginTransaction();
|
||||||
|
try {
|
||||||
|
sync_online_order_stock_reservation($items, $oldStatus, $items, 'rejected');
|
||||||
|
$updateStmt = db()->prepare('UPDATE online_orders SET status = ?, payment_status = ? WHERE id = ?');
|
||||||
|
$updateStmt->execute(['rejected', 'cancelled', $orderId]);
|
||||||
|
db()->commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if (db()->inTransaction()) {
|
||||||
|
db()->rollBack();
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
} elseif ($currentPaymentStatus !== 'paid') {
|
||||||
|
$updateStmt = db()->prepare('UPDATE online_orders SET payment_status = ? WHERE id = ?');
|
||||||
|
$updateStmt->execute(['cancelled', $orderId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$redirectStatus = 'cancelled';
|
||||||
|
$redirectMessage = tr('تم إلغاء الدفع. يمكنك إعادة الطلب أو اختيار الدفع لاحقاً.', 'Payment was cancelled. You can place the order again or choose Pay Later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sendCustomerWhatsapp && wablas_is_configured()) {
|
||||||
|
try {
|
||||||
|
wablas_notify_online_order_by_id($orderId, 'created');
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('Customer WhatsApp notify failed after Thawani payment for order #' . $orderId . ': ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect_to('shop.php', [
|
||||||
|
'payment_status' => $redirectStatus,
|
||||||
|
'message' => $redirectMessage,
|
||||||
|
]);
|
||||||
Loading…
x
Reference in New Issue
Block a user