# Scratchpad — Contract Purchase Order Support

## Context / Decisions (2026-01-05)

- One PO per invoice (single-assignment invoice scope; does not require a single active contract per client).
- Invoice stores PO number as a snapshot at creation time.
- PO number appears in:
  - invoice metadata in-app
  - default invoice PDF header (when present)
  - accounting export/sync reference surfaces (QBO + Xero)
- PO amount (`po_amount`) is advisory “authorized total spend”.
  - Warn, do not block.
  - Consume only for finalized invoices; unconsume when no longer finalized.
- Overages: warn and allow override.
- Batch invoicing: if any invoices in the batch could exceed a PO limit, prompt upfront with allow vs skip; don’t prompt if there’s no possibility of overage.
- Scope includes everything billed on the invoice (contract + non-contract charges); PO context is invoice-level.

## Relevant Code Pointers

- Contract assignment PO fields: `client_contracts.po_required`, `client_contracts.po_number`, `client_contracts.po_amount`
- Billing engine attaches `client_contract_id` to charges: `server/src/lib/billing/billingEngine.ts`
- Invoice generation entry point: `server/src/lib/actions/invoiceGeneration.ts`
- Invoice persistence: `server/src/lib/services/invoiceService.ts`
- Accounting exports:
  - QBO API: `server/src/lib/adapters/accounting/quickBooksOnlineAdapter.ts`
  - QBO CSV: `server/src/lib/adapters/accounting/quickBooksCSVAdapter.ts`
  - Xero API: `server/src/lib/adapters/accounting/xeroAdapter.ts`
  - Xero CSV: `server/src/lib/adapters/accounting/xeroCsvAdapter.ts`

## Known Gaps / Notes

- Current PO-required validation in `generateInvoice` references legacy `client_plan_bundles`; needs alignment to `client_contracts`.
- Invoice table does not currently have an invoice-level PO field; requires migration.
- Xero adapter currently uses `reference` to carry invoice number; we likely need a combined format (invoice + PO) without breaking matching.
- Finalization mapping in UI/actions:
  - `finalizeInvoiceWithKnex` sets `invoices.status = 'sent'` and sets `finalized_at`.
  - `unfinalizeInvoice` clears `finalized_at` and forces `status = 'draft'` when status is not already draft.
  - FinalizedTab treats invoices as “finalized” when `finalized_at` is set OR `status !== 'draft'`.
