CTO — make /api/lia/inbound conversation-aware (no more hard-coded payments_remaining in Make) Goal: When a WhatsApp message comes in, LIA should attach it to the correct dp_conversations row based on the phone number, so Make doesn’t have to send payments_remaining=12 or a conversation_id. Current behaviour: - Make webhook → /api/lia/inbound with: - from_number: "whatsapp:+4479..." - body: "" - channel: "whatsapp" - conversation_id: null - payments_remaining: 12 (hard-coded for testing) - process_inbound_message uses payments_remaining to set stage and behaves as if it’s a 12-month case. Required new behaviour: 1) Phone number normalisation - From payload.from_number, strip the "whatsapp:" prefix if present. - Ensure the stored phone_mobile in dp_customer is in +44… E.164 format. - We’ll assume: - payload.from_number is "whatsapp:+44..." (Twilio format) - dp_customer.phone_mobile is "+44..." (no "whatsapp:") Add a helper in app/db.py: async def get_active_conversation_for_number(phone_e164: str) -> Optional[Record]: """ Given a phone number in +44... format, return the most recent non-DNC conversation for that customer (if any). Join dp_customer.phone_mobile -> dp_conversations.customer_id. Exclude status='dnc'. Order by created_at DESC limit 1. """ 2) In app/lia_inbound.py, process_inbound_message: Replace the “new conversation” branch when payload.conversation_id is null with: - Normalise from_number to E.164 (remove "whatsapp:" if present). - Call get_active_conversation_for_number(phone). - If a conversation is found: - Use its conversation_id, stage, status, soft_no_count, confusion_count, needs_human_review, appointment_status. - DO NOT create a new dp_conversations row. - If none is found: - Create a new conversation as today, but: - stage should be derived from payments_remaining if >0 and in (18,12,9,6,3); otherwise "unknown". - status defaults to "active". - appointment_status = "NONE". - Insert into dp_conversations and use that conversation_id going forward. Important: - When an existing conversation is reused, ignore the payments_remaining in the inbound payload (it’s optional for future). - opt-out / DNC / soft-no / confusion logic stays exactly as today. - dp_messages logging stays as-is (one inbound row, one outbound LIA row). 3) Guardrails: - If a phone number matches multiple customers, pick the most recent conversation across all of them (order by dp_conversations.created_at DESC). - If the matched conversation has status='dnc', treat as DNC and DO NOT respond (or respond with empty reply / 409 upstream – match existing pattern). - Make sure you don’t break the existing path where conversation_id *is* provided (that should still short-circuit and use that conversation). After implementation: - Restart the server. - Run a test: curl -X POST /api/lia/inbound with: { "from_number": "+447700900001", "body": "Hi, could I pop in on Saturday morning?", "channel": "whatsapp", "conversation_id": null, "payments_remaining": 0 } Expect: - 200 OK - JSON with: - conversation_id set to the existing conversation for that number (if any), - stage = "12" (from dp_conversations), - reply = appointment time-choice style message. Also confirm: - dp_messages has both inbound + outbound rows tied to the same conversation_id.