Skip to main content

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 CaseActorTrigger
UC-1Create Draft InvoiceAdmin (Sales/Ops)Company wants to purchase Placement Credits
UC-2aGenerate Invoice PDFAdmin (Sales)Admin clicks "Generate Invoice File" on a draft invoice
UC-2bSend Invoice to ClientAdmin (Sales)Admin clicks "Send Invoice to Client"
UC-2cProcess Mailgun WebhookSystemMailgun POSTs a delivery event webhook to Jod
UC-3Edit Draft InvoiceAdminAdmin needs to correct details before issuing
UC-4Record Bank TransferAdmin (Ops)Customer provides proof of bank transfer
UC-5Verify PaymentAdmin (Finance)Finance confirms money received in the bank account
UC-6Reject PaymentAdmin (Finance)Proof is invalid or money was not received
UC-7Create InvoicePosting GuardSystem (via UC-5)Invoice transitions to paid — creates posting record, no entitlements
UC-8Void Unpaid InvoiceAdminInvoice 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​

FieldDetails
ActorIdentities::Admin (sales/ops)
TriggerCompany wants to purchase Placement Credits
Source Use CaseAli UC 3.0 — Admin-initiated Purchase Flow (Step 2)

Preconditions:

  • Billing::Account exists for the Org::Company
  • An active Billing::Agreement exists with relevant terms (or terms come from list price)
  • Billing::Product and Billing::ProductPrice exist for the desired entitlement and market
  • Billing::LegalEntity exists for the seller jurisdiction
  • Billing::BillToProfile exists for the buyer

Models Touched: Billing::Invoice, Billing::InvoiceLine

System Behaviour:

  1. Admin selects the billing account, product, and quantity
  2. System resolves the applicable Billing::ProductPrice:
    • country_id matching the company's country (via billing_legal_entity.country_id)
    • status = :active (and parent product status = :active)
    • Private price (billing_account_id matches) overrides standard price (billing_account_id IS NULL)
  3. System resolves the active Billing::Agreement to obtain negotiated terms (fee rates, unit prices, discounts)
  4. System generates Billing::Invoice in draft status:
    • Snapshots bill-to fields from Billing::BillToProfile
    • Sets currency from the bill-to profile
    • Associates the seller Billing::LegalEntity
    • Admin enters ref_number (finance-allocated from Xero). System validates global uniqueness
  5. System creates Billing::InvoiceLine rows:
    • 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
  6. System computes and stores subtotal_cents, tax_cents, total_cents on the invoice
  7. 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_number is globally unique across all Jod invoices (GST compliance requirement)
  • ref_number is 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 draft status with computed totals
  • No payments or postings exist yet
  • No entitlements are granted

UC-2a: Generate the Invoice File (PDF)​

FieldDetails
ActorIdentities::Admin (sales)
TriggerAdmin clicks "Generate Invoice File" on a draft invoice
Source Use CaseAli UC 3.0 — Step 2 (invoice review before issuance)

Preconditions:

  • Invoice is in draft status
  • Invoice has at least one line item
  • All required snapshot fields are populated

Models Touched: Billing::Invoice

System Behaviour:

  1. Background job renders the invoice PDF from the snapshotted invoice fields
  2. Job stores the S3 key at file_path and stamps file_generated_at
  3. status remains :draft. delivery_status remains :not_attempted
  4. 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_path is 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_path and file_generated_at are populated
  • Status unchanged

UC-2b: Send the Invoice to the Client​

FieldDetails
ActorIdentities::Admin (sales)
TriggerAdmin clicks "Send Invoice to Client"
Source Use CaseAli UC 3.0 — Step 2 (issuance step)

Preconditions:

  • Invoice is in draft status
  • file_path is populated (UC-2a ran)
  • delivery_status is :not_attempted, :failed, or :bounced (retries allowed)
  • Bill-to email is set on the snapshotted bill-to fields

Models Touched: Billing::Invoice

System Behaviour:

  1. Manager calls Mailgun's messages.send API with the PDF attached and the bill-to email as recipient
  2. Manager appends the synchronous Mailgun response to email_provider_response
  3. Branch on Mailgun's response — in the same DB transaction:
    • HTTP 200/202 (accepted): set delivery_status: :queued, email_sent_at = now, transition status: :draft → :issued, set issued_at = now
    • HTTP 4xx/5xx (rejected): set delivery_status: :failed, email_last_failed_at = now; status stays :draft

Business Rules:

  • A successful Mailgun send is the moment of issuance — issuance is not a separate click
  • ref_number becomes immutable from this point
  • Idempotency guard: if email_sent_at is 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​

FieldDetails
ActorSystem (Mailgun → Jod webhook handler)
TriggerMailgun POSTs a delivery webhook event
Source Use CaseAli UC 3.0 (delivery tracking)

Preconditions:

  • Webhook signature is valid (HMAC verified)
  • Event email_type = "billing_invoice_issued" and invoice_uuid resolves to a known invoice

Models Touched: Billing::Invoice

