Deploy: 2026-06-18 16:46:51
This commit is contained in:
@@ -9,62 +9,160 @@ use App\Services\SiroService;
|
||||
*
|
||||
* 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
|
||||
* Smart features:
|
||||
* • Auto-detects country + payment method (shamcash/cliq)
|
||||
* • Auto-finds pending invoice by phone (no invoice number needed)
|
||||
* • AI receipt verification via payment server Gemini
|
||||
* • Postponement detection (keyword-based)
|
||||
* • Image validation + retry (max 3 attempts)
|
||||
* • Currency-aware messages (SYP/JOD)
|
||||
*/
|
||||
class PaymentFlow extends BaseFlow
|
||||
{
|
||||
private const MAX_RETRIES = 3;
|
||||
|
||||
public function handleStep(string $step, array $messageData, array &$context): FlowResult
|
||||
{
|
||||
$phone = $messageData['phone'] ?? '';
|
||||
$text = $messageData['body'] ?? $messageData['text'] ?? '';
|
||||
$text = isset($messageData['body']) ? trim($messageData['body']) : '';
|
||||
$image = $messageData['image'] ?? '';
|
||||
$imageMimeType = $messageData['imageMimeType'] ?? 'image/jpeg';
|
||||
|
||||
// ── Postponement check (only if flow is active, not on start/finished) ──
|
||||
if ($step !== 'start' && $step !== 'finished' && !empty($text)) {
|
||||
$postpone = $this->detectPostponement($text);
|
||||
if ($postpone !== null) {
|
||||
$context['previous_step'] = $step;
|
||||
$hours = $postpone;
|
||||
return new FlowResult(
|
||||
"حاضر كابتن، تم تأجيل طلب الدفع. سأذكرك بعد {$hours} ساعات.\n"
|
||||
. "للمتابعة لاحقاً، أرسل 'دفع' مرة أخرى.",
|
||||
"postponed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Country + method detection ──
|
||||
$country = $context['country'] ?? SiroService::detectCountry($phone);
|
||||
$context['country'] = $country;
|
||||
|
||||
$paymentMethod = $context['payment_method'] ?? match ($country) {
|
||||
'jordan' => 'cliq',
|
||||
default => 'shamcash',
|
||||
};
|
||||
$context['payment_method'] = $paymentMethod;
|
||||
|
||||
$methodName = $paymentMethod === 'cliq' ? 'كليك (Cliq)' : 'شام كاش (ShamCash)';
|
||||
$currency = $paymentMethod === 'cliq' ? 'دينار أردني' : 'ل.س';
|
||||
$countryName = match ($country) {
|
||||
'jordan' => 'الأردن',
|
||||
'egypt' => 'مصر',
|
||||
default => 'سوريا',
|
||||
};
|
||||
|
||||
switch ($step) {
|
||||
// ─────────────────────────────────────────────────
|
||||
// START: detect country, set method, ask for receipt
|
||||
// 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)',
|
||||
};
|
||||
$context['retry_count'] = 0;
|
||||
|
||||
return new FlowResult(
|
||||
"أهلاً بك. للتحقق من عملية الدفع:\n"
|
||||
. "💰 طريقة الدفع المتوقعة: {$methodName}\n"
|
||||
. "📍 الدولة: " . ($country === 'syria' ? 'سوريا' : ($country === 'jordan' ? 'الأردن' : $country))
|
||||
. "\n\n📸 يرجى إرسال صورة الإيصال أو وصل التحويل للتحقق منه.",
|
||||
"أهلاً بك في خدمة التحقق من الدفع.\n\n"
|
||||
. "📍 الدولة: {$countryName}\n"
|
||||
. "💰 طريقة الدفع: {$methodName}\n"
|
||||
. "💵 العملة: {$currency}\n\n"
|
||||
. "📸 يرجى إرسال صورة واضحة لإيصال الدفع أو صورة الشاشة.\n"
|
||||
. "سيتم التحقق من الفاتورة المعلقة تلقائياً.\n\n"
|
||||
. "🟡 للخروج اكتب: إلغاء\n"
|
||||
. "⏰ للتأجيل اكتب: بعدين",
|
||||
"await_receipt"
|
||||
);
|
||||
|
||||
// ─────────────────────────────────────────────────
|
||||
// AWAIT_RECEIPT: collect receipt image
|
||||
// AWAIT_RECEIPT
|
||||
// ─────────────────────────────────────────────────
|
||||
case 'await_receipt':
|
||||
// ── No image sent ──
|
||||
if (empty($image)) {
|
||||
return new FlowResult(
|
||||
"الرجاء إرسال صورة واضحة لوصل الدفع أو صورة الشاشة:",
|
||||
"📸 يرجى إرسال صورة الإيصال أو وصل التحويل.\n"
|
||||
. "تأكد من أن الصورة واضحة وتظهر المبلغ وتفاصيل التحويل.",
|
||||
"await_receipt"
|
||||
);
|
||||
}
|
||||
|
||||
return $this->sendToVerification($phone, $context, $image, $imageMimeType);
|
||||
// ── Validate image size ──
|
||||
$decoded = base64_decode($image, true);
|
||||
if ($decoded === false || strlen($decoded) < 1024) {
|
||||
$retry = ($context['retry_count'] ?? 0) + 1;
|
||||
$context['retry_count'] = $retry;
|
||||
|
||||
if ($retry >= self::MAX_RETRIES) {
|
||||
return new FlowResult(
|
||||
"عذراً، لم نتمكن من قراءة الصورة بعد {$retry} محاولات.\n"
|
||||
. "يرجى التواصل مع خدمة العملاء لإتمام عملية الدفع يدوياً.",
|
||||
"finished",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return new FlowResult(
|
||||
"⚠️ الصورة غير واضحة أو صغيرة جداً.\n"
|
||||
. "يرجى إرسال صورة واضحة وحجم أكبر (محاولة {$retry} من " . self::MAX_RETRIES . "):",
|
||||
"await_receipt"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Normalize MIME type ──
|
||||
if (strpos($imageMimeType, ';') !== false) {
|
||||
$imageMimeType = trim(explode(';', $imageMimeType)[0]);
|
||||
}
|
||||
|
||||
// ── Send to payment server ──
|
||||
$companyId = $context['company_id'] ?? 1;
|
||||
|
||||
$result = \App\Controllers\WhatsAppController::verifyPaymentSlipStatic(
|
||||
companyId: $companyId,
|
||||
phone: $phone,
|
||||
jsonStr: '',
|
||||
userType: 'driver',
|
||||
paymentMethod: $paymentMethod,
|
||||
invoiceNumber: '',
|
||||
receiptImage: $image,
|
||||
imageMimeType: $imageMimeType,
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
return new FlowResult($result, "finished", true);
|
||||
}
|
||||
|
||||
$retry = ($context['retry_count'] ?? 0) + 1;
|
||||
$context['retry_count'] = $retry;
|
||||
|
||||
if ($retry >= self::MAX_RETRIES) {
|
||||
return new FlowResult(
|
||||
"عذراً، تعذر التحقق من الدفع بعد {$retry} محاولات.\n"
|
||||
. "سيتم مراجعة العملية من قبل الإدارة.",
|
||||
"finished",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return new FlowResult(
|
||||
"لم نتمكن من التحقق من الدفع حالياً (محاولة {$retry} من " . self::MAX_RETRIES . ").\n"
|
||||
. "يرجى إرسال صورة أوضح والإضاءة جيدة:",
|
||||
"await_receipt"
|
||||
);
|
||||
|
||||
case 'postponed':
|
||||
// Resume from postponed
|
||||
$step = $context['previous_step'] ?? 'await_receipt';
|
||||
return new FlowResult(
|
||||
"مرحباً بك مرة أخرى! 👋\n"
|
||||
. "📸 أرسل صورة إيصال الدفع للمتابعة:",
|
||||
$step
|
||||
);
|
||||
|
||||
default:
|
||||
return new FlowResult("حدث خطأ في المسار.", "finished", true);
|
||||
@@ -72,41 +170,25 @@ class PaymentFlow extends BaseFlow
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Detect if user wants to postpone (keyword-based, no AI call)
|
||||
*/
|
||||
private function sendToVerification(
|
||||
string $phone,
|
||||
array &$context,
|
||||
string $image,
|
||||
string $imageMimeType
|
||||
): FlowResult {
|
||||
$companyId = $context['company_id'] ?? 1;
|
||||
private function detectPostponement(string $text): ?int
|
||||
{
|
||||
$keywords = [
|
||||
'بعدين' => 2, 'بكرا' => 12, 'بكرة' => 12, 'بعد' => 3,
|
||||
'شوي' => 1, 'مشغول' => 4, 'تأجيل' => 6, 'لاحقاً' => 6,
|
||||
'لاحقا' => 6, 'الحق' => 6, 'وقت ثاني' => 8, 'تعبان' => 6,
|
||||
'بعدين برسل' => 3, 'بعدين ببعت' => 3, 'بعدين بكمل' => 4,
|
||||
'ببعثها' => 3, 'ببعت' => 3, 'برسل' => 2,
|
||||
];
|
||||
|
||||
if (strpos($imageMimeType, ';') !== false) {
|
||||
$imageMimeType = trim(explode(';', $imageMimeType)[0]);
|
||||
$normalized = trim(mb_strtolower($text));
|
||||
foreach ($keywords as $kw => $hours) {
|
||||
if (mb_strpos($normalized, $kw) !== false) {
|
||||
return $hours;
|
||||
}
|
||||
}
|
||||
|
||||
$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
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user