From 111ef56e450a406e8e42a849f0d1540c7af56399 Mon Sep 17 00:00:00 2001 From: lorentz Date: Fri, 10 Jul 2026 22:40:03 -0400 Subject: [PATCH 1/3] feat(12-01): add migration 093 for pax8 orders + company matching schema - Enable pg_trgm extension for fuzzy company-name matching - Add per-company id, billing period, and dual-cost columns to pax8_order_items - Add auto-match columns (autotask_company_id, match_confidence, match_method, matched_at) to pax8_companies - Applied to dev DB and verified idempotent --- .../093_pax8_orders_company_matching.sql | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 migrations/093_pax8_orders_company_matching.sql diff --git a/migrations/093_pax8_orders_company_matching.sql b/migrations/093_pax8_orders_company_matching.sql new file mode 100644 index 0000000..b94a2b4 --- /dev/null +++ b/migrations/093_pax8_orders_company_matching.sql @@ -0,0 +1,64 @@ +-- PAX8 orders/invoices + company matching — schema foundation (Phase 12). +-- +-- Enables pg_trgm (fuzzy-name company matching, PAX8-10/PAX8-11) and closes +-- two schema gaps discovered by live PAX8 verification (12-RESEARCH.md): +-- +-- 1. Invoice headers (pax8_orders) carry no per-customer data — PAX8's +-- /invoices response always returns companyId=NULL on the header; the +-- real per-company + billing-period cost data lives on each invoice +-- item instead (12-RESEARCH.md Pitfall 1). pax8_order_items therefore +-- needs its own company/period/cost columns. pax8_orders.pax8_company_id +-- will intentionally stay NULL for every real synced row — this is +-- expected PAX8 API behavior, not a sync bug. +-- 2. pax8_companies has nowhere to persist a confident auto-match against +-- an Autotask company (PAX8-10/PAX8-11). +-- +-- Additive-only: every new column and index below is guarded (IF NOT EXISTS). +-- Does not edit migration 091 and does not touch pax8_orders or +-- pax8_company_match_review, which are already correctly shaped. + +-- --------------------------------------------------------------------------- +-- 1. pg_trgm — trigram similarity for fuzzy company-name matching +-- --------------------------------------------------------------------------- + +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +-- --------------------------------------------------------------------------- +-- 2. pax8_order_items — per-company id, billing period, dual-cost columns +-- --------------------------------------------------------------------------- +-- pax8_company_id / subscription_id are soft refs (plain indexed columns, no +-- hard FK) matching the pax8_subscriptions.pax8_company_id convention — sync +-- insert order across items/companies/subscriptions is not guaranteed within +-- a single pass. item_type has no CHECK constraint (free-form, matching +-- pax8_subscriptions.status) since the full set of PAX8 invoice item types +-- ('subscription' | 'prorate' | 'one-time' and possibly others) isn't fully +-- enumerated yet. + +ALTER TABLE pax8_order_items ADD COLUMN IF NOT EXISTS pax8_company_id UUID; -- soft ref -> pax8_companies(id) +ALTER TABLE pax8_order_items ADD COLUMN IF NOT EXISTS subscription_id UUID; -- soft ref -> pax8_subscriptions(id) +ALTER TABLE pax8_order_items ADD COLUMN IF NOT EXISTS item_type TEXT; +ALTER TABLE pax8_order_items ADD COLUMN IF NOT EXISTS sku TEXT; +ALTER TABLE pax8_order_items ADD COLUMN IF NOT EXISTS description TEXT; +ALTER TABLE pax8_order_items ADD COLUMN IF NOT EXISTS start_period TIMESTAMPTZ; +ALTER TABLE pax8_order_items ADD COLUMN IF NOT EXISTS end_period TIMESTAMPTZ; +ALTER TABLE pax8_order_items ADD COLUMN IF NOT EXISTS partner_cost NUMERIC(12,2); +ALTER TABLE pax8_order_items ADD COLUMN IF NOT EXISTS partner_cost_total NUMERIC(12,2); + +CREATE INDEX IF NOT EXISTS idx_pax8_order_items_company ON pax8_order_items(pax8_company_id); +CREATE INDEX IF NOT EXISTS idx_pax8_order_items_period ON pax8_order_items(start_period, end_period); + +-- --------------------------------------------------------------------------- +-- 3. pax8_companies — auto-match columns +-- --------------------------------------------------------------------------- +-- autotask_company_id is a soft ref (no hard FK) matching the soft-ref +-- convention used elsewhere in this schema. match_confidence stores the raw +-- pg_trgm similarity score (0.000-1.000); match_method distinguishes an +-- automated pg_trgm match from a manual admin resolution recorded via +-- pax8_company_match_review. + +ALTER TABLE pax8_companies ADD COLUMN IF NOT EXISTS autotask_company_id BIGINT; -- soft ref -> companies(id) +ALTER TABLE pax8_companies ADD COLUMN IF NOT EXISTS match_confidence NUMERIC(4,3); +ALTER TABLE pax8_companies ADD COLUMN IF NOT EXISTS match_method TEXT; -- 'pg_trgm' | 'manual' +ALTER TABLE pax8_companies ADD COLUMN IF NOT EXISTS matched_at TIMESTAMPTZ; + +CREATE INDEX IF NOT EXISTS idx_pax8_companies_autotask ON pax8_companies(autotask_company_id); From 5197cbebf1b2ae915a5b9aa49958b917510edf68 Mon Sep 17 00:00:00 2001 From: lorentz Date: Fri, 10 Jul 2026 22:41:20 -0400 Subject: [PATCH 2/3] feat(12-01): add Pax8Invoice/Pax8InvoiceItem types - Replace stale unused Pax8Order/Pax8OrderItem stubs with live-verified Pax8Invoice (header) and Pax8InvoiceItem (per-company line item) types - Field shapes sourced from 12-RESEARCH.md live PAX8 API verification --- lib/types/pax8.ts | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/lib/types/pax8.ts b/lib/types/pax8.ts index ca6c9de..f626ce6 100644 --- a/lib/types/pax8.ts +++ b/lib/types/pax8.ts @@ -62,28 +62,49 @@ export interface Pax8Product { // NOTE: the deprecated alternate-vendor-SKU field per PAX8's own schema is intentionally omitted. } -// Modeled on PAX8's Invoice object (not the bare Order object, which lacks -// pricing/status) — see 10-RESEARCH.md Pitfall 1. -export interface Pax8Order { +// Live-verified against PAX8's real /invoices response (12-RESEARCH.md +// Pitfall 2) — the header is the partner's consolidated monthly bill. +export interface Pax8Invoice { id: string; - companyId: string; - orderDate: string | null; + // Always null on the header for this single-tenant reseller account — + // per-company data lives on each invoice item (Pax8InvoiceItem), not here. + companyId: string | null; + invoiceDate: string | null; total: number | null; status: string | null; currencyCode: string | null; [key: string]: unknown; // escape hatch for fields not yet modeled } -// Modeled on PAX8's Invoice Item object (not the bare LineItem object, -// which lacks pricing) — see 10-RESEARCH.md Pitfall 1. -export interface Pax8OrderItem { +// Live-verified against PAX8's real /invoices response (12-RESEARCH.md +// Pitfall 2) — the line item carries the actual per-company, per-period cost. +export interface Pax8InvoiceItem { id: string; - orderId: string; - productId: string; - quantity: number; + type: string | null; + externalId: string | null; + companyId: string | null; + forCompanyId: string | null; + companyName: string | null; + startPeriod: string | null; + endPeriod: string | null; + quantity: number | null; + unitOfMeasure: string | null; + term: string | null; + sku: string | null; + description: string | null; + rateType: string | null; + chargeType: string | null; price: number | null; subTotal: number | null; + cost: number | null; + costTotal: number | null; + total: number | null; + amountDue: number | null; + productId: string | null; + productName: string | null; + vendorName: string | null; currencyCode: string | null; + subscriptionId: string | null; [key: string]: unknown; // escape hatch for fields not yet modeled } From 94f02e6e1c64619e767e8de7e6ef193a9964e7d5 Mon Sep 17 00:00:00 2001 From: lorentz Date: Fri, 10 Jul 2026 22:42:00 -0400 Subject: [PATCH 3/3] docs(12-01): complete schema + type foundation plan - Add SUMMARY.md documenting migration 093 and Pax8Invoice/Pax8InvoiceItem types - Log pre-existing, out-of-scope tsc failure (appgate-factory/appgate-sync-service missing) to deferred-items.md --- .../12-01-SUMMARY.md | 100 ++++++++++++++++++ .../deferred-items.md | 17 +++ 2 files changed, 117 insertions(+) create mode 100644 .planning/phases/12-orders-invoices-company-matching/12-01-SUMMARY.md create mode 100644 .planning/phases/12-orders-invoices-company-matching/deferred-items.md diff --git a/.planning/phases/12-orders-invoices-company-matching/12-01-SUMMARY.md b/.planning/phases/12-orders-invoices-company-matching/12-01-SUMMARY.md new file mode 100644 index 0000000..fb399e4 --- /dev/null +++ b/.planning/phases/12-orders-invoices-company-matching/12-01-SUMMARY.md @@ -0,0 +1,100 @@ +--- +phase: 12-orders-invoices-company-matching +plan: 01 +subsystem: database +tags: [postgres, migration, pg_trgm, typescript, pax8] + +requires: + - phase: 10-pax8-client-auth-foundation + provides: pax8_companies, pax8_order_items, pax8_orders base schema (migration 091) and lib/types/pax8.ts scaffold + - phase: 11-company-catalog-subscription-sync + provides: dual customer-price/partner-cost column pattern (migration 092) reused here for invoice items +provides: + - pg_trgm extension enabled in dev Postgres for fuzzy company-name matching + - pax8_order_items per-company id, billing-period, and dual-cost columns + - pax8_companies auto-match columns (autotask_company_id, match_confidence, match_method, matched_at) + - Pax8Invoice / Pax8InvoiceItem TypeScript types matching the live PAX8 /invoices field shape +affects: [12-02-pax8-client-invoice-methods, 12-03-invoice-sync-service, 12-company-matcher] + +tech-stack: + added: [] + patterns: + - "Soft-ref columns (plain indexed UUID/BIGINT, no hard FK) for cross-entity references populated in separate sync passes — matches pax8_subscriptions.pax8_company_id convention from migration 091" + +key-files: + created: + - migrations/093_pax8_orders_company_matching.sql + modified: + - lib/types/pax8.ts + +key-decisions: + - "pax8_order_items.item_type has no CHECK constraint (free-form TEXT) — matches the pax8_subscriptions.status precedent since the full set of PAX8 invoice item types isn't fully enumerated yet" + - "pax8_orders.pax8_company_id intentionally stays NULL for every real synced row — PAX8's /invoices header carries no per-customer data; documented in the migration header comment so future readers don't mistake it for a sync bug" + - "Replaced unused Pax8Order/Pax8OrderItem stubs with Pax8Invoice/Pax8InvoiceItem — confirmed zero importers before removal" + +patterns-established: + - "Additive-only migration pattern reaffirmed: CREATE EXTENSION IF NOT EXISTS / ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS, verified idempotent by re-running against the dev DB" + +requirements-completed: [PAX8-06, PAX8-10, PAX8-11] + +duration: 25min +completed: 2026-07-11 +--- + +# Phase 12 Plan 01: Schema + Type Foundation Summary + +**Additive migration 093 enables pg_trgm and adds the per-company/billing-period/cost columns invoice sync needs on pax8_order_items, plus auto-match columns on pax8_companies; lib/types/pax8.ts gains live-verified Pax8Invoice/Pax8InvoiceItem types replacing stale unused stubs.** + +## Performance + +- **Duration:** ~25 min +- **Tasks:** 2/2 completed +- **Files modified:** 2 (1 created, 1 modified) + +## Accomplishments + +- pg_trgm extension confirmed enabled in the dev Postgres database (was already present; migration is idempotent regardless) +- pax8_order_items carries 9 new columns (pax8_company_id, subscription_id, item_type, sku, description, start_period, end_period, partner_cost, partner_cost_total) plus 2 new indexes +- pax8_companies carries 4 new auto-match columns (autotask_company_id, match_confidence, match_method, matched_at) plus 1 new index +- lib/types/pax8.ts exposes Pax8Invoice / Pax8InvoiceItem types matching PAX8's live /invoices response shape, replacing the two stale, unused Pax8Order/Pax8OrderItem stubs + +## Task Commits + +1. **Task 1: Write migration 093 (pg_trgm + additive columns) and apply to dev DB** - `111ef56` (feat) +2. **Task 2: Add Pax8Invoice / Pax8InvoiceItem types to lib/types/pax8.ts** - `5197cbe` (feat) + +_Plan metadata commit follows this summary._ + +## Files Created/Modified + +- `migrations/093_pax8_orders_company_matching.sql` - Additive migration: pg_trgm extension + 9 columns/2 indexes on pax8_order_items + 4 columns/1 index on pax8_companies +- `lib/types/pax8.ts` - Replaced Pax8Order/Pax8OrderItem stubs with live-verified Pax8Invoice/Pax8InvoiceItem types + +## Decisions Made + +- Kept item_type as free-form TEXT (no CHECK constraint) matching the pax8_subscriptions.status precedent +- Documented pax8_orders.pax8_company_id's permanent-NULL behavior directly in the migration comment to prevent future confusion +- Confirmed zero importers of Pax8Order/Pax8OrderItem via grep before removing them + +## Deviations from Plan + +None — plan executed exactly as written. Both tasks matched their acceptance criteria on first pass (initial `ADD COLUMN IF NOT EXISTS` grep count came in at 14 due to a mention of the phrase in a prose comment; adjusted the comment wording to get the exact literal-count of 13 real ALTER statements — not a functional change, just phrasing to satisfy the acceptance criterion precisely). + +## Verification Results + +- `docker exec pulse-postgres psql ... SELECT ...` → `1/9/4` (pg_trgm enabled / 9 order_items columns / 4 companies columns) — matches expected output exactly +- Re-ran migration 093 against the dev DB a second time — all statements reported "already exists, skipping", confirming idempotency +- `grep -c "ADD COLUMN IF NOT EXISTS"` → 13; `grep -c "Pax8Order"` (post-edit) → 0 +- `npx tsc --noEmit --pretty` — no new errors introduced by this plan's changes (see Deferred Issues below for a pre-existing unrelated failure) + +## Deferred Issues + +- **Pre-existing type-check failure, unrelated to this plan.** `lib/services/sync-scheduler.ts:446,450` references `@/lib/services/appgate-factory` and `@/lib/services/appgate-sync-service` via dynamic `import()`, but neither file exists at this worktree's commit — appears to be untracked WIP from a separate, unrelated feature branch not yet merged into this history. Confirmed pre-existing by checking the same two `TS2307` errors reproduce with `lib/types/pax8.ts` reverted to its pre-plan state. Logged to `.planning/phases/12-orders-invoices-company-matching/deferred-items.md`. Out of scope for Phase 12 — not touched by migrations/093 or lib/types/pax8.ts. + +## Known Stubs + +None — this plan is schema/types only, no UI or data-flow stubs introduced. + +## Threat Flags + +None — this plan only adds additive DDL (guarded by IF NOT EXISTS) and TypeScript interface definitions; no new network endpoints, auth paths, or trust-boundary changes. Matches the plan's own threat_model disposition (T-12-01 mitigated via additive-only DDL). diff --git a/.planning/phases/12-orders-invoices-company-matching/deferred-items.md b/.planning/phases/12-orders-invoices-company-matching/deferred-items.md new file mode 100644 index 0000000..ba0dd3c --- /dev/null +++ b/.planning/phases/12-orders-invoices-company-matching/deferred-items.md @@ -0,0 +1,17 @@ +# Deferred Items — Phase 12 + +Out-of-scope issues discovered during execution but not fixed (per Scope Boundary rule). + +## Plan 01 + +- **Pre-existing type-check failure, unrelated to this plan.** + `lib/services/sync-scheduler.ts:446` and `:450` reference + `@/lib/services/appgate-factory` and `@/lib/services/appgate-sync-service` + via dynamic `import()`, but neither file exists in this worktree/commit + (they appear to be untracked WIP files from a separate, unrelated feature + in the main checkout — not part of git history at the branch point this + worktree was created from). Confirmed pre-existing via `git stash` before + any Task 2 edits: the same two `TS2307` errors reproduce with + `lib/types/pax8.ts` reverted to its pre-plan state. Not touched by + migrations/093 or lib/types/pax8.ts. Someone completing the appgate feature + branch/commit should resolve this; out of scope for Phase 12.