Skip to main content

Main Branch Alignment — 2026-08-18

While this branch (billing-integration-phase-3-entitlement-engine-docs) was in progress, main merged a further round of design work on the same schema — from a different branch, also called billing-phase-3-placement-uses-entitlement-lots:

This page records what that meant for this branch and what changed today to catch up.

What happened​

  1. Merged main into both branches (jodapp-api: billing-entitlement-hold-partial-unique-index-fix; docs: billing-integration-phase-3-entitlement-engine-docs). main's version was taken for every conflicting file — it is the later, more thorough, CTO/finance-reviewed design (dated through 2026-08-14 across Billing domain decisions D2 through D12), and its own D2 entry explicitly states it supersedes this branch's original design.
  2. Confirmed the Billing::CreditAction rename. main had already replaced the Billing::CreditGrant model this branch drafted with Billing::CreditAction — a broader, domain-level mechanism covering trial_grant, goodwill_grant, and expiry_extension, not Ads-specific. No further naming decision was needed; this branch's docs were updated to match the shipped name.
  3. Rewrote the Ads Phase 3 folder (38-ads/billing-integration-phase-3/) to align with main's schema and decisions — full rewrite, not a link-out, per the explicit choice made this session over a lighter patch-only option.
  4. Deleted two superseded model spec pages: billing-entitlement-hold.md (the billing_entitlement_holds table was removed from the design entirely — D8) and billing-credit-grant.md (superseded by billing-credit-action.md, already present from the main merge).
  5. Trimmed a stale duplicate. 38-ads/billing-integration-phase-2/use-cases/index.md had a full "Phase 3 Use Cases" handoff section (UC-9–UC-12) still describing the old hold-based mechanics. Replaced with a pointer to the Phase 3 use-cases page — one source of truth instead of two documents to keep in sync.

What changed in the design, in plain terms​

  • Placement credits and gig credits share one lot mechanism. This branch already had this (that was this branch's whole point) — main's version refines it further.
  • No hold table. billing_entitlement_holds is gone. A placement's or shift's reserved credits are now a computed read over Billing::LedgerEntry, not a stored row — Billing D8.
  • reference_type/reference_id renamed to source_type/source_id everywhere they appear (billing_ledger_entries, billing_entitlement_lots, billing_credit_actions).
  • A lot tracks five unit counters, not two, with a database CHECK enforcing they always sum to units_purchased: units_available, units_reserved, units_consumed, units_expired, units_refunded.
  • Revenue recognition is recomputed on every movement, not fixed at lot creation: units_moved × deferred_revenue_remaining_cents ÷ (units_available + units_reserved) — not deferred_revenue_total_cents ÷ units_purchased as this branch originally designed. Both reach the same end state (every lot settles to exactly zero), but the new formula needs no last-unit-absorbs-the-remainder special case.
  • Spend order is soonest-expiry-first (FEFO), not oldest-purchase-first (FIFO). A lot with no expiry sorts last.
  • A lot has no expired_at column and no swept flag. "Expired" is a state read off the row (expires_at in the past), not something a job sets — Billing::EntitlementLot — Derived states.
  • A lot's expiry comes from the product (billing_products.validity_months) for paid lots, or from the admin directly on the Billing::CreditAction row for free grants — not from an entitlement-level default_expiry_months column, which does not exist.
  • Billing::CreditAction's action_type enum is trial_grant | goodwill_grant | expiry_extension only. The reason free-text field stays required on every row, but there is no promotional, backfill, correction, or opening_grant type — those were never built. admin_created_by is required on every row, not nullable.
  • The planned Ads production backfill (UC-14) does not exist. Billing D6 decided the placement launch differently: drain free campaigns before launch, start every company at zero, no migration code. This use case is dropped, not rewritten.
  • The Phase 2 posting backfill (UC-13) is kept, since it addresses a different, narrower gap (a real Billing::InvoicePosting row the live system already created, with no ledger entry yet) that D6 does not cover — whether it ever actually fires depends on the real rollout timeline.
  • New mechanics not previously designed on this branch at all: entry_type: :refund (gig only, full-exit only), billing_credit_expiry_notices (the 30/7/3/1-day warning email send log), and entry_type: :adjust (an expiry_extension's zero-movement ledger entry — previously reserved but unused).

What did not change​

  • Placement credits still move through grant → reserve → consume → release, still exercised only by Ads in this phase.
  • The ledger is still the single source of truth; the balance is still a derived projection.
  • Free credits still never blend with paid credits — a free lot still always carries deferred_revenue_total_cents: 0.
  • The numeric outcome of the walkthrough's worked example (Acme Pte Ltd, 100 credits, 7-day campaign, cancelled after 3 days) is unchanged — lot 301's price is a clean $10.00/credit with no rounding, so the old fixed-rate formula and the new remaining-based formula produce identical cents at every step. Only the explanation of why changed.

Left undone​

  • The Phase 3 domain diagram (overview/phase-3-domain.drawio.svg) still needs a manual pass through the draw.io editor to drop the hold node and rename Billing::CreditGrant to Billing::CreditAction. No draw.io CLI was available in this environment to automate it — see the Design Plan's Next Steps checklist.
  • The two repo merges are committed locally, not pushed, and every content change made after resolving the merge conflicts is uncommitted — per explicit instruction, so it can be reviewed before committing.