Skip to main content

Phase 2 — Billing Integration Overview

Phase 2 Domain Overview


Why We Are Building This

Phase 1 delivered the commercial catalog — legal entities, products, and prices. These are the reference data that every invoice reads from. Phase 1 answered: "What are we selling, at what price, under which legal entity?"

Phase 2 answers: "How does a company actually buy credits and start using them?"

Without Phase 2:

  • There is no way for a company to acquire Placement Credits (the credits exist as a concept, but can never be funded)
  • Ads campaigns run without any real billing enforcement — credits are neither reserved nor consumed
  • The billing_entitlement_balances rows that were seeded in Phase 1 will always sit at zero

Phase 2 closes this gap by implementing:

  1. The full invoice lifecycle — from admin creating a draft invoice, through payment, to credits being granted into the account
  2. The entitlement engine for Placement Credits — reserve, consume, and release against the ledger
  3. The Ads integration — campaign placements trigger reserve/consume/release events automatically

Goals

GoalPhaseDescription
Invoice lifecyclePhase 2Admin can create, issue, collect payment for, and mark invoices paid
PDF generation + deliveryPhase 2Invoice PDF generated and sent to client via Mailgun; delivery status tracked
Payment verificationPhase 2Bank transfer payments recorded, verified, and credited toward invoice total
Audit trailPhase 2Every invoice and payment action is traceable to an admin actor
Entitlement grantsPhase 3Placement Credits written to the ledger and made spendable via InvoicePosting
Ads billing enforcementPhase 3Campaign submission reserves credits; insufficient balance blocks submission
Revenue recognitionPhase 3Each daily credit consumption records recognized revenue to the ledger
Idempotent postingPhase 3Posting protected by unique constraint — no double-grants possible

What Gets Built

Admin can create a placement credit invoice

  • Select the billing account (company), bill-to profile, legal entity (seller), and agreement.
  • Choose the placement credit product and quantity — the applicable Billing::ProductPrice is resolved automatically, snapshotting the unit price, tax rate, and platform fee rate at creation time.
  • The system creates a Billing::Invoice in :draft status with one or two Billing::InvoiceLine rows — a :principal line for the credits and a :platform_fee line for the platform margin if applicable. Subtotal, tax, and total are calculated and stored.
  • Draft invoices can be edited freely. Line items are locked once the invoice is issued.

Example: Creating invoice SG-INV-000042 for Company A — 1 × SP-CREDITS-100 pack at SGD 1,000 + 9% GST = SGD 1,090 total. The InvoiceLine snapshots units_to_grant: 100, unit_price_cents: 100_000, tax_rate_bps: 900.


Admin can generate and send the invoice PDF

  • Admin clicks "Generate Invoice File" — system renders a PDF from the snapshotted InvoiceLine fields (price, tax, seller details, buyer details) and stores its S3 key at file_path. file_generated_at is recorded.
  • Admin clicks "Send Invoice to Client" — system sends the PDF via Mailgun to the bill-to email. delivery_status transitions from not_attemptedqueued.
  • Mailgun webhooks update delivery_status to delivered or failed. If delivery fails, email_last_failed_at is recorded and ops can resend.
  • Sending the invoice also transitions Invoice.status from :draft:issued and records issued_at.

Admin can record and verify bank transfer payments

  • Ops records each bank transfer as a Billing::Payment in :submitted status with proof URL, amount, currency, and bank reference number.
  • Finance verifies the transfer — the payment moves to :verified. The invoice's verified_total_cents is recalculated across all verified payments.
  • If verified_total_cents >= total_cents, the invoice transitions to :paid and settled_at is recorded. Partial payments leave the invoice in :partially_paid.
  • Multiple payments are supported — a company can pay in instalments and the invoice recognises the full amount once all transfers are verified.
  • Payments cannot be deleted once submitted; they can only be moved to :rejected if entered incorrectly.

Example: Company A makes two transfers for SG-INV-000042: SGD 500 (verified) then SGD 590 (verified). verified_total_cents = 109_000. Invoice transitions to :paid.


(Phase 3) Entitlement engine and Ads billing enforcement

The following capabilities are fully specified but implementation is deferred to Phase 3:

  • Credits granted when invoice is paidBilling::InvoicePosting auto-triggers on :paid. Writes a grant Billing::LedgerEntry and increments EntitlementBalance.units_available by InvoiceLine.units_to_grant.
  • Ads campaign submission enforces credits — submission checks EntitlementBalance.units_available ≥ credit_cost_snapshot. Insufficient balance blocks the submission.
  • Credits consumed daily — background job writes a consume ledger entry per active placement per day, drawing down units_reserved and recognising proportional revenue.
  • Credits released on cancel/reject — remaining reserved credits returned to units_available via a release ledger entry.

See Use Cases — UC-7, UC-9, UC-10, UC-11 are documented there as Phase 3 use cases.


End-to-End Flow

Invoice Lifecycle Stages

Phase 2 Invoice Lifecycle

Phase 2 Flow — Actor Interactions

Phase 3 picks up from here: when the invoice reaches :paid, Phase 3 auto-triggers Billing::InvoicePosting, writes a grant Billing::LedgerEntry, and updates Billing::EntitlementBalance — making credits spendable. Ads billing enforcement (reserve/consume/release) also lands in Phase 3.


Assumptions

  • Phase 2 scope ends when the invoice reaches :paid. Entitlement granting, Ads billing enforcement, and revenue recognition are Phase 3.
  • Payment is offline bank transfer only — no card or automated payment in Phase 2.
  • Invoice creation is admin-initiated only — self-serve purchase flows are a future phase.
  • The Mailgun email integration for invoice delivery (delivery_status tracking) is in scope for Phase 2.
  • The PDF generation service is in scope for Phase 2.
  • Billing::InvoicePosting is DBML-defined and model-specified in Phase 2 as the handoff contract for Phase 3, but its implementation (posting logic, LedgerEntry writes, EntitlementBalance updates) is Phase 3 work.
  • SOA reporting views are out of scope entirely for both Phase 2 and Phase 3 — deferred to Phase 6.
  • Gig credits (FIFO lots) are deferred to Phase 4.

Design Deviations from Existing DBML

ChangeDiffers FromWhy
Rename invoice_numberref_number on billing_invoicesDBML, Ali's use casesFinance allocates this number in Xero before creating the invoice in Jod. invoice_number implies system-generation; ref_number correctly signals it is externally allocated. external_reference is dropped as redundant. See Invoice ref_number ownership.
Rename billing_invoice_itemsbilling_invoice_linesDBML, Ali's use casesIndustry-standard term; "line" is universally understood as a row on a commercial document
Add billing_agreement_id FK to billing_invoicesDBML (missing column)Audit trail for which agreement's terms were used
Add email delivery tracking columns to billing_invoicesDBML (missing columns)Required for UC-2b/2c (Mailgun delivery tracking)
Allow null on billing_invoice_postings.posted_by_admin_idDBML (no null annotation)System auto-posting when invoice becomes paid should not require an admin actor