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:
Jakob Wennberg
2026-07-09 21:27:03 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 11400b4dec
commit 2e7931b36b
18 changed files with 463 additions and 8 deletions
+5
View File
@@ -660,6 +660,7 @@
"no_contact_info": "No contact details",
"no_business_info": "No business details",
"no_invoices": "No invoices linked to this customer",
"label_customer_number": "Customer number:",
"label_org_number": "Org. no.:",
"label_vat": "VAT:",
"label_payment_terms": "Payment terms:",
@@ -695,6 +696,10 @@
"name_label": "Name *",
"name_placeholder": "Company or person name",
"name_required": "Name is required",
"customer_number_label": "Customer number",
"customer_number_placeholder": "E.g. 1001",
"customer_number_hint": "Shown on the invoice. Leave empty if you do not use customer numbers.",
"customer_number_too_long": "Customer number must be 32 characters or fewer",
"email_label": "Email",
"email_placeholder": "name@company.com",
"email_invalid": "Invalid email address",
+5
View File
@@ -660,6 +660,7 @@
"no_contact_info": "Inga kontaktuppgifter",
"no_business_info": "Inga företagsuppgifter",
"no_invoices": "Inga fakturor kopplade till denna kund",
"label_customer_number": "Kundnummer:",
"label_org_number": "Org.nr:",
"label_vat": "VAT:",
"label_payment_terms": "Betalningsvillkor:",
@@ -695,6 +696,10 @@
"name_label": "Namn *",
"name_placeholder": "Företagsnamn eller personnamn",
"name_required": "Namn krävs",
"customer_number_label": "Kundnummer",
"customer_number_placeholder": "T.ex. 1001",
"customer_number_hint": "Visas på fakturan. Lämna tomt om du inte använder kundnummer.",
"customer_number_too_long": "Kundnumret får vara högst 32 tecken",
"email_label": "E-post",
"email_placeholder": "namn@foretag.se",
"email_invalid": "Ogiltig e-postadress",