Skip to main content

Billing::Payment

Purpose

A Billing::Payment records one offline bank transfer made against an invoice. Ops records a payment when the customer provides proof of transfer; finance verifies it when money is confirmed received in the bank account.

Payments are the mechanism that drives the invoice from issued toward paid. When the sum of all verified payments reaches the invoice total, the invoice becomes paid and posting is triggered.


Responsibilities

  • Record bank transfer details (reference number, proof document) submitted by ops
  • Track the verification lifecycle for each individual transfer
  • Support partial payments — multiple payments can exist per invoice
  • Provide the audit trail for finance reconciliation (who verified, when, how much)

Model Context

ContextDetails
AggregateChild of Billing::Invoice
LayerCommercial Documents
UpstreamBilling::Invoice (parent), Identities::Admin (verifier)
DownstreamTriggers InvoicePosting indirectly — when all verified payments >= invoice total, the invoice becomes paid and posting is auto-triggered

State Machine

Payment status

FromToTriggerNotes
(new)submittedUC-4: Admin records paymentProof provided, awaiting review
submittedverifiedUC-5: Finance confirms receiptInvoice status recomputed
submittedrejectedUC-6: Proof invalidNo effect on invoice

Table Schema

Table name: billing_payments

ColumnTypeConstraintsNotes
idbigintPK, auto-increment
uuidstringNOT NULL, UNIQUEExternal-safe identifier
billing_invoice_idbigintNOT NULL, FK → billing_invoices.idThe invoice being paid
methodenum stringNOT NULL, default bank_transferrails_enum(:bank_transfer, :card). Card is future scope — Phase 2 is bank transfer only
statusenum stringNOT NULLrails_enum(:submitted, :verified, :rejected)
amount_centsintegerNOT NULLTransfer amount in currency minor units
bank_referencestringNOT NULLReference number from the bank statement
received_attimestamptznullableSet by finance when verifying — the actual date money was received
proof_s3_pathstringnullableS3 path to the proof document (screenshot, PDF receipt)
verified_attimestamptznullableWhen finance confirmed receipt
verified_by_admin_idbigintFK → identities_admins.id, nullableWho verified
created_attimestamptz
updated_attimestamptz

Key Indexes

IndexColumnsTypePurpose
billing_invoice_idAll payments for an invoice
(billing_invoice_id, status)Efficient "sum verified payments for this invoice" query

How Payments Drive Invoice Status

When a payment is verified (UC-5), the system recomputes:

verified_total = sum(amount_cents) WHERE billing_invoice_id = X AND status = :verified

Then updates invoice status:

ConditionInvoice Status
verified_total == 0Stays issued
0 < verified_total < invoice.total_centspartially_paid
verified_total >= invoice.total_centspaid → triggers InvoicePosting

This logic means:

  • Multiple partial payments are fully supported — each verification event recomputes the total
  • Overpayment is handled — if a customer pays more than the invoice total, the invoice still becomes paid
  • Accidental duplicate recordings are handled — ops can record the same transfer twice; only the verified total matters

Invariants

  1. Every Payment must belong to exactly one Billing::Invoice
  2. Only payments in submitted status can be transitioned to verified or rejected
  3. A rejected payment does not count toward verified_total — it has no effect on invoice status
  4. verified_by_admin_id and verified_at are set simultaneously when status transitions to verified
  5. received_at reflects the actual date money was received — it may differ from verified_at (e.g., finance processes the record the next business day)
  6. Multiple payments per invoice are allowed — this supports partial payments and prevents corner cases when finance accidentally records a transfer twice
  7. Credits are granted only when verified_total >= invoice.total_cents — partial payments do not grant partial credits

Future: Card Payments

The method enum reserves :card for future use. Card payments would follow a different flow:

  • Payment created automatically when the employer completes checkout
  • Verified automatically via payment gateway webhook (no manual finance step)
  • bank_reference would store the payment gateway transaction ID
  • proof_s3_path would be optional

Card payments are out of scope for Phase 2.

Under Consideration — Pending CTO Review: When card payments are introduced, should a card payment auto-trigger posting immediately on gateway confirmation (no manual verification step), or should the same finance verification gate apply?