Hermes 284313f908
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
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

149 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PRD: QBO Closed-Loop Sync — Slice 2: Credits & Voids
- **Status:** Draft
- **Owner:** Robert Isaacs
- **Created:** 2026-06-11
- **Design:** `../2026-06-11-qbo-phase2-closed-loop/design.md`
- **Depends on:** Slice 1 (sync engine, ops queue, exceptions framework, `computeBalanceDue` helper)
## 1. Problem statement & user value
MSPs issue credits constantly — SLA penalties, billing disputes, true-downs —
and QBO rejects negative-total invoices, so today credits are the records most
likely to force manual double-entry. Worse, Alga's credit application mutates
the posted invoice's `total_amount` (`creditActions.ts:910`), a semantic no
accounting system can sync against. Separately, Alga can only hard-delete
invoices; once documents live in QBO, deletion without a void leaves the books
disagreeing and destroys the cross-system audit trail.
This slice fixes the credit semantics while the credit system is barely used,
then ships credit memo export with application linkage and real void support —
the remaining document flows a bookkeeper expects from an accounting
integration.
## 2. Goals
- Posted invoice totals are immutable; credit application moves the derived
balance due. Historical data backfilled losslessly.
- Credit notes are first-class: explicit `invoice_type`, their own `CM-`
number sequence, exported to QBO as `CreditMemo`s through the existing
pipeline.
- Applying credit to an exported invoice reconciles in QBO (zero-dollar
`Payment` linking CreditMemo → Invoice), so both systems agree on balances.
- A `voidInvoice` action voids (not deletes) finalized invoices and credit
notes, propagating the void to QBO; hard delete is blocked for exported
documents.
## 3. Non-goals
- Importing QBO-originated CreditMemos (exceptions only, per design).
- Prepayment export of any kind (prepayments are excluded from CreditMemo
export with a validation message; mapping them to QBO unapplied payments is
future work, not scheduled in any slice).
- Un-applying credit / application reversal sync (Alga-side unapply, if used,
surfaces as an exception rather than auto-reconciling in QBO).
- Refund receipts / cash refunds.
- Changes to the credit pool ledger internals (`credit_tracking` FIFO,
expirations, reconciliation job).
## 4. Personas & primary flows
- **MSP billing admin:** issues a credit note (negative invoice) for a service
dispute; on finalize it appears in QBO as a CreditMemo within a cycle.
Applies it to an open invoice in Alga; the QBO invoice balance drops to
match. Voids a mis-issued invoice with a reason; QBO shows it voided.
- **MSP bookkeeper (in QBO):** sees credits and voids arrive as proper
documents — CreditMemos and voided invoices — not mystery edits.
- **Client (portal):** sees balance due (after credits), and pays exactly
that via the Stripe link.
## 5. Functional scope
### 5.1 Credit reshape (lands first, own PR)
- `applyCreditToInvoice` stops decrementing `invoices.total_amount`; it only
increments `credit_applied` (and the existing transaction/allocation/pool
writes are unchanged).
- `computeBalanceDue` (introduced in slice 1) becomes the real derivation:
`total_amount credit_applied payments`. All amount-due read sites switch
to it. Known sites to audit (enumerate findings in SCRATCHPAD as the audit
runs): MSP invoice list/detail, client portal invoice list/detail/pay page,
Stripe payment-link amount (`getOrCreateInvoicePaymentLinkUrl`), overdue
detection, invoice PDF/email templates that render an amount due, accounting
export selectors that reason about settledness.
- Backfill migration: `total_amount += credit_applied` where
`credit_applied > 0` (down migration reverses). Lossless because the
historical mutation preserved both operands.
- `invoice_type` column: `standard | credit_note | prepayment`; backfilled
from `is_prepayment` and negative totals; `is_prepayment` retained but
derived. Finalizing a negative-total invoice sets `credit_note`.
- Credit notes draw document numbers from their own sequence with a `CM-`
prefix, configurable alongside the existing invoice numbering settings.
### 5.2 CreditMemo export
- Finalizing a credit note (auto-sync on) enqueues `export_credit_memo`;
manual batches may include credit notes through the same selector (the
preview already carries a `credit_memo` flag).
- Adapter transform sign-flips lines into a QBO `CreditMemo`, reusing
item/tax mappings and the tax-delegation behavior; mapping ledger rows use
`external_entity_type: 'CreditMemo'` with sync-token + total snapshot
(drift baseline).
- Prepayment invoices are excluded from export with a clear validation
message.
- Slice 1's drift detection extends to CreditMemos (CDC already polls them).
### 5.3 Credit application linkage
- `applyCreditToInvoice` enqueues `apply_credit` keyed to the
`credit_allocations` row when the tenant has a connected realm.
- The cycle processes `apply_credit` only when both the credit note and the
target invoice are mapped; otherwise the op stays pending with a reason
(it drains naturally once exports complete).
- Execution creates a zero-dollar QBO `Payment` whose lines link the
CreditMemo and Invoice with the applied amount; a mapping row keyed to the
allocation provides idempotency and echo suppression.
### 5.4 Voids
- `voidInvoice` action (EE-independent core billing change; reason required;
`billing_settings`-equivalent invoice permission): status → `cancelled`,
writes an `invoice_cancelled` transaction, auto-reverses credit
applications (pool/balance restoration + reversal transactions). Blocked
while payments exist. Applies to invoices and unapplied credit notes;
applied credit notes must be unwound first.
- Invoice detail UI: Void action with confirmation dialog + reason field.
- `hardDeleteInvoice` blocked when an external mapping exists; the error
directs to void.
- Outbound `void_invoice` op calls QBO's void operation; mapping status →
`voided`; badge (slice 1) renders it.
## 6. Data model & API notes
- Migrations: `invoices.invoice_type` (+ backfill), `total_amount` backfill,
credit-note number sequence settings.
- New ops on `accounting_sync_operations`: `export_credit_memo`,
`apply_credit`, `void_invoice` (enum values exist from slice 1's table).
- QBO API surface added to the adapter/client: CreditMemo create/update,
zero-dollar Payment create, Invoice/CreditMemo void operation.
## 7. Risks & open questions
- The read-site audit is the riskiest step; an unmigrated site shows inflated
amounts after backfill. Mitigation: the audit feature requires the
enumerated-site list in SCRATCHPAD before the backfill merges, and the
backfill + derivation ship in the same release.
- Stripe payment links start charging balance due — a behavior fix, but call
it out in release notes.
- Tax handling on CreditMemos mirrors invoices (internal vs delegated); the
sign-flip must keep tax lines consistent — covered by transform tests.
## 8. Acceptance criteria / definition of done
- All features in `features.json` implemented; automated tests green;
slice-1 suites and existing credit/Stripe suites unaffected.
- Live sandbox smoke: credit note → CreditMemo in QBO → apply in Alga → QBO
invoice balance drops to match → both systems agree on customer balance;
void an exported invoice → QBO shows voided; portal payment link charges
balance due after a partial credit.