/** * TypeScript types for the Vela invoicing schema. * Keep these in sync with `invoicing-schema.sql`. */ // ── Enums ────────────────────────────────────────────────────── export type InvoiceStatus = "draft" | "sent" | "paid" | "overdue" | "cancelled"; export const INVOICE_STATUS: Record = { draft: "draft", sent: "sent", paid: "paid", overdue: "overdue", cancelled: "cancelled", }; // ── Invoice ──────────────────────────────────────────────────── export interface Invoice { /** UUID — primary key */ id: string; /** UUID — FK to organisations table */ organisation_id: string; /** Auto-generated per org, e.g. "INV-0001" */ invoice_number: string; /** Current lifecycle state */ status: InvoiceStatus; /** ISO-4217 currency code, default "AUD" */ currency: string; // ── Client (inline, no separate table for MVP) ── client_name: string; client_email: string | null; client_address: string | null; // ── Dates ── /** Defaults to CURRENT_DATE UTC on insert */ issue_date: string; // ISO date string: "2026-04-06" due_date: string; // ISO date string: "2026-04-20" /** Legacy amount column in dollars (numeric). Kept for back-compat; new code should use total_cents */ amount: number | null; // ── Amounts stored in cents (integer) to avoid floating-point issues ── subtotal_cents: number; tax_total_cents: number; total_cents: number; // ── Optional ── notes: string | null; // ── Metadata ── created_at: string; // ISO-8601 updated_at: string; // ISO-8601 /** Clerk user_id of the creator (nullable for legacy rows) */ created_by: string | null; } // ── Invoice Line Item ────────────────────────────────────────── export interface InvoiceLineItem { /** UUID — primary key */ id: string; /** UUID — FK to invoices(id), CASCADE on delete */ invoice_id: string; description: string; /** Decimal (12,2) — stored as string from Postgres; parse as needed */ quantity: string; /** Stored in cents */ unit_price_cents: number; /** Percentage, 0–100, with 2 decimal places (e.g. "10.00") */ tax_rate: string; /** Computed & stored: quantity * unit_price_cents */ line_total_cents: number; /** Display / sort order within the invoice */ sort_order: number; created_at: string; // ISO-8601 } // ── Derived / composite types ────────────────────────────────── /** Invoice with its line items eagerly joined (for detail views) */ export interface InvoiceWithLineItems extends Invoice { line_items: InvoiceLineItem[]; } /** Shape for creating a new invoice (client-side) */ export type CreateInvoiceInput = Omit< Invoice, "id" | "invoice_number" | "subtotal_cents" | "tax_total_cents" | "total_cents" | "created_at" | "updated_at" >; /** Shape for creating a new line item (client-side) */ export type CreateInvoiceLineItemInput = Omit< InvoiceLineItem, "id" | "line_total_cents" | "created_at" >; /** Shape for updating an existing invoice (partial) */ export type UpdateInvoiceInput = Partial; /** Shape for updating an existing line item (partial) */ export type UpdateInvoiceLineItemInput = Partial; // ── Helpers ──────────────────────────────────────────────────── /** Convert cents to a decimal string (e.g. 12500 → "125.00") */ export function centsToDecimal(cents: number): string { return (cents / 100).toFixed(2); } /** Convert a decimal amount to cents (e.g. "125.00" → 12500) */ export function decimalToCents(amount: string | number): number { const num = typeof amount === "string" ? parseFloat(amount) : amount; return Math.round(num * 100); } /** Compute line total in cents */ export function computeLineTotalCents(quantity: string | number, unitPriceCents: number): number { const qty = typeof quantity === "string" ? parseFloat(quantity) : quantity; return Math.round(qty * unitPriceCents); } /** Compute invoice subtotal and tax from line items */ export function computeInvoiceTotals(lineItems: { line_total_cents: number; tax_rate: string }[]) { let subtotal_cents = 0; let tax_total_cents = 0; for (const item of lineItems) { subtotal_cents += item.line_total_cents; const rate = parseFloat(item.tax_rate); tax_total_cents += Math.round((item.line_total_cents * rate) / 100); } return { subtotal_cents, tax_total_cents, total_cents: subtotal_cents + tax_total_cents, }; } /** Human-readable invoice status labels */ export const INVOICE_STATUS_LABELS: Record = { draft: "Draft", sent: "Sent", paid: "Paid", overdue: "Overdue", cancelled: "Cancelled", };