import { z } from 'zod/v4'; /** Supported currencies. */ export const CurrencySchema = z.enum([ 'USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY', 'CHF', 'TRY', 'BRL', 'INR', ]); export type Currency = z.infer; /** Invoice lifecycle states. */ export const InvoiceStatusSchema = z.enum([ 'draft', 'sent', 'viewed', 'paid', 'overdue', 'cancelled', 'refunded', ]); export type InvoiceStatus = z.infer; /** Accepted payment methods. */ export const PaymentMethodSchema = z.enum([ 'stripe', 'paypal', 'bank_transfer', 'cash', 'check', 'crypto', 'other', ]); export type PaymentMethod = z.infer; /** A single line item on an invoice. */ export const LineItemSchema = z.object({ description: z.string(), quantity: z.number().min(0.01), unit_price: z.number().min(0), tax_rate: z.number().min(0).max(100).default(0), discount_percent: z.number().min(0).max(100).default(0), amount: z.number(), }); export type LineItem = z.infer; /** Summary of a client's payment activity. */ export const PaymentHistorySchema = z.object({ total_invoices: z.number().default(0), paid_invoices: z.number().default(0), avg_days_to_payment: z.number().nullable().default(null), late_payment_count: z.number().default(0), total_revenue: z.number().default(0), }); export type PaymentHistory = z.infer; /** Client information. */ export const ClientSchema = z.object({ id: z.string().uuid(), name: z.string(), email: z.string().email(), company: z.string().optional(), address: z.string().optional(), city: z.string().optional(), country: z.string().optional(), tax_id: z.string().optional(), phone: z.string().optional(), default_currency: CurrencySchema.optional(), notes: z.string().optional(), payment_history: PaymentHistorySchema.optional(), created_at: z.string().datetime(), updated_at: z.string().datetime(), }); export type Client = z.infer; /** * Flexible date input: accepts either a date-only string (YYYY-MM-DD) * or a full ISO-8601 datetime. Normalises to a full datetime string so * downstream code can rely on the strict `.datetime()` format everywhere * else. This is deliberately permissive because humans (and LLMs) * typically think of invoice dates as "2026-04-15", not * "2026-04-15T00:00:00.000Z". */ export const FlexibleDateSchema = z .string() .refine( (val) => { if (/^\d{4}-\d{2}-\d{2}$/.test(val)) return true; if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(val)) { return !Number.isNaN(new Date(val).getTime()); } return false; }, 'Expected YYYY-MM-DD date or full ISO-8601 datetime', ) .transform((val) => { if (/^\d{4}-\d{2}-\d{2}$/.test(val)) { return `${val}T00:00:00.000Z`; } return new Date(val).toISOString(); }); /** Full invoice representation. */ export const InvoiceSchema = z.object({ id: z.string().uuid(), invoice_number: z.string(), client_id: z.string().uuid(), client_name: z.string(), client_email: z.string().email(), status: InvoiceStatusSchema.default('draft'), currency: CurrencySchema.default('USD'), line_items: z.array(LineItemSchema).min(1), subtotal: z.number(), tax_total: z.number(), discount_total: z.number(), total: z.number(), amount_paid: z.number().default(0), amount_due: z.number(), issue_date: z.string().datetime(), due_date: z.string().datetime(), paid_date: z.string().datetime().nullable(), notes: z.string().optional(), terms: z.string().optional(), payment_method: PaymentMethodSchema.nullable().default(null), risk_score: z.number().min(0).max(100).nullable().default(null), risk_action: z.string().nullable().default(null), reminder_count: z.number().default(0), last_reminder_at: z.string().datetime().nullable(), created_at: z.string().datetime(), updated_at: z.string().datetime(), }); export type Invoice = z.infer; /** Input for creating a new invoice. */ export const InvoiceCreateInputSchema = z.object({ client_id: z.string().uuid().describe('UUID of the client this invoice is billed to. Must exist (created via client_manage). The stored client_email and client_name are snapshotted into the invoice at create time.'), currency: CurrencySchema.optional().describe('ISO 4217 code for the invoice. One of: USD, EUR, GBP, CAD, AUD, JPY, CHF, TRY, BRL, INR. Defaults to the client\'s default_currency, then USD.'), line_items: z.array( z.object({ description: z.string().describe('Free-text description of the product or service. Appears verbatim on the PDF.'), quantity: z.number().min(0.01).describe('Number of units billed. Must be > 0. Decimals allowed for hourly billing (e.g. 1.5 hours).'), unit_price: z.number().min(0).describe('Price per unit before tax and discount, in the invoice currency. Must be ≥ 0.'), tax_rate: z.number().min(0).max(100).optional().describe('Per-line tax percentage (0–100). E.g. 21 for Spanish IVA, 8.875 for NYC sales tax. Defaults to 0.'), discount_percent: z.number().min(0).max(100).optional().describe('Per-line discount percentage (0–100). Applied before tax. Defaults to 0.'), }) ).min(1).describe('Non-empty array of line items. The invoice subtotal, tax_total, discount_total, and total are auto-computed by summing each item\'s (quantity × unit_price), then per-line discount, then per-line tax.'), issue_date: FlexibleDateSchema.optional().describe('Invoice issue date — accepts either YYYY-MM-DD or full ISO-8601. Defaults to today (UTC).'), due_date: FlexibleDateSchema.optional().describe('Payment due date — same format as issue_date. Defaults to issue_date + 30 days. Used by invoice_risk and overdue detection.'), notes: z.string().optional().describe('Free-text notes shown on the PDF (e.g. payment instructions, thank-you).'), terms: z.string().optional().describe('Free-text terms section on the PDF (e.g. "Net 30. Late fee 1.5%/mo.").'), }); export type InvoiceCreateInput = z.infer; /** Input for listing/filtering invoices. */ export const InvoiceListInputSchema = z.object({ status: InvoiceStatusSchema.optional().describe('Restrict to one lifecycle state: draft, sent, viewed, paid, overdue, cancelled, refunded.'), client_id: z.string().uuid().optional().describe('Restrict to invoices billed to this client UUID.'), min_amount: z.number().optional().describe('Inclusive lower bound on total amount (any currency, no FX conversion).'), max_amount: z.number().optional().describe('Inclusive upper bound on total amount.'), from_date: z.string().datetime().optional().describe('Filter to invoices with issue_date ≥ this ISO-8601 datetime.'), to_date: z.string().datetime().optional().describe('Filter to invoices with issue_date ≤ this ISO-8601 datetime.'), overdue_only: z.boolean().default(false).describe('When true, returns only invoices whose due_date is past and status is not paid/cancelled/refunded. Defaults to false.'), limit: z.number().int().min(1).max(100).default(20).describe('Page size, 1–100. Defaults to 20.'), offset: z.number().int().min(0).default(0).describe('Number of results to skip for pagination. Defaults to 0.'), }); export type InvoiceListInput = z.infer; /** Input for creating a new client. */ export const ClientCreateInputSchema = z.object({ name: z.string().min(1).describe('Display name shown on invoices and reports. Required.'), email: z.string().email().describe('Primary billing email. Used as the dedup key — calling client_manage twice with the same email returns the existing record. Also used by payment_reconcile to match incoming payments.'), company: z.string().optional().describe('Optional legal company name (printed on the invoice when set).'), address: z.string().optional().describe('Optional billing street address.'), city: z.string().optional().describe('Optional city.'), country: z.string().optional().describe('Optional country (full name or ISO code — printed verbatim).'), tax_id: z.string().optional().describe('Optional VAT / tax ID (e.g. EU VAT number, US EIN). Printed on PDFs that need it.'), phone: z.string().optional().describe('Optional phone number.'), default_currency: CurrencySchema.optional().describe('Default invoice currency for this client. Used when invoice_create omits currency.'), notes: z.string().optional().describe('Internal notes about this client (not printed on invoices).'), }); export type ClientCreateInput = z.infer; /** Risk level classification. */ export const RiskLevelSchema = z.enum(['low', 'medium', 'high']); export type RiskLevel = z.infer; /** Individual risk factor detail. */ export const RiskFactorSchema = z.object({ factor: z.string(), impact: z.number(), detail: z.string(), }); export type RiskFactor = z.infer; /** Risk assessment for an invoice. */ export const RiskAssessmentSchema = z.object({ invoice_id: z.string().uuid(), risk_score: z.number().min(0).max(100), risk_level: RiskLevelSchema, factors: z.array(RiskFactorSchema), recommended_action: z.string(), next_reminder_date: z.string().datetime().nullable(), }); export type RiskAssessment = z.infer; /** Cash flow summary for a given period. */ export const CashflowReportSchema = z.object({ period: z.string(), total_invoiced: z.number(), total_collected: z.number(), total_outstanding: z.number(), total_overdue: z.number(), collection_rate: z.number(), avg_days_to_payment: z.number().nullable(), projected_income_30d: z.number(), by_status: z.record(z.string(), z.object({ count: z.number(), total: z.number() })), by_client: z.array(z.object({ client_name: z.string(), outstanding: z.number(), overdue: z.number(), })), }); export type CashflowReport = z.infer;