Skip to main content

Summary — RFC: Register Campaign Type to CAA (Chatbot Autoreply), Backend

Digest of rfc-wa-campaign-register-campaign-type-to-caa-be.md (backend RFC, status in-review / Qontak RFC, target 2026-Q3, last updated 2026-07-13, rev4). Owner: Revenue Chat-2. Latest rfc-reviewer score: 8.0 / Strong / PROCEED with notes (…-be-review.md, assessed against rev1 — rev3/rev4 resolve its array-ordering and N+1 findings; a re-score is pending). This summary is a reading aid; the draft is the source of truth.


TL;DR

The chatbot (autoreply) is fed by the CAA webhook (custom_agent_allocation), which today carries only the customer's inbound message. This RFC (backend, hub_core only) enriches the CAA payload with a single, consistent campaign_context envelope{ trigger, broadcasts: [ … ] } — populated by two cases:

  1. Interactive-button reply — the customer tapped a quick-reply button of a specific broadcast (detected the same way the platform records Models::MessageBroadcastInteractiveLog) → trigger = "interactive_button", broadcasts = that one broadcast (incl. replied_button).
  2. Any other replytrigger = "room_broadcasts", broadcasts = all campaign broadcasts in the room within 30 days (newest-first, cap 20).

Consistency (rev4): one envelope, same 10-field per-broadcast schema in both cases, and it is a superset of the legacy last_campaign (which is kept byte-identical but deprecated, with a field mapping). No schema migration — all fields derived at read time; read-only + fail-safe; case-2 resolution batched (no N+1). Gated per-org by a flag; rollback = flag flip.


Problem & Goal

  • Today: CAA sends only inbound messages; a prior campaign is invisible to the bot. The existing last_campaign hash is room-origin-only, last_*-prefixed, thin, and never sets last_sent_at.
  • Goal: on a reply, put the relevant broadcast context inline in one consistent CAA envelope — the precise tapped broadcast (case 1) or the whole room's campaign set (case 2) — no secondary lookup.
  • Success criteria: one campaign_context envelope; same element schema both cases; case 1 → 1 broadcast + interactive_button; case 2 → all in-window + room_broadcasts; empty envelope when no context (flag ON) / absent (flag OFF, byte-compatible); UC4 backdated; no migration; batched (no N+1); last_campaign unchanged.

