feat(customers): let users set a customer number shown on the invoice (#957)
Implements #914 (kundnummer on customers, printed on the invoice PDF). - Migration: nullable text column customers.customer_number, no unique constraint in v1 so existing rows and imports keep working. - API: CreateCustomerSchema/UpdateCustomerSchema accept an optional customer_number (trimmed, max 32 chars, nullable-then-optional so the OpenAPI registry sees it as not required); create/update routes persist it and normalize empty string to null so it can be cleared. - v1 public API: customers create/detail/update round-trip the field (insert and update field lists, response projections, response schemas), and the invoices :send route fetches customer_number in its explicit customer join so the emailed PDF matches the downloaded one (the pdf route already selects customers(*)). - UI: optional Kundnummer field in CustomerForm (next-intl keys in both sv and en), wired into the edit dialog's initialData; read-only Kundnummer row on the customer detail page's business-details card. - Invoice PDF: renders "Kundnr:" / "Customer no.:" in the customer box when set; the PDF reads the live customers join, so no snapshot column is needed. - Tests: route tests cover 400 validation, trimming, clearing with null/empty, and omit-leaves-untouched on POST and PATCH; v1 tests cover the create/update round-trip (insert/update payload + response projection) and the :send customer-join projection. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
11400b4dec
commit
2e7931b36b
@@ -601,6 +601,14 @@ export const MarkInvoicePaidSchema = z.object({
|
||||
export const CreateCustomerSchema = z.object({
|
||||
name: z.string().min(1, 'Customer name is required'),
|
||||
customer_type: CustomerTypeSchema,
|
||||
// Kundnummer shown on invoices. Free text, not unique in v1. Empty string
|
||||
// and null both clear the value (routes normalize '' to null).
|
||||
customer_number: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(32, 'Customer number must be 32 characters or fewer')
|
||||
.nullable()
|
||||
.optional(),
|
||||
email: z.string().email('Invalid email address').optional(),
|
||||
phone: z.string().optional(),
|
||||
address_line1: z.string().optional(),
|
||||
|
||||
@@ -42,6 +42,7 @@ const LABELS = {
|
||||
yourReference: 'Er referens:',
|
||||
ourReference: 'Vår referens:',
|
||||
// Customer box
|
||||
custNo: 'Kundnr:',
|
||||
orgNo: 'Org.nr:',
|
||||
vat: 'VAT:',
|
||||
// Table columns
|
||||
@@ -111,6 +112,7 @@ const LABELS = {
|
||||
deliveryDate: 'Delivery date:',
|
||||
yourReference: 'Your reference:',
|
||||
ourReference: 'Our reference:',
|
||||
custNo: 'Customer no.:',
|
||||
orgNo: 'Reg. no.:',
|
||||
vat: 'VAT:',
|
||||
colDescription: 'Description',
|
||||
@@ -799,6 +801,12 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
|
||||
{customer.country && customer.country !== 'SE' && (
|
||||
<Text>{customer.country}</Text>
|
||||
)}
|
||||
{/* Seller-assigned kundnummer: no per-customer-type guard needed,
|
||||
it identifies the customer in the seller's own register and
|
||||
carries no personal data of its own. */}
|
||||
{customer.customer_number && (
|
||||
<Text style={{ marginTop: 6 }}>{L.custNo} {customer.customer_number}</Text>
|
||||
)}
|
||||
{/* Suppress the identifier row for private customers: their
|
||||
personnummer is not required on a B2C invoice (ML 17 kap 24§
|
||||
asks for name + address only) and printing it is a GDPR
|
||||
|
||||
Reference in New Issue
Block a user