Payments
Payments represent money received from customers to settle their invoices. The payment system tracks incoming funds, allocates them to specific invoices, and integrates with the accounting system through journal entries.
Overview
The payment workflow consists of three main stages:
- Payment Creation - Recording a payment received from a customer
- Payment Allocation - Assigning the payment amount to specific invoices
- Payment Posting - Creating journal entries and finalizing the payment
Payments can be partially allocated, fully allocated, or over-allocated (creating a credit balance). The system automatically tracks allocated and unallocated amounts.
Business Rationale: Why Allocations Are Independent
The Problem with Status-Dependent Allocations
Traditional payment systems often tie allocations to payment status, creating artificial constraints:
- Payments must be in a specific status to allocate
- Once posted, allocations cannot be modified
- Corrections require complex reversal processes
- Real-world scenarios don't fit rigid status flows
Real-World Payment Scenarios
Scenario 1: Late-Applied Payments A customer makes a payment, it's posted to the general ledger, but later the accounting team realizes it should be allocated to different invoices. With status-dependent allocations, this requires reversing the entire payment and reposting.
Scenario 2: Partial Payments Over Time A customer makes a partial payment today, it's posted, then makes another payment next week. The second payment should be allocated to the same invoices. Status-dependent systems make this unnecessarily complex.
Scenario 3: Payment Corrections After posting, you discover an allocation error. With status-dependent allocations, you must reverse, correct, and repost - creating audit trail confusion.
Our Solution: Status-Independent Allocations
Key Principle: Allocations are business operations, not status transitions.
- Allocations = "Which invoices does this payment apply to?"
- Status = "What is the accounting state of this payment?"
These are orthogonal concerns that should not be coupled.
Benefits
- Flexibility - Allocate and reallocate at any time
- Simplicity - No complex status transition rules
- Real-World Fit - Handles actual business scenarios naturally
- Audit Trail - Clear separation between allocation changes and accounting events
- Error Recovery - Easy to correct allocation mistakes without reversing accounting entries
Payment Lifecycle & Allocation Flow
Status Lifecycle (Simple)
Allocation Flow (Independent from Status)
Complete Payment Workflow
Real-World Scenario: Payment Correction
Payment Status
- Draft - Payment created and ready for processing. Allocations can be made at any time.
- Posted - Payment has been posted to the general ledger with journal entries. Allocations can still be added or modified.
- Canceled - Payment has been canceled (reversal journal created if previously posted)
Key Insight: Notice how allocations are operations that happen on a payment, not transitions of the payment status.
Payment Methods
The system supports the following payment methods:
- BankTransfer - Bank wire transfers
- CreditCard - Credit card payments
- DebitCard - Debit card payments
- Cash - Cash payments
- Check - Check payments
- DirectDebit - Direct debit/SEPA payments
- PayPal - PayPal payments
- Other - Other payment methods
Endpoints
List Payments
GET /payments
Returns a list of all payments. Supports OData query parameters for filtering and sorting.
Query Parameters:
$filter- OData filter expression (e.g.,customerId eq 'guid')$orderby- Sort order (e.g.,paymentDate desc)$top- Number of records to return$skip- Number of records to skip$count- Include total count in response$expand- Expand related entities (e.g.,allocations)
Example Request:
GET /payments?$filter=customerId eq '3fa85f64-5717-4562-b3fc-2c963f66afa6'&$orderby=paymentDate desc
Example Response:
{
"value": [
{
"id": "8e9b7c2a-1234-5678-9abc-def012345678",
"paymentNumber": "PAY-2024-00001",
"customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"paymentSourceId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"paymentSourceName": "Main Bank Account",
"amount": 1500.00,
"allocatedAmount": 1500.00,
"unallocatedAmount": 0.00,
"paymentMethod": "BankTransfer",
"status": "Posted",
"paymentDate": "2024-01-20T00:00:00Z",
"externalReference": "TXN-123456",
"notes": "Payment for semester fees",
"journalId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"createdAt": "2024-01-20T10:30:00Z",
"createdById": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"updatedAt": "2024-01-20T11:15:00Z",
"updatedById": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"postedAt": "2024-01-20T11:15:00Z",
"postedById": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"allocations": null
}
],
"@odata.count": 1
}
Get Payment by ID
GET /payments/{id}
Retrieves a single payment by its unique identifier, including all payment allocations.
Path Parameters:
id(UUID, required) - The payment ID
Example Request:
GET /payments/8e9b7c2a-1234-5678-9abc-def012345678
Example Response:
{
"id": "8e9b7c2a-1234-5678-9abc-def012345678",
"paymentNumber": "PAY-2024-00001",
"customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"paymentSourceId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"paymentSourceName": "Main Bank Account",
"amount": 1500.00,
"allocatedAmount": 1500.00,
"unallocatedAmount": 0.00,
"paymentMethod": "BankTransfer",
"status": "Posted",
"paymentDate": "2024-01-20T00:00:00Z",
"externalReference": "TXN-123456",
"notes": "Payment for semester fees",
"journalId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"createdAt": "2024-01-20T10:30:00Z",
"createdById": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"updatedAt": "2024-01-20T11:15:00Z",
"updatedById": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"postedAt": "2024-01-20T11:15:00Z",
"postedById": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"allocations": [
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"paymentId": "8e9b7c2a-1234-5678-9abc-def012345678",
"invoiceId": "f1e2d3c4-b5a6-9788-0011-223344556677",
"invoiceNumber": "INV-2024-00123",
"allocatedAmount": 1000.00,
"allocatedAt": "2024-01-20T10:45:00Z",
"allocatedById": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
},
{
"id": "b2c3d4e5-6789-01bc-def0-234567890abc",
"paymentId": "8e9b7c2a-1234-5678-9abc-def012345678",
"invoiceId": "f2e3d4c5-b6a7-9899-0022-334455667788",
"invoiceNumber": "INV-2024-00124",
"allocatedAmount": 500.00,
"allocatedAt": "2024-01-20T10:45:00Z",
"allocatedById": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
}
]
}
Create Payment
POST /payments
Creates a new payment record.
Request Body:
{
"customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"paymentSourceId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"amount": 1500.00,
"paymentMethod": "BankTransfer",
"paymentDate": "2024-01-20T00:00:00Z",
"externalReference": "TXN-123456",
"notes": "Payment for semester fees"
}
Response: 201 Created
{
"id": "8e9b7c2a-1234-5678-9abc-def012345678",
"paymentNumber": "PAY-2024-00001",
"customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"paymentSourceId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"paymentSourceName": "Main Bank Account",
"amount": 1500.00,
"allocatedAmount": 0.00,
"unallocatedAmount": 1500.00,
"paymentMethod": "BankTransfer",
"status": "Draft",
"paymentDate": "2024-01-20T00:00:00Z",
"externalReference": "TXN-123456",
"notes": "Payment for semester fees",
"journalId": null,
"createdAt": "2024-01-20T10:30:00Z",
"createdById": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"updatedAt": null,
"updatedById": null,
"postedAt": null,
"postedById": null,
"allocations": []
}
Allocate Payment
POST /payments/{id}/allocate
Allocates a payment to one or more invoices. Allocations can be made regardless of payment status - you can allocate Draft, Allocated, or even Posted payments. This allows for flexible payment management including adjustments after posting.
The system automatically validates that:
- The total allocation amount (including existing allocations) does not exceed the payment amount
- All invoices belong to the same customer as the payment
- Invoices are in Issued status
Path Parameters:
id(UUID, required) - The payment ID
Request Body:
{
"allocations": [
{
"invoiceId": "f1e2d3c4-b5a6-9788-0011-223344556677",
"amount": 1000.00
},
{
"invoiceId": "f2e3d4c5-b6a7-9899-0022-334455667788",
"amount": 500.00
}
]
}
Response: 200 OK
The system will:
- Create payment allocation records
- Update invoice payment status
- Update payment allocated/unallocated amounts
- Payment status remains unchanged (Draft or Posted)
Post Payment
POST /payments/{id}/post
Posts a payment to the general ledger by creating journal entries. This action:
- Creates a journal with debit and credit entries
- Debits the payment source account (cash/bank)
- Credits customer receivable accounts for each allocated invoice
- Marks the payment as Posted
- Updates invoice payment status
Path Parameters:
id(UUID, required) - The payment ID
Response: 200 OK
Business Rules:
- Payment must be in Allocated status
- Payment must have at least one allocation
- All allocations must be to valid, issued invoices
Cancel Payment
POST /payments/{id}/cancel
Cancels a payment. If the payment was posted, a reversal journal entry is created.
Path Parameters:
id(UUID, required) - The payment ID
Response: 200 OK
Response Schema
PaymentResponse
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
paymentNumber | string | Auto-generated payment number (e.g., "PAY-2024-00001") |
customerId | UUID | Customer who made the payment |
paymentSourceId | UUID | Payment source used |
paymentSourceName | string | Name of the payment source |
amount | decimal | Total payment amount |
allocatedAmount | decimal | Amount allocated to invoices |
unallocatedAmount | decimal | Remaining unallocated amount |
paymentMethod | enum | Payment method used |
status | enum | Payment status (Draft, Allocated, Posted, Canceled) |
paymentDate | datetime | Date payment was received |
externalReference | string | External reference number (e.g., bank transaction ID) |
notes | string | Optional notes |
journalId | UUID | Associated journal entry ID (when posted) |
createdAt | datetime | Creation timestamp |
createdById | UUID | ID of user who created the payment |
updatedAt | datetime | Last update timestamp |
updatedById | UUID | ID of user who last updated the payment |
postedAt | datetime | Posting timestamp |
postedById | UUID | ID of user who posted the payment |
allocations | array | Payment allocations (see PaymentAllocationResponse) |
PaymentAllocationResponse
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
paymentId | UUID | Associated payment ID |
invoiceId | UUID | Invoice being paid |
invoiceNumber | string | Invoice number for reference |
allocatedAmount | decimal | Amount allocated to this invoice |
allocatedAt | datetime | Allocation timestamp |
allocatedById | UUID | ID of user who created the allocation |
Business Rules
- Customer Matching: All allocated invoices must belong to the same customer as the payment
- Amount Validation: Total allocations (including existing allocations) cannot exceed the payment amount
- Invoice Status: Only issued invoices can receive payment allocations
- Payment Numbering: Payment numbers are auto-generated in sequence (PAY-YYYY-NNNNN)
- Status-Independent Allocations: Allocations can be made on payments in any status (Draft, Posted, or Canceled)
- Status Preservation: Payment status is never changed by allocation operations
- Posting Requirements: Payments must have at least one allocation to be posted
- Cancellation: Posted payments create reversal journal entries when canceled
- Audit Trail: All operations are tracked with user IDs and timestamps
Common Use Cases
Finding Unallocated Payments
GET /payments?$filter=unallocatedAmount gt 0 and status ne 'Canceled'
Getting Customer Payment History
GET /payments?$filter=customerId eq '3fa85f64-5717-4562-b3fc-2c963f66afa6'&$orderby=paymentDate desc
Finding Payments by External Reference
GET /payments?$filter=contains(externalReference, 'TXN-123')
Getting Posted Payments for a Date Range
GET /payments?$filter=postedAt ge 2024-01-01T00:00:00Z and postedAt le 2024-01-31T23:59:59Z
Integration Notes
- Payments are automatically numbered using the format PAY-YYYY-NNNNN
- Flexible Allocation: Allocations can be added to payments at any time, even after posting, enabling payment adjustments and corrections
- Payment allocations update invoice payment status and outstanding amounts
- Posted payments create journal entries that integrate with the accounting system
- The system prevents over-allocation and validates customer/invoice relationships
- Canceled payments maintain audit history and create reversal entries if previously posted
- Status-independent allocations allow for real-world scenarios like partial payments, payment corrections, and late-applied payments