findAllByCompany($request->company_id); // Count sent messages per campaign from database foreach ($campaigns as &$cmp) { $counts = \App\Core\Database::selectOne( "SELECT COUNT(*) as total FROM messages_log WHERE campaign_id = ? AND status = 'sent'", [$cmp['id']] ); $cmp['sent_count'] = $counts['total'] ?? 0; } $response->json([ 'status' => 'success', 'data' => $campaigns ]); } /** * Create a new broadcast campaign and launch it */ public function store(Request $request, Response $response) { $errors = $this->validate($request, [ 'name' => 'required', 'group_id' => 'required', 'session_id' => 'required', 'template_id' => 'required' ]); if (!empty($errors)) { $response->status(400)->json(['status' => 'error', 'errors' => $errors]); return; } $body = $request->getBody(); $campaignModel = new Campaign(); $id = $campaignModel->create([ 'company_id' => $request->company_id, 'name' => $body['name'], 'group_id' => $body['group_id'], 'session_id' => $body['session_id'], 'template_id' => $body['template_id'], 'status' => 'pending', 'scheduled_at' => $body['scheduled_at'] ?? null ]); // Launch campaign in background using PHP fastcgi_finish_request if (function_exists('fastcgi_finish_request')) { $response->status(201); $response->setHeader('Content-Type', 'application/json; charset=utf-8'); $allowedOrigin = getenv('ALLOWED_ORIGIN') ?: '*'; $response->setHeader('Access-Control-Allow-Origin', $allowedOrigin); $response->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); $response->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); $response->sendHeaders(); http_response_code(201); echo json_encode([ 'status' => 'success', 'message' => 'Campaign started in background', 'id' => $id ], JSON_UNESCAPED_UNICODE); fastcgi_finish_request(); $this->dispatchCampaign($id, $request->company_id); exit; } else { // Fallback for environment without PHP-FPM $this->dispatchCampaign($id, $request->company_id); $response->status(201)->json([ 'status' => 'success', 'message' => 'Campaign completed successfully (synchronous fallback)', 'id' => $id ]); } } /** * Dispatch campaign messages in sequence with rate limiting */ private function dispatchCampaign(int $campaignId, int $companyId) { set_time_limit(0); ignore_user_abort(true); $campaign = \App\Core\Database::selectOne( "SELECT * FROM campaigns WHERE id = ? AND company_id = ? LIMIT 1", [$campaignId, $companyId] ); if (!$campaign) return; // Set status to running \App\Core\Database::execute( "UPDATE campaigns SET status = 'running' WHERE id = ?", [$campaignId] ); // Fetch template $template = \App\Models\Template::findByIdAndCompany($campaign['template_id'], $companyId); if (!$template) { \App\Core\Database::execute( "UPDATE campaigns SET status = 'failed' WHERE id = ?", [$campaignId] ); return; } // Fetch whatsapp session $session = \App\Core\Database::selectOne( "SELECT * FROM whatsapp_sessions WHERE id = ? AND company_id = ? LIMIT 1", [$campaign['session_id'], $companyId] ); if (!$session || $session['status'] !== 'connected') { \App\Core\Database::execute( "UPDATE campaigns SET status = 'failed' WHERE id = ?", [$campaignId] ); return; } // Get contacts in group $contacts = \App\Models\ContactGroup::getRawContacts($campaign['group_id']); if (empty($contacts)) { \App\Core\Database::execute( "UPDATE campaigns SET status = 'completed' WHERE id = ?", [$campaignId] ); return; } $gatewayUrl = getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3722'; $sendUrl = $gatewayUrl . '/api/messages/send'; foreach ($contacts as $rawContact) { // Decrypt contact data $contact = \App\Models\Contact::findByPhone($companyId, \App\Core\Security::decrypt($rawContact['phone'])); if (!$contact) continue; // Replace template variables $messageBody = str_replace('{{name}}', $contact['name'], $template['body']); // Send via cURL to Node.js Gateway $payload = json_encode([ 'session_key' => $session['session_key'], 'phone' => $contact['phone'], 'message' => $messageBody, 'media_url' => $template['media_url'] ]); $ch = curl_init($sendUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'X-Webhook-Secret: ' . getenv('WEBHOOK_SECRET') ]); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $status = 'failed'; $errorMsg = null; if ($httpCode === 200) { $status = 'sent'; } else { $resData = json_decode($response, true); $errorMsg = $resData['error'] ?? 'HTTP Code ' . $httpCode; } // Log message securely \App\Models\MessageLog::logMessage([ 'company_id' => $companyId, 'session_id' => $session['id'], 'campaign_id' => $campaignId, 'contact_phone' => $contact['phone'], 'direction' => 'outbound', 'message_type' => $template['type'], 'message_body' => $messageBody, 'media_url' => $template['media_url'], 'status' => $status, 'error_message' => $errorMsg ]); // Wait 2 seconds between messages sleep(2); } // Set status to completed \App\Core\Database::execute( "UPDATE campaigns SET status = 'completed' WHERE id = ?", [$campaignId] ); } }