Releases: nullata/nullInvoice
Releases · nullata/nullInvoice
1.1.0
[1.1.0] - 2026-05-18
Added
- Async invoice generation queue: a new alternative to the synchronous
POST /api/v1/invoices/generateendpoint for callers that don't want to hold an HTTP request open for the generation time. Requests posted toPOST /api/v1/invoice-requestsare persisted immediately to a newinvoice_requestsqueue table (statusPENDING, attempts/maxAttempts counters, full request payload serialized to aLONGTEXTcolumn) and a background worker generates the invoice asynchronously. Clients then pollGET /api/v1/invoice-requests/{requestId}for status, which returnsPENDING/COMPLETED/FAILEDplus theinvoiceNumberonce ready - actual invoice retrieval still uses the existingGET /api/v1/invoices/{invoiceNumber}and/pdfendpoints so the surface stays unified. Both paths funnel through the same supplier-row pessimistic lock inInvoiceService.generateInvoice, so invoice numbers remain monotonically sequential across sync and async generation - no conflicts even when both paths are driven concurrently for the same supplier. - Idempotent queue worker with crash-safe transaction model: each queue row is processed as one atomic transaction (
SELECT ... FOR UPDATEclaim, idempotency check, generate, markCOMPLETED) split across two beans (InvoiceQueueWorkerpolling loop +InvoiceQueueProcessorper-item transaction) to keep Spring's@Transactionalproxy semantics intact. A mid-processing crash rolls the whole unit back and leaves the row atPENDINGfor the next poll cycle - the crash doesn't count toward the retry budget. Failure recording (attempts increment, terminalFAILEDwhenattempts >= max_attempts) runs in a separateREQUIRES_NEWtransaction so it survives the outer rollback. The newinvoices.request_idcolumn (nullable, unique, FK toinvoice_requests) is the idempotency key: if a retry would otherwise create a duplicate, the worker links the queue row to the existing invoice instead. Sync-path invoices leaverequest_idNULL and are unaffected. - Configuration toggle for the queue worker: new
QUEUE_ENABLEDenv var (defaulttrue) plumbed throughdocker-compose.yml,.env.example, andapplication.ymlundernullinvoice.queue.enabled. The worker bean is annotated@ConditionalOnPropertyso when disabled the entire@Scheduledcomponent is excluded - no poller, no overhead. Additionalnullinvoice.queueproperties:poll-interval-ms(default 2000),batch-size(default 10),max-attempts(default 3).@EnableSchedulingis now onNullInvoiceApplication. - Async Queue documentation:
docs/extended/API.mdgains a full "Async Invoice Generation Queue" section covering submit / status / retrieve flow, response shapes forPENDING/COMPLETED/FAILED, the status lifecycle diagram, polling recommendations (2–4s typical end-to-end), and when to pick async over the sync endpoint.docs/extended/CONFIGURATION.mddocuments the newQUEUE_ENABLEDtoggle and thenullinvoice.queue.*properties. The "Features" list inREADME.md(and all localized variants underdocs/) advertises Async Generation Queue as a first-class feature.
Changed
ApiExceptionHandlercovers the new controller:InvoiceRequestControlleris added to the@RestControllerAdvicebasePackageClasseslist, and a new@ExceptionHandler(InvoiceRequestNotFoundException.class)returns a 404 JSONErrorResponse("invoice request not found: {id}") when callers poll an unknown request id - same shape as the existingInvoiceNotFoundExceptionhandler.Invoicesentity gains arequest_idfield: nullableLong requestIdcolumn oninvoices, populated only when the row was generated via the async queue path. Not exposed throughInvoiceResponse- the field is internal idempotency plumbing for the worker, not part of the public API contract.InvoiceRepositoryaddsfindByRequestId(Long)for the worker's idempotency check.InvoiceServiceadds an overloadedgenerateInvoice(request, requestId)used only byInvoiceQueueProcessor; the existing zero-arg andmarkUnpaidoverloads keep their signatures and behavior so the sync API, UI controller, and existing tests are unaffected.