Confirmed via direct service call (session-gated route bypassed for
verification): 118 companies, 445 subscriptions with dual cost, 46
referenced-only products with readable name+category, 0 unreferenced
products, stable soft-delete on re-run, zero PAX8 write calls.
- POST returns 409 if a sync is already in progress, otherwise starts
Pax8SyncService.fullSync() without awaiting and returns immediately
- GET reports inProgress, non-deleted row counts across the three PAX8
tables, and the last 10 sync_history rows for entity_type='pax8'
- Route stays behind the session-cookie check (not added to
middleware.ts's public allowlist) — matches itglue/veeam sync routes
- fullSync() orchestrates companies -> subscriptions -> referenced-only
products, each with independent try/catch returning Pax8EntitySyncResult
- Referenced product catalog resolved in a single pass: listAllProducts()
fetched once, filtered in-memory to subscription-referenced ids (D-01/D-02)
- Each entity's UUID-array tombstone soft-deletes rows PAX8 no longer
returns (is_deleted=true, deleted_at set), skipped when zero ids seen
- sync_history row (entity_type='pax8', sync_type='full') tracks
started/completed/failed with base columns only
- No PAX8 writes: only Pax8Client's read methods are called
- listAllCompanies/listAllSubscriptions/listAllProducts multi-page concat
- size=200 param assertion, single-page no-infinite-loop case
- Assert every request is GET with Authorization: Bearer header
- ALTER TABLE pax8_subscriptions to add price/partner_cost/currency
- Additive, idempotent (IF NOT EXISTS), matches currency convention from 091
- Applied to dev DB via docker exec (Postgres init won't re-run on existing volume)
Pattern mapper output for Phase 11 was missing from the initial plan
commit. Decision coverage gate also flagged D-02/D-07/D-08 as
implemented-but-uncited in 11-02-PLAN.md's must_haves.truths block —
added explicit citations so the translation gate passes cleanly.
- scripts/verify-pax8-auth.ts loads .env.local, calls getPax8Client().listCompanies(0,1), logs only counts/status (never token/secret)
- CLAUDE.md External integrations table gains a PAX8 row (PAX8_* prefix)
- INTEGRATIONS.md does not exist at repo root; edit skipped per plan instructions (noted in SUMMARY)
- isPax8Configured(): both PAX8_CLIENT_ID and PAX8_CLIENT_SECRET required
- getPax8Client(): throws exact error naming both env vars when missing;
caches singleton Pax8Client instance
- _resetPax8Client(): test seam to clear the cached singleton
- follows appgate-factory.ts / 10-RESEARCH.md Pattern 2 verbatim
- Six tables: pax8_companies, pax8_products, pax8_subscriptions,
pax8_orders, pax8_order_items, pax8_company_match_review
- Header/line-item design (D-01) with hard FK order_items->orders CASCADE
- Monetary NUMERIC(12,2) + currency CHAR(3) DEFAULT 'USD' (D-04)
- raw_payload JSONB safety net on all five non-review tables (D-03)
- pax8_company_match_review modeled field-for-field on device_link_review
(migration 080), with soft-ref BIGINT[] candidates and hard FK to
pax8_companies + nullable FK to companies(id)
- getToken() JSON-body OAuth2 client-credentials exchange with audience field
(deviates from msgraph-client.ts's form-encoded body per 10-RESEARCH.md Pitfall 3)
- 60s expiry-buffer token cache, reused across calls
- fetchJson<T>() with 429/Retry-After retry copied from msgraph-client.ts
- listCompanies() auth-proof call parsing the {content,page} envelope
- secret never interpolated into any throw/console call
Decision coverage gate requires an explicit D-NN citation inside a
plan's must_haves/truths block, not just body prose. D-04 was already
cited; D-01/D-02/D-03 were only mentioned in read_first/action text.