RFC Review: WA Campaign — Register Campaign Type to CAA (Chatbot Autoreply), Backend
Executive Summary
- Overall Score:
9.0/10 - Rating:
Agentic-Ready - RFC Type:
backend - Sub-Type:
new-feature - Assessment Confidence:
High - Applied Caps/Gates:
none— no category < 7.5; DMS/DIC/FMC ≥ 8.5; zero dangling decisions - Implementation Readiness Verdict:
PROCEED— executable end-to-end; one agent-facing item (chunk 3 serializer) should take the PM's one-line answer to Open Q#4 first; everything else is external sign-off, not design. - Report Path:
revenue/whatsapp-campaign-register-campaign-type-to-caa/rfcs/rfc-wa-campaign-register-campaign-type-to-caa-be-review.md - RFC Author: Isna Rahmatul Khoir | Reviewed: 2026-07-13 (rev5)
rev5 closes the three findings from the rev1 review (8.0). An AI agent can now implement the RFC end-to-end from the spec without a clarification meeting: the payload is one consistent campaign_context envelope with a per-field type/nullability table, the case-2 read is batched (no N+1), and every decision carries full options + rationale. The biggest strength is decision completeness anchored in verified code (file:line throughout). The single residual agent-facing ambiguity is the serializer's header/footer format (Open Q#4 — a PRD self-contradiction the RFC resolved by choosing the AC, pending a one-line PM confirmation). The one thing worth doing before coding chunk 3: get that confirmation.
Quick Verdict
Why this RFC can be implemented agentically:
- Every decision (D1–D8 + a minimum-coverage block) is a full ADR with options, pros/cons, and a specific rationale; zero "TBD"/dangling choices.
- The contract is one consistent, fully-typed
campaign_contextenvelope (§2.4) with alast_campaign→campaign_contextmigration mapping — a consumer can build against it directly. - Every anchor is verified with file:line (Source Verification), incl. the interactive-log condition + resolution the discriminator reuses, and the list-all / window shapes the reads mirror.
Why an agent might still pause:
- Chunk 3 (serializer) header/footer output is contingent on Open Q#4 (PRD table vs CAA-S03 AC); the RFC picks the AC but flags it — a one-line PM confirmation removes the guess.
- Case-2 supporting index is asserted, not yet verified (Open Q#2 — an EXPLAIN check scheduled inside chunk 5).
PRD → RFC Traceability Matrix
| PRD Element | RFC Section | Coverage |
|---|---|---|
| FR-1 enrich inbound webhook with campaign context | §1 PRD-to-Schema, §2.1/2.2/2.4, D3/D6 | Full |
| FR-1 5 content fields + identity | §2.4 element table (10 fields, typed) | Full |
| FR-1 content-to-text rules | Decision 5, §4.C ch.3 | Partial — header/footer format pending Open Q#4 |
| FR-2 backdated (UC4), 30-day window | Decision 2, §2.2, CAA-S02 | Full |
| FR-3 chatbot reads context | §2.D row 7 (cross-squad) | Full (correctly delegated) |
| CAA-S01 (multiple in room) | Case 2 array (D6/D7) | Full |
| CAA-S02 backdated inclusive boundary | Decision 2 (..) | Full |
| CAA-S03 serialization + missing-field→"" | Decision 5 | Partial — header/footer (Open Q#4) |
| CAA-S04 eligible/interactive discrimination | Decision 3 (interactive-log reuse) | Full |
| CAA-S05 chatbot consumption | §2.D row 7 | Full (n/a — cross-squad) |
Summary: 8/10 elements fully covered; 2 partial, both on the same external PRD ambiguity (header/footer). No unjustified additions.
Scorecard (Backend)
| Category | Score | Evidence-Based Rationale |
|---|---|---|
| PRT — PRD Traceability | 9.0 | Bidirectional matrix + PRD Section Coverage + Detail 1.C; all 5 stories mapped. |
| TDC — Technical Decisions | 9.5 | rev5: D1–D8 full ADR (Context/Options±/Decision/Rationale/Consequences/Reversibility) + minimum-coverage block (caching/third-party/consistency/tenancy/rollout). No dangling choices. |
| DMS — Data Model & Schema | 8.5 | No DDL by design (Decision 1, justified: partitioned hot table + new-sends-only); both read queries shown, case-2 batched; EXPLAIN gate. Docked: supporting index asserted, verified in chunk 5 (Open Q#2). |
| ACV — API Contract & Versioning | 9.0 | rev4/5: single campaign_context envelope with per-field type/nullability table + enum values + last_campaign mapping + examples for case1/case2/empty/flag-off. |
| DIC — Data Integrity & Consistency | 8.5 | Read-only; fail-safe empty envelope on error; interactive-log/reply-record writes untouched; replica-lag documented. |
| FMC — Failure Mode & Retry Coverage | 8.5 | Reuses CaaWorker retry:3 + circuit breaker + auto-disable (cited); new read rescue→empty envelope; Branch & Skip catalog; Error Response Catalog N/A-justified (no sync API). |
| CSS — Concurrency & Scaling | 8.5 | rev3/5: N+1 removed via batched MessageBroadcast.where(id:).includes(:message_template) (D7, Bullet-clean); cap 20; read-only collision map. |
| SAS — Security & Authorization | 8.5 | Tenancy enforced (organization_id+room_id on every read, cited AGENTS.md), org-scoped URL lookup; Role×Endpoint matrix; no new input surface. |
| MRP — Migration & Rollout Plan | 8.5 | Flag register_campaign_type_to_caa (default OFF); stages, kill-switch, rollback = flag off; Config Contract; last_campaign deprecation window (Open Q#10). |
| OBS — Observability Definition | 8.0 | Two named metrics w/ tags (trigger,count,truncated,reason), structured log (no body text), alert threshold; grounded in existing CaptureCustomMetric. Docked: no dashboard panel ref. |
| SBC — Service Boundary & Coupling | 9.0 | hub_core only; single cross-squad handoff (chatbot); workers/model untouched. |
| CPA — Consistency & Pattern Alignment | 9.0 | Patterns table + base classes; reuses interactive-log condition, mark_replied/conversation_sessions shapes, enrich_carousel_cards service style. |
| CDG — Compliance & Data Governance | 7.5 | Triggered (campaign body text → external webhook). Handled: tenant-scoped delivery, no new PII column. Docked: event-log masking deferred to infosec (Open Q#6). |
Overall: 9.0 — Agentic-Ready. (9.0 gate met: all ≥ 7.5; DMS/DIC/FMC ≥ 8.5; no dangling decisions.)
Resource & Cost Advisory
Negligible — case 1: 1 read + 1 resolution; case 2: 1 window read + 1 batched resolution (cap 20) on an already-async path. No new infra/write/storage.
Decision Closure Assessment
Decision Index
| # | Decision | Status | Critical Gaps |
|---|---|---|---|
| 1 | Derive at read time (no column) | Resolved | none |
| 2 | Lookup windows & queries (reuse shapes) | Resolved | none |
| 3 | Case discriminator (reuse interactive-log condition) | Resolved | order-msg/BUTTON_REPLY edge documented (Open Q#3) |
| 4 | Sync enrichment in build_message | Resolved | none |
| 5 | Content serializer (new) | Partial | header/footer output pending Open Q#4 (PRD self-contradiction) |
| 6 | Single campaign_context envelope | Resolved | none — options A/B/C + rationale + mapping |
| 7 | Case-2 batched + capped | Resolved | none (N+1 removed) |
| 8 | Reuse CAA event vs new webhook | Resolved | none |
| 9–13 | Caching / third-party / consistency / tenancy / rollout gate | Resolved | each stated with rationale |
Aggregate: 12 of 13 Resolved, 1 Partial (Decision 5, external PRD ambiguity), 0 Dangling.
Decision 5 — Content serializer (the only Partial)
What was decided: new CampaignContentSerializer; rules table; header+footer → "{header}: {body} — {footer}" per CAA-S03 AC. Why Partial: the PRD conversion table says header/footer are "not available" — a self-contradiction the RFC resolves by following the AC but flags (Open Q#4). Impact: an agent would implement the AC form and may need a one-line PM confirmation. Suggested resolution: PM confirms the AC is authoritative; then the branches are fully pinned (both present / header-only / footer-only / neither). Grounding: extract_content (create.rb:154-164), user_send_hsm.rb:155-177 — verified.
(Decisions 1–4, 6–13 assessed Resolved — chosen option named, alternatives rejected with concrete reasons, grounded in cited files, failure handled, agent-implementable. Abbreviated per the >7.0 optimization.)
Data Integrity Deep-Dive
| Write Path | Transaction Scope | Partial Failure | Idempotency | Consistency | Duplicate Handling |
|---|---|---|---|---|---|
| Enrichment (this RFC) | None — read-only | error → empty campaign_context envelope + log; CAA still delivers | per-delivery uniq_id | read-time → consistent w/ template state | inbound dedup upstream; enrichment is pure |
Strongest possible integrity posture (no writes; interactive-log/reply-record writes explicitly left to their workers). Only residual: replica-lag omission (documented, accepted).
Concurrency Collision Map
| Shared Resource | Writers | Collision | Resolution | Assessment |
|---|---|---|---|---|
| CAA payload build | rapid replies same room | each builds own payload | read-only, no shared state; per-delivery uniq_id | adequate |
messages read vs broadcast insert | enrichment vs campaign write | replica lag | replica snapshot | adequate (documented) |
No true collision points (read-only). Efficiency: case-2 batched load removes the rev1 N+1.
API Contract Completeness Check
| Endpoint | Request | Response | Error Taxonomy | Auth | Idempotency | Examples | Assessment |
|---|---|---|---|---|---|---|---|
custom_agent_allocation webhook (outbound POST → chatbot) | n/a (we send) | complete — envelope + per-field type/nullability table | n/a (delivery via CaaWorker) | specific (org-registered URL + org setting + flag) | consumer-side, same body across retries | yes (case1/case2/empty/flag-off) | 6/6 |
No synchronous inbound endpoint added (correctly N/A). The one contract that matters (outbound envelope) is now fully typed with a legacy-field migration map.
Async Job / Event Consumer Spec
| Job/Consumer | Trigger | Input | Retry | DLQ | Concurrency | Idempotency | Timeout | Assessment |
|---|---|---|---|---|---|---|---|---|
Webhooks::CaaWorker (existing, unchanged) | perform_async | data incl. campaign_context | retry:3 + backoff | Sidekiq dead queue | :custom_agent_allocation | consumer-side | Pigeon timeout | 7/7 (reused as-is) |
Rooms::CampaignContexts (new, synchronous — not a job) | in-process from build_message | (message, room) | none; error → empty envelope | n/a | inherits CAA path | n/a (read) | inherits caller | 6/7 — pin read timeout expectation |
Compliance Trigger Check
| Trigger | Found? | Location | Classification | Assessment |
|---|---|---|---|---|
| PII (indirect) | yes | message_body/message_content_text may embed personalized copy | low-sensitivity business content (already in messages) | handled — tenant-scoped delivery, no new column |
| User content w/ retention | yes | campaign content re-emitted to webhook | existing retention | handled |
| Cross-border | no | org's own registered URL | n/a | n/a |
| CDG Status: Active — scored 7.5. Residual: event-log masking (Open Q#6). |
Strengths
- Decision completeness (rev5) — 8 full ADRs + a minimum-coverage block; the TDC category is now near-exemplary and every choice states why.
- Consistent, typed contract (rev4) — one
campaign_contextenvelope, per-field types, and alast_campaign→campaign_contextmigration map; the payload is no longer two competing shapes. - Grounding + reuse — the case-1 discriminator reuses the exact
MessageBroadcastInteractiveLogcondition/resolution; reads mirrormark_replied/conversation_sessions; all file:line-verified. - Self-correcting history — the rev1 findings (array-ordering, N+1, per-field types) are all closed and recorded in §6.
Biggest Gaps
- Serializer header/footer (Open Q#4) — PRD table vs CAA-S03 AC; the one remaining agent-facing ambiguity. Impact: chunk 3 needs a one-line PM confirmation.
- Case-2 index unverified (Open Q#2) — pruning asserted, EXPLAIN scheduled in chunk 5. Impact: possible slow query if no suitable composite index exists.
- Order-msg /
BUTTON_REPLYdisambiguation (Open Q#3) — documented to fall to case 2; worth a Chatbot confirmation. Impact: minor; behaviour is defined.
Priority Actions
- Open Q#4 — PM confirms CAA-S03 AC is authoritative for header/footer → pin the serializer branches (unblocks chunk 3 fully).
- Open Q#2 — verify/add the
(organization_id, room_id, is_campaign, created_at)composite index; capture the EXPLAIN in the RFC during chunk 5. - Open Q#3 / Q#6 / Q#9 — Chatbot confirms the
BUTTON/BUTTON_REPLYsplit; infosec on event-log masking; Chat-1 accepts the additive envelope. - Infosec approver — fill the Approver(s) metadata before AGREED.
Implementation Readiness Checklist
- PRD → RFC traceability complete
- All technical decisions resolved with alternatives + rationale (12/13; 1 partial on external PRD ambiguity)
- Failure modes handled; error catalog N/A-justified
- Configuration contract (flag, default OFF)
- Pattern alignment verified
- Rollout + rollback (flag off) +
last_campaigndeprecation window - Observability metrics + alert
- Task decomposition with per-chunk AC (8 chunks)
- Schema: N/A by design, justified; both reads shown, batched, EXPLAIN gate
- API contract: fully typed envelope + legacy mapping + examples
- Transaction boundaries: read-only, fail-safe
- Concurrency: read-only collision map; N+1 removed
- Security: tenancy + org-scoped URL
- Service boundary documented (hub_core only)
- Compliance handled (tenant-scoped; masking → Open Q#6)
Fix before/at implementation
- Open Q#4 (header/footer) — before chunk 3
- Open Q#2 (index EXPLAIN) — within chunk 5
Verdict: Ready to implement — confirm Open Q#4 before chunk 3; verify the index during chunk 5.
Dangling Decisions Log
None. (Decision 5 is a Partial pending an external PRD clarification, not an unresolved RFC choice.)
Open Questions
| # | Question | Category | Severity |
|---|---|---|---|
| 4 | Is CAA-S03 AC or the PRD table authoritative for header/footer serialization? | TDC/ACV | Blocking chunk 3 |
| 2 | Composite index for the case-2 room lookup — exists, or add? | DMS | Important |
| 3 | Order-msg BUTTON / BUTTON_REPLY fall to case 2 — confirm | TDC | Important |
| 6 | Mask message_body/message_content_text in the webhook event log? | CDG | Important |
| 9 | Chat-1 accepts the additive campaign_context envelope | ACV/SBC | Important |
| 10 | last_campaign deprecation/removal window | ACV | Nice-to-have (post-GA) |
Evidence Notes
- §2 Technical Decisions (rev5) — full ADRs + minimum-coverage; primary reason TDC → 9.5 and overall → 9.0.
- §2.4 — typed envelope +
last_campaignmapping; lifted ACV to 9.0. - Decision 7 + §2.3 — batched resolution; lifted CSS to 8.5 (was 7.0 in rev1).
- Decision 5 + Open Q#4 — the sole remaining agent-facing ambiguity; keeps a clean 10.0 out of reach.
- §2.3 Open Q#2 — index verification deferred to chunk 5; the reason DMS is 8.5 not higher.