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

7.3 KiB
Raw Blame History

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 CreditMemos 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.