From 9d7decfc5b244a2a30963588ba2b66304b087846 Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Fri, 24 Apr 2026 16:02:00 +0200 Subject: [PATCH] feat(invoices): polymorphic billing entity with snapshot clientName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the billingEntityType/billingEntityId columns (added in PR 1) through the invoice validator and service. Clients can now be billed as either a client or a company; clientName becomes a snapshot derived from the entity at create time. - createInvoiceSchema: replace clientName with billingEntity {type,id} - listInvoicesSchema: add billingEntityType/billingEntityId filters - createInvoice: resolveBillingEntity helper (tenant-scoped; tx-aware) falls back to entity primary email/address when not supplied - listInvoices: honor new billing-entity filters - updateInvoice: unchanged — billing entity is fixed after create - invoice wizard step 1: temporary billing-entity id input (Task 10.2 replaces this with a proper picker) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../[portSlug]/invoices/new/page.tsx | 94 ++++---- src/lib/services/invoices.ts | 189 ++++++++++----- src/lib/validators/invoices.ts | 11 +- .../invoices-billing-entity.test.ts | 224 ++++++++++++++++++ tests/unit/validators.test.ts | 40 +++- 5 files changed, 442 insertions(+), 116 deletions(-) create mode 100644 tests/integration/invoices-billing-entity.test.ts diff --git a/src/app/(dashboard)/[portSlug]/invoices/new/page.tsx b/src/app/(dashboard)/[portSlug]/invoices/new/page.tsx index 605d3b1..3a9d9c0 100644 --- a/src/app/(dashboard)/[portSlug]/invoices/new/page.tsx +++ b/src/app/(dashboard)/[portSlug]/invoices/new/page.tsx @@ -55,7 +55,13 @@ export default function NewInvoicePage() { }, }); - const { register, handleSubmit, watch, setValue, formState: { errors } } = methods; + const { + register, + handleSubmit, + watch, + setValue, + formState: { errors }, + } = methods; const watchedValues = watch(); const lineItems = watchedValues.lineItems ?? []; @@ -87,7 +93,7 @@ export default function NewInvoicePage() { async function goNext() { if (step === 1) { const valid = await methods.trigger([ - 'clientName', + 'billingEntity', 'billingEmail', 'billingAddress', 'dueDate', @@ -112,11 +118,7 @@ export default function NewInvoicePage() {
{/* Header */}
-

New Invoice

@@ -131,22 +133,16 @@ export default function NewInvoicePage() { step > s.id ? 'bg-primary text-primary-foreground' : step === s.id - ? 'bg-primary text-primary-foreground' - : 'bg-muted text-muted-foreground' + ? 'bg-primary text-primary-foreground' + : 'bg-muted text-muted-foreground' }`} > {step > s.id ? : s.id}
- + {s.label} - {idx < STEPS.length - 1 && ( -
- )} + {idx < STEPS.length - 1 &&
}
))}
@@ -161,17 +157,36 @@ export default function NewInvoicePage() {
-
@@ -202,11 +217,7 @@ export default function NewInvoicePage() { - + {errors.dueDate && (

{errors.dueDate.message}

)} @@ -216,7 +227,9 @@ export default function NewInvoicePage() {