Accounts Receivable Receipts
Overview
An AR Receipt records a payment received from a customer against one or more AR Invoices. It tracks how much was paid, through which payment methods, on which date, and in which currency.
Receipts are the financial event that reduces the outstanding balance (balanceDue) on one or more AR Invoices.
Domain Concepts
| Concept | Description |
|---|---|
| Receipt | A payment event that settles all or part of one or more AR invoices |
| detail | JSON array of { accountsReceivableInvoiceId, amount, baseAmount } — which invoices are being paid |
| paymentDetail | JSON array of { paymentMethodId, amount, baseAmount, ... } — how the payment was made |
| documentNumber | Auto-generated sequential identifier (e.g. ARR-000042) |
| status | posted (active) or void (cancelled) |
| exchangeRate | FX rate when the receipt currency differs from the invoice currency |
| notes | Optional internal memo attached to the receipt |
Receipt Lifecycle
Created (posted) ──► Voided (void)
- Receipts are created as
postedby default. - The only valid status transition is
posted → void. - Voiding sets
voidedAtandvoidedByautomatically. - Deletion is only allowed for receipts with neither
postednorvoidstatus (edge cases / legacy data). For posted receipts, use void instead.
Validation Rules
All rules are enforced on create and on any update that modifies detail or paymentDetail:
| Rule | Error Code | Description |
|---|---|---|
| At least one invoice item required | RECEIPT_ITEMS_REQUIRED | detail.items must not be empty |
| Invoice must exist | INVOICE_NOT_FOUND | Each referenced invoice must exist in the DB |
| Invoice status must be payable | INVOICE_STATUS_NOT_APPROVED | Invoice status must be submitted, approved, or scheduled |
| Total must equal sum of items | TOTAL_AMOUNT_MISMATCH | totalAmount must equal the sum of all detail.items[].amount (rounded to minorUnit) |
| No overpayment per invoice | OVERPAYMENT | Applied amount per invoice must not exceed invoice.balanceDue |
| FX rate required for cross-currency | FX_REQUIRED_FOR_CROSS_CURRENCY | When invoice currency ≠ receipt currency, exchangeRate must be > 0 |
| Payment method must be active | PAYMENT_METHOD_INACTIVE | All paymentDetail.items[].paymentMethodId must be active |
Module Structure
accounts-receivable-receipts/
├── accounts-receivable-receipts.module.ts
├── application/
│ ├── accounts-receivable-receipts.service.ts # All use cases + validation
│ ├── accounts-receivable-receipts.service.spec.ts
│ └── events/
│ ├── on-create-accounts-receivable-receipt.event.ts
│ └── on-update-accounts-receivable-receipt.event.ts
├── domain/
│ └── accounts-receivable-receipts-repository.domain.ts # Port interface
├── infrastructure/
│ └── accounts-receivable-receipts.repository.ts # Kysely adapter
└── interfaces/
├── accounts-receivable-receipts.controller.ts
├── dtos/
│ ├── create-accounts-receivable-receipt.dto.ts
│ └── update-accounts-receivable-receipt.dto.ts
└── query/
└── paginate-accounts-receivable-receipts.query.ts
Authorization
All endpoints require a valid Firebase bearer token. Access is controlled via RolesGuard with resource AccountsReceivableReceipt and per-endpoint CASL actions (Create, Read, Update, Delete).
API Endpoints
| Method | Path | Action | Description |
|---|---|---|---|
POST | /accounts-receivable-receipts | Create | Create a new receipt |
GET | /accounts-receivable-receipts | Read | List receipts (paginated, filterable by businessId, customerId) |
GET | /accounts-receivable-receipts/with-invoice-items | Read | List receipts expanded with per-invoice payment details (via DB view) |
GET | /accounts-receivable-receipts/:id | Read | Get single receipt (404 if not found) |
GET | /accounts-receivable-receipts/:id/pdf | Read | Download receipt as PDF (404 if not found) |
GET | /accounts-receivable-receipts/:id/print | Read | Preview receipt as HTML (404 if not found) |
PATCH | /accounts-receivable-receipts/:id | Update | Update / void a receipt (404 if not found) |
DELETE | /accounts-receivable-receipts/:id | Delete | Delete receipt (only non-posted, non-void; 404 if not found) |
Supported orderBy values
paymentDate · totalAmount · documentNumber · status · createdAt
Create Receipt — Example Payload
{
"customerId": "<uuid>",
"status": "posted",
"currencyId": "<uuid>",
"currencyCode": "GTQ",
"minorUnit": 2,
"exchangeRate": 1.00,
"currency": { "id": "<uuid>", "name": "Quetzal", "symbol": "Q" },
"totalAmount": 112.00,
"totalBaseAmount": 112.00,
"paymentDate": "2026-03-12",
"businessId": "<uuid>",
"createdBy": "<uuid>",
"detail": {
"items": [
{
"accountsReceivableInvoiceId": "<uuid>",
"amount": 112.00,
"baseAmount": 112.00
}
]
},
"paymentDetail": {
"items": [
{
"paymentMethodId": "<uuid>",
"paymentMethodName": "Cash",
"amount": 112.00,
"baseAmount": 112.00,
"currencyId": "<uuid>",
"currencyCode": "GTQ",
"minorUnit": 2,
"exchangeRate": 1.00
}
]
},
"notes": "Optional memo"
}
Void Receipt — Example Payload
{
"status": "void",
"updatedBy": "<uuid>"
}
Events
| Event | Trigger | Payload |
|---|---|---|
accountsReceivableReceipt.create | Receipt created | Full receipt record |
accountsReceivableReceipt.update | Receipt updated or voided | Original + updated records |
Database
Table: accounts_receivable_receipt
View: vw_accounts_receivable_receipt_details — denormalizes receipt + invoice payment mapping (one row per invoice line)
Key columns
| Column | Type | Notes |
|---|---|---|
id | UUID | Auto-generated |
document_number | text | Sequential (e.g. ARR-000001), generated in transaction |
status | enum | posted | void |
detail | jsonb | Invoice payment items |
payment_detail | jsonb | Payment method items |
voided_at / voided_by | timestamp/uuid | Set automatically on void |
Design Decisions
-
detailandpaymentDetailare JSON columns — Both the invoice line allocation and the payment method breakdown are stored as JSON, consistent with other financial document modules in this codebase. This avoids additional join tables for simple cases and simplifies the document structure. -
GET /with-invoice-itemsuses a DB view —vwAccountsReceivableReceiptDetailsis a PostgreSQL view that unnests thedetailJSON into relational rows, enabling efficient filtering byaccountsReceivableInvoiceIdat the DB level. -
GET /does not supportaccountsReceivableInvoiceIdfiltering — The invoice ID is stored inside thedetailJSON column, not as a relational column. OnlyGET /with-invoice-items(which queries the view) supports this filter. -
Transactions used for mutations only — Create and update operations wrap DB writes inside Kysely transactions to ensure atomicity. Read operations (
findAll,findById) execute directly without transactions. -
Voiding never deletes — Voids preserve the full audit trail. Only use DELETE for receipts that were never properly committed (no POSTED or VOID status).
Related Documentation
- AR Invoices — the invoices that receipts are applied against
- Cash Register Sessions — links between receipt payments and shift reconciliation
- Payment Methods — active payment method validation