Promo Credits Location-Borne
Status 2026-07-23: ADR-026 reaffirmed and extended by ADR-028 after a same-day reversal cycle (Paul briefly ruled pay-at-redemption with top-up backing, then withdrew it). This spec remains the implementation plan; PR #917 (reopened) is the built implementation. ADR-028 adds the explicit rule that NO free hourly booking (comp, tour, dedicated) ever moves money.
Spec written 2026-07-22 while the investigation context was live. Decision: ADR-026. One PR, reviewer pass, Aaron merge. Ship BEFORE the reserve-gate enforce flip if possible (it simplifies the reserve to purchased-only and removes the promo-fraction path from enforcement).
Behavior change
At credit redemption, the location payout is computed per consumed balance origin:
- purchased origin (
credit_package_idIS NOT NULL ORsubscription_deposit_amountIS NOT NULL): pays the standard ~90c/credit (payout rate frompayment_method_feescredit_transfer, as today) - promo origin (neither set): pays $0
A redemption consuming mixed balances transfers only the purchased-step sum; a pure-promo redemption creates NO transfer (and no credit_transfers row — mirror the free-practice model of no money movement, rather than a zero-amount row).
Touch points
- Credit redemption payout path (
CreditTransferServiceand the consumption-plan producers inBalanceOperationService/ReservationCreationService): the consumption plan already steps through balances withbalanceIdper CREDIT_USAGE debit — derive the paid amount from the purchased-origin steps only.releasesObligationCentson the transfer = the purchased-portion obligation (gate parity: after this change only purchased credits are in the reserve, so releases must match). - ReserveService:
PROMO_RESERVE_FRACTION→ 0 (or remove the promo term outright; prefer removal with a comment citing ADR-026 — dead config invites reactivation by accident). analytics.platform_eventsview: the CREDITS-unit liability arm currently values ALL credits at the payout rate. Make it origin-aware: promo-origin grant/usage rows carryobligation_delta_cents = 0(they remain visible as events — truth-first — with zero obligation weight). Same join used by the audit scripts (credit_package_id/subscription_deposit_amountvia the balance FK — remember the physical column isbalance_id; resolve live in raw SQL).audit-platform-liabilities.ts: drop the promo reserve line (keep printing the outstanding promo credit count as an informational line labeled location-borne marketing).- Dashboards: timeline buckets card promo row relabels to "Promo credits (location-borne marketing, no platform obligation)"; dashboard-18 credit-obligations number becomes purchased-only (this RESOLVES their item 4 — coordinate with the other thread).
- Adjudication + annotation seeds: add an adjudication row (ruling
policy, ADR-026, the $2,797 face / ~$1,145 realistic exposure transferred to locations) and a timeline annotation for the ship. docs/business/credits/: record the payout policy per origin (member-facing terms unchanged; this is a location-settlement rule).
Preconditions
- Locations are MG-owned LLCs, so this is intercompany allocation, not partner negotiation: an accounting note only, since MG operates every location and the LLCs are purely a business/accounting split (location P&L shows zero revenue on promo bookings going forward; the grandfathered $1,372 stays as location revenue by design — desired revenue-growth optics at that location).
- Confirm the creditPackageId stamp fix has no unstamped stragglers in prod (one query: confirmed balances with purchases but neither origin marker — expected zero after the misbucket fix).
Tests
- Mixed-balance redemption: transfer amount equals purchased-step sum only.
- Pure-promo redemption: no transfer created, booking succeeds, usage debits recorded.
- Pure-purchased redemption: unchanged behavior (regression).
- Reserve math: promo balances contribute zero to
requiredReserveCents(); gate releases parity on a mixed redemption. - View: promo grant/usage events present with zero obligation delta; snapshot obligations drop by the promo reserve amount on cutover day (expected, annotated).
Rollout
Single PR: payout logic + reserve change + view migration + audit script + seeds + docs. After merge: run the seed script, verify the snapshot step-change matches the promo reserve exactly, update the funding brief and proof sheet (promo row moves out of MUST HOLD into a labeled location-borne line), and notify the other thread so dashboard-18 encodes purchased-only.