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
122 lines
6.0 KiB
Markdown
122 lines
6.0 KiB
Markdown
# 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 1–3. 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 1–3 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.
|