511 lines
24 KiB
PHP
511 lines
24 KiB
PHP
<?php
|
|
|
|
namespace App\Core\Flows;
|
|
|
|
use App\Services\GeminiService;
|
|
use App\Services\SiroService;
|
|
use App\Models\DriverOcrData;
|
|
use App\Models\ChatbotRule;
|
|
use App\Models\DriverReminder;
|
|
use App\Core\Database;
|
|
|
|
/**
|
|
* DriverRegistrationFlow
|
|
* Handles step-by-step driver and vehicle registration using Gemini OCR.
|
|
* Integrates with Siro platform for driver registration across Syria, Jordan, and Egypt.
|
|
*/
|
|
class DriverRegistrationFlow extends BaseFlow
|
|
{
|
|
private array $prompts = [];
|
|
private string $country = 'syria';
|
|
|
|
private array $stepToDocType = [
|
|
'id_front' => 'id_front',
|
|
'id_back' => 'id_back',
|
|
'driving_license_front' => 'driver_license_front',
|
|
'driving_license_back' => 'driver_license_back',
|
|
'vehicle_license_front' => 'car_license_front',
|
|
'vehicle_license_back' => 'car_license_back',
|
|
'criminal_record' => 'criminal_record',
|
|
];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->prompts = SiroService::getDocumentPrompts($this->country);
|
|
}
|
|
|
|
public function handleStep(string $step, array $messageData, array &$context): FlowResult
|
|
{
|
|
$text = isset($messageData['body']) ? trim($messageData['body']) : '';
|
|
$phone = $messageData['phone'];
|
|
$companyId = $context['company_id'] ?? 1;
|
|
|
|
// Detect country from phone number
|
|
$this->country = SiroService::detectCountry($phone);
|
|
$context['country'] = $this->country;
|
|
|
|
// Set country-specific prompts
|
|
$this->prompts = SiroService::getDocumentPrompts($this->country);
|
|
|
|
// Country name in Arabic for messages
|
|
$countryNames = [
|
|
'syria' => 'سوريا',
|
|
'jordan' => 'الأردن',
|
|
'egypt' => 'مصر',
|
|
];
|
|
$countryName = $countryNames[$this->country] ?? 'سوريا';
|
|
|
|
// App name based on country
|
|
$appNames = [
|
|
'syria' => 'سيرو',
|
|
'jordan' => 'سيرو',
|
|
'egypt' => 'سيرو',
|
|
];
|
|
$appName = $appNames[$this->country] ?? 'سيرو';
|
|
|
|
// If currently postponed and user sends a message, resume the flow
|
|
if ($step === 'postponed') {
|
|
$activeReminder = DriverReminder::findActive($companyId, $phone);
|
|
if ($activeReminder) {
|
|
DriverReminder::update($activeReminder['id'], ['status' => 'cancelled']);
|
|
}
|
|
$step = $context['previous_step'] ?? 'ask_name';
|
|
}
|
|
|
|
// Check if user requests postponement/delay (only if already started and not finished)
|
|
if ($step !== 'start' && $step !== 'finished' && !empty($text)) {
|
|
$rule = ChatbotRule::findActiveForRule($companyId);
|
|
$configuredGeminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : null;
|
|
$apiKey = GeminiService::getGeminiApiKey($configuredGeminiKey);
|
|
if (!empty($apiKey)) {
|
|
$postponeData = $this->detectPostponement($text, $apiKey, $companyId);
|
|
if ($postponeData !== null) {
|
|
$hours = $postponeData['hours'];
|
|
$postponeCount = ($context['postpone_count'] ?? 0) + 1;
|
|
|
|
if ($postponeCount > 3) {
|
|
return new FlowResult(
|
|
"عذراً كابتن، لقد تجاوزت الحد الأقصى لمرات التأجيل (3 مرات). تم إلغاء طلب التسجيل الحالي. يمكنك البدء من جديد عندما تكون جاهزاً بكتابة 'تسجيل'.",
|
|
"finished",
|
|
true
|
|
);
|
|
}
|
|
|
|
$context['postpone_count'] = $postponeCount;
|
|
$context['previous_step'] = $step;
|
|
|
|
$scheduledAt = date('Y-m-d H:i:s', strtotime("+{$hours} hours"));
|
|
DriverReminder::saveReminder([
|
|
'company_id' => $companyId,
|
|
'phone' => $phone,
|
|
'scheduled_at' => $scheduledAt,
|
|
'postpone_count' => $postponeCount,
|
|
'status' => 'pending'
|
|
]);
|
|
|
|
return new FlowResult(
|
|
"حاضر كابتن، قمت بتأجيل التسجيل. سأقوم بتذكيرك بعد {$hours} ساعة لإكمال خطوات التسجيل. بالتوفيق!",
|
|
"postponed"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
switch ($step) {
|
|
case 'start':
|
|
return new FlowResult(
|
|
"أهلاً بك كابتن في خدمة تسجيل كباتن تطبيق {$appName} في {$countryName} 🚖.\nيرجى إرسال اسمك الثلاثي الكامل للبدء:",
|
|
"ask_name"
|
|
);
|
|
|
|
case 'ask_name':
|
|
if (empty($text)) {
|
|
return new FlowResult("يرجى إدخال اسمك الثلاثي الكامل للاستمرار:", "ask_name");
|
|
}
|
|
$context['name'] = $text;
|
|
return new FlowResult(
|
|
"شكراً كابتن {$text}.\nالآن يرجى إرسال صورة **الوجه الأمامي للهوية الشخصية** (تأكد من أن الصورة واضحة والإضاءة جيدة):",
|
|
"id_front"
|
|
);
|
|
|
|
case 'id_front':
|
|
return $this->processOcrStep(
|
|
$step,
|
|
$messageData,
|
|
$context,
|
|
"id_front_sy",
|
|
"national_number",
|
|
"عذراً كابتن، لم أتمكن من قراءة الرقم الوطني من الهوية بوضوح. يرجى إرسال صورة أخرى للوجه الأمامي للهوية الشخصية تكون أكثر وضوحاً:",
|
|
"تم استخراج الرقم الوطني بنجاح ✅.\nالآن، يرجى إرسال صورة **الوجه الخلفي للهوية الشخصية**:",
|
|
"id_back"
|
|
);
|
|
|
|
case 'id_back':
|
|
return $this->processOcrStep(
|
|
$step,
|
|
$messageData,
|
|
$context,
|
|
"id_back_sy",
|
|
"gender",
|
|
"عذراً كابتن، لم أتمكن من قراءة بيانات الوجه الخلفي للهوية بوضوح. يرجى إرسال صورة أخرى للوجه الخلفي للهوية الشخصية:",
|
|
"تم استخراج البيانات بنجاح ✅.\nيرجى إرسال صورة **الوجه الأمامي لرخصة القيادة**:",
|
|
"driving_license_front"
|
|
);
|
|
|
|
case 'driving_license_front':
|
|
return $this->processOcrStep(
|
|
$step,
|
|
$messageData,
|
|
$context,
|
|
"driving_license_sy_front",
|
|
"national_number",
|
|
"عذراً كابتن، لم أتمكن من قراءة رخصة القيادة بوضوح. يرجى إرسال صورة أخرى واضحة للوجه الأمامي لرخصة القيادة:",
|
|
"تم استخراج بيانات رخصة القيادة بنجاح ✅.\nيرجى إرسال صورة **الوجه الخلفي لرخصة القيادة**:",
|
|
"driving_license_back"
|
|
);
|
|
|
|
case 'driving_license_back':
|
|
return $this->processOcrStep(
|
|
$step,
|
|
$messageData,
|
|
$context,
|
|
"driving_license_sy_back",
|
|
"license_number",
|
|
"عذراً كابتن، لم أتمكن من قراءة الوجه الخلفي لرخصة القيادة بوضوح. يرجى إعادة إرسال الصورة بشكل أكثر وضوحاً:",
|
|
"تم استخراج البيانات بنجاح ✅.\nيرجى إرسال صورة **الوجه الأمامي لرخصة السيارة (الرخصة البرتقالية)**:",
|
|
"vehicle_license_front"
|
|
);
|
|
|
|
case 'vehicle_license_front':
|
|
return $this->processOcrStep(
|
|
$step,
|
|
$messageData,
|
|
$context,
|
|
"vehicle_license_sy_front",
|
|
"car_plate",
|
|
"عذراً كابتن، لم أتمكن من قراءة رقم لوحة السيارة بوضوح. يرجى إرسال صورة واضحة للوجه الأمامي لرخصة السيارة:",
|
|
"تم استخراج رقم اللوحة بنجاح ✅.\nيرجى إرسال صورة **الوجه الخلفي لرخصة السيارة (الرخصة البرتقالية)**:",
|
|
"vehicle_license_back"
|
|
);
|
|
|
|
case 'vehicle_license_back':
|
|
return $this->processOcrStep(
|
|
$step,
|
|
$messageData,
|
|
$context,
|
|
"vehicle_license_sy_back",
|
|
"chassis",
|
|
"عذراً كابتن، لم أتمكن من قراءة مواصفات السيارة بوضوح. يرجى إرسال صورة واضحة للوجه الخلفي لرخصة السيارة:",
|
|
"تم استخراج مواصفات السيارة بنجاح ✅.\nيرجى إرسال صورة **وثيقة غير محكوم (لا حكم عليه)**:",
|
|
"criminal_record"
|
|
);
|
|
|
|
case 'criminal_record':
|
|
if (empty($messageData['image']) || empty($messageData['imageMimeType'])) {
|
|
return new FlowResult("الرجاء إرسال صورة وثيقة غير محكوم (لا حكم عليه) للاستمرار:", "criminal_record");
|
|
}
|
|
|
|
// Save non-OCR criminal record image
|
|
$imageUrl = $this->saveIncomingImage($step, $phone, $messageData);
|
|
if (!$imageUrl) {
|
|
return new FlowResult("عذراً، فشل حفظ الصورة. الرجاء إعادة إرسال صورة الوثيقة:", "criminal_record");
|
|
}
|
|
|
|
// Upload criminal record to Siro
|
|
$fullPath = __DIR__ . '/../../../../public' . $imageUrl;
|
|
$criminalSiroUrl = SiroService::uploadDocument(
|
|
$this->country,
|
|
SiroService::formatPhone($phone, $this->country),
|
|
'criminal_record',
|
|
$fullPath,
|
|
$messageData['imageMimeType']
|
|
);
|
|
$context['criminal_record_siro_url'] = $criminalSiroUrl;
|
|
|
|
// Securely save registration data to local database
|
|
try {
|
|
DriverOcrData::saveSecure([
|
|
'company_id' => $companyId,
|
|
'phone' => $phone,
|
|
'name' => $context['name'],
|
|
'id_front_url' => $context['id_front_url'] ?? null,
|
|
'id_front_ocr' => $context['id_front_ocr'] ?? null,
|
|
'id_back_url' => $context['id_back_url'] ?? null,
|
|
'id_back_ocr' => $context['id_back_ocr'] ?? null,
|
|
'driving_license_front_url' => $context['driving_license_front_url'] ?? null,
|
|
'driving_license_front_ocr' => $context['driving_license_front_ocr'] ?? null,
|
|
'driving_license_back_url' => $context['driving_license_back_url'] ?? null,
|
|
'driving_license_back_ocr' => $context['driving_license_back_ocr'] ?? null,
|
|
'vehicle_license_front_url' => $context['vehicle_license_front_url'] ?? null,
|
|
'vehicle_license_front_ocr' => $context['vehicle_license_front_ocr'] ?? null,
|
|
'vehicle_license_back_url' => $context['vehicle_license_back_url'] ?? null,
|
|
'vehicle_license_back_ocr' => $context['vehicle_license_back_ocr'] ?? null,
|
|
'criminal_record_url' => $imageUrl,
|
|
'status' => 'ocr_completed'
|
|
]);
|
|
} catch (\Exception $dbEx) {
|
|
error_log("[Registration Flow Error] DB Write Failed: " . $dbEx->getMessage());
|
|
return new FlowResult("عذراً، حدث خطأ أثناء حفظ طلبك في قاعدة البيانات. يرجى المحاولة مرة أخرى لاحقاً.", "criminal_record");
|
|
}
|
|
|
|
// Register driver in Siro with Siro-hosted URLs
|
|
$docUrls = [
|
|
'id_front' => $context['id_front_siro_url'] ?? '',
|
|
'id_back' => $context['id_back_siro_url'] ?? '',
|
|
'driving_license_front' => $context['driving_license_front_siro_url'] ?? '',
|
|
'driving_license_back' => $context['driving_license_back_siro_url'] ?? '',
|
|
'vehicle_license_front' => $context['vehicle_license_front_siro_url'] ?? '',
|
|
'vehicle_license_back' => $context['vehicle_license_back_siro_url'] ?? '',
|
|
'criminal_record' => $criminalSiroUrl ?? '',
|
|
];
|
|
|
|
$idOcr = $context['id_front_ocr'] ?? [];
|
|
$vlOcr = $context['vehicle_license_front_ocr'] ?? [];
|
|
$vlbOcr = $context['vehicle_license_back_ocr'] ?? [];
|
|
$dlOcr = $context['driving_license_front_ocr'] ?? [];
|
|
|
|
$formattedPhone = SiroService::formatPhone($phone, $this->country);
|
|
$driverId = 'DRV' . date('YmdHis') . rand(100, 999);
|
|
|
|
$driverData = [
|
|
'phone' => $formattedPhone,
|
|
'password' => substr(md5($formattedPhone . time()), 0, 12),
|
|
'first_name' => explode(' ', $context['name'] ?? '')[0] ?? $context['name'],
|
|
'last_name' => implode(' ', array_slice(explode(' ', $context['name'] ?? ''), 1)) ?: $context['name'],
|
|
'name_arabic' => $context['name'] ?? '',
|
|
'national_number' => $idOcr['national_number'] ?? '',
|
|
'birthdate' => $idOcr['dob'] ?? '',
|
|
'address' => $idOcr['address'] ?? '',
|
|
'id' => $driverId,
|
|
];
|
|
|
|
$carData = [
|
|
'vin' => $vlbOcr['chassis'] ?? $vlOcr['vin'] ?? '',
|
|
'car_plate' => $vlOcr['car_plate'] ?? '',
|
|
'make' => $vlbOcr['make'] ?? '',
|
|
'model' => $vlbOcr['model'] ?? '',
|
|
'year' => $vlbOcr['year'] ?? '',
|
|
'color' => $vlOcr['color'] ?? '',
|
|
'color_hex' => $vlOcr['color_hex'] ?? '#000000',
|
|
'owner' => $vlOcr['owner'] ?? '',
|
|
'fuel' => $vlbOcr['fuel'] ?? '',
|
|
'expiration_date' => $vlOcr['issue_date'] ?? '',
|
|
'vehicle_category_id' => 1,
|
|
];
|
|
|
|
$syroResult = SiroService::registerDriver($driverData, $carData, $docUrls, $this->country);
|
|
|
|
$appNames = [
|
|
'syria' => 'سيرو',
|
|
'jordan' => 'سيرو',
|
|
'egypt' => 'سيرو',
|
|
];
|
|
$appName = $appNames[$this->country] ?? 'سيرو';
|
|
|
|
if ($syroResult && ($syroResult['status'] ?? '') === 'success') {
|
|
DriverOcrData::saveSecure([
|
|
'company_id' => $companyId,
|
|
'phone' => $phone,
|
|
'name' => $context['name'],
|
|
'status' => 'registered'
|
|
]);
|
|
|
|
return new FlowResult(
|
|
"شكراً لك كابتن، تم تسجيلك بنجاح في تطبيق {$appName} 🚖✅\nسيتم مراجعة طلبك من قبل فريق الخدمة وتفعيل حسابك قريباً. يومك سعيد!",
|
|
"finished",
|
|
true
|
|
);
|
|
} else {
|
|
$errMsg = $syroResult['message'] ?? 'خطأ غير معروف';
|
|
error_log("[Registration Flow] Siro registration failed: " . json_encode($syroResult));
|
|
|
|
return new FlowResult(
|
|
"تم حفظ مستنداتك بنجاح في نظامنا. لكن حدث تأخير في تسجيلك على تطبيق {$appName}. سيقوم فريقنا بمراجعة بياناتك وتفعيل حسابك في أقرب وقت. شكراً لصبرك! 🙏",
|
|
"finished",
|
|
true
|
|
);
|
|
}
|
|
|
|
default:
|
|
return new FlowResult("خطأ في تحديد خطوة المسار.", "finished", true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process document upload and OCR extraction step
|
|
*/
|
|
private function processOcrStep(
|
|
string $step,
|
|
array $messageData,
|
|
array &$context,
|
|
string $promptKey,
|
|
string $requiredJsonKey,
|
|
string $failMessage,
|
|
string $successMessage,
|
|
string $nextStep
|
|
): FlowResult {
|
|
if (empty($messageData['image']) || empty($messageData['imageMimeType'])) {
|
|
return new FlowResult("الرجاء إرسال الصورة المطلوبة للمتابعة:", $step);
|
|
}
|
|
|
|
$imageUrl = $this->saveIncomingImage($step, $messageData['phone'], $messageData);
|
|
if (!$imageUrl) {
|
|
return new FlowResult("عذراً، فشل حفظ الصورة. الرجاء إعادة المحاولة وإرسال الصورة:", $step);
|
|
}
|
|
|
|
// Upload to Siro and store the signed URL
|
|
$fullPath = __DIR__ . '/../../../../public' . $imageUrl;
|
|
$siroUrl = SiroService::uploadDocument(
|
|
$this->country,
|
|
SiroService::formatPhone($messageData['phone'], $this->country),
|
|
$this->stepToDocType[$step] ?? $step,
|
|
$fullPath,
|
|
$messageData['imageMimeType']
|
|
);
|
|
if ($siroUrl) {
|
|
$context[$step . '_siro_url'] = $siroUrl;
|
|
error_log("[DriverRegistrationFlow] Uploaded {$step} to Siro: {$siroUrl}");
|
|
} else {
|
|
$context[$step . '_siro_url'] = null;
|
|
error_log("[DriverRegistrationFlow] Warning: Failed to upload {$step} to Siro, using local URL");
|
|
}
|
|
|
|
$companyId = $context['company_id'] ?? 1;
|
|
|
|
// Check subscription limit for OCR
|
|
if ($companyId !== 1) {
|
|
if (!\App\Models\CompanySubscriptionUsage::hasRemainingLimit($companyId, 'ocr')) {
|
|
error_log("[DriverRegistrationFlow] Company {$companyId} has exceeded its OCR limit.");
|
|
return new FlowResult("⚠️ عذراً، لقد استهلك هذا المتجر الحد المسموح له من تحليل الصور والوصولات لهذا الشهر. يرجى إرسال استفسارك نصياً.", $step);
|
|
}
|
|
}
|
|
|
|
$rule = ChatbotRule::findActiveForRule($companyId);
|
|
$configuredGeminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : null;
|
|
$apiKey = GeminiService::getGeminiApiKey($configuredGeminiKey);
|
|
|
|
if (empty($apiKey)) {
|
|
error_log("[DriverRegistrationFlow] Gemini API key not configured.");
|
|
return new FlowResult("عذراً، عطل فني في خادم معالجة الصور بالذكاء الاصطناعي. يرجى المحاولة لاحقاً.", $step);
|
|
}
|
|
|
|
$prompt = $this->prompts[$step] ?? '';
|
|
$rawOcr = GeminiService::generateOcrFromImage($apiKey, $prompt, $messageData['image'], $messageData['imageMimeType']);
|
|
|
|
if (!$rawOcr) {
|
|
error_log("[DriverRegistrationFlow] OCR response empty or model request failed.");
|
|
return new FlowResult($failMessage, $step);
|
|
}
|
|
|
|
$ocrData = json_decode($rawOcr, true);
|
|
if (!$ocrData || (empty($ocrData[$requiredJsonKey]) && !array_key_exists($requiredJsonKey, $ocrData))) {
|
|
error_log("[DriverRegistrationFlow] Missing required key '$requiredJsonKey' in OCR response: " . $rawOcr);
|
|
return new FlowResult($failMessage, $step);
|
|
}
|
|
|
|
// Increment stats on successful OCR processing
|
|
if ($companyId !== 1) {
|
|
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'ocr');
|
|
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'request');
|
|
}
|
|
|
|
// Save URL and OCR JSON string in the conversation context
|
|
$context[$step . '_url'] = $imageUrl;
|
|
$context[$step . '_ocr'] = $ocrData;
|
|
|
|
return new FlowResult($successMessage, $nextStep);
|
|
}
|
|
|
|
/**
|
|
* Decode base64 image data and save it to the public directory
|
|
*/
|
|
private function saveIncomingImage(string $step, string $phone, array $messageData): ?string
|
|
{
|
|
try {
|
|
$extension = 'jpg';
|
|
if (strpos($messageData['imageMimeType'], 'png') !== false) {
|
|
$extension = 'png';
|
|
}
|
|
|
|
$uniqueName = 'driver_' . $step . '_' . md5($phone . time()) . '.' . $extension;
|
|
$uploadDir = __DIR__ . '/../../../../public/uploads/documents/';
|
|
|
|
if (!is_dir($uploadDir)) {
|
|
mkdir($uploadDir, 0755, true);
|
|
}
|
|
|
|
$uploadPath = $uploadDir . $uniqueName;
|
|
$imgData = base64_decode($messageData['image']);
|
|
|
|
if (file_put_contents($uploadPath, $imgData) === false) {
|
|
error_log("[DriverRegistrationFlow] Failed to write image file: " . $uploadPath);
|
|
return null;
|
|
}
|
|
|
|
return '/uploads/documents/' . $uniqueName;
|
|
} catch (\Exception $e) {
|
|
error_log("[DriverRegistrationFlow Exception] Failed to save image: " . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detect if user wants to postpone, and return hours_delay if so.
|
|
*/
|
|
private function detectPostponement(string $text, string $apiKey, int $companyId): ?array
|
|
{
|
|
if (empty($text)) {
|
|
return null;
|
|
}
|
|
|
|
// Quick heuristic check: if the message is too long, or clearly doesn't contain postponement keywords, skip to save API costs
|
|
$keywords = ['بعدين', 'بكرا', 'بكرة', 'بعد', 'شوي', 'ثانية', 'مشغول', 'المسا', 'الليل', 'تأجيل', 'وقت ثاني', 'تعبان', 'بعدين برسل', 'بعدين ببعت', 'ببعثهم بعدين', 'ببعتهم بعدين', 'بعدين بكمل'];
|
|
$hasKeyword = false;
|
|
foreach ($keywords as $kw) {
|
|
if (mb_strpos($text, $kw) !== false) {
|
|
$hasKeyword = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$hasKeyword && mb_strlen($text) > 100) {
|
|
return null;
|
|
}
|
|
|
|
$systemPrompt = "You are an assistant that detects if a user wants to postpone, delay, or complete a registration flow later. Analyze the Arabic message.";
|
|
$userMessage = <<<EOT
|
|
Analyze the following Arabic message (often in Syrian dialect) to determine if the user wants to postpone/delay sending documents or complete the registration later.
|
|
|
|
Message: "{$text}"
|
|
|
|
Respond with ONLY a valid JSON object matching this schema:
|
|
{
|
|
"wants_postpone": true/false,
|
|
"hours_delay": 12
|
|
}
|
|
Do not include any markdown, code blocks, or explanations.
|
|
EOT;
|
|
|
|
$response = GeminiService::generateResponse($apiKey, $systemPrompt, $userMessage);
|
|
if (empty($response)) {
|
|
return null;
|
|
}
|
|
|
|
// Increment request limit on successful postponement check API call
|
|
if ($companyId !== 1) {
|
|
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'request');
|
|
}
|
|
|
|
// Clean markdown block if present
|
|
$response = trim(preg_replace('/```json|```/', '', $response));
|
|
$data = json_decode($response, true);
|
|
if (isset($data['wants_postpone']) && $data['wants_postpone'] === true) {
|
|
return [
|
|
'hours' => isset($data['hours_delay']) ? (int)$data['hours_delay'] : 12
|
|
];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|