189 lines
7.0 KiB
PHP
189 lines
7.0 KiB
PHP
<?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' => 'خطأ أثناء تأكيد الدفعة'];
|
||
}
|
||
}
|