Update: 2026-05-09 18:11:10

This commit is contained in:
Hamza-Ayed
2026-05-09 18:11:10 +03:00
parent e1bdda3cbf
commit 0dbf812be4
4 changed files with 127 additions and 63 deletions

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Core;
class PaymentParser
{
/**
* Extract reference number from raw SMS text
*/
public static function extractReference(string $text): ?string
{
$text = trim($text);
if (empty($text)) return null;
// If it's already a single word (likely just the ref number), return it
if (!str_contains($text, ' ') && strlen($text) > 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;
}
}

View File

@@ -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) {

View File

@@ -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());

View File

@@ -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',