diff --git a/backend/app/Controllers/CloudApiController.php b/backend/app/Controllers/CloudApiController.php
new file mode 100644
index 0000000..cd27ae7
--- /dev/null
+++ b/backend/app/Controllers/CloudApiController.php
@@ -0,0 +1,235 @@
+company_id;
+
+ $session = \App\Core\Database::selectOne(
+ "SELECT * FROM meta_cloud_sessions WHERE company_id = ? LIMIT 1",
+ [$companyId]
+ );
+
+ if (!$session) {
+ $response->json([
+ 'status' => 'success',
+ 'data' => null
+ ]);
+ return;
+ }
+
+ // Hide token from frontend
+ unset($session['access_token']);
+ unset($session['verify_token']);
+
+ $response->json([
+ 'status' => 'success',
+ 'data' => $session
+ ]);
+ }
+
+ /**
+ * Connect or update a Cloud API session
+ */
+ public function connect(Request $request, Response $response)
+ {
+ $companyId = $request->company_id;
+ $body = $request->getBody();
+
+ if (empty($body['phone_number_id']) || empty($body['waba_id']) || empty($body['access_token'])) {
+ $response->status(400)->json(['error' => 'Missing required fields']);
+ return;
+ }
+
+ $session = \App\Core\Database::selectOne(
+ "SELECT id FROM meta_cloud_sessions WHERE company_id = ? LIMIT 1",
+ [$companyId]
+ );
+
+ $verifyToken = 'nabeh_cloud_' . bin2hex(random_bytes(8));
+
+ if ($session) {
+ \App\Core\Database::execute(
+ "UPDATE meta_cloud_sessions SET phone_number_id = ?, waba_id = ?, access_token = ?, status = 'active' WHERE id = ?",
+ [$body['phone_number_id'], $body['waba_id'], $body['access_token'], $session['id']]
+ );
+ } else {
+ \App\Core\Database::execute(
+ "INSERT INTO meta_cloud_sessions (company_id, phone_number_id, waba_id, access_token, verify_token, status) VALUES (?, ?, ?, ?, ?, 'active')",
+ [$companyId, $body['phone_number_id'], $body['waba_id'], $body['access_token'], $verifyToken]
+ );
+ }
+
+ $updated = \App\Core\Database::selectOne(
+ "SELECT * FROM meta_cloud_sessions WHERE company_id = ? LIMIT 1",
+ [$companyId]
+ );
+
+ unset($updated['access_token']);
+
+ $response->json([
+ 'status' => 'success',
+ 'message' => 'WhatsApp Cloud API connected successfully',
+ 'data' => $updated
+ ]);
+ }
+
+ /**
+ * Disconnect Cloud API
+ */
+ public function disconnect(Request $request, Response $response)
+ {
+ $companyId = $request->company_id;
+
+ \App\Core\Database::execute(
+ "DELETE FROM meta_cloud_sessions WHERE company_id = ?",
+ [$companyId]
+ );
+
+ $response->json([
+ 'status' => 'success',
+ 'message' => 'Disconnected successfully'
+ ]);
+ }
+
+ /**
+ * Fetch templates from Meta and sync them locally
+ */
+ public function syncTemplates(Request $request, Response $response)
+ {
+ $companyId = $request->company_id;
+
+ $session = \App\Core\Database::selectOne(
+ "SELECT * FROM meta_cloud_sessions WHERE company_id = ? LIMIT 1",
+ [$companyId]
+ );
+
+ if (!$session || $session['status'] !== 'active') {
+ $response->status(400)->json(['error' => 'Cloud API not connected']);
+ return;
+ }
+
+ try {
+ // Call the Node.js Cloud API Service via Gateway
+ $gatewayUrl = rtrim(getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3722', '/');
+ $syncUrl = $gatewayUrl . '/api/cloud/templates/sync';
+
+ $payload = json_encode([
+ 'waba_id' => $session['waba_id'],
+ 'access_token' => $session['access_token']
+ ]);
+
+ $ch = curl_init($syncUrl);
+ 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, 15);
+ $res = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode === 200) {
+ $resData = json_decode($res, true);
+ $templates = $resData['data'] ?? [];
+
+ // Clear old templates and insert new ones
+ \App\Core\Database::execute("DELETE FROM message_templates WHERE company_id = ?", [$companyId]);
+
+ foreach ($templates as $tpl) {
+ \App\Core\Database::execute(
+ "INSERT INTO message_templates (company_id, meta_template_id, name, language, category, status, body_text) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ [$companyId, $tpl['id'], $tpl['name'], $tpl['language'], strtolower($tpl['category']), strtolower($tpl['status']), 'Synced from Meta']
+ );
+ }
+
+ $response->json([
+ 'status' => 'success',
+ 'message' => 'Templates synced successfully',
+ 'count' => count($templates)
+ ]);
+ } else {
+ $response->status($httpCode)->json(['error' => 'Failed to sync templates from Meta']);
+ }
+ } catch (\Exception $e) {
+ $response->status(500)->json(['error' => $e->getMessage()]);
+ }
+ }
+
+ /**
+ * Webhook to receive incoming messages from Meta
+ */
+ public function webhook(Request $request, Response $response)
+ {
+ // Meta GET verification request
+ if ($request->getMethod() === 'GET') {
+ $mode = $request->get('hub_mode');
+ $token = $request->get('hub_verify_token');
+ $challenge = $request->get('hub_challenge');
+
+ if ($mode === 'subscribe' && $token) {
+ $session = \App\Core\Database::selectOne(
+ "SELECT * FROM meta_cloud_sessions WHERE verify_token = ? LIMIT 1",
+ [$token]
+ );
+
+ if ($session) {
+ echo $challenge;
+ exit;
+ }
+ }
+ $response->status(403)->send('Forbidden');
+ return;
+ }
+
+ // Handle POST incoming messages
+ $body = $request->getBody();
+ if (isset($body['entry'][0]['changes'][0]['value']['messages'])) {
+ $metaMessages = $body['entry'][0]['changes'][0]['value']['messages'];
+ $metadata = $body['entry'][0]['changes'][0]['value']['metadata'];
+ $phoneNumberId = $metadata['phone_number_id'];
+
+ $session = \App\Core\Database::selectOne(
+ "SELECT * FROM meta_cloud_sessions WHERE phone_number_id = ? AND status = 'active' LIMIT 1",
+ [$phoneNumberId]
+ );
+
+ if ($session) {
+ foreach ($metaMessages as $msg) {
+ // Log incoming message
+ $msgText = $msg['text']['body'] ?? '[Media/Other]';
+ \App\Models\MessageLog::logMessage([
+ 'company_id' => $session['company_id'],
+ 'contact_phone' => $msg['from'],
+ 'direction' => 'inbound',
+ 'message_type' => $msg['type'],
+ 'message_body' => $msgText,
+ 'whatsapp_message_id' => $msg['id'],
+ 'status' => 'read'
+ ]);
+
+ // Note: Here you can integrate Gemini AI for auto-replies over Cloud API
+ // Similar to triggerAutoReply() in WhatsAppController
+ }
+ }
+ }
+
+ $response->status(200)->send('EVENT_RECEIVED');
+ }
+}
diff --git a/backend/migrate_whatsapp_cloud_api.php b/backend/migrate_whatsapp_cloud_api.php
new file mode 100644
index 0000000..245c359
--- /dev/null
+++ b/backend/migrate_whatsapp_cloud_api.php
@@ -0,0 +1,62 @@
+getMessage() . "\n";
+}
diff --git a/backend/public/index.html b/backend/public/index.html
index dd672bd..646fca8 100644
--- a/backend/public/index.html
+++ b/backend/public/index.html
@@ -901,6 +901,7 @@
@@ -1946,6 +1993,12 @@
whatsappMaxSessions: 1,
newSessionName: '',
metaSessions: [],
+ cloudSession: null,
+ newCloudSession: {
+ phone_number_id: '',
+ waba_id: '',
+ access_token: ''
+ },
newMetaSession: {
channel_type: 'messenger',
page_id: '',
@@ -2375,6 +2428,68 @@
this.actionLoading = false;
}
},
+ async fetchCloudSession() {
+ if (!this.token) return;
+ try {
+ const response = await fetch('/api/cloud/status', {
+ headers: { 'Authorization': `Bearer ${this.token}` }
+ });
+ const data = await response.json();
+ if (response.ok && data.status === 'success') {
+ this.cloudSession = data.data;
+ }
+ } catch (err) {
+ console.error('Failed to fetch cloud session', err);
+ }
+ },
+ async connectCloudSession() {
+ if (!this.newCloudSession.phone_number_id || !this.newCloudSession.waba_id || !this.newCloudSession.access_token) {
+ alert(this.lang === 'ar' ? 'الرجاء تعبئة جميع الحقول' : 'Please fill all fields');
+ return;
+ }
+ this.actionLoading = true;
+ try {
+ const response = await fetch('/api/cloud/connect', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${this.token}`
+ },
+ body: JSON.stringify(this.newCloudSession)
+ });
+ const data = await response.json();
+ if (response.ok && data.status === 'success') {
+ this.cloudSession = data.data;
+ this.newCloudSession = { phone_number_id: '', waba_id: '', access_token: '' };
+ } else {
+ alert(data.message || data.error || 'Connection failed');
+ }
+ } catch (err) {
+ alert('Network error connecting Cloud API');
+ } finally {
+ this.actionLoading = false;
+ }
+ },
+ async disconnectCloudSession() {
+ if (!confirm(this.lang === 'ar' ? 'هل أنت متأكد من قطع الاتصال بحساب Meta؟' : 'Are you sure you want to disconnect Meta Account?')) {
+ return;
+ }
+ this.actionLoading = true;
+ try {
+ const response = await fetch('/api/cloud/disconnect', {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${this.token}` }
+ });
+ const data = await response.json();
+ if (response.ok && data.status === 'success') {
+ this.cloudSession = null;
+ }
+ } catch (err) {
+ console.error('Failed to disconnect cloud session', err);
+ } finally {
+ this.actionLoading = false;
+ }
+ },
async deleteMetaSession(id) {
if (!confirm(this.lang === 'ar' ? 'هل أنت متأكد من حذف هذه القناة؟' : 'Are you sure you want to disconnect this channel?')) {
return;
diff --git a/backend/public/index.php b/backend/public/index.php
index a97f8b9..c566aff 100644
--- a/backend/public/index.php
+++ b/backend/public/index.php
@@ -103,6 +103,14 @@ $router->delete('/api/meta/sessions', [\App\Controllers\MetaWebhookContr
$router->get('/api/webhooks/meta', [\App\Controllers\MetaWebhookController::class, 'verify']);
$router->post('/api/webhooks/meta', [\App\Controllers\MetaWebhookController::class, 'webhook']);
+// WhatsApp Cloud API (Meta Official) Routes
+$router->get('/api/cloud/status', [\App\Controllers\CloudApiController::class, 'status'], [\App\Middlewares\AuthMiddleware::class]);
+$router->post('/api/cloud/connect', [\App\Controllers\CloudApiController::class, 'connect'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);
+$router->post('/api/cloud/disconnect', [\App\Controllers\CloudApiController::class, 'disconnect'], [\App\Middlewares\AuthMiddleware::class]);
+$router->post('/api/cloud/templates/sync', [\App\Controllers\CloudApiController::class, 'syncTemplates'],[\App\Middlewares\AuthMiddleware::class]);
+$router->get('/api/cloud/webhook', [\App\Controllers\CloudApiController::class, 'webhook']); // GET verify
+$router->post('/api/cloud/webhook', [\App\Controllers\CloudApiController::class, 'webhook']); // POST incoming messages
+
// Customer Service Agents (Staff) Routes
$router->get('/api/staff', [\App\Controllers\StaffController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/staff', [\App\Controllers\StaffController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);
diff --git a/whatsapp-gateway/cloud-api-client.js b/whatsapp-gateway/cloud-api-client.js
new file mode 100644
index 0000000..55546ff
--- /dev/null
+++ b/whatsapp-gateway/cloud-api-client.js
@@ -0,0 +1,414 @@
+/**
+ * CloudApiClient — WhatsApp Cloud API (Meta Official) Integration for Nabeh
+ *
+ * Handles communication with Meta's Graph API for WhatsApp Business:
+ * - Sending text, template, media, and interactive messages
+ * - Receiving and verifying incoming webhooks
+ * - Managing message templates
+ * - Uploading media assets
+ */
+
+const axios = require('axios');
+
+const GRAPH_API_VERSION = 'v20.0';
+const GRAPH_API_BASE = `https://graph.facebook.com/${GRAPH_API_VERSION}`;
+
+class CloudApiClient {
+ /**
+ * Send a text message via WhatsApp Cloud API
+ */
+ static async sendTextMessage(phoneNumberId, accessToken, to, text) {
+ const url = `${GRAPH_API_BASE}/${phoneNumberId}/messages`;
+ const payload = {
+ messaging_product: 'whatsapp',
+ recipient_type: 'individual',
+ to: to,
+ type: 'text',
+ text: { body: text }
+ };
+
+ return this._makeRequest(url, accessToken, payload);
+ }
+
+ /**
+ * Send a template message (pre-approved by Meta)
+ * @param {string} phoneNumberId - Meta Phone Number ID
+ * @param {string} accessToken - Meta Access Token
+ * @param {string} to - Recipient phone number (international format, no +)
+ * @param {string} templateName - Template name as registered in Meta
+ * @param {string} languageCode - e.g., 'ar', 'en'
+ * @param {Array} components - Template components (header, body, button params)
+ */
+ static async sendTemplateMessage(phoneNumberId, accessToken, to, templateName, languageCode = 'ar', components = []) {
+ const url = `${GRAPH_API_BASE}/${phoneNumberId}/messages`;
+ const template = {
+ name: templateName,
+ language: { code: languageCode }
+ };
+
+ if (components.length > 0) {
+ template.components = components;
+ }
+
+ const payload = {
+ messaging_product: 'whatsapp',
+ recipient_type: 'individual',
+ to: to,
+ type: 'template',
+ template: template
+ };
+
+ return this._makeRequest(url, accessToken, payload);
+ }
+
+ /**
+ * Send an image message
+ */
+ static async sendImageMessage(phoneNumberId, accessToken, to, imageUrl, caption = '') {
+ const url = `${GRAPH_API_BASE}/${phoneNumberId}/messages`;
+ const payload = {
+ messaging_product: 'whatsapp',
+ recipient_type: 'individual',
+ to: to,
+ type: 'image',
+ image: {
+ link: imageUrl,
+ caption: caption
+ }
+ };
+
+ return this._makeRequest(url, accessToken, payload);
+ }
+
+ /**
+ * Send a document message
+ */
+ static async sendDocumentMessage(phoneNumberId, accessToken, to, documentUrl, filename = 'document', caption = '') {
+ const url = `${GRAPH_API_BASE}/${phoneNumberId}/messages`;
+ const payload = {
+ messaging_product: 'whatsapp',
+ recipient_type: 'individual',
+ to: to,
+ type: 'document',
+ document: {
+ link: documentUrl,
+ filename: filename,
+ caption: caption
+ }
+ };
+
+ return this._makeRequest(url, accessToken, payload);
+ }
+
+ /**
+ * Send an interactive message with buttons
+ */
+ static async sendInteractiveButtons(phoneNumberId, accessToken, to, bodyText, buttons) {
+ const url = `${GRAPH_API_BASE}/${phoneNumberId}/messages`;
+ const payload = {
+ messaging_product: 'whatsapp',
+ recipient_type: 'individual',
+ to: to,
+ type: 'interactive',
+ interactive: {
+ type: 'button',
+ body: { text: bodyText },
+ action: {
+ buttons: buttons.slice(0, 3).map((btn, i) => ({
+ type: 'reply',
+ reply: {
+ id: btn.id || `btn_${i}`,
+ title: btn.title.substring(0, 20)
+ }
+ }))
+ }
+ }
+ };
+
+ return this._makeRequest(url, accessToken, payload);
+ }
+
+ /**
+ * Mark a message as read
+ */
+ static async markAsRead(phoneNumberId, accessToken, messageId) {
+ const url = `${GRAPH_API_BASE}/${phoneNumberId}/messages`;
+ const payload = {
+ messaging_product: 'whatsapp',
+ status: 'read',
+ message_id: messageId
+ };
+
+ return this._makeRequest(url, accessToken, payload);
+ }
+
+ /**
+ * Get message templates for a WhatsApp Business Account
+ */
+ static async getTemplates(wabaId, accessToken, limit = 100) {
+ const url = `${GRAPH_API_BASE}/${wabaId}/message_templates?limit=${limit}`;
+
+ try {
+ const response = await axios.get(url, {
+ headers: { 'Authorization': `Bearer ${accessToken}` },
+ timeout: 15000
+ });
+ return response.data;
+ } catch (err) {
+ console.error(`[CloudAPI] Failed to fetch templates:`, err.response?.data || err.message);
+ throw err;
+ }
+ }
+
+ /**
+ * Create a new message template
+ */
+ static async createTemplate(wabaId, accessToken, templateData) {
+ const url = `${GRAPH_API_BASE}/${wabaId}/message_templates`;
+
+ try {
+ const response = await axios.post(url, templateData, {
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ },
+ timeout: 15000
+ });
+ return response.data;
+ } catch (err) {
+ console.error(`[CloudAPI] Failed to create template:`, err.response?.data || err.message);
+ throw err;
+ }
+ }
+
+ /**
+ * Get the WhatsApp Business Profile
+ */
+ static async getBusinessProfile(phoneNumberId, accessToken) {
+ const url = `${GRAPH_API_BASE}/${phoneNumberId}/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical`;
+
+ try {
+ const response = await axios.get(url, {
+ headers: { 'Authorization': `Bearer ${accessToken}` },
+ timeout: 10000
+ });
+ return response.data;
+ } catch (err) {
+ console.error(`[CloudAPI] Failed to get business profile:`, err.response?.data || err.message);
+ throw err;
+ }
+ }
+
+ /**
+ * Upload media to WhatsApp servers
+ */
+ static async uploadMedia(phoneNumberId, accessToken, filePath, mimeType) {
+ const fs = require('fs');
+ const FormData = require('form-data');
+ const url = `${GRAPH_API_BASE}/${phoneNumberId}/media`;
+
+ const form = new FormData();
+ form.append('messaging_product', 'whatsapp');
+ form.append('file', fs.createReadStream(filePath));
+ form.append('type', mimeType);
+
+ try {
+ const response = await axios.post(url, form, {
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ ...form.getHeaders()
+ },
+ timeout: 30000
+ });
+ return response.data;
+ } catch (err) {
+ console.error(`[CloudAPI] Failed to upload media:`, err.response?.data || err.message);
+ throw err;
+ }
+ }
+
+ /**
+ * Verify webhook callback from Meta (GET request verification)
+ * @param {object} query - Request query parameters
+ * @param {string} verifyToken - Your configured verify token
+ * @returns {string|null} - Challenge string if valid, null otherwise
+ */
+ static verifyWebhook(query, verifyToken) {
+ const mode = query['hub.mode'];
+ const token = query['hub.verify_token'];
+ const challenge = query['hub.challenge'];
+
+ if (mode === 'subscribe' && token === verifyToken) {
+ console.log(`[CloudAPI Webhook] ✅ Verification successful`);
+ return challenge;
+ }
+
+ console.warn(`[CloudAPI Webhook] ❌ Verification failed. Mode: ${mode}, Token match: ${token === verifyToken}`);
+ return null;
+ }
+
+ /**
+ * Parse incoming webhook payload from Meta
+ * Returns an array of message objects or empty array
+ */
+ static parseIncomingWebhook(body) {
+ const messages = [];
+
+ if (!body || !body.entry) return messages;
+
+ for (const entry of body.entry) {
+ if (!entry.changes) continue;
+
+ for (const change of entry.changes) {
+ if (change.field !== 'messages') continue;
+ const value = change.value;
+
+ if (!value || !value.messages) continue;
+
+ const metadata = value.metadata || {};
+ const contacts = value.contacts || [];
+
+ for (const msg of value.messages) {
+ const contact = contacts.find(c => c.wa_id === msg.from) || {};
+
+ const parsed = {
+ id: msg.id,
+ from: msg.from,
+ name: contact.profile?.name || '',
+ timestamp: msg.timestamp,
+ type: msg.type,
+ phoneNumberId: metadata.phone_number_id,
+ displayPhone: metadata.display_phone_number,
+ body: '',
+ image: null,
+ audio: null,
+ document: null,
+ interactive: null
+ };
+
+ switch (msg.type) {
+ case 'text':
+ parsed.body = msg.text?.body || '';
+ break;
+ case 'image':
+ parsed.image = {
+ id: msg.image?.id,
+ mimeType: msg.image?.mime_type,
+ caption: msg.image?.caption || ''
+ };
+ parsed.body = msg.image?.caption || '';
+ break;
+ case 'audio':
+ parsed.audio = {
+ id: msg.audio?.id,
+ mimeType: msg.audio?.mime_type
+ };
+ break;
+ case 'document':
+ parsed.document = {
+ id: msg.document?.id,
+ mimeType: msg.document?.mime_type,
+ filename: msg.document?.filename
+ };
+ break;
+ case 'interactive':
+ parsed.interactive = {
+ type: msg.interactive?.type,
+ buttonReplyId: msg.interactive?.button_reply?.id,
+ buttonReplyTitle: msg.interactive?.button_reply?.title,
+ listReplyId: msg.interactive?.list_reply?.id,
+ listReplyTitle: msg.interactive?.list_reply?.title
+ };
+ parsed.body = msg.interactive?.button_reply?.title || msg.interactive?.list_reply?.title || '';
+ break;
+ case 'reaction':
+ parsed.body = `[Reaction: ${msg.reaction?.emoji || ''}]`;
+ break;
+ default:
+ parsed.body = `[${msg.type}]`;
+ }
+
+ messages.push(parsed);
+ }
+
+ // Also process status updates if needed
+ if (value.statuses) {
+ for (const status of value.statuses) {
+ messages.push({
+ type: 'status_update',
+ id: status.id,
+ from: status.recipient_id,
+ status: status.status, // sent, delivered, read, failed
+ timestamp: status.timestamp,
+ errors: status.errors || []
+ });
+ }
+ }
+ }
+ }
+
+ return messages;
+ }
+
+ /**
+ * Download media from WhatsApp Cloud API by media ID
+ */
+ static async downloadMedia(mediaId, accessToken) {
+ // Step 1: Get the media URL
+ const metaUrl = `${GRAPH_API_BASE}/${mediaId}`;
+ try {
+ const metaResponse = await axios.get(metaUrl, {
+ headers: { 'Authorization': `Bearer ${accessToken}` },
+ timeout: 10000
+ });
+
+ const mediaUrl = metaResponse.data.url;
+ if (!mediaUrl) throw new Error('No media URL returned');
+
+ // Step 2: Download the actual media
+ const mediaResponse = await axios.get(mediaUrl, {
+ headers: { 'Authorization': `Bearer ${accessToken}` },
+ responseType: 'arraybuffer',
+ timeout: 30000
+ });
+
+ return {
+ buffer: Buffer.from(mediaResponse.data),
+ mimeType: metaResponse.data.mime_type,
+ fileSize: metaResponse.data.file_size
+ };
+ } catch (err) {
+ console.error(`[CloudAPI] Failed to download media ${mediaId}:`, err.response?.data || err.message);
+ throw err;
+ }
+ }
+
+ /**
+ * Internal HTTP request helper
+ */
+ static async _makeRequest(url, accessToken, payload) {
+ try {
+ const response = await axios.post(url, payload, {
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ },
+ timeout: 15000
+ });
+
+ console.log(`[CloudAPI] ✅ Message sent successfully. ID: ${response.data?.messages?.[0]?.id || 'N/A'}`);
+ return response.data;
+ } catch (err) {
+ const errorData = err.response?.data?.error || {};
+ console.error(`[CloudAPI] ❌ Send failed:`, {
+ code: errorData.code,
+ type: errorData.type,
+ message: errorData.message,
+ fbtrace_id: errorData.fbtrace_id
+ });
+ throw new Error(errorData.message || err.message);
+ }
+ }
+}
+
+module.exports = CloudApiClient;