Test messages work with test number and hello_world template

This commit is contained in:
dvirlabs 2026-02-12 01:23:51 +02:00
parent d7185786e7
commit 09dfacd142
9 changed files with 2561 additions and 68 deletions

View File

@ -1,5 +1,6 @@
from pydantic_settings import BaseSettings
from typing import List
import os
class Settings(BaseSettings):
# Database
@ -23,6 +24,7 @@ class Settings(BaseSettings):
WHATSAPP_PROVIDER: str = "mock" # mock, cloud, or telegram
WHATSAPP_CLOUD_ACCESS_TOKEN: str = ""
WHATSAPP_CLOUD_PHONE_NUMBER_ID: str = ""
WHATSAPP_CLOUD_PHONE_NUMBER_ID_OVERRIDE: str = ""
WHATSAPP_WEBHOOK_VERIFY_TOKEN: str = ""
# Telegram Provider (for testing)
@ -38,7 +40,8 @@ class Settings(BaseSettings):
return [origin.strip() for origin in self.CORS_ORIGINS.split(",")]
class Config:
env_file = ".env"
# Look for .env in the backend directory
env_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), ".env")
case_sensitive = True
settings = Settings()

View File

@ -1,8 +1,15 @@
import sys
import os
# Add the backend directory to the Python path so imports work when running from app directory
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.api import auth, contacts, lists, templates, campaigns, imports, google, webhooks, stats, workers
import logging
import uvicorn
# Configure logging
logging.basicConfig(
@ -17,9 +24,10 @@ app = FastAPI(
)
# CORS
cors_origins = list({*settings.cors_origins_list, "http://localhost:5173"})
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins_list,
allow_origins=cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@ -48,3 +56,6 @@ def root():
@app.get("/health")
def health_check():
return {"status": "healthy"}
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000, reload=False)

View File

