docs(12-04): complete sync service wiring plan

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
lorentz 2026-07-10 22:55:25 -04:00
parent 59294cee5e
commit 6c7b70dfc3
2 changed files with 123 additions and 0 deletions

View file

@ -0,0 +1,111 @@
---
phase: 12-orders-invoices-company-matching
plan: 04
subsystem: api
tags: [pax8, sync-service, invoices, company-matching, vitest]
# Dependency graph
requires:
- phase: 12-02
provides: Pax8Client.listAllInvoices/listAllInvoiceItems + confirmed live cost-field mapping
- phase: 12-03
provides: matchPax8Companies() fuzzy company matcher (Pax8CompanyMatchResult)
provides:
- "Pax8SyncService.syncOrders() — nested invoice-header/item upsert + child-then-parent tombstone"
- "Pax8SyncService.syncCompanyMatches() — Pax8EntitySyncResult wrapper around matchPax8Companies()"
- "fullSync() wired with both new entity steps, rolling into the existing success/status/totals reducer"
- "First unit tests for Pax8SyncService (lib/services/pax8-sync-service.test.ts)"
affects: [12-05]
# Tech tracking
tech-stack:
added: []
patterns:
- "resolveCostColumns(item) helper — named seam for per-item-type cost mapping, currently a single default branch since 12-02-SUMMARY.md recorded CONFIRM for all observed types"
- "Child-then-parent tombstone ordering (pax8_order_items before pax8_orders) to respect the FK"
key-files:
created:
- lib/services/pax8-sync-service.test.ts
modified:
- lib/services/pax8-sync-service.ts
- .planning/phases/12-orders-invoices-company-matching/deferred-items.md
key-decisions:
- "resolveCostColumns() collapses to a single default branch (unit_price<-price, line_total<-amountDue, partner_cost<-cost, partner_cost_total<-costTotal) because 12-02-SUMMARY.md recorded a CONFIRM verdict for every observed item type (subscription, prorate, one-time) no divergence to encode. Kept as a named switch/helper rather than inline ternaries so a future divergence has an obvious place to branch."
- "syncCompanyMatches() repurposes Pax8EntitySyncResult.tombstoned to carry the flagged-for-review count (flaggedAmbiguous + flaggedNoCandidate), documented inline, so the matcher fits the existing rollup shape without a new result field."
requirements-completed: [PAX8-06, PAX8-10, PAX8-11]
# Metrics
duration: 25min
completed: 2026-07-11
---
# Phase 12 Plan 04: Sync Service Wiring (Orders + Company Matching) Summary
**Wired both halves of Phase 12 into `Pax8SyncService.fullSync()` — a new `syncOrders()` step performing the nested invoice-header/item upsert with per-type cost-column resolution, and a new `syncCompanyMatches()` step delegating to Plan 03's matcher — plus the service's first unit tests.**
## Performance
- **Duration:** ~25 min
- **Started:** 2026-07-11T22:52:00Z
- **Completed:** 2026-07-11T22:55:30Z
- **Tasks:** 3
- **Files modified:** 3 (1 created, 1 modified, 1 tracking doc updated)
## Accomplishments
- `Pax8SyncService.syncOrders()` added: pages all invoice headers via `listAllInvoices()`, then for each header pages its items via `listAllInvoiceItems(invoiceId)` (12-RESEARCH.md Pattern 1 nested fetch). Upserts headers into `pax8_orders` (with `pax8_company_id` intentionally left `NULL` per Pitfall 1) and items into `pax8_order_items` (with `pax8_company_id`, `subscription_id`, `item_type`, `sku`, `description`, `start_period`, `end_period`, and the four dual-cost columns populated). Tombstones child (`pax8_order_items`) before parent (`pax8_orders`) using the existing `id <> ALL($1::uuid[])` pattern.
- `resolveCostColumns(item)` helper added: a named, reviewable seam that branches on `item.type`. Per 12-02-SUMMARY.md, all three observed types (`subscription`, `prorate`, `one-time`) were live-verified as CONFIRM against the default mapping, so the switch currently collapses to a single default branch — exactly as the plan's acceptance criteria anticipated for the "all CONFIRM" case.
- `Pax8SyncService.syncCompanyMatches()` added: delegates to Plan 03's `matchPax8Companies()`, shaping its `Pax8CompanyMatchResult` into a standard `Pax8EntitySyncResult` (`entity: 'company_matches'`).
- `fullSync()` now pushes `ordersResult` then `matchResult` after the products step, so `pax8_companies` is fully populated (via `syncCompanies()`) before matching runs. The existing rollup (`success = entities.every(...)`, `totalUpserted`/`totalTombstoned` reduce) picks both up unchanged — no changes needed to the rollup logic itself.
- `lib/services/pax8-sync-service.test.ts` created — the service's first unit tests. Mocks `postgresClient.query` and `pax8-company-matcher`'s `matchPax8Companies`, injects a mock `Pax8Client` via the constructor's optional `client` parameter, and exercises `fullSync()` end to end against a two-invoice/two-item fixture.
## Task Commits
Each task was committed atomically:
1. **Task 1: Add syncOrders() (nested invoice → item upsert + tombstone)** - `a06c1d3` (feat)
2. **Task 2: Add syncCompanyMatches() and wire both steps into fullSync()** - `5f96060` (feat)
3. **Task 3: Create lib/services/pax8-sync-service.test.ts** - `59294ce` (test)
_No TDD gate — plan is `autonomous: true` without a plan-level `type: tdd` frontmatter. Task 3 used `tdd="true"` but, matching 12-02-SUMMARY.md's precedent for its own Task 2, was executed as a single test-addition commit after the implementation (Tasks 1-2) already existed, following the existing test file's established pattern (`pax8-company-matcher.test.ts`'s mocking discipline)._
## Files Created/Modified
- `lib/services/pax8-sync-service.ts` - Added the `Pax8Invoice`/`Pax8InvoiceItem` type imports, the `resolveCostColumns()` module-level helper, the `syncOrders()` and `syncCompanyMatches()` private methods, the `matchPax8Companies` import, and the two new `entities.push(...)` calls in `fullSync()`
- `lib/services/pax8-sync-service.test.ts` - New file: 5 test cases covering header/item upserts with correct param binding, child-then-parent tombstoning, the default cost-column mapping across both fixture item types, matcher delegation, and the `fullSync()` entities array shape
- `.planning/phases/12-orders-invoices-company-matching/deferred-items.md` - Logged that the pre-existing `sync-scheduler.ts` TS2307 errors (unrelated `appgate-factory`/`appgate-sync-service` imports) still reproduce unchanged, plus 2 pre-existing, unrelated `itglue-search.test.ts` failures surfaced by the full `npm test` run
## Decisions Made
- **`resolveCostColumns()` collapses to a single default branch, exactly as anticipated.** 12-02-SUMMARY.md's live spot-check recorded a CONFIRM verdict for every observed item type (`subscription`, `prorate`, `one-time`) — no DIVERGENCE was found for any type. Per the plan's acceptance criteria (option 3: "If 12-02-SUMMARY.md marks CONFIRM for ALL types, the switch collapses to a single default branch"), the helper is still written as a named `switch (item.type)` with a documented default rather than inline ternaries, so a future divergence discovered for a new item type has an obvious, reviewable place to branch.
- **`syncCompanyMatches()`'s `tombstoned` field repurposing is documented inline**, per the plan's explicit instruction: it carries `flaggedAmbiguous + flaggedNoCandidate` (the count of `pax8_companies` rows flagged for manual review this run), not a soft-delete count, so the matcher step fits the existing `Pax8EntitySyncResult` shape without introducing a new result field.
## Deviations from Plan
None - plan executed exactly as written. Both cost columns and the matcher wrapper match the plan's action instructions field-for-field.
## Known Stubs
None. `resolveCostColumns()`'s single-branch switch is not a stub — it is the explicit, plan-anticipated outcome of 12-02-SUMMARY.md recording CONFIRM for every observed item type; it is a fully functional mapping, just not yet exercising a second branch because no divergence exists to encode.
## Issues Encountered
- Pre-existing, out-of-scope: `lib/services/sync-scheduler.ts:446`/`:450` TS2307 errors (missing `appgate-factory`/`appgate-sync-service` modules) reproduce unchanged before and after this plan's edits — confirmed unrelated (not touched by any Plan 04 task), consistent with Plans 01/02's prior findings.
- Pre-existing, out-of-scope: `npm test`'s full-suite run surfaces 2 failures in `lib/services/analyzer/itglue-search.test.ts` (unrelated to PAX8; that file was not modified by this plan). All PAX8-scoped suites pass green: `npx vitest run lib/services/pax8-client.test.ts lib/services/pax8-company-matcher.test.ts lib/services/pax8-sync-service.test.ts lib/services/pax8-factory.test.ts` → 31/31 passing.
## User Setup Required
None - no external service configuration required. This plan only extends existing, already-configured service code and adds tests.
## Next Phase Readiness
- `Pax8SyncService.fullSync()` now runs the complete Phase 12 pipeline: companies → subscriptions → products → orders (invoices + line items) → company matching, all rolling into one `Pax8SyncResult`.
- Plan 05 (or the next wave) can build on a fully wired sync service — no further sync-orchestration work is needed for PAX8-06/PAX8-10/PAX8-11.
- No blockers identified for subsequent plans.
---
*Phase: 12-orders-invoices-company-matching*
*Completed: 2026-07-11*

View file

@ -21,3 +21,15 @@ Out-of-scope issues discovered during execution but not fixed (per Scope Boundar
- Same pre-existing `sync-scheduler.ts:446`/`:450` TS2307 errors reproduce
unchanged after Task 1's `pax8-client.ts` edits (`listAllInvoices` /
`listAllInvoiceItems`). Confirmed unrelated to this plan's files.
## Plan 04
- Same pre-existing `sync-scheduler.ts:446`/`:450` TS2307 errors reproduce
unchanged after this plan's `pax8-sync-service.ts` edits. Confirmed
unrelated to this plan's files.
- `npm test` full-suite run surfaces 2 pre-existing failures in
`lib/services/analyzer/itglue-search.test.ts` ("tolerates per-call
failures" tests, lines ~129/253) unrelated to this plan — that file was
not touched by any Plan 04 task and not modified in the working tree.
All PAX8-scoped suites (`pax8-client.test.ts`, `pax8-company-matcher.test.ts`,
`pax8-sync-service.test.ts`, `pax8-factory.test.ts`) pass green (31/31).