Skip to main content

RFC Review: Deduction V2.0 — Authorize-at-Send + Settle-Against-Meta

Companion review for deduction-v2.md, produced by the rfc-reviewer skill. Lives beside the RFC; valid only for the RFC revision in reviewed_rfc_last_updated.

Note on continuity: the prior deduction-v2-review.md (R0, 2026-06-23, 7.5/10) reviewed the now-replaced debit-at-webhook design. Because the design changed wholesale (different tables, send-gating, settlement model), its findings (DED-S07 Figma, Modpanel held_deducted, chunk 9) do not carry forward — this is a fresh ledger (R1) for the authorize-at-send design.

Executive Summary

  • Overall Score: 6.5/10
  • Rating: Needs Work
  • RFC Type: backend
  • Sub-Type: new-feature
  • Assessment Confidence: High
  • Applied Caps/Gates: none triggered (no category below 5.0; PRT/TDC/DMS/DIC/SAS ≥ 5.0; FMC ≥ 4.0). 6.5 is a judgment score, not a cap.
  • Implementation Readiness Verdict: HOLD — the backend ledger (T1, T2, T4, T6) is executable today, but four things would force an agent to guess or a reviewer to reject: the PRD contradicts the headline decision (OQ-1), the one consumer-facing endpoint contract is unspecified (REV-2), customer_ref may store a customer phone in plaintext (REV-3), and settlement worker-level failure/alerting is unpinned (REV-4).
  • Report Path: bifrost/deduction-v2/rfcs/deduction-v2-review.md
  • RFC Author: addo.hernando@mekari.com, hafriz.damarsidi@mekari.com | Reviewed: 2026-06-29

