Update: 2026-05-08 04:58:23

This commit is contained in:
Hamza-Ayed
2026-05-08 04:58:23 +03:00
parent 4721ca83da
commit 6db8986fca
48 changed files with 2212 additions and 108 deletions

View File

@@ -0,0 +1,188 @@
<?php
/**
* SMS Bank Integration — Receive & Auto-Match Payments
* POST /v1/sms/receive
*
* Flow:
* 1. Android SMS Bot intercepts bank/wallet SMS
* 2. Sends it here: { "sender": "BANK_NAME", "message": "تم تحويل 45 دينار..." }
* 3. We save it in raw_sms_log with status "pending"
* 4. We immediately try to match it against pending payment requests
* 5. If matched → confirm payment → update subscription → notify user
*/
declare(strict_types=1);
use App\Core\Database;
use App\Core\AuditLogger;
// Auth: Verify webhook secret (shared between Android bot and server)
$webhookSecret = env('SMS_WEBHOOK_SECRET', '');
$incomingSecret = $_SERVER['HTTP_X_WEBHOOK_SECRET'] ?? $_SERVER['HTTP_X_SMS_SECRET'] ?? '';
if (!empty($webhookSecret) && !hash_equals($webhookSecret, $incomingSecret)) {
http_response_code(401);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
exit;
}
$json_data = file_get_contents('php://input');
$data = json_decode($json_data, true);
if (!$data || empty($data['sender']) || empty($data['message'])) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'بيانات غير مكتملة. يجب إرسال sender و message.']);
exit;
}
$sender = trim($data['sender']);
$message = trim($data['message']);
$db = Database::getInstance();
try {
// 1. Save raw SMS log
$smsId = \App\Core\Database::generateUuid();
$stmt = $db->prepare("
INSERT INTO raw_sms_log (id, sender, message_body, status, received_at)
VALUES (?, ?, ?, 'pending', NOW())
");
$stmt->execute([$smsId, $sender, $message]);
// 2. Try to auto-match with pending payments
$matchResult = matchPayment($db, $smsId, $sender, $message);
http_response_code(200);
echo json_encode([
'status' => 'success',
'message' => 'SMS received and processed.',
'matched' => $matchResult['matched'],
'details' => $matchResult['details'] ?? null,
], JSON_UNESCAPED_UNICODE);
} catch (\Exception $e) {
error_log("[sms/receive] Error: " . $e->getMessage());
http_response_code(200); // Return 200 so bot doesn't retry
echo json_encode(['status' => 'error', 'message' => 'خطأ داخلي في المعالجة.']);
}
/**
* Try to match the incoming SMS with a pending payment request.
*
* Matching logic:
* 1. Extract reference number from SMS (formats: MSQ-XXXX, REF-XXXX, or plain digits)
* 2. Extract amount from SMS
* 3. Find pending payment request matching reference OR amount
* 4. If matched → confirm payment → activate/extend subscription
*/
function matchPayment(\PDO $db, string $smsId, string $sender, string $message): array
{
// Extract reference number (MSQ-XXXX pattern or any 6+ digit number)
$reference = null;
if (preg_match('/MSQ-([A-Z0-9]{4,10})/i', $message, $m)) {
$reference = 'MSQ-' . strtoupper($m[1]);
} elseif (preg_match('/REF[:\s-]*([A-Z0-9]{4,12})/i', $message, $m)) {
$reference = $m[1];
}
// Extract amount (Arabic or English digits)
$amount = null;
$msgNormalized = strtr($message, ['٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9']);
if (preg_match('/(\d+[\.,]?\d{0,3})\s*(دينار|JOD|JD)/iu', $msgNormalized, $m)) {
$amount = (float)str_replace(',', '.', $m[1]);
} elseif (preg_match('/(\d+[\.,]\d{2})/', $msgNormalized, $m)) {
$amount = (float)str_replace(',', '.', $m[1]);
}
if (!$reference && !$amount) {
// Can't match — mark SMS as unmatched
$db->prepare("UPDATE raw_sms_log SET status = 'unmatched', processed_at = NOW() WHERE id = ?")->execute([$smsId]);
return ['matched' => false, 'details' => 'لم يتم العثور على مرجع أو مبلغ في الرسالة'];
}
// Search for pending payment request
$where = "pr.status = 'pending'";
$params = [];
if ($reference) {
$where .= " AND pr.reference_number = ?";
$params[] = $reference;
}
if ($amount) {
$where .= " AND pr.amount = ?";
$params[] = $amount;
}
$stmt = $db->prepare("
SELECT pr.*, t.name as tenant_name
FROM payment_requests pr
LEFT JOIN tenants t ON pr.tenant_id = t.id
WHERE {$where}
ORDER BY pr.created_at DESC
LIMIT 1
");
$stmt->execute($params);
$payment = $stmt->fetch();
if (!$payment) {
$db->prepare("UPDATE raw_sms_log SET status = 'unmatched', extracted_ref = ?, extracted_amount = ?, processed_at = NOW() WHERE id = ?")
->execute([$reference, $amount, $smsId]);
return ['matched' => false, 'details' => "مرجع: {$reference}, مبلغ: {$amount} — لم يتطابق مع أي طلب دفع"];
}
// MATCH FOUND — Process payment
$db->beginTransaction();
try {
// 1. Update payment request → confirmed
$db->prepare("
UPDATE payment_requests SET status = 'confirmed', sms_log_id = ?, confirmed_at = NOW() WHERE id = ?
")->execute([$smsId, $payment['id']]);
// 2. Update SMS log → matched
$db->prepare("
UPDATE raw_sms_log SET status = 'matched', payment_request_id = ?, extracted_ref = ?, extracted_amount = ?, processed_at = NOW() WHERE id = ?
")->execute([$payment['id'], $reference, $amount, $smsId]);
// 3. Activate/extend subscription
$planMonths = (int)($payment['plan_months'] ?? 1);
$db->prepare("
UPDATE subscriptions
SET is_active = 1,
started_at = COALESCE(started_at, NOW()),
expires_at = DATE_ADD(COALESCE(expires_at, NOW()), INTERVAL ? MONTH),
updated_at = NOW()
WHERE tenant_id = ?
")->execute([$planMonths, $payment['tenant_id']]);
// 4. Notify user
\App\Services\SmartNotifications::send(
$payment['tenant_id'],
$payment['user_id'] ?? '',
'payment_confirmed',
'✅ تم تأكيد الدفع!',
"تم تأكيد دفعة بقيمة {$payment['amount']} دينار. اشتراكك فعّال الآن.",
['payment_id' => $payment['id'], 'amount' => $payment['amount']]
);
// 5. Audit log
AuditLogger::log('payment.auto_confirmed', 'payment', $payment['id'], null, [
'sms_id' => $smsId,
'sender' => $sender,
'reference' => $reference,
'amount' => $amount,
], ['user_id' => 'system', 'tenant_id' => $payment['tenant_id'], 'role' => 'system']);
$db->commit();
return [
'matched' => true,
'details' => "تم مطابقة الدفعة: {$payment['amount']} دينار — الاشتراك مُفعّل",
];
} catch (\Exception $e) {
$db->rollBack();
error_log("[sms/match] Failed: " . $e->getMessage());
return ['matched' => false, 'details' => 'خطأ أثناء تأكيد الدفعة'];
}
}