WA template

This commit is contained in:
debu-2 2026-10-02 21:59:13 +05:30
parent 7a2e21ba61
commit d272ae0bd3
12 changed files with 1359 additions and 5 deletions

View file

@ -0,0 +1,277 @@
import re
import secrets
from datetime import datetime, timedelta, timezone
from fastapi import HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.orm import Session
from .models import (
AgentMessageDispatchLog,
AgentMessageTemplate,
AgentTemplateSubscription,
AgentTemplateTestLog,
UserAccount,
)
from .whatsapp_config import resolved_whatsapp_phone_number_id
from .whatsapp_graph_client import WhatsAppGraphClient, WhatsAppGraphError
from .whatsapp_messages import extract_wa_message_id, normalize_wa_number
class SendAgentMessagePayload(BaseModel):
template: str = Field(min_length=2, max_length=64)
to: str = Field(min_length=10, max_length=20)
customer_name: str = Field(min_length=1, max_length=120)
report_id: str | None = Field(default=None, max_length=120)
def serialize_agent_template(template: AgentMessageTemplate, *, subscribed: bool = False) -> dict:
return {
"id": str(template.id),
"slug": template.slug,
"name": template.name,
"description": template.description,
"header_text": template.header_text,
"body_preview": template.body_preview,
"footer_text": template.footer_text,
"meta_template_name": template.meta_template_name,
"language": template.language,
"category": template.category,
"parameters": template.parameters,
"button_label": template.button_label,
"button_url_base": template.button_url_base,
"is_active": template.is_active,
"subscribed": subscribed,
}
def get_template_by_slug(db: Session, slug: str) -> AgentMessageTemplate:
template = db.scalar(
select(AgentMessageTemplate).where(
AgentMessageTemplate.slug == slug,
AgentMessageTemplate.is_active.is_(True),
)
)
if not template:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Agent template not found")
return template
def require_subscription(db: Session, user: UserAccount, template: AgentMessageTemplate) -> AgentTemplateSubscription:
subscription = db.scalar(
select(AgentTemplateSubscription).where(
AgentTemplateSubscription.user_id == user.id,
AgentTemplateSubscription.template_id == template.id,
)
)
if not subscription:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Subscribe to this agent template before sending messages",
)
return subscription
def _validate_report_id(report_id: str) -> str:
cleaned = report_id.strip()
if not cleaned or len(cleaned) > 120:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid report_id")
if not re.fullmatch(r"[A-Za-z0-9._-]+", cleaned):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="report_id may only contain letters, numbers, dots, underscores, and hyphens",
)
return cleaned
def _build_template_components(template: AgentMessageTemplate, customer_name: str, report_id: str) -> list[dict]:
components: list[dict] = [
{
"type": "body",
"parameters": [
{
"type": "text",
"parameter_name": "customer_name",
"text": customer_name.strip(),
}
],
}
]
if template.button_label and template.button_url_base:
components.append(
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [{"type": "text", "text": report_id}],
}
)
return components
def _whatsapp_client(db: Session) -> WhatsAppGraphClient:
return WhatsAppGraphClient(phone_number_id=resolved_whatsapp_phone_number_id(db))
async def send_agent_template_message(
db: Session,
user: UserAccount,
payload: SendAgentMessagePayload,
*,
source: str = "api",
) -> dict:
template = get_template_by_slug(db, payload.template)
require_subscription(db, user, template)
try:
recipient = normalize_wa_number(payload.to)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
report_id = _validate_report_id(payload.report_id or f"run-{secrets.token_hex(3)}")
components = _build_template_components(template, payload.customer_name, report_id)
client = _whatsapp_client(db)
try:
result = await client.send_template(
recipient,
template.meta_template_name,
language_code=template.language,
components=components,
)
except WhatsAppGraphError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
wa_message_id = extract_wa_message_id(result)
log = AgentMessageDispatchLog(
user_id=user.id,
template_id=template.id,
recipient=recipient,
parameters={
"customer_name": payload.customer_name.strip(),
"report_id": report_id,
},
wa_message_id=wa_message_id,
status="sent",
source=source,
)
db.add(log)
db.commit()
db.refresh(log)
report_url = f"{template.button_url_base.rstrip('/')}/{report_id}" if template.button_url_base else None
return {
"success": True,
"template": template.slug,
"to": recipient,
"customer_name": payload.customer_name.strip(),
"report_id": report_id,
"report_url": report_url,
"wa_message_id": wa_message_id,
"dispatch_id": str(log.id),
}
def can_run_console_test(db: Session, user: UserAccount, template: AgentMessageTemplate) -> dict:
since = datetime.now(timezone.utc) - timedelta(days=1)
recent = db.scalar(
select(AgentTemplateTestLog)
.where(
AgentTemplateTestLog.user_id == user.id,
AgentTemplateTestLog.template_id == template.id,
AgentTemplateTestLog.tested_at >= since,
AgentTemplateTestLog.success.is_(True),
)
.order_by(AgentTemplateTestLog.tested_at.desc())
)
if recent:
retry_after = recent.tested_at + timedelta(days=1)
return {
"allowed": False,
"retry_after": retry_after,
"last_tested_at": recent.tested_at,
}
return {"allowed": True, "retry_after": None, "last_tested_at": None}
async def run_console_template_test(
db: Session,
user: UserAccount,
template: AgentMessageTemplate,
*,
to: str,
customer_name: str,
report_id: str | None = None,
) -> dict:
require_subscription(db, user, template)
test_status = can_run_console_test(db, user, template)
if not test_status["allowed"]:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Console test limit reached. You can test this template once per day.",
)
payload = SendAgentMessagePayload(
template=template.slug,
to=to,
customer_name=customer_name,
report_id=report_id,
)
result = await send_agent_template_message(db, user, payload, source="console_test")
db.add(
AgentTemplateTestLog(
user_id=user.id,
template_id=template.id,
recipient=result["to"],
success=True,
)
)
db.commit()
return result
def build_agent_message_docs(template: AgentMessageTemplate) -> dict:
return {
"template": serialize_agent_template(template),
"api": {
"method": "POST",
"path": "/api/v1/agent/messages/send",
"summary": (
"Send an approved agent WhatsApp template via Urmi. Urmi authenticates your API key "
"and relays the message through the Urmi AI WhatsApp Business account."
),
"authentication": {
"type": "api_key",
"header": "Authorization",
"format": "Bearer urmi_live_<key>",
"alternate_header": "X-Api-Key",
},
"parameters": {
"template": f"Template slug. Use `{template.slug}`.",
"to": "Recipient WhatsApp number with country code, e.g. 919149154337",
"customer_name": "Recipient name inserted into the template body",
"report_id": "Optional unique report id for the View Report button URL suffix",
},
"example": {
"template": template.slug,
"to": "919149154337",
"customer_name": "Suyash",
"report_id": "run-abc123",
},
"response_example": {
"success": True,
"template": template.slug,
"to": "919149154337",
"customer_name": "Suyash",
"report_id": "run-abc123",
"report_url": f"{template.button_url_base or ''}run-abc123",
"wa_message_id": "wamid.HBg...",
"dispatch_id": "00000000-0000-0000-0000-000000000001",
},
},
"notes": [
"Subscribe to the template in the Urmi console before calling this API.",
"Messages are sent from the Urmi AI WhatsApp business number on your behalf.",
"If report_id is omitted, Urmi generates a unique id automatically.",
],
}