1️⃣ Copy–paste this to Replit (payments + duplicate safety) You are a senior Python/FastAPI engineer working on the LIA backend. /api/lia/outbound is working and returns message_text, conversation_id, stage, and to_number. Next, we need two safety features: 1) Only allow outbound for specific payment bands (18, 12, 9, 6, 3). 2) Prevent duplicate conversations for the same customer/campaign/stage/channel. Do NOT change any LLM/URE logic or db schema. Just add validation + dedupe in the outbound flow. ──────────────────────── TASK 1 – Validate payments_remaining band In app/lia_outbound.py (where the outbound request model and process_outbound_message live): 1. After parsing the request payload, enforce: ```python if payload.payments_remaining not in (18, 12, 9, 6, 3): raise HTTPException( status_code=400, detail="payments_remaining must be one of [18, 12, 9, 6, 3] for this endpoint" ) Make sure the stage you pass into the rules engine is derived from payments_remaining, not the campaign: 18 → "18" 12 → "12" 9 → "9" 6 → "6" 3 → "3" If there is already a helper that maps payments_remaining → stage, reuse it. Otherwise, add a simple mapping function. ──────────────────────── TASK 2 – Duplicate-conversation fail-safe Definition of duplicate: Same customer_id Same campaign Same stage (derived from payments_remaining) Same channel We must NOT start a new conversation if one already exists for that combination. Before creating a new dp_conversations row, add a check using the existing db connection helper: existing = await conn.fetchrow( """ SELECT conversation_id, status FROM dp_conversations WHERE customer_id = $1 AND campaign = $2 AND stage = $3 AND channel = $4 LIMIT 1 """, customer_uuid, # UUID for customer_id payload.campaign, stage, # e.g. "12" payload.channel, ) If existing is not None: Do NOT create a new conversation. Do NOT generate a new opener for Twilio. Raise an HTTP 409 with a clear JSON detail, for example: raise HTTPException( status_code=409, detail={ "error": "Conversation already exists for this customer, campaign, stage and channel", "conversation_id": str(existing["conversation_id"]), "stage": stage, }, ) (If your existing error handling expects detail to be a string, you can instead return a string and log the conversation_id, but JSON is preferred.) If existing is None: Proceed with the current logic: create the dp_conversations row, generate message_text, return OutboundResponse as you do now. This duplicate check must run regardless of sandbox_mode. Status (active/paused/dnc) should NOT bypass the check; any existing row for the same tuple should block a new conversation. ──────────────────────── CONSTRAINTS Do NOT change any table schemas. Do NOT change how message_text, conversation_id, to_number, or stage are built when the request is valid and not a duplicate. Do NOT modify inbound or nudge endpoints. ──────────────────────── TESTS After the change, test /api/lia/outbound with: Valid first-time request (should be 200): { "customer_id": "44f01301-7a59-4e34-b0a8-d0d89e061aab", "payments_remaining": 12, "campaign": "TEST_12_PCP", "asset_type": "new", "channel": "whatsapp", "sandbox_mode": true, "from_number": "whatsapp:+44YOUR_SANDBOX_OR_BUSINESS_NUMBER" } Expected: 200 with message_text, conversation_id, stage "12", and to_number. Duplicate request (same payload again): Expected: HTTP 409 with detail including: error message, existing conversation_id, stage. Invalid band: Same as above but with e.g. "payments_remaining": 10 Expected: HTTP 400 with: { "detail": "payments_remaining must be one of [18, 12, 9, 6, 3] for this endpoint" }