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
| Context | Details |
|---|---|
| Aggregate | Child of Billing::Invoice |
| Layer | Commercial Documents |
| Upstream | Billing::Invoice (parent), Identities::Admin (verifier) |
| Downstream | Triggers InvoicePosting indirectly — when all verified payments >= invoice total, the invoice becomes paid and posting is auto-triggered |
State Machine
Payment status
| From | To | Trigger | Notes |
|---|---|---|---|
| (new) | submitted | UC-4: Admin records payment | Proof provided, awaiting review |
submitted | verified | UC-5: Finance confirms receipt | Invoice status recomputed |
submitted | rejected | UC-6: Proof invalid | No effect on invoice |
Table Schema
Table name: billing_payments
| Column | Type | Constraints | Notes |
|---|---|---|---|
id | bigint | PK, auto-increment | |
uuid | string | NOT NULL, UNIQUE | External-safe identifier |
billing_invoice_id | bigint | NOT NULL, FK → billing_invoices.id | The invoice being paid |
method | enum string | NOT NULL, default bank_transfer | rails_enum(:bank_transfer, :card). Card is future scope — Phase 2 is bank transfer only |
status | enum string | NOT NULL | rails_enum(:submitted, :verified, :rejected) |
amount_cents | integer | NOT NULL | Transfer amount in currency minor units |
bank_reference | string | NOT NULL | Reference number from the bank statement |
received_at | timestamptz | nullable | Set by finance when verifying — the actual date money was received |
proof_s3_path | string | nullable | S3 path to the proof document (screenshot, PDF receipt) |
verified_at | timestamptz | nullable | When finance confirmed receipt |
verified_by_admin_id | bigint | FK → identities_admins.id, nullable | Who verified |
created_at | timestamptz | ||
updated_at | timestamptz |
Key Indexes
| Index | Columns | Type | Purpose |
|---|---|---|---|
| — | billing_invoice_id | — | All 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:
| Condition | Invoice Status |
|---|---|
verified_total == 0 | Stays issued |
0 < verified_total < invoice.total_cents | partially_paid |
verified_total >= invoice.total_cents | paid → 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
- Every
Paymentmust belong to exactly oneBilling::Invoice - Only payments in
submittedstatus can be transitioned toverifiedorrejected - A
rejectedpayment does not count towardverified_total— it has no effect on invoice status verified_by_admin_idandverified_atare set simultaneously when status transitions toverifiedreceived_atreflects the actual date money was received — it may differ fromverified_at(e.g., finance processes the record the next business day)- Multiple payments per invoice are allowed — this supports partial payments and prevents corner cases when finance accidentally records a transfer twice
- 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_referencewould store the payment gateway transaction IDproof_s3_pathwould 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?