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:
co-authored by
Claude Opus 4.8
parent
27b88426e2
commit
cbffcd7292
@@ -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'],
|
||||
|
||||
@@ -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
@@ -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'),
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user