Update: 2026-05-08 04:58:23
This commit is contained in:
188
app/modules_app/sms/receive.php
Normal file
188
app/modules_app/sms/receive.php
Normal 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' => 'خطأ أثناء تأكيد الدفعة'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user