import { z } from 'zod'; import { baseListQuerySchema } from '@/lib/api/route-helpers'; export const INVOICE_KINDS = ['general', 'deposit'] as const; export type InvoiceKind = (typeof INVOICE_KINDS)[number]; export const createInvoiceSchema = z .object({ 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), paymentTerms: z .enum(['immediate', 'net10', 'net15', 'net30', 'net45', 'net60']) .default('net30'), currency: z.string().length(3).default('USD'), notes: z.string().max(2000).optional(), /** Optional link to a sales interest. Required when kind === 'deposit'. */ interestId: z.string().min(1).optional(), kind: z.enum(INVOICE_KINDS).default('general'), lineItems: z .array( z.object({ description: z.string().min(1), quantity: z.coerce.number().positive().default(1), unitPrice: z.coerce.number().min(0), }), ) .optional(), expenseIds: z.array(z.string()).optional(), }) .refine((data) => data.kind !== 'deposit' || !!data.interestId, { message: 'Deposit invoices must be linked to an interest', path: ['interestId'], }) .refine( (data) => (data.lineItems && data.lineItems.length > 0) || (data.expenseIds && data.expenseIds.length > 0), { message: 'Invoice must have at least one line item or linked expense' }, ); export const updateInvoiceSchema = z.object({ clientName: z.string().min(1).max(200).optional(), 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(), currency: z.string().length(3).optional(), notes: z.string().max(2000).optional(), interestId: z.string().min(1).nullable().optional(), kind: z.enum(INVOICE_KINDS).optional(), lineItems: z .array( z.object({ description: z.string().min(1), quantity: z.coerce.number().positive().default(1), unitPrice: z.coerce.number().min(0), }), ) .optional(), expenseIds: z.array(z.string()).optional(), }); export const recordPaymentSchema = z.object({ paymentDate: z.string().min(1), paymentMethod: z.string().optional(), paymentReference: z.string().optional(), }); export const listInvoicesSchema = baseListQuerySchema.extend({ status: z.string().optional(), clientName: z.string().optional(), dateFrom: z.string().optional(), dateTo: z.string().optional(), billingEntityType: z.enum(['client', 'company']).optional(), billingEntityId: z.string().optional(), }); // `z.input` keeps fields with `.default()` (paymentTerms, currency, kind) // optional from the caller's perspective. The schema parser still fills in // the defaults, so the service body can rely on them being present at runtime. export type CreateInvoiceInput = z.input; export type UpdateInvoiceInput = z.input; export type RecordPaymentInput = z.infer; export type ListInvoicesInput = z.infer;