Skip to main content

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_since is dropped from the task breakdown entirely; the on/off half of this record's question (a) is still needed, but is now answered by replicating Services::Billing::FeatureFlag in moderator-be, not by the DeductionV2Status service 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-lead sign-off
  • Deciders: Bifrost eng (Hafriz); sign-off owner: bifrost-tech-lead
  • Scope: moderator-be modpanel surface only (DED-S07 aggregate history + DED-S08 MCC download gating). Task M1 in deduction-v2.task-breakdown.md (Part 2 — the former companion deduction-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:

  1. Derive the boundary = earliest wa_conversation_logs.created_at where origin_type='reconciliation_settlement' for the org (the first day V2 settlement produced records).
  2. Simplify the gate — if a CID is V2-on now, force daily aggregation for all MCC ranges.
  3. 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_settlement row — 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 DeductionV2Status public interface (supersede this ADR, keep the method signatures).