"""WhatsApp webhook handler — Meta Graph API integration.""" from __future__ import annotations import logging from datetime import datetime, timezone from typing import Annotated import httpx from fastapi import APIRouter, Depends, Query from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.database import get_db from app.models.whatsapp_log import WhatsAppLog from app.schemas.whatsapp import ( MetaWebhookRequest, MockWhatsAppLogEntry, WebhookVerificationResponse, WhatsAppReplyResponse, ) from app.services.ticket import create_ticket logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/whatsapp", tags=["whatsapp"]) REPLY_TEMPLATE = ( "Thank you for contacting Denya OneCare. " "Your ticket number is {ticket_number}. " "We will get back to you soon." ) # ── Meta Graph API helpers ────────────────────────────────────────── async def send_whatsapp_reply( to_phone: str, text: str, ) -> WhatsAppReplyResponse: """Send a text message via Meta Graph API.""" url = f"{settings.META_GRAPH_BASE}/{settings.WHATSAPP_PHONE_NUMBER_ID}/messages" headers = { "Authorization": f"Bearer {settings.WHATSAPP_ACCESS_TOKEN}", "Content-Type": "application/x-www-form-urlencoded", } data = { "messaging_product": "whatsapp", "to": to_phone, "type": "text", "text": {"body": text}, } # Encode nested dict as JSON string for form data (Meta requirement) data["text"] = '{"body":' + f'"{text}"' + "}" try: async with httpx.AsyncClient() as client: response = await client.post(url, headers=headers, data=data, timeout=15.0) response.raise_for_status() return WhatsAppReplyResponse(success=True, message="Reply sent") except httpx.HTTPError as exc: logger.error("Failed to send WhatsApp reply: %s", exc) return WhatsAppReplyResponse(success=False, message=str(exc)) # ── Webhook endpoint ──────────────────────────────────────────────── @router.post("/webhook") async def whatsapp_webhook( body: MetaWebhookRequest, db: Annotated[AsyncSession, Depends(get_db)], hub_verify_token: str | None = Query(None, alias="hub.verify_token"), mode: str | None = Query(None), hub_challenge: str | None = Query(None), ) -> dict: """Handle incoming WhatsApp webhook from Meta.""" # ── Verification GET request (Meta sends this on webhook setup) ── if mode and hub_challenge: if hub_verify_token != settings.WHATSAPP_VERIFY_TOKEN: return {"error": "Verify token mismatch"} return WebhookVerificationResponse(challenge=hub_challenge).model_dump() # ── Process inbound messages ──────────────────────────────────── if not body.entry: return {"status": "no entry"} for entry in body.entry: if not entry.changes: continue for change in entry.changes: if not change.message or not change.message.text: logger.info("Non-text message received, skipping") continue message_text = change.message.text.text sender = change.message.from_field wa_msg_id = change.message.id or change.id # ── Create ticket ────────────────────────────────────── try: ticket = await create_ticket( db, data={ "description": message_text, "reporter": sender, "reported_via": "WhatsApp", }, ) except Exception as exc: logger.error("Failed to create ticket: %s", exc) # Still log the message even if ticket creation fails log = WhatsAppLog( from_number=sender, message_text=message_text, wa_message_id=wa_msg_id, ticket_id=None, ticket_number=None, received_at=datetime.now(timezone.utc), ) db.add(log) await db.flush() return {"status": "ticket creation failed, message logged"} # ── Store WhatsApp log ───────────────────────────────── log = WhatsAppLog( from_number=sender, message_text=message_text, wa_message_id=wa_msg_id, ticket_id=ticket.id, ticket_number=ticket.ticket_number, received_at=datetime.now(timezone.utc), ) db.add(log) await db.flush() # ── Send auto-reply ──────────────────────────────────── reply_text = REPLY_TEMPLATE.format(ticket_number=ticket.ticket_number) reply_result = await send_whatsapp_reply(sender, reply_text) if not reply_result.success: logger.warning( "Auto-reply failed for ticket %s: %s", ticket.ticket_number, reply_result.message, ) return { "status": "processed", "ticket_id": ticket.id, "ticket_number": ticket.ticket_number, "reply_sent": reply_result.success, } return {"status": "no messages"} # ── Legacy debug endpoint ────────────────────────────────────────── @router.get("/mock-log", response_model=list[MockWhatsAppLogEntry]) async def mock_whatsapp_log( db: Annotated[AsyncSession, Depends(get_db)], limit: int = 50, ) -> list[MockWhatsAppLogEntry]: """Return recent WhatsApp webhook submissions for debugging.""" result = await db.execute( select(WhatsAppLog).order_by(WhatsAppLog.received_at.desc()).limit(limit) ) return list(result.scalars().all())