feat(invoices): polymorphic billing entity with snapshot clientName

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) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-24 16:02:00 +02:00
parent c685c9fada
commit 9d7decfc5b
5 changed files with 442 additions and 116 deletions

View File

@@ -3,7 +3,10 @@ import { baseListQuerySchema } from '@/lib/api/route-helpers';
export const createInvoiceSchema = z
.object({
clientName: z.string().min(1).max(200),
billingEntity: z.object({
type: z.enum(['client', 'company']),
id: z.string().min(1),
}),
billingEmail: z.string().email().optional(),
billingAddress: z.string().max(500).optional(),
dueDate: z.string().min(1),
@@ -35,9 +38,7 @@ export const updateInvoiceSchema = z.object({
billingEmail: z.string().email().optional(),
billingAddress: z.string().max(500).optional(),
dueDate: z.string().min(1).optional(),
paymentTerms: z
.enum(['immediate', 'net10', 'net15', 'net30', 'net45', 'net60'])
.optional(),
paymentTerms: z.enum(['immediate', 'net10', 'net15', 'net30', 'net45', 'net60']).optional(),
currency: z.string().length(3).optional(),
notes: z.string().max(2000).optional(),
lineItems: z
@@ -63,6 +64,8 @@ export const listInvoicesSchema = baseListQuerySchema.extend({
clientName: z.string().optional(),
dateFrom: z.string().optional(),
dateTo: z.string().optional(),
billingEntityType: z.enum(['client', 'company']).optional(),
billingEntityId: z.string().optional(),
});
export type CreateInvoiceInput = z.infer<typeof createInvoiceSchema>;