Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
126 lines
6.6 KiB
Markdown
126 lines
6.6 KiB
Markdown
# PRD — Contract Purchase Order Support (Invoice + Limit Advisory)
|
||
|
||
- Slug: `contract-purchase-order-support`
|
||
- Date: `2026-01-05`
|
||
- Status: Draft
|
||
|
||
## Summary
|
||
|
||
Add purchase order (PO) support to contract billing in two ways:
|
||
|
||
1) When an invoice is created from a PO-enabled client contract assignment, the invoice stores a **snapshot** of the PO number and surfaces it in the UI + default PDF header.
|
||
2) When a PO has an authorized spend amount, the system computes **advisory** overage warnings (do not block) and provides batch-invoicing upfront handling (allow vs skip) only when an overage is possible.
|
||
|
||
## Problem
|
||
|
||
MSPs frequently must include customer PO numbers on invoices to avoid AP rejections. Today, Alga stores PO context on the client contract assignment (`client_contracts`) but invoices do not reliably surface or export it, and PO authorized-spend limits are not enforced/advised during billing.
|
||
|
||
## Goals
|
||
|
||
- Invoice records created from contract billing include an invoice-level `po_number` snapshot (immutably tied to that invoice).
|
||
- PO number appears on:
|
||
- Invoice metadata in-app
|
||
- Default invoice PDF header (when present)
|
||
- Accounting export/sync “reference” surfaces (QBO + Xero + CSV adapters where applicable)
|
||
- PO authorized spend (`po_amount`) is treated as an **advisory total spend cap**:
|
||
- Warn (do not block) when a newly generated invoice would exceed remaining PO amount.
|
||
- “Consume” PO amount only for **finalized** invoices; “unconsume” automatically when invoice is unfinalized (and if an invoice is cancelled/voided).
|
||
- Batch invoicing prompts **upfront** for overage handling when (and only when) at least one invoice *could* overrun a PO limit:
|
||
- Allow overages
|
||
- Skip invoices that would overrun
|
||
|
||
## Non-goals
|
||
|
||
- Supporting more than one PO per invoice.
|
||
- Full PO lifecycle management (creation, multi-PO allocation, splitting charges across POs).
|
||
- Blocking invoice generation due to PO limit overage.
|
||
- Retroactively changing historical invoices when contract PO values are edited later.
|
||
|
||
## Users and Primary Flows
|
||
|
||
- Billing admin / finance ops
|
||
1) Configure contract assignment PO fields (`po_required`, `po_number`, optional `po_amount`).
|
||
2) Generate invoice(s) (single or batch).
|
||
3) Review invoice details and PDF; PO is visible.
|
||
4) Export invoices to QBO/Xero; PO reference is available externally.
|
||
|
||
## UX / UI Notes
|
||
|
||
- Invoice detail view:
|
||
- Display PO number when present.
|
||
- Display PO “authorized” amount (from contract assignment) and “consumed/remaining” summary when `po_amount` is present.
|
||
- If invoice would exceed remaining PO amount, show a warning banner with overage amount; provide an explicit “Proceed anyway” confirmation.
|
||
- Batch invoicing:
|
||
- If none of the invoices being generated can overrun a PO limit, proceed without any extra prompts.
|
||
- If one or more invoices could overrun, present a single prompt up front:
|
||
- Allow overages (generate all)
|
||
- Skip overages (generate only invoices that fit)
|
||
- Summarize results (generated count, skipped count + reasons).
|
||
|
||
## Requirements
|
||
|
||
### Functional Requirements
|
||
|
||
- Invoice PO snapshot
|
||
- Add `invoices.po_number` populated at invoice creation time from the associated `client_contracts.po_number`.
|
||
- Add `invoices.client_contract_id` populated for invoices created from contract billing (single-assignment invoice scope; independent of whether the client has other active assignments).
|
||
- Invoices created without contract context (pure manual, etc.) may leave these fields null.
|
||
- PO-required behavior
|
||
- If `client_contracts.po_required = true`, invoice generation remains blocked when `po_number` is missing.
|
||
- PO limit advisory (authorized total spend)
|
||
- Finalized mapping (based on current UI + invoice modification logic):
|
||
- Treat invoice as finalized when `finalized_at` is set OR `status` is one of `sent`, `paid`, `overdue`, `prepayment`, `partially_applied`.
|
||
- Do not count `draft`, `pending`, or `cancelled` toward consumption.
|
||
- If `client_contracts.po_amount` is set, compute:
|
||
- `consumed`: sum of `invoices.total_amount` for invoices tied to the same `client_contract_id` where invoice is finalized per the mapping above.
|
||
- `remaining`: `po_amount - consumed`.
|
||
- `overage`: `max(0, invoice.total_amount - remaining)`.
|
||
- Warn (do not block) if `overage > 0`.
|
||
- PDF header
|
||
- Default invoice template includes PO number in the header when present.
|
||
- Accounting exports
|
||
- Include PO number on exported invoices in the external system’s “reference/memo” surface(s):
|
||
- QBO API export: include in `PrivateNote` (or other supported memo field).
|
||
- QBO CSV export: include in `Memo`.
|
||
- Xero API export: include in `reference` while preserving the invoice identifier (format TBD).
|
||
- Xero CSV export: include in `Reference` while preserving existing matching identifiers (format TBD).
|
||
|
||
### Non-functional Requirements
|
||
|
||
- No new external dependencies.
|
||
- Overage checks should be query-efficient (indexes on new invoice columns; avoid N+1 in batch mode).
|
||
|
||
## Data / API / Integrations
|
||
|
||
- DB migration(s)
|
||
- Add `invoices.po_number` (text, nullable).
|
||
- Add `invoices.client_contract_id` (uuid, nullable; FK optional depending on schema conventions).
|
||
- Add indexes to support lookups by `client_contract_id` and status.
|
||
- Invoice view models / API responses should include the new invoice-level PO fields for UI + exports.
|
||
- Accounting export adapters should load invoice `po_number` and include it in outbound payloads.
|
||
|
||
## Security / Permissions
|
||
|
||
- No new permissions beyond existing invoice read/generate/export permissions.
|
||
- Ensure client portal visibility rules remain unchanged (only show PO number where the invoice is visible).
|
||
|
||
## Observability
|
||
|
||
- Not in scope beyond existing logs/errors.
|
||
|
||
## Rollout / Migration
|
||
|
||
- Ship a migration adding invoice columns and backfill as “best effort” if feasible (optional).
|
||
- Ensure invoice generation still works when PO columns are absent (if runtime schema checks are used elsewhere).
|
||
|
||
## Open Questions
|
||
|
||
- Formatting rules for external-system references (length limits, delimiter choice, and preserving matching keys).
|
||
|
||
## Acceptance Criteria (Definition of Done)
|
||
|
||
- Creating an invoice from a PO-enabled contract assignment stores `invoices.po_number` and shows it in invoice UI + default PDF header.
|
||
- When `po_amount` is set, invoice preview/generation shows an overage warning when applicable and allows an explicit override.
|
||
- Batch invoicing prompts for overage handling only when an overage is possible, and can skip overage invoices when requested.
|
||
- QBO + Xero exports include PO reference content in their respective memo/reference field(s) without breaking existing matching/mapping.
|