Files
musadaq-saas/app/modules_app/sms/receive.php
2026-05-08 04:58:23 +03:00

189 lines
7.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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' => 'خطأ أثناء تأكيد الدفعة'];
}
}