"""WhatsApp Business API service via Meta Cloud API. Handles sending and receiving WhatsApp messages. Uses Meta's Cloud API (free for 1,000 service conversations/month). Setup: 1. Create a Meta Business account at business.facebook.com 2. Create a WhatsApp Business app at developers.facebook.com 3. Get a permanent access token and Phone Number ID 4. Set environment variables: WHATSAPP_TOKEN= WHATSAPP_PHONE_ID= WHATSAPP_VERIFY_TOKEN= 5. Configure the webhook URL in Meta Developer Console: https:///pos/api/whatsapp/webhook """ import logging import requests from config import WHATSAPP_TOKEN, WHATSAPP_PHONE_ID logger = logging.getLogger(__name__) API_VERSION = "v18.0" BASE_URL = f"https://graph.facebook.com/{API_VERSION}" def _headers(): return { "Authorization": f"Bearer {WHATSAPP_TOKEN}", "Content-Type": "application/json", } def is_configured(): """Return True if WhatsApp credentials are set.""" return bool(WHATSAPP_TOKEN and WHATSAPP_PHONE_ID) # ── Sending ────────────────────────────────────────────────────────────── def send_message(to_phone, message_text): """Send a plain-text message via WhatsApp Cloud API. Args: to_phone: recipient phone in E.164 format (e.g. '5215512345678') message_text: text content Returns: dict with 'wa_message_id' on success or 'error' on failure. """ if not is_configured(): return {"error": "WhatsApp not configured — set WHATSAPP_TOKEN and WHATSAPP_PHONE_ID"} url = f"{BASE_URL}/{WHATSAPP_PHONE_ID}/messages" payload = { "messaging_product": "whatsapp", "recipient_type": "individual", "to": to_phone, "type": "text", "text": {"preview_url": False, "body": message_text}, } try: resp = requests.post(url, json=payload, headers=_headers(), timeout=15) data = resp.json() if resp.status_code in (200, 201): wa_id = data.get("messages", [{}])[0].get("id", "") return {"wa_message_id": wa_id} else: err = data.get("error", {}).get("message", resp.text) logger.error("WhatsApp send failed: %s", err) return {"error": err} except Exception as e: logger.exception("WhatsApp send exception") return {"error": str(e)} def send_template(to_phone, template_name, parameters=None): """Send a pre-approved template message (for initiating conversations). Args: to_phone: E.164 phone number template_name: approved template name (e.g. 'order_confirmation') parameters: list of {"type": "text", "text": "value"} dicts """ if not is_configured(): return {"error": "WhatsApp not configured"} url = f"{BASE_URL}/{WHATSAPP_PHONE_ID}/messages" components = [] if parameters: components.append({ "type": "body", "parameters": parameters, }) payload = { "messaging_product": "whatsapp", "to": to_phone, "type": "template", "template": { "name": template_name, "language": {"code": "es_MX"}, "components": components, }, } try: resp = requests.post(url, json=payload, headers=_headers(), timeout=15) data = resp.json() if resp.status_code in (200, 201): wa_id = data.get("messages", [{}])[0].get("id", "") return {"wa_message_id": wa_id} else: err = data.get("error", {}).get("message", resp.text) logger.error("WhatsApp template send failed: %s", err) return {"error": err} except Exception as e: logger.exception("WhatsApp template send exception") return {"error": str(e)} def send_quote(to_phone, quote_data): """Send a formatted quotation to a customer. Args: to_phone: E.164 phone number quote_data: dict with keys: business_name, quote_number, items[], subtotal, tax, total, validity_days, notes (optional) """ biz = quote_data.get("business_name", "Nexus Autoparts") qnum = quote_data.get("quote_number", "—") items = quote_data.get("items", []) subtotal = quote_data.get("subtotal", 0) tax = quote_data.get("tax", 0) total = quote_data.get("total", 0) validity = quote_data.get("validity_days", 7) lines = [ f"*{biz}*", f"Cotizacion #{qnum}", "─────────────────", ] for it in items: name = it.get("name", "Articulo") qty = it.get("quantity", 1) price = it.get("unit_price", 0) lines.append(f" {qty}x {name} — ${price:,.2f}") lines.append("─────────────────") lines.append(f"Subtotal: ${subtotal:,.2f}") lines.append(f"IVA: ${tax:,.2f}") lines.append(f"*Total: ${total:,.2f}*") lines.append(f"\nVigencia: {validity} dias") notes = quote_data.get("notes") if notes: lines.append(f"Nota: {notes}") return send_message(to_phone, "\n".join(lines)) def send_order_confirmation(to_phone, sale_data): """Send order / sale confirmation. Args: to_phone: E.164 phone number sale_data: dict with keys: business_name, folio, items[], total, payment_method """ biz = sale_data.get("business_name", "Nexus Autoparts") folio = sale_data.get("folio", "—") total = sale_data.get("total", 0) method = sale_data.get("payment_method", "efectivo") lines = [ f"*{biz}*", f"Confirmacion de venta #{folio}", "─────────────────", ] for it in sale_data.get("items", []): name = it.get("name", "Articulo") qty = it.get("quantity", 1) lines.append(f" {qty}x {name}") lines.append("─────────────────") lines.append(f"*Total: ${total:,.2f}*") lines.append(f"Pago: {method}") lines.append("\nGracias por su compra!") return send_message(to_phone, "\n".join(lines)) def send_stock_alert(to_phone, alert_data): """Send stock alert to owner/manager. Args: to_phone: E.164 phone alert_data: dict with keys: items[] (each has name, current_stock, min_stock) """ lines = [ "*ALERTA DE INVENTARIO*", "Los siguientes articulos estan bajos en stock:", "", ] for it in alert_data.get("items", []): name = it.get("name", "?") current = it.get("current_stock", 0) minimum = it.get("min_stock", 0) lines.append(f" {name}: {current} uds (min: {minimum})") lines.append("\nRevisa el inventario en tu POS.") return send_message(to_phone, "\n".join(lines)) # ── Incoming webhook processing ────────────────────────────────────────── def process_incoming(webhook_data): """Process incoming WhatsApp webhook message. Routes to AI chatbot for auto-response or queues for employee. Args: webhook_data: the full webhook payload from Meta Returns: dict with parsed message info: phone, text, wa_message_id, response (if auto-replied) """ result = {"handled": False} try: entry = webhook_data.get("entry", []) if not entry: return result changes = entry[0].get("changes", []) if not changes: return result value = changes[0].get("value", {}) # Handle status updates (delivered, read, etc.) statuses = value.get("statuses", []) if statuses: return { "type": "status_update", "statuses": statuses, "handled": True, } messages = value.get("messages", []) if not messages: return result msg = messages[0] phone = msg.get("from", "") wa_message_id = msg.get("id", "") msg_type = msg.get("type", "text") text = "" if msg_type == "text": text = msg.get("text", {}).get("body", "") elif msg_type == "interactive": interactive = msg.get("interactive", {}) if interactive.get("type") == "button_reply": text = interactive.get("button_reply", {}).get("title", "") elif interactive.get("type") == "list_reply": text = interactive.get("list_reply", {}).get("title", "") else: text = f"[{msg_type} message]" # Contact info contacts = value.get("contacts", []) contact_name = "" if contacts: profile = contacts[0].get("profile", {}) contact_name = profile.get("name", "") result = { "type": "message", "phone": phone, "contact_name": contact_name, "text": text, "message_type": msg_type, "wa_message_id": wa_message_id, "handled": True, } # Attempt AI auto-response if the ai_chat service is available try: from services.ai_chat import chat as ai_chat_fn ai_result = ai_chat_fn(text, []) ai_reply = ai_result.get("message", "") if ai_reply: send_result = send_message(phone, ai_reply) result["auto_reply"] = ai_reply result["auto_reply_wa_id"] = send_result.get("wa_message_id", "") except Exception: # AI not available — message stays in queue for employee logger.debug("AI auto-reply not available, message queued for employee") return result except Exception as e: logger.exception("Error processing incoming WhatsApp message") return {"error": str(e), "handled": False}