Skip to content

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_id IS NOT NULL OR subscription_deposit_amount IS NOT NULL): pays the standard ~90c/credit (payout rate from payment_method_fees credit_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

  1. Credit redemption payout path (CreditTransferService and the consumption-plan producers in BalanceOperationService / ReservationCreationService): the consumption plan already steps through balances with balanceId per CREDIT_USAGE debit — derive the paid amount from the purchased-origin steps only. releasesObligationCents on the transfer = the purchased-portion obligation (gate parity: after this change only purchased credits are in the reserve, so releases must match).
  2. 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).
  3. analytics.platform_events view: the CREDITS-unit liability arm currently values ALL credits at the payout rate. Make it origin-aware: promo-origin grant/usage rows carry obligation_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_amount via the balance FK — remember the physical column is balance_id; resolve live in raw SQL).
  4. audit-platform-liabilities.ts: drop the promo reserve line (keep printing the outstanding promo credit count as an informational line labeled location-borne marketing).
  5. 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).
  6. 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.
  7. 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.