fix(invoices): accept empty self-billing fields on invoice create (#920)

#911 added self-billing fields to the shared CreateInvoiceSchema with
external_invoice_number: z.string().min(1), but the invoice form has
always sent that field (plus self_billing_agreement_ref and
received_date) as '' on every normal invoice. The empty string failed
min(1), so every invoice create returned 400.

Normalise the empty optional self-billing strings to undefined in the
schema (matching the existing optionalIsoDate / deduction_brf_org_number
patterns), and strip the unused empty carriers client-side before the
form POSTs. Required-when-self-billed is still enforced post-parse in the
v1 route, so the self-billed path is unaffected. Adds schema regression
tests.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-07-07 13:20:23 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 27b88426e2
commit cbffcd7292
3 changed files with 67 additions and 6 deletions
+20 -3
View File
@@ -806,6 +806,23 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
)
}
// The form always carries the self-billing fields (they default to '' in both
// create and edit mode). This editor's normal create/draft/edit flows never
// use self-billing, that goes through the dedicated /api/invoices/self-billed
// path, so drop these empty carriers before spreading the form data into the
// /api/invoices (or PATCH) body: a bare external_invoice_number: '' otherwise
// trips the shared CreateInvoiceSchema's min(1). Belt-and-suspenders; the
// server schema also coerces '' to undefined for these fields.
function stripSelfBillingFields(data: FormData): FormData {
const {
external_invoice_number: _ein,
self_billing_agreement_ref: _sbar,
received_date: _rd,
...rest
} = data
return rest
}
// Self-billing path: no review dialog, no PDF, no send: it arrives already
// booked. POST straight to the dedicated endpoint and open the verifikat.
async function handleSelfBilledSubmit(data: FormData) {
@@ -943,7 +960,7 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
return rest
})
const sanitizedPayload: CreateInvoiceInput & { default_dimensions: Record<string, string> } = {
...(pendingData as CreateInvoiceInput),
...(stripSelfBillingFields(pendingData) as CreateInvoiceInput),
ore_rounding: oreRounding,
// Invoice-level default dims: always sent so an edited draft can clear
// them; {} means "no defaults".
@@ -1020,7 +1037,7 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
return rest
})
const payload: CreateInvoiceInput & { default_dimensions: Record<string, string> } = {
...(data as CreateInvoiceInput),
...(stripSelfBillingFields(data) as CreateInvoiceInput),
save_as_draft: true,
ore_rounding: oreRounding,
default_dimensions: defaultDims,
@@ -1079,7 +1096,7 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
return rest
})
const payload: CreateInvoiceInput & { default_dimensions: Record<string, string> } = {
...(data as CreateInvoiceInput),
...(stripSelfBillingFields(data) as CreateInvoiceInput),
ore_rounding: oreRounding,
default_dimensions: defaultDims,
items: sanitizedItems as CreateInvoiceInput['items'],