feat(payments): payment-batch API surface (betalfil 2/3) (#1504)

Preview/create/list/get/file/cancel routes under
/api/supplier-invoices/payment-batches, SI_BATCH_* structured errors,
and a fail-closed batch-membership pre-check in the supplier invoice
DELETE route (the FK RESTRICT is the backstop). File downloads stamp
file_generated_at + download_count but regenerate byte-identically.

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:
Jakob Wennberg
2026-08-10 20:00:42 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent f776e375c5
commit 576a34750a
9 changed files with 903 additions and 0 deletions
+33
View File
@@ -1081,6 +1081,39 @@ export const UpdateSupplierInvoiceSchema = z.object({
notes: z.string().optional(),
})
// ============================================================
// Supplier payment batch (betalfil) schemas
// ============================================================
// v1 gates the API to pain001; the DB CHECK also allows 'bg_lb' so a future
// format lands without a migration.
const supplierPaymentBatchFormat = z.enum(['pain001'])
export const PreviewSupplierPaymentBatchSchema = z.object({
format: supplierPaymentBatchFormat,
ids: z.array(z.string().uuid()).min(1).max(100),
})
export const SupplierPaymentBatchItemInputSchema = z.object({
supplier_invoice_id: z.string().uuid(),
// Defaults to the invoice's remaining amount.
amount: z.number().positive().optional(),
// Defaults to max(due_date, today); past dates are normalized to today.
payment_date: isoDate.optional(),
})
export const CreateSupplierPaymentBatchSchema = z.object({
format: supplierPaymentBatchFormat,
items: z.array(SupplierPaymentBatchItemInputSchema).min(1).max(100),
confirm_already_batched: z.boolean().optional(),
})
export const SupplierPaymentBatchListQuerySchema = z.object({
status: z.enum(['created', 'cancelled', 'all']).default('all'),
limit: z.coerce.number().int().min(1).max(100).default(50),
offset: z.coerce.number().int().min(0).default(0),
})
// ============================================================
// Journal entry schemas
// ============================================================