Update: 2026-05-09 18:11:10
This commit is contained in:
66
app/Core/PaymentParser.php
Normal file
66
app/Core/PaymentParser.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user