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\Database;
|
||||||
use App\Core\Security;
|
use App\Core\Security;
|
||||||
use App\Core\Validator;
|
use App\Core\Validator;
|
||||||
|
use App\Core\PaymentParser;
|
||||||
|
|
||||||
$data = Security::sanitize(input());
|
$data = Security::sanitize(input());
|
||||||
|
|
||||||
@@ -37,22 +38,10 @@ $bankReference = trim($data['bank_reference'] ?? '');
|
|||||||
$amount = (float)($data['amount'] ?? 0);
|
$amount = (float)($data['amount'] ?? 0);
|
||||||
$senderName = $data['sender_name'] ?? 'غير معروف';
|
$senderName = $data['sender_name'] ?? 'غير معروف';
|
||||||
|
|
||||||
// Robust Parsing Fallback (for Orange Money / CliQ Jordan)
|
// Robust Parsing (for Orange Money / CliQ Jordan)
|
||||||
if (empty($bankReference) || $amount <= 0) {
|
if (empty($bankReference) || $amount <= 0) {
|
||||||
// Try to extract amount: بمبلغ 15 دينار
|
$bankReference = PaymentParser::extractReference($rawMessage) ?: $bankReference;
|
||||||
if (preg_match('/بمبلغ\s+([\d\.]+)\s+دينار/', $rawMessage, $matches)) {
|
$amount = PaymentParser::extractAmount($rawMessage) ?: $amount;
|
||||||
$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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($bankReference) || $amount <= 0) {
|
if (empty($bankReference) || $amount <= 0) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
use App\Core\Database;
|
use App\Core\Database;
|
||||||
use App\Core\AI;
|
use App\Core\AI;
|
||||||
|
use App\Core\PaymentParser;
|
||||||
use App\Middleware\AuthMiddleware;
|
use App\Middleware\AuthMiddleware;
|
||||||
|
|
||||||
$decoded = AuthMiddleware::check();
|
$decoded = AuthMiddleware::check();
|
||||||
@@ -20,18 +21,17 @@ if (!in_array($decoded['role'], ['admin', 'accountant'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$paymentId = $_POST['payment_id'] ?? null;
|
$paymentId = $_POST['payment_id'] ?? null;
|
||||||
$bankRef = trim($_POST['bank_reference'] ?? '');
|
$rawBankRef = trim($_POST['bank_reference'] ?? '');
|
||||||
|
$bankRef = PaymentParser::extractReference($rawBankRef) ?: $rawBankRef;
|
||||||
|
|
||||||
if (!$paymentId) {
|
if (!$paymentId) {
|
||||||
json_error('معرف طلب الدفع مطلوب.', 422);
|
json_error('معرف طلب الدفع مطلوب.', 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$bankRef) {
|
$hasReceipt = isset($_FILES['receipt']) && $_FILES['receipt']['error'] === UPLOAD_ERR_OK;
|
||||||
json_error('رقم مرجع الحوالة مطلوب للتفعيل الآلي.', 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($_FILES['receipt']) || $_FILES['receipt']['error'] !== UPLOAD_ERR_OK) {
|
if (!$bankRef && !$hasReceipt) {
|
||||||
json_error('صورة وصل الدفع مطلوبة.', 422);
|
json_error('الرجاء إدخال رقم المرجع (أو نص الرسالة) أو إرفاق صورة الوصل.', 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
$db = Database::getInstance();
|
$db = Database::getInstance();
|
||||||
@@ -79,7 +79,16 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. If no immediate match, save the receipt and wait for AI/Bot backup
|
// 3. If no immediate match and no receipt image, we can't do more
|
||||||
|
if (!$hasReceipt && !$transaction) {
|
||||||
|
json_error('لم نتمكن من التحقق التلقائي من الرقم المرجعي. يرجى إرفاق صورة الوصل للمراجعة اليدوية.', 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$aiResult = [];
|
||||||
|
$matchScore = 0.0;
|
||||||
|
$filepath = null;
|
||||||
|
|
||||||
|
if ($hasReceipt) {
|
||||||
$uploadDir = STORAGE_PATH . '/receipts/' . $tenantId;
|
$uploadDir = STORAGE_PATH . '/receipts/' . $tenantId;
|
||||||
if (!is_dir($uploadDir)) {
|
if (!is_dir($uploadDir)) {
|
||||||
mkdir($uploadDir, 0750, true);
|
mkdir($uploadDir, 0750, true);
|
||||||
@@ -93,10 +102,10 @@ try {
|
|||||||
json_error('فشل في حفظ صورة الوصل.', 500);
|
json_error('فشل في حفظ صورة الوصل.', 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. AI Analysis of receipt image
|
// 4. AI Analysis of receipt image
|
||||||
$aiResult = analyzeReceipt($filepath, $payment);
|
$aiResult = analyzeReceipt($filepath, $payment);
|
||||||
|
|
||||||
// 4. Smart Match: Use AI-extracted reference to search bank_transactions
|
// 5. Smart Match: Use AI-extracted reference to search bank_transactions
|
||||||
$aiExtractedRef = trim($aiResult['reference_number'] ?? '');
|
$aiExtractedRef = trim($aiResult['reference_number'] ?? '');
|
||||||
if (!empty($aiExtractedRef) && $aiExtractedRef !== 'unknown') {
|
if (!empty($aiExtractedRef) && $aiExtractedRef !== 'unknown') {
|
||||||
$stmt = $db->prepare("SELECT * FROM bank_transactions WHERE bank_reference = ? AND is_claimed = 0 LIMIT 1");
|
$stmt = $db->prepare("SELECT * FROM bank_transactions WHERE bank_reference = ? AND is_claimed = 0 LIMIT 1");
|
||||||
@@ -108,12 +117,9 @@ try {
|
|||||||
$actualAmount = (float)$aiMatchTransaction['amount'];
|
$actualAmount = (float)$aiMatchTransaction['amount'];
|
||||||
|
|
||||||
if (abs($expectedAmount - $actualAmount) < 0.1) {
|
if (abs($expectedAmount - $actualAmount) < 0.1) {
|
||||||
// AI FOUND THE MATCH!
|
|
||||||
activateSubscription($db, $payment, $decoded['user_id']);
|
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 = $db->prepare("UPDATE payment_requests SET status = 'approved', bank_reference = ?, verified_at = NOW() WHERE id = ?");
|
$stmt->execute([$aiExtractedRef, $filepath, $paymentId]);
|
||||||
$stmt->execute([$aiExtractedRef, $paymentId]);
|
|
||||||
|
|
||||||
$stmt = $db->prepare("UPDATE bank_transactions SET is_claimed = 1 WHERE id = ?");
|
$stmt = $db->prepare("UPDATE bank_transactions SET is_claimed = 1 WHERE id = ?");
|
||||||
$stmt->execute([$aiMatchTransaction['id']]);
|
$stmt->execute([$aiMatchTransaction['id']]);
|
||||||
|
|
||||||
@@ -127,10 +133,11 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Calculate match score (for legacy manual review fallback)
|
// 6. Calculate match score
|
||||||
$matchScore = calculateMatchScore($aiResult, $payment);
|
$matchScore = calculateMatchScore($aiResult, $payment);
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Update payment request with AI data
|
// 7. Update payment request
|
||||||
$newStatus = $matchScore >= 85.0 ? 'verified' : 'uploaded';
|
$newStatus = $matchScore >= 85.0 ? 'verified' : 'uploaded';
|
||||||
|
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
@@ -150,7 +157,7 @@ try {
|
|||||||
$paymentId
|
$paymentId
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 7. Final attempt activation if high confidence match score
|
// 8. Final attempt activation if high confidence
|
||||||
if ($matchScore >= 90.0) {
|
if ($matchScore >= 90.0) {
|
||||||
activateSubscription($db, $payment, $decoded['user_id']);
|
activateSubscription($db, $payment, $decoded['user_id']);
|
||||||
$stmt = $db->prepare("UPDATE payment_requests SET status = 'approved', verified_at = NOW() WHERE id = ?");
|
$stmt = $db->prepare("UPDATE payment_requests SET status = 'approved', verified_at = NOW() WHERE id = ?");
|
||||||
@@ -166,8 +173,8 @@ try {
|
|||||||
json_success([
|
json_success([
|
||||||
'status' => $newStatus,
|
'status' => $newStatus,
|
||||||
'match_score' => $matchScore,
|
'match_score' => $matchScore,
|
||||||
'message' => $matchScore >= 70 ? 'تم استلام الوصل بنجاح، جاري المراجعة النهائية.' : 'تم استلام الوصل، بانتظار تأكيد الحوالة من البنك.'
|
'message' => $matchScore >= 70 ? 'تم استلام الوصل بنجاح، جاري المراجعة النهائية.' : 'تم استلام الطلب، بانتظار تأكيد الحوالة من البنك.'
|
||||||
], 'تم رفع الوصل');
|
], 'تم الاستلام');
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("Payment Receipt Upload Error: " . $e->getMessage());
|
error_log("Payment Receipt Upload Error: " . $e->getMessage());
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ declare(strict_types=1);
|
|||||||
use App\Core\Database;
|
use App\Core\Database;
|
||||||
use App\Core\Security;
|
use App\Core\Security;
|
||||||
use App\Core\Validator;
|
use App\Core\Validator;
|
||||||
|
use App\Core\PaymentParser;
|
||||||
use App\Middleware\AuthMiddleware;
|
use App\Middleware\AuthMiddleware;
|
||||||
|
|
||||||
$decoded = AuthMiddleware::check();
|
$decoded = AuthMiddleware::check();
|
||||||
@@ -23,7 +24,8 @@ if (!in_array($decoded['role'], ['admin', 'accountant', 'super_admin'])) {
|
|||||||
|
|
||||||
$data = Security::sanitize(input());
|
$data = Security::sanitize(input());
|
||||||
$paymentId = $data['payment_id'] ?? null;
|
$paymentId = $data['payment_id'] ?? null;
|
||||||
$bankReference = trim($data['bank_reference'] ?? '');
|
$rawBankRef = trim($data['bank_reference'] ?? '');
|
||||||
|
$bankReference = PaymentParser::extractReference($rawBankRef) ?: $rawBankRef;
|
||||||
|
|
||||||
$errors = Validator::validate($data, [
|
$errors = Validator::validate($data, [
|
||||||
'payment_id' => 'required',
|
'payment_id' => 'required',
|
||||||
|
|||||||
Reference in New Issue
Block a user