@ -1,15 +1,158 @@
import httpx
from typing import Dict, Any, Optional
import json
import logging
import re
import time
from typing import Dict, Any, Optional, List
from app.providers.base import BaseProvider
from app.core.config import settings
logger = logging.getLogger(__name__)
class WhatsAppCloudProvider(BaseProvider):
"""WhatsApp Cloud API provider (Meta)"""
"""WhatsApp Cloud API provider (Meta)
IMPORTANT: WhatsApp templates are scoped to a Phone Number ID.
A template created under phone number A will ALWAYS return error 132001
if you try to send it using phone number B's credentials.
The provider MUST use the correct PHONE_NUMBER_ID that owns the template.
"""
def __init__(self):
self.access_token = settings.WHATSAPP_CLOUD_ACCESS_TOKEN
self.phone_number_id = settings.WHATSAPP_CLOUD_PHONE_NUMBER_ID
self.base_url = "https://graph.facebook.com/v18.0"
self.phone_number_id_override = settings.WHATSAPP_CLOUD_PHONE_NUMBER_ID_OVERRIDE
self.base_url = "https://graph.facebook.com/v22.0"
def _mask_to(self, to: str) -> str:
digits = re.sub(r"\D", "", to or "")
if len(digits) <= 4:
return "****"
return f"***{digits[-4:]}"
def _mask_token(self, token: str) -> str:
if not token:
return ""
if len(token) <= 6:
return "***"
return f"***{token[-6:]}"
def _resolve_phone_number_id(self) -> str:
resolved = (self.phone_number_id_override or "").strip() or (self.phone_number_id or "").strip()
if self.phone_number_id_override and self.phone_number_id_override.strip():
logger.info(
"WhatsApp Cloud phone_number_id override active",
extra={"phone_number_id": resolved}
)
return resolved
def _validate_phone_number_id(self, phone_number_id: str) -> None:
if not phone_number_id:
raise ValueError(
"Set WHATSAPP_CLOUD_PHONE_NUMBER_ID to the value from Meta API Setup URL: "
"https://graph.facebook.com/v22.0/<ID>/messages"
)
if not phone_number_id.isdigit():
raise ValueError(
"Set WHATSAPP_CLOUD_PHONE_NUMBER_ID to the value from Meta API Setup URL: "
"https://graph.facebook.com/v22.0/<ID>/messages"
)
if len(phone_number_id) < 10 or len(phone_number_id) > 20:
raise ValueError(
"Set WHATSAPP_CLOUD_PHONE_NUMBER_ID to the value from Meta API Setup URL: "
"https://graph.facebook.com/v22.0/<ID>/messages"
)
def _extract_placeholder_names(self, template_body: str) -> List[str]:
if not template_body:
return []
return re.findall(r"\{\{(\w+)\}\}", template_body)
def _build_template_parameters(
self,
template_body: str,
variables: Dict[str, str]
) -> List[Dict[str, str]]:
params: List[Dict[str, str]] = []
# Meta templates use ordered parameters ({{1}}, {{2}}, ...); we map
# app placeholders (e.g. {{first_name}}) to that ordered list.
for name in self._extract_placeholder_names(template_body):
value = variables.get(name) or "Friend"
params.append({"type": "text", "text": value})
return params
def _safe_json(self, response: httpx.Response) -> Dict[str, Any]:
try:
return response.json()
except Exception:
return {"raw": response.text}
def _should_retry_with_en_us(
self,
language: str,
response_json: Dict[str, Any]
) -> bool:
if language != "en":
return False
error = response_json.get("error", {}) if isinstance(response_json, dict) else {}
error_code = error.get("code")
# Do NOT retry on 132001 — that's a phone number mismatch, not language
if error_code == 132001:
return False
message = " ".join([
str(error.get("message", "")),
str(error.get("error_user_msg", "")),
json.dumps(error.get("error_data", {}))
]).lower()
# Only retry if error explicitly mentions language or locale
return "language" in message or "locale" in message
def _post_with_retries(
self,
url: str,
headers: Dict[str, str],
payload: Dict[str, Any],
retries: int = 2
) -> httpx.Response:
with httpx.Client(timeout=30.0) as client:
for attempt in range(retries + 1):
response = client.post(url, headers=headers, json=payload)
if response.status_code >= 500 and attempt < retries:
time.sleep(0.5 * (attempt + 1))
continue
return response
return response
def _log_response(
self,
phone_number_id: str,
to: str,
template_name: Optional[str],
language: str,
url: str,
response_json: Dict[str, Any]
) -> None:
logger.info(
"WhatsApp Cloud response",
extra={
"phone_number_id": phone_number_id,
"url": url,
"to": self._mask_to(to),
"template_name": template_name,
"language": language,
"response": response_json
}
)
def _raise_api_error(self, response: httpx.Response, response_json: Dict[str, Any]) -> None:
if isinstance(response_json, dict) and response_json.get("error"):
message = response_json["error"].get("message", "Unknown error")
else:
message = response.text
raise Exception(f"WhatsApp API error: {response.status_code} - {message}")
def send_message(
self,
@ -22,72 +165,136 @@ class WhatsAppCloudProvider(BaseProvider):
"""
Send message via WhatsApp Cloud API.
If template_name is provided, sends a template message.
Otherwise, sends a text message.
Requires:
- WHATSAPP_CLOUD_PHONE_NUMBER_ID: The phone number ID that owns the template
- WHATSAPP_CLOUD_ACCESS_TOKEN: Valid Meta access token
Template ownership matters: templates are scoped to a specific Phone Number ID.
Sending from a different phone number will fail with error 132001.
"""
url = f"{self.base_url}/{self.phone_number_id}/messages"
phone_number_id = self._resolve_phone_number_id()
self._validate_phone_number_id(phone_number_id)
if not template_name:
raise Exception("WhatsApp Cloud requires an approved template name")
# Send template message
# Build template parameters only if template body has placeholders
parameters = self._build_template_parameters(template_body, variables)
# Build template object - only include components if there are parameters
template_obj = {
"name": template_name,
"language": {
"code": language
}
}
if parameters:
template_obj["components"] = [
{
"type": "body",
"parameters": parameters
}
]
url = f"{self.base_url}/{phone_number_id}/messages"
headers = {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json"
}
if template_name:
# Send template message
# Build template parameters
components = []
if variables:
parameters = [
{"type": "text", "text": value}
for value in variables.values()
]
components.append({
"type": "body",
"parameters": parameters
})
payload = {
"messaging_product": "whatsapp",
"to": to.replace("+", ""),
"type": "template",
"template": template_obj
}
payload = {
"messaging_product": "whatsapp",
"to": to.replace("+", ""),
"type": "template",
"template": {
"name": template_name,
"language": {
"code": language
},
"components": components
}
logger.info(
"WhatsApp Cloud config",
extra={
"phone_number_id": phone_number_id,
"url": url,
"template_name": template_name,
"language": language,
"access_token": self._mask_token(self.access_token),
"to": self._mask_to(to)
}
else:
# Send text message (only for contacts with conversation window)
# Substitute variables in body
message_text = template_body
for key, value in variables.items():
message_text = message_text.replace(f"{{{{{key}}}}}", value)
)
logger.info(
"WhatsApp Cloud config details: phone_number_id=%s url=%s template=%s language=%s access_token=%s to=%s",
phone_number_id,
url,
template_name,
language,
self._mask_token(self.access_token),
self._mask_to(to)
)
payload = {
"messaging_product": "whatsapp",
"to": to.replace("+", ""),
"type": "text",
"text": {
"body": message_text
}
}
# Log sanitized payload (mask sensitive data)
sanitized_payload = payload.copy()
sanitized_payload["to"] = self._mask_to(sanitized_payload["to"])
logger.info(
"WhatsApp Cloud request payload: %s",
json.dumps(sanitized_payload, ensure_ascii=True)
)
try:
with httpx.Client(timeout=30.0) as client:
response = client.post(url, headers=headers, json=payload)
response.raise_for_status()
response = self._post_with_retries(url, headers, payload)
response_json = self._safe_json(response)
self._log_response(phone_number_id, to, template_name, language, url, response_json)
logger.info(
"WhatsApp Cloud response details: status=%s response=%s",
response.status_code,
json.dumps(response_json, ensure_ascii=True)
)
data = response.json()
message_id = data.get("messages", [{}])[0].get("id")
if response.is_error and template_name and self._should_retry_with_en_us(language, response_json):
logger.info(f"Retrying with en_US language code")
payload["template"]["language"]["code"] = "en_US"
response = self._post_with_retries(url, headers, payload)
response_json = self._safe_json(response)
self._log_response(phone_number_id, to, template_name, "en_US", url, response_json)
logger.info(
"WhatsApp Cloud response details (en_US retry): status=%s response=%s",
response.status_code,
json.dumps(response_json, ensure_ascii=True)
)
if not message_id:
raise Exception("No message ID in response")
if response.is_error:
error_code = response_json.get("error", {}).get("code") if isinstance(response_json, dict) else None
if error_code == 132001:
logger.error(
"WhatsApp Cloud template not found (132001)",
extra={
"phone_number_id": phone_number_id,
"template_name": template_name,
"language": language,
"response": response_json
}
)
logger.error(
"WhatsApp Cloud template not found (132001) details: phone_number_id=%s template=%s language=%s response=%s",
phone_number_id,
template_name,
language,
json.dumps(response_json, ensure_ascii=True)
)
raise Exception(
f"WhatsApp API error 132001: Template '{template_name}' not found for "
f"Phone Number ID {phone_number_id}. "
f"Make sure the template exists and is associated with this phone number. "
f"(Full error: {response_json.get('error', {}).get('message', 'unknown')})"
)
else:
self._raise_api_error(response, response_json)
return message_id
message_id = response_json.get("messages", [{}])[0].get("id")
if not message_id:
raise Exception("No message ID in response")
return message_id
except httpx.HTTPStatusError as e:
raise Exception(f"WhatsApp API error: {e.response.status_code} - {e.response.text}")
except Exception as e:
raise Exception(f"Send error: {str(e)}")

185
backend/schema.sql Normal file
View File

@ -0,0 +1,185 @@
-- Enums
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'userrole') THEN
CREATE TYPE userrole AS ENUM ('ADMIN', 'USER');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'campaignstatus') THEN
CREATE TYPE campaignstatus AS ENUM ('DRAFT', 'SCHEDULED', 'SENDING', 'DONE', 'FAILED');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'recipientstatus') THEN
CREATE TYPE recipientstatus AS ENUM ('PENDING', 'SENT', 'DELIVERED', 'READ', 'FAILED');
END IF;
END$$;
-- users
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR NOT NULL,
password_hash VARCHAR NOT NULL,
role userrole NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS ix_users_email ON users (email);
CREATE INDEX IF NOT EXISTS ix_users_id ON users (id);
-- contacts
CREATE TABLE IF NOT EXISTS contacts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
phone_e164 VARCHAR NOT NULL,
first_name VARCHAR,
last_name VARCHAR,
email VARCHAR,
opted_in BOOLEAN NOT NULL,
conversation_window_open BOOLEAN NOT NULL,
source VARCHAR,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
CONSTRAINT uq_user_phone UNIQUE (user_id, phone_e164)
);
CREATE INDEX IF NOT EXISTS ix_contacts_user_phone ON contacts (user_id, phone_e164);
CREATE INDEX IF NOT EXISTS ix_contacts_id ON contacts (id);
CREATE INDEX IF NOT EXISTS ix_contacts_user_id ON contacts (user_id);
-- contact_tags
CREATE TABLE IF NOT EXISTS contact_tags (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_contact_tags_id ON contact_tags (id);
CREATE INDEX IF NOT EXISTS ix_contact_tags_user_id ON contact_tags (user_id);
-- contact_tag_map
CREATE TABLE IF NOT EXISTS contact_tag_map (
id SERIAL PRIMARY KEY,
contact_id INTEGER NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES contact_tags(id) ON DELETE CASCADE,
CONSTRAINT uq_contact_tag UNIQUE (contact_id, tag_id)
);
CREATE INDEX IF NOT EXISTS ix_contact_tag_map_id ON contact_tag_map (id);
-- dnd_list
CREATE TABLE IF NOT EXISTS dnd_list (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
phone_e164 VARCHAR NOT NULL,
reason TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
CONSTRAINT uq_dnd_user_phone UNIQUE (user_id, phone_e164)
);
CREATE INDEX IF NOT EXISTS ix_dnd_list_id ON dnd_list (id);
CREATE INDEX IF NOT EXISTS ix_dnd_list_phone_e164 ON dnd_list (phone_e164);
CREATE INDEX IF NOT EXISTS ix_dnd_list_user_id ON dnd_list (user_id);
-- lists
CREATE TABLE IF NOT EXISTS lists (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_lists_id ON lists (id);
CREATE INDEX IF NOT EXISTS ix_lists_user_id ON lists (user_id);
-- list_members
CREATE TABLE IF NOT EXISTS list_members (
id SERIAL PRIMARY KEY,
list_id INTEGER NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
contact_id INTEGER NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
CONSTRAINT uq_list_contact UNIQUE (list_id, contact_id)
);
CREATE INDEX IF NOT EXISTS ix_list_members_contact_id ON list_members (contact_id);
CREATE INDEX IF NOT EXISTS ix_list_members_id ON list_members (id);
CREATE INDEX IF NOT EXISTS ix_list_members_list_id ON list_members (list_id);
-- templates
CREATE TABLE IF NOT EXISTS templates (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR NOT NULL,
language VARCHAR NOT NULL,
body_text TEXT NOT NULL,
is_whatsapp_template BOOLEAN NOT NULL,
provider_template_name VARCHAR,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_templates_id ON templates (id);
CREATE INDEX IF NOT EXISTS ix_templates_user_id ON templates (user_id);
-- campaigns
CREATE TABLE IF NOT EXISTS campaigns (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR NOT NULL,
template_id INTEGER REFERENCES templates(id) ON DELETE SET NULL,
list_id INTEGER REFERENCES lists(id) ON DELETE SET NULL,
status campaignstatus NOT NULL,
scheduled_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_campaigns_id ON campaigns (id);
CREATE INDEX IF NOT EXISTS ix_campaigns_status ON campaigns (status);
CREATE INDEX IF NOT EXISTS ix_campaigns_user_id ON campaigns (user_id);
-- campaign_recipients
CREATE TABLE IF NOT EXISTS campaign_recipients (
id SERIAL PRIMARY KEY,
campaign_id INTEGER NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
contact_id INTEGER NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
status recipientstatus NOT NULL,
provider_message_id VARCHAR,
last_error TEXT,
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_campaign_recipients_campaign_status ON campaign_recipients (campaign_id, status);
CREATE INDEX IF NOT EXISTS ix_campaign_recipients_campaign_id ON campaign_recipients (campaign_id);
CREATE INDEX IF NOT EXISTS ix_campaign_recipients_contact_id ON campaign_recipients (contact_id);
CREATE INDEX IF NOT EXISTS ix_campaign_recipients_id ON campaign_recipients (id);
-- send_logs
CREATE TABLE IF NOT EXISTS send_logs (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
campaign_id INTEGER REFERENCES campaigns(id) ON DELETE CASCADE,
contact_id INTEGER REFERENCES contacts(id) ON DELETE CASCADE,
provider VARCHAR NOT NULL,
request_payload_json JSON,
response_payload_json JSON,
status VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_send_logs_campaign_id ON send_logs (campaign_id);
CREATE INDEX IF NOT EXISTS ix_send_logs_created_at ON send_logs (created_at);
CREATE INDEX IF NOT EXISTS ix_send_logs_id ON send_logs (id);
CREATE INDEX IF NOT EXISTS ix_send_logs_user_id ON send_logs (user_id);
-- jobs
CREATE TABLE IF NOT EXISTS jobs (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR NOT NULL,
payload_json JSON NOT NULL,
status VARCHAR NOT NULL,
attempts INTEGER NOT NULL,
run_after TIMESTAMPTZ NOT NULL DEFAULT now(),
last_error TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_jobs_status_run_after ON jobs (status, run_after);
CREATE INDEX IF NOT EXISTS ix_jobs_id ON jobs (id);
CREATE INDEX IF NOT EXISTS ix_jobs_user_id ON jobs (user_id);
-- google_tokens
CREATE TABLE IF NOT EXISTS google_tokens (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
encrypted_token TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS ix_google_tokens_user_id ON google_tokens (user_id);
CREATE INDEX IF NOT EXISTS ix_google_tokens_id ON google_tokens (id);

View File

@ -0,0 +1,52 @@
import os
import json
from app.providers.whatsapp_cloud import WhatsAppCloudProvider
def main() -> None:
to_number = os.getenv("WHATSAPP_TEST_TO", "")
if not to_number:
raise SystemExit("ERROR: Set WHATSAPP_TEST_TO to an E.164 number (e.g., 14155552671 or +14155552671)")
template_name = os.getenv("WHATSAPP_TEMPLATE_NAME", "hello_world")
template_language = os.getenv("WHATSAPP_TEMPLATE_LANGUAGE", "en_US")
first_name = os.getenv("WHATSAPP_TEST_FIRST_NAME", "Friend")
# Determine template body based on template name
# hello_world has NO variables (no {{...}})
# custom_hello_world has {{first_name}}
if template_name == "hello_world":
template_body = "Hello World" # No variables
variables = {}
else:
template_body = "Hello {{first_name}}, welcome!" # Has variable
variables = {"first_name": first_name}
print(f"\n{'='*70}")
print(f"WhatsApp Cloud Template Debug Test")
print(f"{'='*70}")
print(f"Phone Number: {to_number}")
print(f"Template Name: {template_name}")
print(f"Template Language: {template_language}")
print(f"Template Body: {template_body}")
print(f"Variables: {variables}")
print(f"{'='*70}\n")
try:
provider = WhatsAppCloudProvider()
message_id = provider.send_message(
to=to_number,
template_name=template_name,
template_body=template_body,
variables=variables,
language=template_language
)
print(f"✅ SUCCESS: Message sent with ID: {message_id}\n")
return 0
except Exception as e:
print(f"❌ FAILED: {str(e)}\n")
return 1
if __name__ == "__main__":
exit(main())

View File

@ -31,11 +31,11 @@ services:
GOOGLE_CLIENT_SECRET: your-google-client-secret
GOOGLE_REDIRECT_URI: http://localhost:8000/api/imports/google/callback
GOOGLE_TOKEN_ENCRYPTION_KEY: your-fernet-key-32-bytes-base64
WHATSAPP_PROVIDER: telegram
WHATSAPP_CLOUD_ACCESS_TOKEN: your-whatsapp-token
WHATSAPP_CLOUD_PHONE_NUMBER_ID: your-phone-number-id
WHATSAPP_PROVIDER: cloud
WHATSAPP_CLOUD_ACCESS_TOKEN: EAAMdmYX7DJUBQoJYK1CyY9X6QLZAO4FkdMnhDZBbn0JjpbpHTPIBAXNA5nGq1cO9Ge96uti7CLq1kTfaMEnLoZBOsYNbOQGyQBnNbyZCmHmo7liDKSzahww3RE4WZB3Cej6UNHEttzN0gQHeAMJ7ZA3vEhCCIBNphKILJYglqoY8IYTm1G5pdqsELaPjvNRmBLVwMtEZBuKr2NLpsJqHIFiU7UUFk9DkuEXCCR8xwmYmZBrjDhsCEYwVjLO7URq8RagmGl7GBYdwQRX51zlrtqSiGaU1
WHATSAPP_CLOUD_PHONE_NUMBER_ID: "968263383040867"
WHATSAPP_WEBHOOK_VERIFY_TOKEN: your-webhook-verify-token
TELEGRAM_BOT_TOKEN: 8428015346:AAH2MOb9D1HUlINOxcDFMa6q98qGo4rPSYo
TELEGRAM_BOT_TOKEN: ""
depends_on:
postgres:
condition: service_healthy

2033
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,10 @@
import axios from 'axios';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
const API_URL = import.meta.env.VITE_API_URL;
const baseURL = API_URL ? `${API_URL.replace(/\/$/, '')}/api` : '/api';
const api = axios.create({
baseURL: `${API_URL}/api`,
baseURL,
headers: {
'Content-Type': 'application/json'
}

View File

@ -91,6 +91,7 @@ export const TemplatesPage = () => {
<label className="form-label">Language</label>
<select className="form-select" value={formData.language} onChange={(e) => setFormData({ ...formData, language: e.target.value })}>
<option value="en">English</option>
<option value="en_US">English (United States)</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="ar">Arabic</option>