From 0dbf812be4962de05ebdca5e149f4d123e27aa95 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 9 May 2026 18:11:10 +0300 Subject: [PATCH] Update: 2026-05-09 18:11:10 --- app/Core/PaymentParser.php | 66 ++++++++++++ app/modules_app/payments/bot_webhook.php | 19 +--- app/modules_app/payments/upload_receipt.php | 101 ++++++++++-------- app/modules_app/payments/verify_reference.php | 4 +- 4 files changed, 127 insertions(+), 63 deletions(-) create mode 100644 app/Core/PaymentParser.php diff --git a/app/Core/PaymentParser.php b/app/Core/PaymentParser.php new file mode 100644 index 0000000..111f05f --- /dev/null +++ b/app/Core/PaymentParser.php @@ -0,0 +1,66 @@ + 5) { + return strtoupper($text); + } + + // 1. Orange Money / Jordanian Arabic format: بالرقم المرجعي JIBA... or OJM... + if (preg_match('/بالرقم المرجعي\s+([A-Z0-9\-]+)/i', $text, $matches)) { + return strtoupper($matches[1]); + } + + // 2. English "Ref" format: Ref CS260210... + if (preg_match('/Ref\s+([A-Z0-9]+)/i', $text, $matches)) { + return strtoupper($matches[1]); + } + + // 3. Generic "Reference" or "رقم الحوالة" + if (preg_match('/(?:Reference|المرجع|رقم الحوالة|رقم العملية)[:\s]+([A-Z0-9\-]+)/iu', $text, $matches)) { + return strtoupper($matches[1]); + } + + // 4. Try to find any long alphanumeric string that looks like a ref (8+ chars) + // This is a fallback and might be risky, but useful for copy-pasting just the ref. + if (preg_match('/([A-Z]{1,4}[0-9]{5,})/i', $text, $matches)) { + return strtoupper($matches[0]); + } + + return null; + } + + /** + * Extract amount from raw SMS text + */ + public static function extractAmount(string $text): float + { + // بمبلغ 61.25 دينار + if (preg_match('/بمبلغ\s+([\d\.]+)/u', $text, $matches)) { + return (float)$matches[1]; + } + + // JOD 28.550 + if (preg_match('/JOD\s+([\d\.]+)/i', $text, $matches)) { + return (float)$matches[1]; + } + + // 1.5 دينار اردني + if (preg_match('/([\d\.]+)\s+دينار/u', $text, $matches)) { + return (float)$matches[1]; + } + + return 0.0; + } +} diff --git a/app/modules_app/payments/bot_webhook.php b/app/modules_app/payments/bot_webhook.php index 67810af..bb50ca9 100644 --- a/app/modules_app/payments/bot_webhook.php +++ b/app/modules_app/payments/bot_webhook.php @@ -13,6 +13,7 @@ declare(strict_types=1); use App\Core\Database; use App\Core\Security; use App\Core\Validator; +use App\Core\PaymentParser; $data = Security::sanitize(input()); @@ -37,22 +38,10 @@ $bankReference = trim($data['bank_reference'] ?? ''); $amount = (float)($data['amount'] ?? 0); $senderName = $data['sender_name'] ?? 'غير معروف'; -// Robust Parsing Fallback (for Orange Money / CliQ Jordan) +// Robust Parsing (for Orange Money / CliQ Jordan) if (empty($bankReference) || $amount <= 0) { - // Try to extract amount: بمبلغ 15 دينار - if (preg_match('/بمبلغ\s+([\d\.]+)\s+دينار/', $rawMessage, $matches)) { - $amount = (float)$matches[1]; - } - - // Try to extract reference: بالرقم المرجعي JIBA... - if (preg_match('/بالرقم المرجعي\s+([A-Z0-9a-z]+)/', $rawMessage, $matches)) { - $bankReference = $matches[1]; - } - - // Try to extract sender: من FERAS... - if (preg_match('/من\s+([^من]+)\s+من مزود/', $rawMessage, $matches)) { - $senderName = trim($matches[1]); - } + $bankReference = PaymentParser::extractReference($rawMessage) ?: $bankReference; + $amount = PaymentParser::extractAmount($rawMessage) ?: $amount; } if (empty($bankReference) || $amount <= 0) { diff --git a/app/modules_app/payments/upload_receipt.php b/app/modules_app/payments/upload_receipt.php index 46ab2b5..f257c88 100644 --- a/app/modules_app/payments/upload_receipt.php +++ b/app/modules_app/payments/upload_receipt.php @@ -11,6 +11,7 @@ declare(strict_types=1); use App\Core\Database; use App\Core\AI; +use App\Core\PaymentParser; use App\Middleware\AuthMiddleware; $decoded = AuthMiddleware::check(); @@ -20,18 +21,17 @@ if (!in_array($decoded['role'], ['admin', 'accountant'])) { } $paymentId = $_POST['payment_id'] ?? null; -$bankRef = trim($_POST['bank_reference'] ?? ''); +$rawBankRef = trim($_POST['bank_reference'] ?? ''); +$bankRef = PaymentParser::extractReference($rawBankRef) ?: $rawBankRef; if (!$paymentId) { json_error('معرف طلب الدفع مطلوب.', 422); } -if (!$bankRef) { - json_error('رقم مرجع الحوالة مطلوب للتفعيل الآلي.', 422); -} +$hasReceipt = isset($_FILES['receipt']) && $_FILES['receipt']['error'] === UPLOAD_ERR_OK; -if (!isset($_FILES['receipt']) || $_FILES['receipt']['error'] !== UPLOAD_ERR_OK) { - json_error('صورة وصل الدفع مطلوبة.', 422); +if (!$bankRef && !$hasReceipt) { + json_error('الرجاء إدخال رقم المرجع (أو نص الرسالة) أو إرفاق صورة الوصل.', 422); } $db = Database::getInstance(); @@ -79,58 +79,65 @@ try { } } - // 3. If no immediate match, save the receipt and wait for AI/Bot backup - $uploadDir = STORAGE_PATH . '/receipts/' . $tenantId; - if (!is_dir($uploadDir)) { - mkdir($uploadDir, 0750, true); + // 3. If no immediate match and no receipt image, we can't do more + if (!$hasReceipt && !$transaction) { + json_error('لم نتمكن من التحقق التلقائي من الرقم المرجعي. يرجى إرفاق صورة الوصل للمراجعة اليدوية.', 422); } - $ext = pathinfo($_FILES['receipt']['name'], PATHINFO_EXTENSION) ?: 'jpg'; - $filename = $paymentId . '_' . time() . '.' . $ext; - $filepath = $uploadDir . '/' . $filename; + $aiResult = []; + $matchScore = 0.0; + $filepath = null; - if (!move_uploaded_file($_FILES['receipt']['tmp_name'], $filepath)) { - json_error('فشل في حفظ صورة الوصل.', 500); - } + if ($hasReceipt) { + $uploadDir = STORAGE_PATH . '/receipts/' . $tenantId; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0750, true); + } - // 3. AI Analysis of receipt image - $aiResult = analyzeReceipt($filepath, $payment); + $ext = pathinfo($_FILES['receipt']['name'], PATHINFO_EXTENSION) ?: 'jpg'; + $filename = $paymentId . '_' . time() . '.' . $ext; + $filepath = $uploadDir . '/' . $filename; - // 4. Smart Match: Use AI-extracted reference to search bank_transactions - $aiExtractedRef = trim($aiResult['reference_number'] ?? ''); - if (!empty($aiExtractedRef) && $aiExtractedRef !== 'unknown') { - $stmt = $db->prepare("SELECT * FROM bank_transactions WHERE bank_reference = ? AND is_claimed = 0 LIMIT 1"); - $stmt->execute([$aiExtractedRef]); - $aiMatchTransaction = $stmt->fetch(); + if (!move_uploaded_file($_FILES['receipt']['tmp_name'], $filepath)) { + json_error('فشل في حفظ صورة الوصل.', 500); + } - if ($aiMatchTransaction) { - $expectedAmount = (float)$payment['amount_jod']; - $actualAmount = (float)$aiMatchTransaction['amount']; + // 4. AI Analysis of receipt image + $aiResult = analyzeReceipt($filepath, $payment); - if (abs($expectedAmount - $actualAmount) < 0.1) { - // AI FOUND THE MATCH! - activateSubscription($db, $payment, $decoded['user_id']); + // 5. Smart Match: Use AI-extracted reference to search bank_transactions + $aiExtractedRef = trim($aiResult['reference_number'] ?? ''); + if (!empty($aiExtractedRef) && $aiExtractedRef !== 'unknown') { + $stmt = $db->prepare("SELECT * FROM bank_transactions WHERE bank_reference = ? AND is_claimed = 0 LIMIT 1"); + $stmt->execute([$aiExtractedRef]); + $aiMatchTransaction = $stmt->fetch(); - $stmt = $db->prepare("UPDATE payment_requests SET status = 'approved', bank_reference = ?, verified_at = NOW() WHERE id = ?"); - $stmt->execute([$aiExtractedRef, $paymentId]); + if ($aiMatchTransaction) { + $expectedAmount = (float)$payment['amount_jod']; + $actualAmount = (float)$aiMatchTransaction['amount']; - $stmt = $db->prepare("UPDATE bank_transactions SET is_claimed = 1 WHERE id = ?"); - $stmt->execute([$aiMatchTransaction['id']]); + if (abs($expectedAmount - $actualAmount) < 0.1) { + activateSubscription($db, $payment, $decoded['user_id']); + $stmt = $db->prepare("UPDATE payment_requests SET status = 'approved', bank_reference = ?, receipt_image_path = ?, verified_at = NOW() WHERE id = ?"); + $stmt->execute([$aiExtractedRef, $filepath, $paymentId]); + $stmt = $db->prepare("UPDATE bank_transactions SET is_claimed = 1 WHERE id = ?"); + $stmt->execute([$aiMatchTransaction['id']]); - json_success([ - 'status' => 'approved', - 'auto_verified' => true, - 'method' => 'ai_ref_matching', - 'message' => 'تم العثور على الحوالة بنجاح وتفعيل الاشتراك آلياً!' - ], 'تم تفعيل الاشتراك بنجاح'); + json_success([ + 'status' => 'approved', + 'auto_verified' => true, + 'method' => 'ai_ref_matching', + 'message' => 'تم العثور على الحوالة بنجاح وتفعيل الاشتراك آلياً!' + ], 'تم تفعيل الاشتراك بنجاح'); + } } } + + // 6. Calculate match score + $matchScore = calculateMatchScore($aiResult, $payment); } - // 5. Calculate match score (for legacy manual review fallback) - $matchScore = calculateMatchScore($aiResult, $payment); - - // 6. Update payment request with AI data + // 7. Update payment request $newStatus = $matchScore >= 85.0 ? 'verified' : 'uploaded'; $stmt = $db->prepare(" @@ -150,7 +157,7 @@ try { $paymentId ]); - // 7. Final attempt activation if high confidence match score + // 8. Final attempt activation if high confidence if ($matchScore >= 90.0) { activateSubscription($db, $payment, $decoded['user_id']); $stmt = $db->prepare("UPDATE payment_requests SET status = 'approved', verified_at = NOW() WHERE id = ?"); @@ -166,8 +173,8 @@ try { json_success([ 'status' => $newStatus, 'match_score' => $matchScore, - 'message' => $matchScore >= 70 ? 'تم استلام الوصل بنجاح، جاري المراجعة النهائية.' : 'تم استلام الوصل، بانتظار تأكيد الحوالة من البنك.' - ], 'تم رفع الوصل'); + 'message' => $matchScore >= 70 ? 'تم استلام الوصل بنجاح، جاري المراجعة النهائية.' : 'تم استلام الطلب، بانتظار تأكيد الحوالة من البنك.' + ], 'تم الاستلام'); } catch (\Exception $e) { error_log("Payment Receipt Upload Error: " . $e->getMessage()); diff --git a/app/modules_app/payments/verify_reference.php b/app/modules_app/payments/verify_reference.php index 9349095..6b30b8a 100644 --- a/app/modules_app/payments/verify_reference.php +++ b/app/modules_app/payments/verify_reference.php @@ -13,6 +13,7 @@ declare(strict_types=1); use App\Core\Database; use App\Core\Security; use App\Core\Validator; +use App\Core\PaymentParser; use App\Middleware\AuthMiddleware; $decoded = AuthMiddleware::check(); @@ -23,7 +24,8 @@ if (!in_array($decoded['role'], ['admin', 'accountant', 'super_admin'])) { $data = Security::sanitize(input()); $paymentId = $data['payment_id'] ?? null; -$bankReference = trim($data['bank_reference'] ?? ''); +$rawBankRef = trim($data['bank_reference'] ?? ''); +$bankReference = PaymentParser::extractReference($rawBankRef) ?: $rawBankRef; $errors = Validator::validate($data, [ 'payment_id' => 'required',