fix(payments): make supplier payment batch creation atomic via create_supplier_payment_batch RPC (#1989)
createSupplierPaymentBatch wrote the batch header and its items as two separate PostgREST inserts, and the active-batch recheck ran app-side before either. Two concurrent creates selecting the same invoice could both pass that check and both land an active batch without confirm_already_batched, and an item-insert failure after the header landed could leave an empty 'created' batch behind when the best-effort cancel also failed. The new SECURITY DEFINER RPC is now the single write path: it locks the selected invoices FOR UPDATE in id order, re-checks payability, amounts and active batches inside the transaction, and inserts header + items together so a constraint violation rolls both back. TypeScript keeps the shared eligibility evaluation and the msg_id minting (branding lives in TS); the service result union is unchanged so the route and UI are untouched. Closes #1503 Claude-Session: https://claude.ai/code/session_01FkUfWtuFCUkNtRAgMQCse2 Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
4f939ebb21
commit
533df34369
@@ -194,13 +194,14 @@ describe('POST /api/supplier-invoices/payment-batches', () => {
|
||||
})
|
||||
|
||||
it('creates a batch and returns 201', async () => {
|
||||
// Queue is shared between from() and rpc(): the last entry is the
|
||||
// create_supplier_payment_batch RPC result.
|
||||
enqueueMany([
|
||||
{ data: companyRow },
|
||||
{ data: settingsRow },
|
||||
{ data: [invoiceRow()] },
|
||||
{ data: [] },
|
||||
{ data: batchRow() },
|
||||
{ data: null },
|
||||
{ data: { ok: true, batch: batchRow() } },
|
||||
])
|
||||
const response = await createBatch(
|
||||
createMockRequest('/api/supplier-invoices/payment-batches', {
|
||||
|
||||
Reference in New Issue
Block a user