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

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