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
+31
View File
@@ -384,6 +384,37 @@ describe('CreateInvoiceSchema', () => {
}))
expect(result.success).toBe(true)
})
// Regression: the dashboard invoice form always sends the self-billing fields
// (default '' for a normal invoice). Empty strings must read as "not
// provided", not fail min(1)/isoDate, or every regular invoice create 400s.
it('treats empty self-billing strings as omitted (not a validation error)', () => {
const result = CreateInvoiceSchema.safeParse(validInvoice({
external_invoice_number: '',
self_billing_agreement_ref: '',
received_date: '',
}))
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.external_invoice_number).toBeUndefined()
expect(result.data.self_billing_agreement_ref).toBeUndefined()
expect(result.data.received_date).toBeUndefined()
}
})
it('still accepts real self-billing values', () => {
const result = CreateInvoiceSchema.safeParse(validInvoice({
is_self_billed: true,
external_invoice_number: 'CUST-2026-014',
self_billing_agreement_ref: 'AVTAL-7',
received_date: '2026-07-07',
}))
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.external_invoice_number).toBe('CUST-2026-014')
expect(result.data.received_date).toBe('2026-07-07')
}
})
})
describe('UpdateInvoiceSchema', () => {
+16 -3
View File
@@ -401,9 +401,22 @@ export const CreateInvoiceSchema = z.object({
// invoice. A plain optional flag (no schema refine) so UpdateInvoiceSchema's
// .omit() keeps working on this object.
is_self_billed: z.boolean().optional(),
external_invoice_number: z.string().min(1).max(64).optional(),
self_billing_agreement_ref: z.string().max(128).optional(),
received_date: isoDate.optional(),
// The dashboard invoice form always sends these self-billing fields (default
// '' in create/edit mode) even for a normal invoice, so an empty string must
// read as "not provided", not fail validation. Otherwise a plain
// external_invoice_number: '' trips the min(1) and 400s every regular invoice
// create. Required-when-self-billed is still enforced in the v1 route via a
// falsy check after parse, so normalising '' -> undefined here is safe.
external_invoice_number: z
.union([z.string().min(1).max(64), z.literal('')])
.transform((v) => v || undefined)
.optional(),
self_billing_agreement_ref: z
.string()
.max(128)
.transform((v) => v || undefined)
.optional(),
received_date: optionalIsoDate,
items: z.array(CreateInvoiceItemSchema).min(1, 'At least one item is required'),
})