Phase 2 — Billing Integration 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_balancesrows that were seeded in Phase 1 will always sit at zero
Phase 2 closes this gap by implementing:
- The full invoice lifecycle — from admin creating a draft invoice, through payment, to credits being granted into the account
- The entitlement engine for Placement Credits — reserve, consume, and release against the ledger
- The Ads integration — campaign placements trigger reserve/consume/release events automatically
Goals
| Goal | Phase | Description |
|---|---|---|
| Invoice lifecycle | Phase 2 | Admin can create, issue, collect payment for, and mark invoices paid |
| PDF generation + delivery | Phase 2 | Invoice PDF generated and sent to client via Mailgun; delivery status tracked |
| Payment verification | Phase 2 | Bank transfer payments recorded, verified, and credited toward invoice total |
| Audit trail | Phase 2 | Every invoice and payment action is traceable to an admin actor |
| Entitlement grants | Phase 3 | Placement Credits written to the ledger and made spendable via InvoicePosting |
| Ads billing enforcement | Phase 3 | Campaign submission reserves credits; insufficient balance blocks submission |
| Revenue recognition | Phase 3 | Each daily credit consumption records recognized revenue to the ledger |
| Idempotent posting | Phase 3 | Posting 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::ProductPriceis resolved automatically, snapshotting the unit price, tax rate, and platform fee rate at creation time. - The system creates a
Billing::Invoicein:draftstatus with one or twoBilling::InvoiceLinerows — a:principalline for the credits and a:platform_feeline 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
InvoiceLinefields (price, tax, seller details, buyer details) and stores its S3 key atfile_path.file_generated_atis recorded. - Admin clicks "Send Invoice to Client" — system sends the PDF via Mailgun to the bill-to email.
delivery_statustransitions fromnot_attempted→queued. - Mailgun webhooks update
delivery_statustodeliveredorfailed. If delivery fails,email_last_failed_atis recorded and ops can resend. - Sending the invoice also transitions
Invoice.statusfrom:draft→:issuedand recordsissued_at.
Admin can record and verify bank transfer payments
- Ops records each bank transfer as a
Billing::Paymentin:submittedstatus with proof URL, amount, currency, and bank reference number. - Finance verifies the transfer — the payment moves to
:verified. The invoice'sverified_total_centsis recalculated across all verified payments. - If
verified_total_cents >= total_cents, the invoice transitions to:paidandsettled_atis 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
:rejectedif 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 paid —
Billing::InvoicePostingauto-triggers on:paid. Writes agrantBilling::LedgerEntryand incrementsEntitlementBalance.units_availablebyInvoiceLine.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
consumeledger entry per active placement per day, drawing downunits_reservedand recognising proportional revenue. - Credits released on cancel/reject — remaining reserved credits returned to
units_availablevia areleaseledger 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 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_statustracking) is in scope for Phase 2. - The PDF generation service is in scope for Phase 2.
Billing::InvoicePostingis 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
| Change | Differs From | Why |
|---|---|---|
Rename invoice_number → ref_number on billing_invoices | DBML, Ali's use cases | Finance 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_items → billing_invoice_lines | DBML, Ali's use cases | Industry-standard term; "line" is universally understood as a row on a commercial document |
Add billing_agreement_id FK to billing_invoices | DBML (missing column) | Audit trail for which agreement's terms were used |
Add email delivery tracking columns to billing_invoices | DBML (missing columns) | Required for UC-2b/2c (Mailgun delivery tracking) |
Allow null on billing_invoice_postings.posted_by_admin_id | DBML (no null annotation) | System auto-posting when invoice becomes paid should not require an admin actor |