Deploy: 2026-05-22 21:52:51
This commit is contained in:
@@ -1073,6 +1073,7 @@
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<label class="form-label" x-text="chatbotSettings.trigger_type === 'gemini_ai' ? 'System Instruction Prompt' : 'Predefined Auto-Reply Message'" style="margin-bottom: 0;"></label>
|
||||
<div x-show="chatbotSettings.trigger_type === 'gemini_ai'" style="display: flex; gap: 0.35rem; flex-wrap: wrap; align-items: center;">
|
||||
<button type="button" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto;" @click="loadPromptTemplate('intaleq')">قالب خدمة عملاء انطلق (سوري)</button>
|
||||
<button type="button" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto;" @click="loadPromptTemplate('nabeh')">قالب تطبيق نبيه (سوري)</button>
|
||||
<button type="button" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto;" @click="loadPromptTemplate('store')">قالب متجر إلكتروني</button>
|
||||
<button type="button" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto;" @click="loadPromptTemplate('general')">قالب عام (إنجليزي)</button>
|
||||
@@ -2006,7 +2007,22 @@
|
||||
},
|
||||
|
||||
loadPromptTemplate(type) {
|
||||
if (type === 'nabeh') {
|
||||
if (type === 'intaleq') {
|
||||
this.chatbotSettings.ai_prompt = `أنت روبوت خدمة العملاء الخاص بشركة "انطلق" (Intaleq). مهمتك هي مساعدة المستخدمين والإجابة على استفساراتهم حول خدماتنا بلهجة سورية ودودة ومهنية للغاية.
|
||||
|
||||
التزم بالقواعد والتفاصيل التالية بدقة عند الرد:
|
||||
1. اسم الشركة: انطلق.
|
||||
2. ساعات العمل: يومياً من الساعة 11 صباحاً حتى 5 مساءً.
|
||||
3. طرق الدفع المتاحة: شام كاش، سيريتل، إم تي إن، وبطاقات البنك.
|
||||
4. طريقة التسجيل: يمكن للمستخدمين تحميل التطبيق من متجر جوجل بلاي، آبل ستور، أو عبر رابط مباشر. عملية التسجيل سهلة وسريعة.
|
||||
5. مميزات التطبيق:
|
||||
- عمولة التطبيق هي 10% فقط (وهي الأقل في السوق).
|
||||
- يوجد ذكاء اصطناعي متطور لتحليل المشاكل وحلها.
|
||||
- ضمان حقوق السائق والركاب بشكل كامل.
|
||||
- خدمة الطلب من أي مكان وبكل سهولة.
|
||||
6. تحدث دائماً باللهجة السورية اللطيفة والترحيبية والودية عند التحدث باللغة العربية (مثال: "ياهلا بك"، "كيف بقدر أساعدك اليوم؟").
|
||||
7. كوني مختصرة ومباشرة ومفيدة في إجاباتكِ وتجنبي الإطالة غير الضرورية.`;
|
||||
} else if (type === 'nabeh') {
|
||||
this.chatbotSettings.ai_prompt = `أنتِ "سارة"، موظفة خدمة العملاء الافتراضية الذكية والودودة لتطبيق "نبيه" (Nabeh).
|
||||
مهمتكِ هي مساعدة المستخدمين والإجابة على استفساراتهم بلطف وأدب بالمسائل التقنية والتجارية المتعلقة بالتطبيق.
|
||||
|
||||
|
||||
@@ -148,5 +148,237 @@ $router->post('/api/external/verify-payment', function ($request, $response) {
|
||||
});
|
||||
|
||||
|
||||
// REST API for Entaleq Driver OCR Details (GET)
|
||||
$router->get('/api/external/driver-ocr', function ($request, $response) {
|
||||
$apiKey = getenv('ENTALEQ_API_KEY');
|
||||
$incomingKey = $request->getHeader('x-api-key') ?? '';
|
||||
|
||||
if (empty($apiKey) || $incomingKey !== $apiKey) {
|
||||
$response->status(401)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Unauthorized'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$phone = $request->get('phone') ?? '';
|
||||
$companyId = $request->get('company_id') ?? '';
|
||||
|
||||
if (empty($phone)) {
|
||||
$response->status(400)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Missing phone parameter'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
\App\Models\DriverOcrData::ensureTableExists();
|
||||
$hash = \App\Core\Security::blindIndex($phone);
|
||||
|
||||
if ($companyId) {
|
||||
$records = \App\Core\Database::select(
|
||||
"SELECT * FROM driver_ocr_data WHERE company_id = ? AND phone_hash = ?",
|
||||
[$companyId, $hash]
|
||||
);
|
||||
} else {
|
||||
$records = \App\Core\Database::select(
|
||||
"SELECT * FROM driver_ocr_data WHERE phone_hash = ?",
|
||||
[$hash]
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($records)) {
|
||||
$response->status(404)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'No driver records found for this phone number'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$decryptedRecords = [];
|
||||
$host = ($request->getHeader('x-forwarded-proto') ?: 'http') . '://' . ($request->getHeader('host') ?: 'localhost');
|
||||
|
||||
foreach ($records as $record) {
|
||||
$decrypted = \App\Models\DriverOcrData::decryptRecord($record);
|
||||
if ($decrypted) {
|
||||
// Include absolute paths for all document URLs
|
||||
$urlFields = [
|
||||
'id_front_url', 'id_back_url',
|
||||
'driving_license_front_url', 'driving_license_back_url',
|
||||
'vehicle_license_front_url', 'vehicle_license_back_url',
|
||||
'criminal_record_url'
|
||||
];
|
||||
foreach ($urlFields as $field) {
|
||||
if (!empty($decrypted[$field])) {
|
||||
$decrypted[$field . '_absolute'] = $host . $decrypted[$field];
|
||||
} else {
|
||||
$decrypted[$field . '_absolute'] = null;
|
||||
}
|
||||
}
|
||||
$decryptedRecords[] = $decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'data' => $decryptedRecords
|
||||
]);
|
||||
});
|
||||
|
||||
// REST API to Mark Driver Registered (POST)
|
||||
$router->post('/api/external/register-driver', function ($request, $response) {
|
||||
$apiKey = getenv('ENTALEQ_API_KEY');
|
||||
$incomingKey = $request->getHeader('x-api-key') ?? '';
|
||||
|
||||
if (empty($apiKey) || $incomingKey !== $apiKey) {
|
||||
$response->status(401)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Unauthorized'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$phone = $request->get('phone') ?? '';
|
||||
$companyId = $request->get('company_id') ?? '';
|
||||
|
||||
if (empty($phone)) {
|
||||
$response->status(400)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Missing phone parameter'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
\App\Models\DriverOcrData::ensureTableExists();
|
||||
$hash = \App\Core\Security::blindIndex($phone);
|
||||
|
||||
if ($companyId) {
|
||||
$existing = \App\Core\Database::selectOne(
|
||||
"SELECT id FROM driver_ocr_data WHERE company_id = ? AND phone_hash = ? LIMIT 1",
|
||||
[$companyId, $hash]
|
||||
);
|
||||
} else {
|
||||
$existing = \App\Core\Database::selectOne(
|
||||
"SELECT id FROM driver_ocr_data WHERE phone_hash = ? LIMIT 1",
|
||||
[$hash]
|
||||
);
|
||||
}
|
||||
|
||||
if (!$existing) {
|
||||
$response->status(404)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Driver record not found'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
\App\Core\Database::execute(
|
||||
"UPDATE driver_ocr_data SET status = 'registered' WHERE id = ?",
|
||||
[$existing['id']]
|
||||
);
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Driver status successfully updated to registered'
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// Cron job endpoint to send pending driver registration reminders
|
||||
$router->get('/api/external/cron/send-reminders', function ($request, $response) {
|
||||
$apiKey = getenv('ENTALEQ_API_KEY');
|
||||
$incomingKey = $request->getHeader('x-api-key') ?? '';
|
||||
|
||||
if (empty($apiKey) || $incomingKey !== $apiKey) {
|
||||
$response->status(401)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Unauthorized'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
\App\Models\DriverReminder::ensureTableExists();
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$reminders = \App\Core\Database::select(
|
||||
"SELECT * FROM driver_registration_reminders WHERE status = 'pending' AND scheduled_at <= ?",
|
||||
[$now]
|
||||
);
|
||||
|
||||
$processed = 0;
|
||||
foreach ($reminders as $reminder) {
|
||||
$companyId = $reminder['company_id'];
|
||||
$phone = $reminder['phone'];
|
||||
|
||||
// Get company chatbot rule details
|
||||
$rule = \App\Models\ChatbotRule::findActiveForRule($companyId);
|
||||
$geminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : getenv('GEMINI_API_KEY');
|
||||
$elApiKey = ($rule && !empty($rule['elevenlabs_api_key'])) ? $rule['elevenlabs_api_key'] : getenv('ELEVENLABS_API_KEY');
|
||||
$elVoiceId = ($rule && !empty($rule['elevenlabs_voice_id'])) ? $rule['elevenlabs_voice_id'] : getenv('ELEVENLABS_VOICE_ID');
|
||||
|
||||
// Fetch company WhatsApp session
|
||||
$session = \App\Models\WhatsAppSession::findByCompany($companyId);
|
||||
if (!$session || $session['status'] !== 'connected') {
|
||||
error_log("Cron Reminder: WhatsApp session not connected for company {$companyId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to get driver name from active conversation flow state
|
||||
$driverName = '';
|
||||
$state = \App\Models\ConversationState::findActive($companyId, $phone);
|
||||
if ($state) {
|
||||
$ctx = json_decode($state['context_data'] ?: '{}', true);
|
||||
$driverName = $ctx['name'] ?? '';
|
||||
}
|
||||
|
||||
$nameStr = $driverName ? " كابتن " . $driverName : " كابتن";
|
||||
$reminderMsg = "أهلاً بك{$nameStr}، حابين نذكرك تكمل خطوات تسجيلك لتنضم لعائلة انطلق 🚖. بقية الأوراق كتير مهمة لنفعل حسابك ونبدأ سوا. بانتظار إرسالها!";
|
||||
|
||||
// Generate Audio voice note if key is present
|
||||
$audioData = null;
|
||||
if (!empty($geminiKey)) {
|
||||
$audioData = \App\Services\GeminiService::generateAudioResponse(
|
||||
$geminiKey,
|
||||
"أنت روبوت خدمة العملاء لشركة انطلق، تتحدث باللهجة السورية الودودة.",
|
||||
$reminderMsg,
|
||||
'Puck',
|
||||
$elApiKey ?: null,
|
||||
$elVoiceId ?: null
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Send the text reminder
|
||||
\App\Core\Flows\ConversationFlowEngine::sendReply($session, $phone, $reminderMsg);
|
||||
|
||||
// 2. Send the voice note reminder if generated successfully
|
||||
if ($audioData && !empty($audioData['audio'])) {
|
||||
\App\Core\Flows\ConversationFlowEngine::sendReply(
|
||||
$session,
|
||||
$phone,
|
||||
'',
|
||||
null,
|
||||
$audioData['audio'],
|
||||
$audioData['mimeType'] ?? 'audio/mp4'
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Mark the reminder as sent
|
||||
\App\Models\DriverReminder::update($reminder['id'], [
|
||||
'status' => 'sent'
|
||||
]);
|
||||
$processed++;
|
||||
} catch (\Exception $ex) {
|
||||
error_log("Failed to process reminder ID {$reminder['id']}: " . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'processed_count' => $processed,
|
||||
'total_pending_due' => count($reminders)
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// 4. Dispatch the request
|
||||
$router->dispatch($request, $response);
|
||||
|
||||
139
backend/public/test_simulation.php
Normal file
139
backend/public/test_simulation.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* Driver Registration & Postponement Flow Simulation Script
|
||||
* Run this on the server: php backend/public/test_simulation.php
|
||||
*/
|
||||
|
||||
require_once dirname(__DIR__) . '/app/bootstrap.php';
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Models\ConversationState;
|
||||
use App\Models\DriverReminder;
|
||||
use App\Core\Flows\ConversationFlowEngine;
|
||||
use App\Models\WhatsAppSession;
|
||||
|
||||
echo "=== Starting Driver Registration & Reminder Flow Simulation ===\n\n";
|
||||
|
||||
$companyId = 1;
|
||||
$phone = "963999999999"; // Test Phone Number
|
||||
|
||||
// Setup mock session
|
||||
$session = WhatsAppSession::findOrCreate($companyId);
|
||||
// Ensure session is marked connected for simulation to go through sendReply
|
||||
WhatsAppSession::updateState($session['id'], [
|
||||
'phone' => $phone,
|
||||
'status' => 'connected'
|
||||
]);
|
||||
// Refresh session data
|
||||
$session = WhatsAppSession::findByCompany($companyId);
|
||||
|
||||
echo "1. Cleaning up existing test states/reminders...\n";
|
||||
Database::execute("DELETE FROM conversation_states WHERE contact_phone = ?", [$phone]);
|
||||
Database::execute("DELETE FROM driver_registration_reminders WHERE phone = ?", [$phone]);
|
||||
Database::execute("DELETE FROM driver_ocr_data WHERE phone = ?", [$phone]);
|
||||
|
||||
echo "2. Initializing conversation to 'id_front' state...\n";
|
||||
ConversationState::saveState([
|
||||
'company_id' => $companyId,
|
||||
'contact_phone' => $phone,
|
||||
'flow_name' => 'driver_registration_flow',
|
||||
'current_step' => 'id_front',
|
||||
'context_data' => json_encode(['name' => 'جميل الشام'], JSON_UNESCAPED_UNICODE),
|
||||
'expires_at' => date('Y-m-d H:i:s', strtotime('+1 hour'))
|
||||
]);
|
||||
|
||||
echo "3. Simulating user postponement request: 'بكرة المسا ببعتلك الهوية الشخصية'...\n";
|
||||
$msgData = [
|
||||
'phone' => $phone,
|
||||
'body' => 'بكرة المسا ببعتلك الهوية الشخصية'
|
||||
];
|
||||
|
||||
$handled = ConversationFlowEngine::processMessage($session, $msgData);
|
||||
echo " Message processed by flow engine: " . ($handled ? "YES" : "NO") . "\n";
|
||||
|
||||
// Fetch the new state and scheduled reminders
|
||||
$state = ConversationState::findActive($companyId, $phone);
|
||||
echo " New Flow Step: " . ($state ? $state['current_step'] : 'None (finished)') . "\n";
|
||||
echo " Context: " . ($state ? $state['context_data'] : 'None') . "\n";
|
||||
|
||||
$reminders = Database::select("SELECT * FROM driver_registration_reminders WHERE phone = ?", [$phone]);
|
||||
echo " Scheduled Reminders Count: " . count($reminders) . "\n";
|
||||
if (!empty($reminders)) {
|
||||
foreach ($reminders as $r) {
|
||||
echo " - ID: {$r['id']}, Scheduled At: {$r['scheduled_at']}, Postpone Count: {$r['postpone_count']}, Status: {$r['status']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n4. Testing Cron Scheduler (Simulate reminder execution by setting scheduled_at to the past)...\n";
|
||||
if (!empty($reminders)) {
|
||||
Database::execute("UPDATE driver_registration_reminders SET scheduled_at = ? WHERE id = ?", [
|
||||
date('Y-m-d H:i:s', strtotime('-1 minute')),
|
||||
$reminders[0]['id']
|
||||
]);
|
||||
|
||||
echo " Triggering cron process via internal endpoint request simulation...\n";
|
||||
// We call the main cron logic directly by querying and executing the reminder
|
||||
$dueReminders = Database::select(
|
||||
"SELECT * FROM driver_registration_reminders WHERE status = 'pending' AND scheduled_at <= ?",
|
||||
[date('Y-m-d H:i:s')]
|
||||
);
|
||||
|
||||
echo " Due Reminders found: " . count($dueReminders) . "\n";
|
||||
foreach ($dueReminders as $reminder) {
|
||||
$rule = \App\Models\ChatbotRule::findActiveForRule($companyId);
|
||||
$geminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : getenv('GEMINI_API_KEY');
|
||||
$elApiKey = ($rule && !empty($rule['elevenlabs_api_key'])) ? $rule['elevenlabs_api_key'] : getenv('ELEVENLABS_API_KEY');
|
||||
$elVoiceId = ($rule && !empty($rule['elevenlabs_voice_id'])) ? $rule['elevenlabs_voice_id'] : getenv('ELEVENLABS_VOICE_ID');
|
||||
|
||||
$driverName = 'جميل الشام';
|
||||
$nameStr = $driverName ? " كابتن " . $driverName : " كابتن";
|
||||
$reminderMsg = "أهلاً بك{$nameStr}، حابين نذكرك تكمل خطوات تسجيلك لتنضم لعائلة انطلق 🚖. بقية الأوراق كتير مهمة لنفعل حسابك ونبدأ سوا. بانتظار إرسالها!";
|
||||
|
||||
echo " Generating reminder Audio via TTS...\n";
|
||||
$audioData = null;
|
||||
if (!empty($geminiKey)) {
|
||||
$audioData = \App\Services\GeminiService::generateAudioResponse(
|
||||
$geminiKey,
|
||||
"أنت روبوت خدمة العملاء لشركة انطلق، تتحدث باللهجة السورية الودودة.",
|
||||
$reminderMsg,
|
||||
'Puck',
|
||||
$elApiKey ?: null,
|
||||
$elVoiceId ?: null
|
||||
);
|
||||
}
|
||||
|
||||
echo " Sending outbound simulated message...\n";
|
||||
ConversationFlowEngine::sendReply($session, $phone, $reminderMsg);
|
||||
|
||||
if ($audioData && !empty($audioData['audio'])) {
|
||||
echo " Audio voice note generated successfully (Length: " . strlen($audioData['audio']) . " bytes). Sending...\n";
|
||||
ConversationFlowEngine::sendReply(
|
||||
$session,
|
||||
$phone,
|
||||
'',
|
||||
null,
|
||||
$audioData['audio'],
|
||||
$audioData['mimeType'] ?? 'audio/mp4'
|
||||
);
|
||||
} else {
|
||||
echo " Audio voice note generation skipped or failed (Gemini Key may be missing or empty).\n";
|
||||
}
|
||||
|
||||
DriverReminder::update($reminder['id'], ['status' => 'sent']);
|
||||
echo " Reminder ID {$reminder['id']} status set to 'sent'.\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n5. Simulating user resuming registration (sending message 'هلا يا كابتن')...\n";
|
||||
$msgData = [
|
||||
'phone' => $phone,
|
||||
'body' => 'هلا يا كابتن'
|
||||
];
|
||||
|
||||
$handled = ConversationFlowEngine::processMessage($session, $msgData);
|
||||
echo " Resumed Message handled: " . ($handled ? "YES" : "NO") . "\n";
|
||||
|
||||
$state = ConversationState::findActive($companyId, $phone);
|
||||
echo " Resumed Flow Step: " . ($state ? $state['current_step'] : 'None') . "\n";
|
||||
|
||||
echo "\n=== Simulation Finished ===\n";
|
||||
Reference in New Issue
Block a user