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

6.0 KiB
Raw Blame History

PRD: QBO Closed-Loop Sync — Slice 4: Payments Out, Class Tracking, Multi-Realm

  • Status: Draft
  • Owner: Robert Isaacs
  • Created: 2026-06-11
  • Design: ../2026-06-11-qbo-phase2-closed-loop/design.md
  • Depends on: Slice 1 (engine, ops queue, payment mapping/echo suppression); slice 2's record_payment-adjacent adapter work is independent

1. Problem statement & user value

Three remaining asymmetries after slices 13. Client-portal Stripe payments land in Alga but not in QBO, so the bookkeeper re-keys them (or the books disagree) — the one direction of payment flow still manual. MSPs that segment their P&L by class or location in QBO get invoices with no ClassRef/ DepartmentRef, making the integration useless for their reporting. And tenants with more than one QBO company can connect both but can only operate the default from the UI.

2. Goals

  • Stripe/portal payments recorded in Alga appear in QBO as Payment objects against the right customer and invoice, deposited to a tenant-configured account, within one cycle.
  • Exported invoice lines carry ClassRef (per item mapping, falling back to a tenant default) and invoices carry DepartmentRef (tenant default).
  • Multi-realm tenants can see all connections, switch the default, and pick a realm in the batch dialog and wizard.

3. Non-goals

  • Intuit webhooks (excluded by decision 2026-06-11; plan separately if polling latency ever becomes a real complaint).
  • Manual payment entry in Alga (no such action exists; when one is added it should ride the same record_payment op).
  • Per-client or per-invoice class/department overrides beyond the item mapping metadata (tenant default + per-item is the v1 granularity).
  • Prepayment → QBO unapplied-payment mapping (still unscheduled).
  • Per-realm divergent mappings UI beyond what slice 1 already scopes by realm.

4. Personas & primary flows

  • Client (portal): pays an invoice by card; minutes later the bookkeeper sees a QBO Payment with the Stripe reference in Undeposited Funds (or the configured account) — no re-keying, no unexplained paid invoice.
  • MSP billing admin (class tracking): sets a default class "Managed Services" and overrides per item where needed; QBO P&L by class works.
  • MSP billing admin (two companies): connects both realms, makes the right one default, and routes the occasional batch to the second company from the batch dialog.

5. Functional scope

5.1 Alga-originated payments to QBO

  • Producer: the Stripe success path in recordExternalPayment (provider stripe) enqueues record_payment when the tenant has a connected realm and the invoice is mapped; unmapped invoice → op is skipped with a stat (not an exception — the invoice may predate go-live).
  • Cycle execution: create a QBO Payment (CustomerRef from the client mapping, Line linking the mapped invoice with the paid amount, PaymentRefNum = Stripe reference, DepositToAccountRef from settings). The payment mapping row is written at push time — slice 1's echo suppression makes the next CDC poll a no-op.
  • getQboAccounts server action (account list filtered to valid deposit targets) feeding a deposit-account picker in QBO settings; unset → Undeposited Funds resolved at delivery time.
  • Failures (deleted invoice in QBO, account invalid) file accounting_sync_export_error exceptions through the slice-1 framework.

5.2 Class & department tracking

  • getQboClasses / getQboDepartments server actions (catalog pattern).
  • Tenant defaults (class, department) configured in QBO settings via pickers; stored as tenant settings.
  • Item mapping metadata accepts classId; the mapping dialog's JSON editor documents it and the items tab shows a class column when set.
  • Invoice transform: per-line SalesItemLineDetail.ClassRef from item mapping metadata, else tenant default class, else omitted; header DepartmentRef from tenant default, else omitted. CreditMemos (slice 2) get the same treatment.

5.3 Multi-realm UX

  • Explicit default_realm tenant setting consumed by getDefaultQboRealmId (replacing first-stored-key ordering); make default action on the settings connection list, which now renders one row per realm (company name, status, last cycle).
  • Batch creation dialog and the slice-3 wizard show a realm picker only when more than one realm is connected.
  • Cycle scheduling registers per realm (slice 1 keyed it per tenant×realm already; registration now enumerates realms on connect/disconnect).
  • Realm-scoped surfaces (badges, health panel counts, mapping tabs) read the selected/default realm consistently; with one realm nothing changes.

6. Data model & API notes

  • Tenant settings: default_realm, deposit_account_ref, default_class_ref, default_department_ref. No new tables.
  • QBO API surface: Payment create, Account/Class/Department queries (all via QboClientService).

7. Risks & open questions

  • Double-entry guard: if a tenant's bookkeeper ALSO records the same Stripe payout manually in QBO, CDC will deliver it as a new payment against an invoice that Alga already shows paid — the applier's idempotency makes the second application a no-op only if amounts align; overlapping different payments surface as an exception (already-paid invoice receiving new allocation). Verify this path in tests.
  • QBO requires Payment currency to match the customer's; multi-currency tenants exercise the slice-1 currency-mismatch exception path.
  • Realm enumeration on disconnect must deregister only that realm's cycle.

8. Acceptance criteria / definition of done

  • Features implemented; automated tests green; slices 13 suites unaffected.
  • Live sandbox smoke: portal Stripe payment appears as a QBO Payment with the Stripe reference in the configured account and does not echo back; class/department visible on a delivered QBO invoice; second sandbox company connected — default switch, realm-routed batch, and per-realm health all work.