-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Add an async invoice generation path that coexists with the current synchronous endpoints. Incoming requests are persisted immediately to a queue table, a background worker processes them, and clients poll for status/results.
The existing synchronous endpoints (POST /api/v1/invoices/generate and POST /invoices/generate) must remain unchanged.
Design justification:
Pessimistic write lock (SELECT ... FOR UPDATE on the supplier row) is acquired inside InvoiceService.generateInvoice(). It doesn't matter who calls it - the sync API, the UI endpoint, or the new queue worker. All callers funnel through the same lockSupplierForUpdate(), so the database serializes invoice number assignment across all paths. No changes to the existing locking logic are needed.
New API Endpoints Spec
Endpoint 1: Submit to Queue
POST /api/v1/invoice-requests
Request body should use the same GenerateInvoiceRequest used by the existing sync endpoint
Response 201:
{
"requestId": 42,
"status": "PENDING",
"message": "invoice generation request queued"
}
Calls invoiceRequestService.submitRequest(request). Executes ASAP - just a single INSERT.
Endpoint 2: Check Status (Polling)
GET /api/v1/invoice-requests/{requestId}
Response 200:
{
"requestId": 42,
"status": "COMPLETED",
"invoiceId": 107,
"invoiceNumber": "INV-000042",
"errorMessage": null,
"createdAt": "2026-03-30T10:00:00Z",
"completedAt": "2026-03-30T10:00:02Z"
}
Use ENUM status: PENDING, PROCESSING, COMPLETED, FAILED.
When COMPLETED, includes invoiceId and invoiceNumber so the client can decide whether to fetch the full invoice.
Endpoint 3: Get Invoice by Request ID
GET /api/v1/invoice-requests/{requestId}/invoice
Response 200: Returns the full InvoiceResponse - same shape as GET /api/v1/invoices/{invoiceNumber}
Error responses:
- 404 - request ID not found
- 409 Conflict - request exists but status is not COMPLETED (body includes current status)
Crash Scenarios
| Crash Point | Queue Row State | Invoice State | Recovery |
|---|---|---|---|
| After queue INSERT, before worker picks up | PENDING | Does not exist | Worker processes it on restart |
Worker set PROCESSING, mid-generateInvoice() |
PROCESSING (rolled back to PENDING by tx rollback) | Rolled back | Worker retries on restart - attempts not incremented since tx rolled back |
After generateInvoice() commits but before queue row updated to COMPLETED |
PROCESSING | Invoice exists | Edge case - see below |
| After queue row set to COMPLETED | COMPLETED | Invoice exists | Clean - client polls and finds it |
Edge Case: Invoice created but queue row stuck at PROCESSING
This happens if generateInvoice() runs in a separate transaction (REQUIRES_NEW) from the queue row update, and the app crashes between the two commits. On restart, the worker sees PROCESSING, retries, and generateInvoice() would try to create a duplicate.
Mitigation: The unique constraint ux_invoices_supplier_number on (supplier_party_id, invoice_number) prevents a true duplicate. The retry would fail, and the worker should catch DataIntegrityViolationException, look up the existing invoice, link it to the queue row, and mark COMPLETED.