Phase 2 — Use Cases
This document covers only the use cases required for Phase 2. The original Billing use cases are maintained in use-cases.md.
Use Case Map
Phase 2 (invoice lifecycle — ends at invoice :paid with InvoicePosting guard created):
| # | Use Case | Actor | Trigger |
|---|---|---|---|
| UC-1 | Create Draft Invoice | Admin (Sales/Ops) | Company wants to purchase Placement Credits |
| UC-2a | Generate Invoice PDF | Admin (Sales) | Admin clicks "Generate Invoice File" on a draft invoice |
| UC-2b | Send Invoice to Client | Admin (Sales) | Admin clicks "Send Invoice to Client" |
| UC-2c | Process Mailgun Webhook | System | Mailgun POSTs a delivery event webhook to Jod |
| UC-3 | Edit Draft Invoice | Admin | Admin needs to correct details before issuing |
| UC-4 | Record Bank Transfer | Admin (Ops) | Customer provides proof of bank transfer |
| UC-5 | Verify Payment | Admin (Finance) | Finance confirms money received in the bank account |
| UC-6 | Reject Payment | Admin (Finance) | Proof is invalid or money was not received |
| UC-7 | Create InvoicePosting Guard | System (via UC-5) | Invoice transitions to paid — creates posting record, no entitlements |
| UC-8 | Void Unpaid Invoice | Admin | Invoice was created in error, or customer cancels before paying |
UC-9 through UC-12 (grant, reserve, consume, release) are Phase 3 scope — see below.
UC-1: Create a Draft Invoice for a Company's Credit Purchase
| Field | Details |
|---|---|
| Actor | Identities::Admin (sales/ops) |
| Trigger | Company wants to purchase Placement Credits |
| Source Use Case | Ali UC 3.0 — Admin-initiated Purchase Flow (Step 2) |
Preconditions:
Billing::Accountexists for theOrg::Company- An active
Billing::Agreementexists with relevant terms (or terms come from list price) Billing::ProductandBilling::ProductPriceexist for the desired entitlement and marketBilling::LegalEntityexists for the seller jurisdictionBilling::BillToProfileexists for the buyer
Models Touched: Billing::Invoice, Billing::InvoiceLine
System Behaviour:
- Admin selects the billing account, product, and quantity
- System resolves the applicable
Billing::ProductPrice:country_idmatching the company's country (viabilling_legal_entity.country_id)status = :active(and parent productstatus = :active)- Private price (
billing_account_idmatches) overrides standard price (billing_account_id IS NULL)
- System resolves the active
Billing::Agreementto obtain negotiated terms (fee rates, unit prices, discounts) - System generates
Billing::Invoiceindraftstatus:- Snapshots bill-to fields from
Billing::BillToProfile - Sets
currencyfrom the bill-to profile - Associates the seller
Billing::LegalEntity - Admin enters
ref_number(finance-allocated from Xero). System validates global uniqueness
- Snapshots bill-to fields from
- System creates
Billing::InvoiceLinerows:- Placement Credits: 1 line,
line_type: :principal, full GST - Gig Credits (future): 2 lines —
:principal(tax-exempt) +:platform_fee(taxed) - Every line snapshots:
description,quantity,unit_price_cents,amount_cents,tax_cents,units_to_grant,line_type
- Placement Credits: 1 line,
- System computes and stores
subtotal_cents,tax_cents,total_centson the invoice - Admin audit trail recorded (
admin_created_by)
Business Rules:
- Invoice lines snapshot all pricing and tax at creation time — later product/price/agreement changes do not affect existing invoices
ref_numberis globally unique across all Jod invoices (GST compliance requirement)ref_numberis required at create time; format is owned by finance- Pricing is never hard-coded — it always comes from
Billing::ProductPrice - A Placement Credits invoice has exactly 1 line item
Postconditions:
- Invoice exists in
draftstatus with computed totals - No payments or postings exist yet
- No entitlements are granted
UC-2a: Generate the Invoice File (PDF)
| Field | Details |
|---|---|
| Actor | Identities::Admin (sales) |
| Trigger | Admin clicks "Generate Invoice File" on a draft invoice |
| Source Use Case | Ali UC 3.0 — Step 2 (invoice review before issuance) |
Preconditions:
- Invoice is in
draftstatus - Invoice has at least one line item
- All required snapshot fields are populated
Models Touched: Billing::Invoice
System Behaviour:
- Background job renders the invoice PDF from the snapshotted invoice fields
- Job stores the S3 key at
file_pathand stampsfile_generated_at statusremains:draft.delivery_statusremains:not_attempted- Admin can re-generate while invoice is in
draft(each generation overwrites the previous file)
Business Rules:
- Generation is idempotent — if the draft changes after generation, admin must re-generate before sending
- File is access-controlled —
file_pathis an S3 key in a private bucket, not a URL. Admins must request a short-lived signed URL (GET .../signed_url) to open the file.
Postconditions:
file_pathandfile_generated_atare populated- Status unchanged
UC-2b: Send the Invoice to the Client
| Field | Details |
|---|---|
| Actor | Identities::Admin (sales) |
| Trigger | Admin clicks "Send Invoice to Client" |
| Source Use Case | Ali UC 3.0 — Step 2 (issuance step) |
Preconditions:
- Invoice is in
draftstatus file_pathis populated (UC-2a ran)delivery_statusis:not_attempted,:failed, or:bounced(retries allowed)- Bill-to email is set on the snapshotted bill-to fields
Models Touched: Billing::Invoice
System Behaviour:
- Manager calls Mailgun's
messages.sendAPI with the PDF attached and the bill-to email as recipient - Manager appends the synchronous Mailgun response to
email_provider_response - Branch on Mailgun's response — in the same DB transaction:
- HTTP 200/202 (accepted): set
delivery_status: :queued,email_sent_at = now, transitionstatus: :draft → :issued, setissued_at = now - HTTP 4xx/5xx (rejected): set
delivery_status: :failed,email_last_failed_at = now; status stays:draft
- HTTP 200/202 (accepted): set
Business Rules:
- A successful Mailgun send is the moment of issuance — issuance is not a separate click
ref_numberbecomes immutable from this point- Idempotency guard: if
email_sent_atis already set within the last 5 seconds, treat as no-op (prevents double-send from double-click)
Postconditions:
- On accepted send: invoice is
:issued,delivery_status: :queued - On rejected send: invoice stays
:draft,delivery_status: :failed
UC-2c: Process a Mailgun Webhook Event
| Field | Details |
|---|---|
| Actor | System (Mailgun → Jod webhook handler) |
| Trigger | Mailgun POSTs a delivery webhook event |
| Source Use Case | Ali UC 3.0 (delivery tracking) |
Preconditions:
- Webhook signature is valid (HMAC verified)
- Event
email_type = "billing_invoice_issued"andinvoice_uuidresolves to a known invoice
Models Touched: Billing::Invoice
System Behaviour:
- Verify HMAC — reject with 401 if invalid
- Identify invoice by
invoice_uuidfrom webhook custom variables; return 200 if not found - Check for duplicate event by Mailgun event identifier; return 200 if already processed
- Append event to
email_provider_response - Update
delivery_statusper mapping:
| Mailgun event | Effect |
|---|---|
delivered | delivery_status: :delivered |
permanent_fail | delivery_status: :bounced, email_last_failed_at = now |
temporary_fail | No change (Mailgun is retrying) |
| others | Append to audit log only |
Business Rules:
- Webhook handler is idempotent — duplicate events are ignored after first processing
- Stale events are appended to the audit log but do not change
delivery_status - Commercial
statusis not changed by webhook events
Postconditions:
email_provider_responsehas one new entrydelivery_statusmay have changed
UC-3: Edit a Draft Invoice Before Issuance
| Field | Details |
|---|---|
| Actor | Identities::Admin |
| Trigger | Admin needs to correct details before issuing |
| Source Use Case | Ali UC 3.0 — Step 2 |
Preconditions:
- Invoice is in
draftstatus
Models Touched: Billing::Invoice, Billing::InvoiceLine
System Behaviour:
- Admin modifies invoice fields (due date, bill-to details) and/or adds, removes, or updates line items
- System recomputes
subtotal_cents,tax_cents,total_cents - Admin audit trail recorded
Business Rules:
- Editing is only allowed while status is
draft - All creation-time validations still apply
Postconditions:
- Invoice reflects updated values with recomputed totals
UC-4: Record a Bank Transfer Payment
| Field | Details |
|---|---|
| Actor | Identities::Admin (ops) |
| Trigger | Customer provides proof of bank transfer |
| Source Use Case | Ali UC 3.0 — Step 3 |
Preconditions:
- Invoice is in
issuedorpartially_paidstatus
Models Touched: Billing::Payment
System Behaviour:
- Admin creates a
Billing::Paymentwith:method: :bank_transferstatus: :submittedamount_centsbank_reference(from bank statement)proof_s3_path(S3 path)
Business Rules:
- Multiple payments per invoice are supported
- A
submittedpayment does not change the invoice status — verification is required first
Postconditions:
- Payment exists in
submittedstatus - Invoice status unchanged
UC-5: Verify a Payment and Settle the Invoice
| Field | Details |
|---|---|
| Actor | Identities::Admin (finance) |
| Trigger | Finance confirms money received in the bank account |
| Source Use Case | Ali UC 3.0 — Step 4 |
Preconditions:
- Payment is in
submittedstatus - Invoice is in
issuedorpartially_paidstatus
Models Touched: Billing::Payment, Billing::Invoice, Billing::InvoicePosting (when invoice → paid)
System Behaviour:
- Finance admin marks the payment as
verified - System records
verified_at,verified_by_admin_id, andreceived_at - System recomputes
verified_total = sum(amount_cents WHERE status = :verified)for this invoice - System updates invoice status:
verified_total == 0→ staysissued0 < verified_total < total_cents→partially_paidverified_total >= total_cents→paid,settled_at = now
- If invoice becomes
paid, the system creates aBilling::InvoicePostingrecord in the same transaction (UC-7):posted_at = nowposted_by_admin_id = verifying adminidempotency_key = "invoice_posting_#{invoice.id}"- No entitlement grants yet — those are Phase 3
Business Rules:
- Only
submittedpayments can be verified - Credits are granted only when the invoice reaches
paid— partial payments do not grant partial credits - Verification records
verified_by_admin_idfor audit - The UNIQUE constraint on
InvoicePosting.billing_invoice_idensures that if two verify requests race for the same invoice, the second transaction rolls back — no duplicate posting, no duplicate entitlements
Postconditions:
- Payment
status = :verified - Invoice status updated per verified total
- If invoice became
paid:Billing::InvoicePostingguard record created (UC-7); entitlement grants deferred to Phase 3 (UC-9)
UC-6: Reject an Invalid Payment
| Field | Details |
|---|---|
| Actor | Identities::Admin (finance) |
| Trigger | Proof is invalid or money was not received |
| Source Use Case | Ali UC 3.0 — Step 4 |
Preconditions:
- Payment is in
submittedstatus
Models Touched: Billing::Payment
System Behaviour:
- Finance admin marks the payment as
rejected
Business Rules:
- Only
submittedpayments can be rejected - Rejecting a payment does not change the invoice status
- Ops team can record a new payment to try again
Postconditions:
- Payment
status = :rejected - Invoice status unchanged
UC-7: Create InvoicePosting Guard When Invoice Becomes Paid
| Field | Details |
|---|---|
| Actor | System (called inside UC-5 verify transaction) |
| Trigger | Invoice transitions to paid during payment verification |
| Source Use Case | Ali UC 3.0 — Step 5 (Phase 2 portion only) |
Preconditions:
- Invoice
statusis transitioning topaid(i.e.,verified_total >= total_cents) - No
Billing::InvoicePostingexists for this invoice
Models Touched: Billing::InvoicePosting
System Behaviour:
- Inside the verify transaction, create
Billing::InvoicePosting:posted_at = Time.currentposted_by_admin_id = verifying admin IDidempotency_key = "invoice_posting_#{invoice.id}"
Business Rules:
- Posting creation is inside the same DB transaction as payment verify and invoice status update — if posting fails, the entire verify rolls back
- UNIQUE constraint on
billing_invoice_idprevents double-posting under race conditions - No entitlement logic in Phase 2 — the posting record is the guard only
Postconditions:
Billing::InvoicePostingrecord exists withposted_atandposted_by_admin_id- No ledger entries written, no entitlement balances updated (Phase 3)
UC-8: Void an Unpaid Invoice
| Field | Details |
|---|---|
| Actor | Identities::Admin |
| Trigger | Invoice was created in error, or customer cancels before paying |
| Source Use Case | Ali UC 3.0 (invoice void path) |
Preconditions:
- Invoice is in
draftorissuedstatus - No verified payments exist for this invoice
Models Touched: Billing::Invoice
System Behaviour:
- Admin voids the invoice
- System transitions invoice to
voidstatus
Business Rules:
- Cannot void an invoice with verified payments
- Cannot void
paidorcreditedinvoices - A voided invoice is kept for audit (never deleted)
- To correct a voided invoice: create a new one
- The
ref_numberis consumed on void — it cannot be reused
Postconditions:
- Invoice
status = :void - No entitlements are affected
Under Consideration — Pending CTO Review: Should voiding require a reason field? Should submitted (unverified) payments be auto-rejected when an invoice is voided?
Phase 3 Use Cases
UC-9 through UC-12 (grant, reserve, consume, release) were originally drafted here as the Phase 3
handoff contract. They have moved — the authoritative, current version is
Phase 3 — Use Cases, rewritten
2026-08-18 to match the design that shipped to main (lot-based mechanics, no hold table,
Billing::CreditAction). Keeping a second copy here would mean keeping two use-case documents in
sync forever — this page now points to the one source of truth instead.
Phase 2 handoff state, unchanged: by the time Phase 3 is implemented, every paid invoice will
already have a Billing::InvoicePosting record created by UC-7 above. Phase 3 extends the posting
flow to add entitlement grants — it does not re-create posting records. Billing has not shipped to
production yet, so a posting that predates Phase 3 with no grant is manual test data, not a real
gap — see
Billing D13.