160) { error_out('message must be 160 characters or fewer'); } $db = db(); // Resolve message text from template if provided $messageBody = $customBody; if ($templateId !== null) { $t = $db->prepare('SELECT body FROM sms_templates WHERE id = :id'); $t->execute([':id' => $templateId]); $tpl = $t->fetch(); if (!$tpl) { error_out('Template not found', 404); } $messageBody = $tpl['body']; } // Fetch recipients if ($scope === 'all') { $recipients = $db->query('SELECT id, mobile_number FROM attendees')->fetchAll(); } else { $stmt = $db->prepare( 'SELECT a.id, a.mobile_number FROM attendees a JOIN registrations r ON r.attendee_id = a.id WHERE r.workshop_session_id = :ws_id' ); $stmt->execute([':ws_id' => $workshopId]); $recipients = $stmt->fetchAll(); } if (empty($recipients)) { error_out('No recipients found for the selected scope'); } $db->beginTransaction(); // Create the send job record (worker will update status once it has sent) $jobStmt = $db->prepare( 'INSERT INTO send_jobs (template_id, custom_body, recipient_scope, workshop_session_id, status) VALUES (:template_id, :custom_body, :scope, :ws_id, "pending")' ); $jobStmt->execute([ ':template_id' => $templateId, ':custom_body' => $customBody !== '' ? $customBody : null, ':scope' => $scope, ':ws_id' => $workshopId, ]); $jobId = (int) $db->lastInsertId(); $db->commit(); // Dispatch to Python worker (synchronous — waits for all messages to send) $ch = curl_init(WORKER_URL . '/send'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(['job_id' => $jobId]), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($curlError || $httpCode !== 200) { $db->prepare("UPDATE send_jobs SET status='failed' WHERE id = :id") ->execute([':id' => $jobId]); error_out('SMS worker unavailable: ' . ($curlError ?: "HTTP $httpCode"), 503); } $result = json_decode((string) $response, true) ?? []; json_out([ 'job_id' => $jobId, 'sent' => (int) ($result['sent'] ?? 0), 'failed' => (int) ($result['failed'] ?? 0), 'recipients' => count($recipients), ]); } catch (PDOException $e) { if (isset($db) && $db->inTransaction()) { $db->rollBack(); } error_out('Database error: ' . $e->getMessage(), 500); }