Deploy: 2026-06-18 15:04:52
This commit is contained in:
@@ -742,11 +742,53 @@ class WhatsAppController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call external API to verify payment slip
|
* Verify payment — sends data to payment server for AI verification or status query.
|
||||||
|
*
|
||||||
|
* Two modes:
|
||||||
|
* 1. AI Verification (receiptImage given): sends phone + invoice_number + receipt_image
|
||||||
|
* to payment server, which runs GeminiAi::verifyPayment().
|
||||||
|
* 2. Status Query (only jsonStr): parses {transaction_id, amount, method} from Gemini
|
||||||
|
* chatbot auto-detection, sends as status query to payment server.
|
||||||
*/
|
*/
|
||||||
public static function verifyPaymentSlipStatic(int $companyId, string $phone, string $jsonStr): ?string
|
public static function verifyPaymentSlipStatic(
|
||||||
|
int $companyId,
|
||||||
|
string $phone,
|
||||||
|
string $jsonStr,
|
||||||
|
string $userType = 'driver',
|
||||||
|
string $paymentMethod = '',
|
||||||
|
string $invoiceNumber = '',
|
||||||
|
string $receiptImage = '',
|
||||||
|
string $imageMimeType = 'image/jpeg'
|
||||||
|
): ?string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$apiUrl = getenv('PAYMENT_API_URL') ?: getenv('ENTALEQ_PAYMENT_API_URL');
|
||||||
|
if (empty($apiUrl)) {
|
||||||
|
$appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/');
|
||||||
|
$apiUrl = $appUrl . '/api/external/verify-payment';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($apiUrl, '/ride/nabeh/verify_payment.php') === false
|
||||||
|
&& strpos($apiUrl, '/api/external/verify-payment') === false) {
|
||||||
|
$apiUrl = rtrim($apiUrl, '/') . '/ride/nabeh/verify_payment.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = getenv('NABEH_API_KEY') ?: '';
|
||||||
|
$headers = [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'X-API-Key: ' . $apiKey,
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Mode 1: AI Verification (receipt image → auto-find invoice by phone) ──
|
||||||
|
if (!empty($receiptImage)) {
|
||||||
|
$payload = json_encode([
|
||||||
|
'phone' => $phone,
|
||||||
|
'payment_method' => $paymentMethod,
|
||||||
|
'receipt_image' => $receiptImage,
|
||||||
|
'image_mime_type' => $imageMimeType,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// ── Mode 2: Status query from chatbot auto-detection ──────────────
|
||||||
$data = json_decode($jsonStr, true);
|
$data = json_decode($jsonStr, true);
|
||||||
if (!$data) {
|
if (!$data) {
|
||||||
return null;
|
return null;
|
||||||
@@ -754,76 +796,60 @@ class WhatsAppController extends BaseController
|
|||||||
|
|
||||||
$transactionId = $data['transaction_id'] ?? '';
|
$transactionId = $data['transaction_id'] ?? '';
|
||||||
$amount = $data['amount'] ?? '';
|
$amount = $data['amount'] ?? '';
|
||||||
$method = $data['method'] ?? '';
|
$method = $data['method'] ?? $paymentMethod;
|
||||||
|
|
||||||
if (empty($transactionId) || empty($amount)) {
|
if (empty($transactionId) && empty($amount) && empty($invoiceNumber)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find configured endpoint for verify_payment
|
|
||||||
$endpoint = \App\Models\CompanyEndpoint::findByAction($companyId, 'verify_payment');
|
|
||||||
$apiUrl = $endpoint ? $endpoint['endpoint_url'] : null;
|
|
||||||
|
|
||||||
if (empty($apiUrl)) {
|
|
||||||
// Fallback to local default mock
|
|
||||||
$apiUrl = getenv('ENTALEQ_PAYMENT_API_URL');
|
|
||||||
if (empty($apiUrl)) {
|
|
||||||
$appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/');
|
|
||||||
$apiUrl = $appUrl . '/api/external/verify-payment';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$payload = json_encode([
|
$payload = json_encode([
|
||||||
'phone' => $phone,
|
'phone' => $phone,
|
||||||
|
'payment_method' => $method,
|
||||||
'transaction_id' => $transactionId,
|
'transaction_id' => $transactionId,
|
||||||
'amount' => $amount,
|
'amount' => is_numeric($amount) ? (float) $amount : 0,
|
||||||
'method' => $method
|
'invoice_number' => $invoiceNumber,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$headers = ['Content-Type: application/json'];
|
|
||||||
if ($endpoint) {
|
|
||||||
if (!empty($endpoint['api_key'])) {
|
|
||||||
$headers[] = 'X-API-Key: ' . $endpoint['api_key'];
|
|
||||||
$headers[] = 'Authorization: Bearer ' . $endpoint['api_key'];
|
|
||||||
}
|
|
||||||
if (!empty($endpoint['headers'])) {
|
|
||||||
$customHeaders = json_decode($endpoint['headers'], true);
|
|
||||||
if (is_array($customHeaders)) {
|
|
||||||
foreach ($customHeaders as $key => $value) {
|
|
||||||
$headers[] = "$key: $value";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$headers[] = 'X-API-Key: ' . (getenv('ENTALEQ_API_KEY') ?: 'mock-key');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$ch = curl_init($apiUrl);
|
$ch = curl_init($apiUrl);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt_array($ch, [
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
CURLOPT_POST => true,
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
CURLOPT_POSTFIELDS => $payload,
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
CURLOPT_TIMEOUT => 30,
|
||||||
|
]);
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
if ($httpCode !== 200) {
|
if ($httpCode !== 200) {
|
||||||
return "⏳ تم استلام وصل الدفع ويجري التحقق منه حالياً من قبل المحاسب يدوياً. سنقوم بشحن رصيدك وتنبيهك فور انتهاء العملية.";
|
error_log("[PaymentVerify] HTTP $httpCode from $apiUrl: $response");
|
||||||
|
return "⏳ تم استلام معلومات الدفع ويجري التحقق منها يدوياً. سنقوم بالرد فور الانتهاء.";
|
||||||
}
|
}
|
||||||
|
|
||||||
$resData = json_decode($response, true);
|
$resData = json_decode($response, true);
|
||||||
if (isset($resData['status']) && $resData['status'] === 'success') {
|
if (!$resData) {
|
||||||
$amtStr = $resData['data']['amount'] ?? $amount;
|
return "⏳ تم استلام معلومات الدفع. يجري التحقق منها يدوياً.";
|
||||||
return "✅ تم التحقق من وصل الدفع تلقائياً بنجاح!\n• رقم العملية: " . $transactionId . "\n• القيمة: " . $amtStr . " دينار\n• تم تحديث رصيد حسابك بنجاح.";
|
|
||||||
} else {
|
|
||||||
$reason = $resData['message'] ?? 'العملية مسجلة مسبقاً أو غير صالحة';
|
|
||||||
return "⚠️ لم نتمكن من تأكيد العملية تلقائياً:\n• السبب: " . $reason . "\n\nيجري الآن تحويل المعاملة للمراجعة اليدوية من قبل الإدارة وسنقوم بالرد عليك قريباً.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (($resData['status'] ?? '') === 'success') {
|
||||||
|
$verified = $resData['verified'] ?? false;
|
||||||
|
if ($verified) {
|
||||||
|
return "✅ تم التحقق من عملية الدفع تلقائياً بنجاح!\n"
|
||||||
|
. "• تم تحديث رصيد حسابك.";
|
||||||
|
}
|
||||||
|
|
||||||
|
$reason = $resData['message']
|
||||||
|
?? $resData['ai_reason']
|
||||||
|
?? 'لم يتم التأكيد بعد';
|
||||||
|
return "⚠️ " . $reason . "\n\nسيتم مراجعة العملية من قبل الإدارة والرد عليك قريباً.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "⏳ تم استلام معلومات الدفع. يجري التحقق منها يدوياً.";
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("[Payment Verification Exception] " . $e->getMessage());
|
error_log("[PaymentVerify Exception] " . $e->getMessage());
|
||||||
return "⏳ تم استلام وصل الدفع بنجاح. يجري الآن مراجعته وتدقيقه يدوياً من قبل الإدارة الفنية لتأكيد شحن رصيدك.";
|
return "⏳ تم استلام معلومات الدفع. يجري التحقق منها يدوياً.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,75 +2,111 @@
|
|||||||
|
|
||||||
namespace App\Core\Flows;
|
namespace App\Core\Flows;
|
||||||
|
|
||||||
use App\Services\GeminiService;
|
use App\Services\SiroService;
|
||||||
use App\Models\ChatbotRule;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PaymentFlow
|
* PaymentFlow — Smart Payment Verification
|
||||||
* Handles payment receipt uploads and verification.
|
*
|
||||||
|
* Flow: start → await_receipt → finished
|
||||||
|
*
|
||||||
|
* Automatically:
|
||||||
|
* • Detects country from phone prefix (963→Syria, 962→Jordan)
|
||||||
|
* • Sets payment method (Syria→shamcash, Jordan→cliq)
|
||||||
|
* • Forwards raw receipt image to payment server for AI verification
|
||||||
|
* • Payment server auto-finds the latest pending invoice by phone
|
||||||
|
* → no need for the user to type an invoice number
|
||||||
*/
|
*/
|
||||||
class PaymentFlow extends BaseFlow
|
class PaymentFlow extends BaseFlow
|
||||||
{
|
{
|
||||||
public function handleStep(string $step, array $messageData, array &$context): FlowResult
|
public function handleStep(string $step, array $messageData, array &$context): FlowResult
|
||||||
{
|
{
|
||||||
$companyId = $context['company_id'] ?? 1;
|
|
||||||
$phone = $messageData['phone'] ?? '';
|
$phone = $messageData['phone'] ?? '';
|
||||||
|
$text = $messageData['body'] ?? $messageData['text'] ?? '';
|
||||||
|
$image = $messageData['image'] ?? '';
|
||||||
|
$imageMimeType = $messageData['imageMimeType'] ?? 'image/jpeg';
|
||||||
|
|
||||||
switch ($step) {
|
switch ($step) {
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
// START: detect country, set method, ask for receipt
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
case 'start':
|
case 'start':
|
||||||
|
$country = SiroService::detectCountry($phone);
|
||||||
|
$paymentMethod = match ($country) {
|
||||||
|
'jordan' => 'cliq',
|
||||||
|
default => 'shamcash',
|
||||||
|
};
|
||||||
|
|
||||||
|
$context['payment_method'] = $paymentMethod;
|
||||||
|
$context['country'] = $country;
|
||||||
|
$context['user_type'] = 'driver';
|
||||||
|
|
||||||
|
$methodName = match ($paymentMethod) {
|
||||||
|
'cliq' => 'كليك (Cliq)',
|
||||||
|
default => 'شام كاش (ShamCash)',
|
||||||
|
};
|
||||||
|
|
||||||
return new FlowResult(
|
return new FlowResult(
|
||||||
"أهلاً بك كابتن. يرجى إرسال صورة **إيصال التحويل المالي أو وصل الدفع** لكي نقوم بمراجعته وإضافته لحسابك:",
|
"أهلاً بك. للتحقق من عملية الدفع:\n"
|
||||||
"awaiting_receipt"
|
. "💰 طريقة الدفع المتوقعة: {$methodName}\n"
|
||||||
|
. "📍 الدولة: " . ($country === 'syria' ? 'سوريا' : ($country === 'jordan' ? 'الأردن' : $country))
|
||||||
|
. "\n\n📸 يرجى إرسال صورة الإيصال أو وصل التحويل للتحقق منه.",
|
||||||
|
"await_receipt"
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'awaiting_receipt':
|
// ─────────────────────────────────────────────────
|
||||||
if (empty($messageData['image']) || empty($messageData['imageMimeType'])) {
|
// AWAIT_RECEIPT: collect receipt image
|
||||||
return new FlowResult("الرجاء إرسال صورة وصل الدفع بوضوح للاستمرار، أو اكتب 'إلغاء' للخروج:", "awaiting_receipt");
|
// ─────────────────────────────────────────────────
|
||||||
|
case 'await_receipt':
|
||||||
|
if (empty($image)) {
|
||||||
|
return new FlowResult(
|
||||||
|
"الرجاء إرسال صورة واضحة لوصل الدفع أو صورة الشاشة:",
|
||||||
|
"await_receipt"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($companyId !== 1) {
|
return $this->sendToVerification($phone, $context, $image, $imageMimeType);
|
||||||
if (!\App\Models\CompanySubscriptionUsage::hasRemainingLimit($companyId, 'ocr')) {
|
|
||||||
return new FlowResult("⚠️ عذراً، تجاوز المتجر الحد المسموح لمعالجة الصور لهذا الشهر.", "finished", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$rule = ChatbotRule::findActiveForRule($companyId);
|
|
||||||
$configuredGeminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : null;
|
|
||||||
$apiKey = GeminiService::getGeminiApiKey($configuredGeminiKey);
|
|
||||||
|
|
||||||
if (empty($apiKey)) {
|
|
||||||
return new FlowResult("عذراً، عطل فني في خادم معالجة الصور بالذكاء الاصطناعي. يرجى المحاولة لاحقاً.", "finished", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$imageSystemPrompt = "أنت خبير في مراجعة إيصالات الدفع. استخرج البيانات التالية بدقة بالغة واكتبها بصيغة JSON محاطة بـ [PAYMENT_RECEIPT: { ... }] كالتالي:\n[PAYMENT_RECEIPT: {\"transaction_id\": \"رقم المعاملة أو الحوالة هنا\", \"amount\": \"المبلغ المستخرج كأرقام فقط\", \"method\": \"طريقة الدفع مثل Syriatel Cash أو Bemo Bank\"}]\nفي حال عدم وضوح الإيصال، أرجع JSON فارغًا.";
|
|
||||||
|
|
||||||
$mimeType = $messageData['imageMimeType'];
|
|
||||||
if (strpos($mimeType, ';') !== false) {
|
|
||||||
$mimeType = trim(explode(';', $mimeType)[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$replyText = GeminiService::generateResponseFromImage($apiKey, $imageSystemPrompt, $messageData['image'], $mimeType);
|
|
||||||
|
|
||||||
if ($companyId !== 1) {
|
|
||||||
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'ocr');
|
|
||||||
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'request');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($replyText) && preg_match('/\[PAYMENT_RECEIPT:\s*(\{.*?\})\]/s', $replyText, $matches)) {
|
|
||||||
$jsonStr = $matches[1];
|
|
||||||
$verificationResult = \App\Controllers\WhatsAppController::verifyPaymentSlipStatic($companyId, $phone, $jsonStr);
|
|
||||||
|
|
||||||
if ($verificationResult) {
|
|
||||||
return new FlowResult("تم فحص الإيصال:\n" . $verificationResult, "finished", true);
|
|
||||||
} else {
|
|
||||||
return new FlowResult("لم نتمكن من التأكد من بيانات الإيصال. يرجى إرسال صورة أوضح أو التواصل مع الدعم الفني.", "finished", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FlowResult("لم أتمكن من التعرف على بيانات الإيصال المرفق. يرجى التأكد من وضوح الصورة وإعادة المحاولة.", "awaiting_receipt");
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return new FlowResult("حدث خطأ في المسار.", "finished", true);
|
return new FlowResult("حدث خطأ في المسار.", "finished", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forward receipt image + phone to payment server for AI verification.
|
||||||
|
* Payment server internally resolves phone→driverID via Siro backend.
|
||||||
|
* Nabeh only makes ONE API call (to payment server).
|
||||||
|
*/
|
||||||
|
private function sendToVerification(
|
||||||
|
string $phone,
|
||||||
|
array &$context,
|
||||||
|
string $image,
|
||||||
|
string $imageMimeType
|
||||||
|
): FlowResult {
|
||||||
|
$companyId = $context['company_id'] ?? 1;
|
||||||
|
|
||||||
|
if (strpos($imageMimeType, ';') !== false) {
|
||||||
|
$imageMimeType = trim(explode(';', $imageMimeType)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = \App\Controllers\WhatsAppController::verifyPaymentSlipStatic(
|
||||||
|
companyId: $companyId,
|
||||||
|
phone: $phone,
|
||||||
|
jsonStr: '',
|
||||||
|
userType: $context['user_type'] ?? 'driver',
|
||||||
|
paymentMethod: $context['payment_method'] ?? 'shamcash',
|
||||||
|
invoiceNumber: '',
|
||||||
|
receiptImage: $image,
|
||||||
|
imageMimeType: $imageMimeType,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return new FlowResult($result, "finished", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FlowResult(
|
||||||
|
"لم نتمكن من التحقق من الدفع حالياً. يرجى المحاولة مرة أخرى أو التواصل مع الدعم الفني.",
|
||||||
|
"finished",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user