System Behaviour:

  1. Verify HMAC — reject with 401 if invalid
  2. Identify invoice by invoice_uuid from webhook custom variables; return 200 if not found
  3. Check for duplicate event by Mailgun event identifier; return 200 if already processed
  4. Append event to email_provider_response
  5. Update delivery_status per mapping:
Mailgun eventEffect
delivereddelivery_status: :delivered
permanent_faildelivery_status: :bounced, email_last_failed_at = now
temporary_failNo change (Mailgun is retrying)
othersAppend 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 status is not changed by webhook events

Postconditions:

  • email_provider_response has one new entry
  • delivery_status may have changed

UC-3: Edit a Draft Invoice Before Issuance​

FieldDetails
ActorIdentities::Admin
TriggerAdmin needs to correct details before issuing
Source Use CaseAli UC 3.0 — Step 2

Preconditions:

  • Invoice is in draft status

Models Touched: Billing::Invoice, Billing::InvoiceLine

System Behaviour:

  1. Admin modifies invoice fields (due date, bill-to details) and/or adds, removes, or updates line items
  2. System recomputes subtotal_cents, tax_cents, total_cents
  3. 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​

FieldDetails
ActorIdentities::Admin (ops)
TriggerCustomer provides proof of bank transfer
Source Use CaseAli UC 3.0 — Step 3

Preconditions:

  • Invoice is in issued or partially_paid status

Models Touched: Billing::Payment

System Behaviour:

  1. Admin creates a Billing::Payment with:
    • method: :bank_transfer
    • status: :submitted
    • amount_cents
    • bank_reference (from bank statement)
    • proof_s3_path (S3 path)

Business Rules:

  • Multiple payments per invoice are supported
  • A submitted payment does not change the invoice status — verification is required first

Postconditions:

  • Payment exists in submitted status
  • Invoice status unchanged

UC-5: Verify a Payment and Settle the Invoice​

FieldDetails
ActorIdentities::Admin (finance)
TriggerFinance confirms money received in the bank account
Source Use CaseAli UC 3.0 — Step 4

Preconditions:

  • Payment is in submitted status
  • Invoice is in issued or partially_paid status

Models Touched: Billing::Payment, Billing::Invoice, Billing::InvoicePosting (when invoice → paid)

System Behaviour:

  1. Finance admin marks the payment as verified
  2. System records verified_at, verified_by_admin_id, and received_at
  3. System recomputes verified_total = sum(amount_cents WHERE status = :verified) for this invoice
  4. System updates invoice status:
    • verified_total == 0 → stays issued
    • 0 < verified_total < total_cents → partially_paid
    • verified_total >= total_cents → paid, settled_at = now
  5. If invoice becomes paid, the system creates a Billing::InvoicePosting record in the same transaction (UC-7):
    • posted_at = now
    • posted_by_admin_id = verifying admin
    • idempotency_key = "invoice_posting_#{invoice.id}"
    • No entitlement grants yet — those are Phase 3

Business Rules:

  • Only submitted payments can be verified
  • Credits are granted only when the invoice reaches paid — partial payments do not grant partial credits
  • Verification records verified_by_admin_id for audit
  • The UNIQUE constraint on InvoicePosting.billing_invoice_id ensures 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::InvoicePosting guard record created (UC-7); entitlement grants deferred to Phase 3 (UC-9)

UC-6: Reject an Invalid Payment​

FieldDetails
ActorIdentities::Admin (finance)
TriggerProof is invalid or money was not received
Source Use CaseAli UC 3.0 — Step 4

Preconditions:

  • Payment is in submitted status

Models Touched: Billing::Payment

System Behaviour:

  1. Finance admin marks the payment as rejected

Business Rules:

  • Only submitted payments 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​

FieldDetails
ActorSystem (called inside UC-5 verify transaction)
TriggerInvoice transitions to paid during payment verification
Source Use CaseAli UC 3.0 — Step 5 (Phase 2 portion only)

Preconditions:

  • Invoice status is transitioning to paid (i.e., verified_total >= total_cents)
  • No Billing::InvoicePosting exists for this invoice

Models Touched: Billing::InvoicePosting

System Behaviour:

  1. Inside the verify transaction, create Billing::InvoicePosting:
    • posted_at = Time.current
    • posted_by_admin_id = verifying admin ID
    • idempotency_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_id prevents double-posting under race conditions
  • No entitlement logic in Phase 2 — the posting record is the guard only

Postconditions:

  • Billing::InvoicePosting record exists with posted_at and posted_by_admin_id
  • No ledger entries written, no entitlement balances updated (Phase 3)

UC-8: Void an Unpaid Invoice​

FieldDetails
ActorIdentities::Admin
TriggerInvoice was created in error, or customer cancels before paying
Source Use CaseAli UC 3.0 (invoice void path)

Preconditions:

  • Invoice is in draft or issued status
  • No verified payments exist for this invoice

Models Touched: Billing::Invoice

System Behaviour:

  1. Admin voids the invoice
  2. System transitions invoice to void status

Business Rules:

  • Cannot void an invoice with verified payments
  • Cannot void paid or credited invoices
  • A voided invoice is kept for audit (never deleted)
  • To correct a voided invoice: create a new one
  • The ref_number is 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.