This RFC is unusually well-grounded on the ledger mechanics: six full ADRs, DDL-precise tables, a Source Verification table where every anchor is a real quoted file in hub_core, and a greenfield grep proving none of it is half-built. An agent can implement the hold table, CreateHold, the webhook transition, and the sweeper directly. The score is held down by the edges of the system, not its core: the get usage balance change (the single client-visible contract) is described as "return available_wa_balance" with no path/schema/error taxonomy; settlement workers are retry: 0 with no DLQ/alert/timeout spec; observability names events but not metrics/SLOs; and customer_ref introduces a plaintext-PII question the existing wa_conversation_log answers with Lockbox. The one thing that must change before approval is OQ-1: the source PRD still forbids send-blocking (Non-Goal #4), so the RFC's central feature has no PRD mandate — that is a governance blocker, not a code one.


Quick Verdict

Why this RFC can be implemented agentically:

  • The backend ledger spine (tables, models, CreateHold, webhook transition, FIFO settlement, sweeper) is DDL- and file-precise, grounded against verified hub_core anchors with a 7-chunk execution plan.
  • Decisions are closed as ADRs with reversibility (flag-off restores current behavior), and the greenfield grep removes "is this already built?" ambiguity.

Why this RFC will cause agent guessing or rework:

  • The Available-only balance endpoint has no contract (path, request/response schema, error codes) — an agent would invent the response shape (REV-2).
  • customer_ref storage (customer phone or bsuid) has no PII classification or encryption decision while the sibling wa_conversation_log Lockbox-encrypts phone_number — an agent would guess plaintext (REV-3).
  • Settlement workers are retry: 0 with no DLQ/timeout/alert thresholds — an agent would ship a job that silently dies (REV-4).

Findings Ledger (carry-forward)

IDSeverityFinding (one line)RFC locationStatusFirst seenResolved inEvidence / fix
REV-1blockerSource PRD contradicts the RFC's headline decision — PRD §5 Non-Goal #4 forbids send-blocking, which the send-gate does; no PRD mandate for the core feature§1 banner, §1.A, OQ-1openR1Refresh the PRD (or author a new one) to the authorize-at-send design before approved. RFC flags it honestly but cannot self-resolve.
REV-2majorBalance endpoint (get usage balance) contract unspecified — no path, request/response schema, field shape, or error taxonomy for the Available-only change§2.4openR1Add an endpoint contract row: exact path, the response field that changes, before/after example payloads, error codes.
REV-3majorcustomer_ref (customer phone or bsuid) stored on wa_balance_holds with no PII classification/encryption/retention, while wa_conversation_log Lockbox-encrypts phone_number§2.3 DDL, §3openR1Classify customer_ref; decide Lockbox vs plaintext + retention; if only bsuid is ever stored, state that constraint and enforce it.
REV-4majorSettlement/sweeper workers are retry: 0 with no DLQ, per-message timeout, or alert thresholds; recovery leans solely on the pending-reopen window§2.F, §3openR1Specify what catches a crashed worker run (re-dispatch? alert?), a timeout, and concrete alert thresholds for missing_hold_on_delivered / reconciliation_shortfall.
REV-5minorObservability names events but not exact metric identifiers/tags, SLO targets, or dashboard location; no "debug at 3am" note§3openR1Pin metric names + tags, an SLO (settlement success %, tally-exactness), and a dashboard pointer.
REV-6minorDMS lacks quantified volume/growth projection and an example row for either new table§2.3openR1Add rows/day + steady-state estimate and one example row per table.
REV-7minorSettlement money-movement atomic write-set described in prose, not pinned — which writes are in the one locked txn (batch upsert + hold stamp + N WaConversationLog rows + pool deduction)?Decision 4, §2.GopenR1Enumerate the exact transaction boundary and ordering for the per-bucket settlement commit.
REV-8minorOQ-2 (Services::Preference entry point in hub_core) and OQ-3 (Meta currency conversion source/rate) unresolved§2.0 note, OQ-2/3openR1Confirm the hub_core flag-read API; define the currency source + conversion point.

Ledger summary: 8 open (1 blocker / 3 major / 4 minor), 0 fixed this cycle (fresh ledger for the replaced design), 0 accepted-risk. All 8 promoted to the RFC Open-Questions surface below (REV-1/2/4/8 already partly tracked as OQ-1/2/3).


PRD → RFC Traceability Matrix

The source PRD (v1.4) documents the earlier debit-at-webhook design. The RFC maps the correspondence and flags divergence (good practice), but the underlying truth is that the PRD does not justify — and in one case forbids — this design. That is the dominant PRT finding (REV-1).

PRD ElementRFC SectionCoverage
DED-S01 real-time hold§2.2 send seq, CreateHoldPartial — re-scoped from webhook-time to send-time
DED-S01/AC-2 balance nets holds§2.4, Decision 5Full (mechanism differs: reserve vs debit)
DED-S02 daily reconcile to settled§2.2 settle seq, SettleDailyFull (reverse→re-credit replaced by settle→capture)
DED-S02/AC-2 history shows settledreport rows onlyPartial — Meta-actual rows, but no aggregate-row UI (dropped)
DED-S03 date-scoped re-triggerSettleDaily target_dateFull
DED-S04 WABA TZ stampingWaTimezoneResolver in settle workerFull
DED-S05/S06 delayed local-midnight toggleMissing (intentionally dropped; plain flag instead)
DED-S07 daily-aggregate UIMissing (intentionally dropped; Out of Scope #5)
PRD §5 Non-Goal #4 "no send-blocking"send-gate (Decision 5, T3)Contradicted — RFC builds the opposite (REV-1)
Send-gate / over-spend block (RFC core)Decision 5, T3No PRD driver — net-new, contradicts a Non-Goal
wa_balance_holds/wa_reconciliation_batches§2.3No direct PRD driver (PRD specified held_deducted/wa_held_deduction_logs)

Summary: ~5 of 9 PRD stories fully covered, 2 partial, 2 intentionally dropped; 1 direct contradiction (Non-Goal #4) and ≥2 core RFC decisions without PRD justification. The divergence is documented, not silent — which keeps PRT off the floor — but the contradiction means an agent reading PRD+RFC together gets conflicting direction on the headline feature.


Scorecard

Backend Scorecard (12 core + 1 conditional)

CategoryScoreEvidence-Based Rationale
PRT — PRD Traceability6.0Bidirectional matrix present and honest (§1.A), but the PRD contradicts the core decision (Non-Goal #4) and ≥2 decisions have no PRD driver (REV-1). Documented divergence ≠ justified scope.
TDC — Technical Decisions8.0Six full ADRs (Decisions 1–6) with options/rationale/consequences/reversibility. Residuals are scoped to OQs, not left dangling in the decision bodies.
DMS — Data Model & Schema7.5Full DDL for both tables, every index justified, partial-unique double-charge guard, uuid tenant ids, lifecycle table. Missing: volume/growth projection, example rows (REV-6).
ACV — API Contract & Versioning5.0§2.4 extends get usage balance to "return available_wa_balance" with no path, request/response schema, field, or error taxonomy. Webhook reused (no contract change). The one client-facing contract is underspecified (REV-2).
DIC — Data Integrity & Consistency7.5§2.G collision map; idempotency via partial-unique (external_id,state) + batch unique key + reconciliation_batch_id IS NULL; Meta-fail → batch pending; reset interplay via is_auto_deduct=false. Atomic write-set for settlement is prose, not enumerated (REV-7).
FMC — Failure Mode & Retry Coverage6.0Good design-level recovery (pending-reopen, 30d shortfall, missing-hold metric, reuses Meta 3× retry). But workers are retry: 0 with no DLQ/timeout, and no error-response catalog for the balance endpoint (rubric gate for ≥7.0). (REV-4)
CSS — Concurrency & Scaling7.0§2.G covers package with_lock, hold dedup, batch idempotency, bounded gate↔hold over-commit (A-5), FIFO delivered_at ASC, sweeper in_batches(500). New-worker concurrency caps not restated (inherits existing throttle by reference).
SAS — Security & Authorization7.0§3 threat model (double-charge = top threat, mitigated by flag + partial-unique), tenancy by organization_id, parameterized AR, rake arg validation, Brakeman, no name/phone on holds — except the customer_ref plaintext-PII gap (REV-3) and balance-read authz left as "existing oauth2".
MRP — Migration & Rollout Plan7.5Flag wa_hold_settlement default-off; deploy-all-off → register → enable per org; stages; stop conditions; rollback = flag off + target_date backfill; additive migrations. No data backfill needed (holds accrue forward).
OBS — Observability Definition6.0Events named conceptually (missing_hold_on_delivered, shortfall, expired) + one alert family, but no exact metric names/tags, no SLO, no dashboard, no debug runbook (REV-5).
SBC — Service Boundary & Coupling8.0Clear: all logic in hub_core; hub_service ingress unchanged; Meta external; moderator-be only for flag registration. Per-service responsibility diagram present.
CPA — Pattern Alignment8.5Patterns-to-Follow + Source Verification cite real files (AbstractModelBilling, Dry::Monads, dispatcher/per-org worker pair, find_or_create_by+with_lock, AbstractHttp, colocated specs). Strongest category.
CDG — Compliance & Data Governance6.0Active (stores customer_ref = customer phone/bsuid; tenant ids; money path). Holds deliberately avoid name/phone, but customer_ref is uncharacterized and unencrypted vs the Lockbox precedent (REV-3). No retention/right-to-delete for the hold rows.

Resource & Cost Advisory

  • No advisory blocker. Net-new: one INSERT + indexed Σ(holds) read per billable send; a nightly settlement batch reusing the existing pricing_analytics fetch (no new external calls); two new tables on the :billing shard growing with billable PMP volume (bounded only if a hold-purge is added later — not in scope). Route table-growth to infra planning if PMP volume is high.

Decision Closure Assessment

Decision Index

#DecisionStatusCritical Gaps
1Authorize-at-send + capture-at-EOD modelResolvednone — alternatives + reversibility documented
2Meta pricing_analytics as report source-of-truthResolvednone
3Two new tables, uuid tenant idsResolvedvolume projection, example rows (REV-6)
4FIFO allocation, residual-to-last, pending-reopenResolved*atomic write-set not enumerated (REV-7)
5Available-only balance via one helperPartialendpoint contract unspecified (REV-2)
6Single flag wa_hold_settlement (nested in wa_new_pricing_v2)Partialhub_core flag-read entry point unconfirmed (REV-8/OQ-2); "nested" semantics not defined
Meta currency conversionDanglingsource/rate/conversion point undefined (OQ-3)

Aggregate: 4 Resolved, 2 Partial, 1 Dangling.

Decision 5 — Available-only balance display (Partial)

  • What was decided: show Available = Pooled − Reserved; the get usage balance endpoint and the send-gate both call Helpers#available_wa_balance (Decision 5, §2.4).
  • Interface specification: incomplete. The helper signature is implied (available_wa_balance(wa_package)), but the endpoint contract is not: no path, no response field name/type, no before/after JSON, no error codes. An agent can write the helper but will guess the API response shape.
  • Failure handling: not specified for the endpoint (what does it return if the hold-sum query fails?).
  • Agent implementability: helper yes; endpoint no.
  • Suggested resolution: add an ACV row — path, the exact field that flips to Available, an example payload pre/post-flag, and the error code on read failure.

Decision 6 — Single flag wa_hold_settlement (Partial)

  • What was decided: gate every new branch on Services::Preference flag wa_hold_settlement, nested in wa_new_pricing_v2 (Decision 6).
  • Grounding: the flag is read in hub_core (e.g. wa_new_pricing_v2 in specs) but the Services::Preference definition was found in moderator-be — the exact hub_core entry point is an Open Question (OQ-2). "Nested inside wa_new_pricing_v2" is asserted but the operational meaning (does wa_new_pricing_v2 gate wa_hold_settlement? both must be on?) is not defined.
  • Suggested resolution: confirm the hub_core enabled? call site and state the nesting rule explicitly (e.g. "wa_hold_settlement is only honored when wa_new_pricing_v2 is also on").

Data Integrity Deep-Dive

Write PathTransaction ScopePartial Failure BehaviorIdempotency KeyConsistencyDuplicate Handling
Create hold at sendsingle INSERT, no pool mutationnothing to roll back (insert-only); on dup → Successpartial-unique (external_id, state) (rescue RecordNotUnique → Success)strong (single row)idempotent insert
Webhook transition (held→delivered/→refunded)single-row state updateidempotent (re-delivery = no-op); missing hold → Success+metric, no deductionhold external_idstrongidempotent transition
EOD settlement (per bucket)"one locked txn per bucket" — exact write-set not enumerated (REV-7)Meta-fail → batch stays pending, retried; mid-write crash behavior unstatedwa_reconciliation_batches unique key + reconciliation_batch_id IS NULL candidate filterstrong per bucketre-run consumes only unsettled holds → idempotent
Monthly reset interplayshared package with_lock; settlement rows is_auto_deduct=falsereset gap queries (L222-238) ignore settlement rowsn/astrongn/a

Strong overall; the single gap is REV-7 — name the writes inside the per-bucket commit and their ordering.

Concurrency Collision Map

#Shared ResourceWritersCollisionResolutionLock-Failure BehaviorAssessment
1package poolssettlement vs monthly reset vs (flag-off) live deductionconcurrent debitpackage with_lock + is_auto_deduct=false for settlement; settlement re-enqueues on StaleObjectErrorsecond waits / re-enqueueadequate
2wa_balance_holds (same wamid)dup send, out-of-order webhooktwo holds / two transitionspartial-unique (external_id, state)second insert rejected → Successadequate
3settlement bucketconcurrent settle runsdouble-settlebatch unique key + reconciliation_batch_id IS NULLreload on RecordNotUniqueadequate, but candidate-select→stamp window not shown atomic (relates REV-7)
4gate ↔ holdconcurrent sendsover-commitbounded over-commit accepted (A-5), not serializedn/aadequate (documented trade-off)

API Contract Completeness Check

EndpointRequestResponseError TaxonomyAuthIdempotencyExamplesAssessment
get usage balance (extended)n/a (read)missing (field/shape unspecified)missing"existing oauth2" (vague)n/a (read)no1/6 — underspecified (REV-2)
POST /webhook/wa/:codereusedreusedreusedchannel-code (existing)partial-unique on holdn/areuse, no contract change

Async Job / Event Consumer Spec

JobTriggerInputRetryDLQConcurrencyIdempotencyTimeoutAssessment
CreateHoldsend success (inline)message/channel ctxn/an/an/apartial-unique (external_id,state)n/a6/7 (inline, fine)
SettleWaHoldsDispatcherWorkercron ~01:00 ICTretry: 0nonefan-outnone3/7 (REV-4)
SettleWaHoldsWorkerdispatcher(org)retry: 0 + re-enqueue on StaleObjectErrornoneper [waba,tz]batch keynone4/7 (REV-4)
SweepStaleWaHoldsWorkercron ~02:00 ICTretry: 0nonein_batches(500)state='held' onlynone4/7

retry: 0 is consistent with the existing daily workers (a deliberate pattern), but with no DLQ/alert a crashed run is silent until the next night. REV-4: pin the alert + re-dispatch behavior.

Compliance Trigger Check

TriggerFound?Data LocationClassificationAssessment
PII (phone)yeswa_balance_holds.customer_ref (customer phone or bsuid)uncharacterizedunhandled — plaintext vs Lockbox undecided (REV-3)
PII (business number)yeswa_balance_holds.phone_recipientbusiness identifier (not customer PII)acceptable (it's the Meta match key)
Payment/financialyespool deduction, settlement amountsfinancialhandled (no card data)
Cross-borderpartialMeta pricing_analytics (external)out of scope of this change

CDG Status: Active — scored 6.0. The deliberate "no name/phone on holds" stance is undercut by customer_ref.


Strengths

  • Decision discipline (TDC 8.0, CPA 8.5). Six ADRs with reversibility, every pattern tied to a quoted hub_core file, and a Source Verification table + greenfield grep — the agent never has to ask "does this exist?" or "what pattern?".
  • Ledger correctness model (DIC 7.5). The "report is written only by settlement, only with Meta's number" invariant (Decision 2) makes the tally guarantee structural, and idempotency is layered (partial-unique + batch key + reconciliation_batch_id IS NULL).
  • DDL precision (DMS 7.5). Both tables are migration-ready: types, defaults, the partial-unique (external_id, state) double-charge guard, and uuid tenant ids corrected from the plan's bigint.

Biggest Gaps

  • REV-1 (governance blocker): the PRD forbids the headline feature (send-blocking, Non-Goal #4). Until the PRD is refreshed, the RFC's central decision has no product mandate — §1/§1.A flag it but cannot resolve it.
  • REV-2 (ACV 5.0): the only client-facing contract — the Available-only balance read — has no schema/path/error spec (§2.4). An agent ships a guessed response shape.
  • REV-3 (CDG/SAS): customer_ref stores a customer phone in plaintext on a new table while wa_conversation_log Lockbox-encrypts the same datum (§2.3). An agent picks the wrong (insecure) default.

Priority Actions

  1. OQ-1 / REV-1 — Refresh the PRD to the authorize-at-send design (reverse Non-Goal #4, restate stories around hold→deliver→settle). This is the approval gate; the code is implementable but unmandated until then.
  2. §2.4 / REV-2 — Specify the balance endpoint contract: path, the exact response field that becomes Available, before/after example payloads (flag off vs on), and the read-failure error code. Without it ACV cannot clear 7.0.
  3. §2.3 / REV-3 — Decide customer_ref handling: classify it, choose Lockbox-encrypt vs store-bsuid-only, and set retention. Mirror the wa_conversation_log precedent unless there's a stated reason not to.
  4. §2.F + §3 / REV-4 — Pin worker failure + alerting: what catches a crashed settlement/sweep run, a per-run timeout, and numeric alert thresholds for missing_hold_on_delivered and reconciliation_shortfall.

Backend Contract Addendum

Endpoint Contract Details

EndpointMethod/PathAuthZRequestResponseErrorIdempotency/VersioningStatus
Usage balanceGET <path — UNSPECIFIED>"existing oauth2, own org" (under-specified)n/aMISSING — which field carries Available? type?MISSINGadditive behind flagMissing fields (REV-2)

Database Changes Details

ChangeTableDDL / ShapeMigrationRollbackCompat WindowStatus
new tablewa_balance_holdscomplete (§2.3, all cols + 6 indexes + partial-unique)additive createflag-off (behavioral); drop if abandonedn/a (greenfield)Complete
new tablewa_reconciliation_batchescomplete (§2.3, unique bucket key)additive createflag-off; drop if abandonedn/aComplete
customer_ref PIIwa_balance_holdscolumn present; classification/encryption undecidedMissing (REV-3)

Implementation Readiness Checklist

Unblocked (agent can proceed)

  • Technical decisions resolved with alternatives (6 ADRs)
  • Schema at DDL precision (both tables)
  • Transaction boundaries/idempotency per write path (except settlement atomic-set, REV-7)
  • Concurrency collision points listed with resolutions
  • Pattern alignment verified (Source Verification table)
  • Rollout plan + flag + rollback
  • Task decomposition with acceptance criteria (task breakdown, 7 chunks)
  • Mermaid diagrams valid (9/9 parse)

Blocked (must fix first)

  • REV-1 — PRD refreshed to mandate send-blocking (governance)
  • REV-2 — balance endpoint contract specified
  • REV-3 — customer_ref PII classification + encryption decided
  • REV-4 — settlement worker DLQ/timeout/alert thresholds

Verdict: Fix 4 blockers first (1 governance, 3 spec). The BE ledger chunks (T1, T2, T4, T6) are safe to start in parallel; T3 (balance display) waits on REV-2, and approval/T5-rollout waits on REV-1/REV-3/REV-4.

Task Manifest

The RFC specifies decomposition in deduction-v2.task-breakdown.md (T1–T7). Verified — chunks map to §4.C and carry files + commands + acceptance criteria. No re-derivation needed; the manifest is sound. The only manifest-level note: T3 should not start until REV-2 (endpoint contract) is closed.

Dangling Decisions Log

#DecisionLocationOwnerDeadline
1Meta currency conversion source/rate/pointOQ-3, Decision 4 consequencesBifrost Engbefore T5
2wa_hold_settlement nesting semantics vs wa_new_pricing_v2Decision 6Bifrost Engbefore T1 flag wiring

Open Questions

#QuestionCategorySeverity
1Will the PRD be refreshed to mandate send-blocking, or is this RFC the new source of truth (PRD deprecated)?PRTBlocking
2Exact response contract of the Available-only balance endpoint?ACVBlocking
3customer_ref: Lockbox-encrypt, or constrain to bsuid-only? Retention?CDG/SASBlocking
4Crashed settlement/sweep run: re-dispatch, alert, timeout?FMC/OBSImportant
5Exact metric names/tags + SLO for settlement tally-exactness?OBSImportant
6hub_core Services::Preference entry point (OQ-2) + currency (OQ-3)TDCImportant

Evidence Notes

  • §2.3 DDL — both tables migration-ready (drove DMS 7.5); customer_ref line drove CDG to 6.0 and REV-3.
  • §2.4 APIs — "return available_wa_balance" with no schema drove ACV to 5.0 (REV-2).
  • §2.F async spec + §3retry: 0, no DLQ/timeout, events-not-metrics drove FMC 6.0 / OBS 6.0 (REV-4/5).
  • §1 banner + §1.A traceability — honest PRD-divergence flagging kept PRT at 6.0 rather than lower, but the Non-Goal #4 contradiction is REV-1.
  • Decisions 1–6 + Source Verification — drove TDC 8.0 / CPA 8.5; the strongest part of the RFC.
  • Mermaid — 9/9 blocks validated with mmdc (topology, per-service, balance, repo map, ER, state, 3 sequences); no parse failures.

Review History

CycleDateReviewed RFC revisionScoreVerdictFindings open → fixedNotes
R02026-06-23last_updated 2026-06-23 (debit-at-webhook design)7.5PROCEED w/ notesn/aReviewed the now-replaced design; superseded by the design pivot.
R12026-06-29last_updated 2026-06-29 / 19dd270 (authorize-at-send)6.5HOLD0 fixed, 8 open (fresh ledger)First review of the authorize-at-send design. Strong ledger core; blocked by PRD contradiction + endpoint/PII/worker-failure gaps.