Out of scope: chatbot-side consumption (CAA-S05); removing last_campaign (deprecated here, removed later — Open Q#10); retroactive registration; the message_interaction webhook; modifying the interactive-log / reply-recording writes; BUTTON_REPLY / order-message BUTTON (fall to case 2).


What gets built (hub_core only — additive, read-time, no migration)

Schema — none (derived at read time).

New code

ArtifactRole
Whatsapp::Services::InteractiveBroadcastReplycase-1 detector: buttons[0].type=='BUTTON' + reply_id → parent → raw_message['message_broadcast_id']{broadcast_id, replied_button}, else nil
Repositories::Rooms::CampaignContextsorchestrates cases 1/2 → the envelope; case-2 window + batched MessageBroadcast.where(id: ids).includes(:message_template), newest-first, cap 20
Whatsapp::Services::CampaignMessageTypeResolvercompose message_type + message_category
Whatsapp::Services::CampaignContentSerializermessage_content_text (no existing serializer)
Entities/Builders::Webhooks::CampaignContextthe 10-field per-broadcast object (identity + PRD content + replied_button)

Extended / reused

ContractChange
CustomAgentAllocation#build_messageextended — merge the additive campaign_context envelope (flag-gated); last_campaign unchanged
last_campaign (get_last_campaign)reused unchanged + deprecated — superseded by campaign_context (field mapping in Decision 6)
Interactive-log condition + resolution / reply-window queriesreused as patterns — workers/model untouched
Webhooks::CaaWorker, Creates::Customer fan-outreused
feature flag register_campaign_type_to_caanew (default OFF)

Key design decisions (D1–D7)

#DecisionSummary
D1StorageDerive fields at read time — no column/migration.
D2WindowsCase 1: single via reply_id → parent → message_broadcast_id (pruned). Case 2: all via room window (reply-30d..reply).
D3Case discriminatorReuse the interactive-log condition (buttons[0]=='BUTTON' + resolvable broadcast parent) → case 1; else case 2.
D4Trigger & timingEnrich synchronously in build_message — CAA fires before the async interactive-log + reply workers.
D5Content-to-textNew stateless CampaignContentSerializer; buttons ignored in the text.
D6Consistent payloadSingle campaign_context envelope { trigger, broadcasts:[…] }, one canonical 10-field per-broadcast schema (superset of last_campaign); last_campaign preserved + deprecated (field mapping).
D7Case-2 costAll in-window campaigns, batched resolution (no N+1), newest-first, cap 20.

Canonical broadcasts[] element: broadcast_id, broadcast_name, template_id, template_name, message_type, message_category, campaign_sent_at, message_body, message_content_text, replied_button (case 1 only). last_campaigncampaign_context mapping: last_campaign_id→broadcast_id, last_campaign_name→broadcast_name, last_sent_template_id→template_id, last_sent_template_name→template_name, last_sent_at→campaign_sent_at (+ the 5 new fields).


Key flow

Customer replies
→ Messages::Creates::Customer#call
├─ (:60-63 / :295 async) interactive-log + reply-record workers (linkage reused, not modified)
└─ (:40-43 sync, no active agent) deliver_caa_webhook!
→ CustomAgentAllocation#build_message (flag register_campaign_type_to_caa ON?)
→ Rooms::CampaignContexts(message, room)
→ InteractiveBroadcastReply: buttons[0]=='BUTTON' & reply_id→parent→message_broadcast_id?
yes (CASE 1) → 1 broadcast + replied_button → { trigger:'interactive_button', broadcasts:[1] }
no (CASE 2) → room window (is_campaign, 30d) → batched resolve, cap 20
→ { trigger:'room_broadcasts', broadcasts:[N] }
none → { trigger:null, broadcasts:[] }
→ merge campaign_context (envelope); leave last_campaign → enqueue CaaWorker → HTTPS POST

Skip: bot stopped (agent assigned) · flag OFF → campaign_context absent (byte-compatible with today).


Rollout, rollback & observability

  • Rollout: merge BE (flag OFF) → seed flag → enable internal/design-partner → chatbot ships CAA-S05 (reads campaign_context) → enable globally → (later) deprecate/remove last_campaign (Open Q#10).
  • Feature flag: register_campaign_type_to_caa (default OFF, per-org). Kill-switch = flip OFF → campaign_context stops (last_campaign unaffected); no data to unwind.
  • Rollback trigger: caa_campaign_context_skipped{reason="error"} > 1% of CAA fires, or any CAA delivery-rate regression.
  • Observability: caa_campaign_context_enriched{trigger,count,truncated} + caa_campaign_context_skipped{reason}; [caa campaign enrichment] log (no raw body text).

Status & open items

Execution plan: 8 ordered chunks (seed flag → InteractiveBroadcastReply → content serializer → type-resolver + entity/builder (10 fields) → CampaignContexts repo (envelope, batched, cap 20) → extend CustomAgentAllocation → observability → full regression). Ready for agent execution: no.

Open questions:

#ItemTypeDeadline
1Confirm case-2 eligible set + newest-first cap 20PM/ChatbotBefore GA
2Verify/add composite index for the case-2 room lookup; EXPLAIN pruningVerificationBefore chunk 5 merge
3Confirm order-msg BUTTON / BUTTON_REPLY fall to case 2PM/ChatbotBefore Stage 2
4Header/footer serialization: PRD table vs CAA-S03 AC (RFC follows AC)PM sign-offRFC sign-off
6Infosec: mask body/content text in the webhook event log?InfosecRFC sign-off
9Chat-1 accepts the additive campaign_context envelopeCoordinationRFC sign-off
10last_campaign deprecation window → remove in a follow-upCoordinationPost-GA

(Also Open Q#5 — Services::Preference fail-open-as-OFF, verified during chunk 6.)


Dependencies

  • Chatbot / Automation squad: consume campaign_context (CAA-S05 / QC-23109). Blocking for product value, not BE delivery.
  • Chat-1 / Inbox: owns the CAA webhook contract; accepts the additive envelope; co-owns last_campaign deprecation.
  • Infra/Platform: seed register_campaign_type_to_caa (default OFF).

Related: PRD 51233095746 · Epic QC-22896 · Stories QC-23105…QC-23109 · sibling RFC hub_core/docs/rfcs/rfc-wa-campaign-product-card-carousel-be.md. Revision history in the draft's §6 Comment logs (v1 → rev4, 2026-07-13).