ADR-0001: Derive the modpanel Deduction-V2 activation boundary from the first settlement row
Delivery / Program Management — decision record (ADR-style).
⚠️ Superseded 2026-07-20 by ADR-0002. Eng chose to revisit this record's own rejected Option 2 ("if a CID is V2-on now, force daily aggregation for all MCC ranges") instead of shipping Option 1 below — the per-range activation boundary this record derives is no longer needed for DED-S08.
active_sinceis dropped from the task breakdown entirely; the on/off half of this record's question (a) is still needed, but is now answered by replicatingServices::Billing::FeatureFlagin moderator-be, not by theDeductionV2Statusservice this record assumed. Kept for history — do not implement Option 1.
- Status: superseded (2026-07-20) — see banner above. Was: proposed (2026-07-15) —
adopted, pending
bifrost-tech-leadsign-off - Deciders: Bifrost eng (Hafriz); sign-off owner: bifrost-tech-lead
- Scope:
moderator-bemodpanel surface only (DED-S07 aggregate history + DED-S08 MCC download gating). Task M1 indeduction-v2.task-breakdown.md(Part 2 — the former companiondeduction-v2-modpanel-ui.task-breakdown.md, merged in 2026-07-20).
Context
DED-S07 (daily-aggregate history) and DED-S08 (per-message download gating) both need moderator-be to answer, per org: (a) is this CID on Deduction V2? and (b) from what date? — the "V2-era range" DED-S08 gates on, and the settled window DED-S07 aggregates.
The original PRD (v1.4) assumed a stored v2_effective_date (the DED-S05/S06 delayed-toggle design).
The shipped authorize-at-send design dropped that field entirely — enablement is a
company-scoped billing feature flag (Services::Billing::FeatureFlag, :wa_hold_settlement) with no
per-org activation timestamp. moderator-be also has no V2-detection today (grep for
deduction_v2 / wa_hold_settlement / v2_effective_date → zero matches). So the activation
boundary has no stored source. Three options were considered:
- Derive the boundary = earliest
wa_conversation_logs.created_atwhereorigin_type='reconciliation_settlement'for the org (the first day V2 settlement produced records). - Simplify the gate — if a CID is V2-on now, force daily aggregation for all MCC ranges.
- Introduce an activation date — new column /
extras['wa_hold_settlement_since']written at opt-in.
Decision
We will use Option 1 — derive the activation boundary from the first settlement row.
Core::Services::Billing::DeductionV2Status#active_since(organization_id) returns the minimum
created_at of the org's Billings::WaConversationLog rows with
origin_type='reconciliation_settlement' (nil when the org is off or has produced no settled rows
yet); #enabled?(company_id) reads the shared billing wa_hold_settlement flag. This is adopted
pending bifrost-tech-lead sign-off.
We did not choose Option 2 (over-gates genuinely pre-V2 historical ranges, which still hold valid
V1 per-message records — breaks DED-S08/AC-2) or Option 3 (adds a write path to the enablement flow
and a new schema/extras contract that the hub_core engine would also have to populate — larger
blast radius for a boundary that the settlement data already implies).
Consequences
- No new schema, no new write path. The boundary is a read over data the hub_core engine already
produces (
SettleDaily, BIF-8744). M1 stays a 2-day, read-only service. - The boundary is "first settled day," not "toggle-on day." These differ by up to one settlement cycle (a CID toggled on midday first settles ~01:00 ICT the next day). For gating this is the correct edge: per-message V1 records genuinely exist right up to the first settled day, and only from that day do daily-aggregate records exist — so DED-S08/AC-2 (pre-activation ranges keep the per-message option) and AC-3 (spanning ranges force daily) both hold against the real data.
- A V2-on org with zero settled rows yet reads as
active_since = nil→ treated as "no V2-era boundary yet," so MCC downloads keep both options until the first settlement lands. Acceptable: no aggregate records exist to protect until then. - Depends on settlement having run. The boundary is meaningful only after the first
reconciliation_settlementrow — fine, since there is nothing to aggregate or gate before then. - Follow-up: if the enablement flow later gains a real activation timestamp, this derivation can
be superseded by Option 3 without changing the
DeductionV2Statuspublic interface (supersede this ADR, keep the method signatures).