use(\App\Middlewares\SecurityMiddleware::class); // 4. Define API Routes // Serve index.html dashboard on root path $router->get('/', function ($request, $response) { $response->setHeader('Content-Type', 'text/html; charset=utf-8'); $response->sendHeaders(); readfile(__DIR__ . '/index.html'); exit; }); // Health Check — no php_version or environment in production to avoid info disclosure $router->get('/api/health', function ($request, $response) { $response->json([ 'status' => 'success', 'message' => 'Nabeh API is healthy', 'app_name' => getenv('APP_NAME') ?: 'Nabeh', 'time' => date('Y-m-d H:i:s') ]); }); // Authentication Routes (Rate-limited: 5 attempts per 60 seconds per IP) $router->post('/api/auth/register', [\App\Controllers\AuthController::class, 'register'], [\App\Middlewares\RateLimitMiddleware::class]); $router->post('/api/auth/login', [\App\Controllers\AuthController::class, 'login'], [\App\Middlewares\RateLimitMiddleware::class]); $router->get('/api/auth/me', [\App\Controllers\AuthController::class, 'me'], [\App\Middlewares\AuthMiddleware::class]); // WhatsApp Gateway Routes $router->get('/api/whatsapp/status', [\App\Controllers\WhatsAppController::class, 'status'], [\App\Middlewares\AuthMiddleware::class]); $router->post('/api/whatsapp/qr', [\App\Controllers\WhatsAppController::class, 'requestQr'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->post('/api/whatsapp/disconnect', [\App\Controllers\WhatsAppController::class, 'disconnect'], [\App\Middlewares\AuthMiddleware::class]); $router->post('/api/whatsapp/webhook', [\App\Controllers\WhatsAppController::class, 'webhook']); // No AuthMiddleware (Protected by WEBHOOK_SECRET internally) // Phase 4 & 5: CRM, Templates & Campaigns Routes $router->get('/api/contacts', [\App\Controllers\ContactController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]); $router->post('/api/contacts', [\App\Controllers\ContactController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->get('/api/groups', [\App\Controllers\GroupController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]); $router->post('/api/groups', [\App\Controllers\GroupController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->post('/api/groups/add', [\App\Controllers\GroupController::class, 'addContact'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->post('/api/groups/bulk-add', [\App\Controllers\GroupController::class, 'bulkAddContacts'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->get('/api/templates', [\App\Controllers\TemplateController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]); $router->post('/api/templates', [\App\Controllers\TemplateController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->get('/api/campaigns', [\App\Controllers\CampaignController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]); $router->post('/api/campaigns', [\App\Controllers\CampaignController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->get('/api/chatbot/rules', [\App\Controllers\ChatbotController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]); $router->post('/api/chatbot/rules',[\App\Controllers\ChatbotController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->post('/api/chatbot/generate-prompt-from-audio', [\App\Controllers\ChatbotController::class, 'generatePromptFromAudio'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); // Custom Integration Endpoints Routes (Phase 5) $router->get('/api/endpoints', [\App\Controllers\EndpointController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]); $router->post('/api/endpoints', [\App\Controllers\EndpointController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->delete('/api/endpoints', [\App\Controllers\EndpointController::class, 'delete'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); // Salla Platform Integration Routes (Phase 6+) $router->get('/api/integrations/salla/auth', [\App\Controllers\SallaController::class, 'auth']); $router->get('/api/integrations/salla/callback', [\App\Controllers\SallaController::class, 'callback']); $router->get('/api/integrations/salla/status', [\App\Controllers\SallaController::class, 'status'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->post('/api/integrations/salla/disconnect',[\App\Controllers\SallaController::class, 'disconnect'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->post('/api/webhooks/salla', [\App\Controllers\SallaController::class, 'webhook']); // WooCommerce Store Integration Routes $router->post('/api/integrations/woocommerce/connect', [\App\Controllers\WooCommerceController::class, 'connect'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->get('/api/integrations/woocommerce/status', [\App\Controllers\WooCommerceController::class, 'status'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->post('/api/integrations/woocommerce/disconnect', [\App\Controllers\WooCommerceController::class, 'disconnect'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]); $router->post('/api/webhooks/woocommerce', [\App\Controllers\WooCommerceController::class, 'webhook']); // Mock External API for Entaleq Driver Info (Used to fetch real-time driver data) $router->post('/api/external/driver-info', function ($request, $response) { $body = $request->getBody(); $phone = $body['phone'] ?? ''; if (empty($phone)) { $response->status(400)->json([ 'status' => 'error', 'message' => 'Missing phone number' ]); return; } $response->json([ 'status' => 'success', 'data' => [ 'name' => 'أحمد الشريف', 'phone' => $phone, 'role' => 'سائق', 'status' => 'نشط', 'balance' => 75.25, 'vehicle' => 'تويوتا كامري 2023', 'trips_count' => 1420 ] ]); }); // Mock External API for Entaleq Payment Verification (Used to demo automated slip validation) $router->post('/api/external/verify-payment', function ($request, $response) { $body = $request->getBody(); $phone = $body['phone'] ?? ''; $transactionId = $body['transaction_id'] ?? ''; $amount = $body['amount'] ?? ''; $method = $body['method'] ?? ''; if (empty($transactionId) || empty($amount)) { $response->status(400)->json([ 'status' => 'error', 'message' => 'Missing transaction_id or amount' ]); return; } // Mock validation rules: // If transaction_id contains 'fail' or starts with '9', mock verification failure if (strpos(strtolower($transactionId), 'fail') !== false || (is_string($transactionId) && $transactionId[0] === '9')) { $response->json([ 'status' => 'failed', 'message' => 'هذا الرقم التعريفي للعملية تم استخدامه مسبقاً أو غير صحيح' ]); return; } $response->json([ 'status' => 'success', 'message' => 'Payment verified and driver balance updated', 'data' => [ 'driver_phone' => $phone, 'transaction_id' => $transactionId, 'amount' => $amount, 'method' => $method, 'new_balance' => 150.00 ] ]); }); // 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); $configuredGeminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : null; $geminiKey = \App\Services\GeminiService::getGeminiApiKey($configuredGeminiKey); $configuredElKey = ($rule && !empty($rule['elevenlabs_api_key'])) ? $rule['elevenlabs_api_key'] : null; $elApiKey = \App\Services\GeminiService::getElevenLabsApiKey($configuredElKey); $configuredVoiceId = ($rule && !empty($rule['elevenlabs_voice_id'])) ? $rule['elevenlabs_voice_id'] : null; $elVoiceId = \App\Services\GeminiService::getElevenLabsVoiceId($configuredVoiceId); // 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);