feat(10-02): create PAX8 schema migration 091
- 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)
This commit is contained in:
parent
8b975be700
commit
4d20e45ce8
1 changed files with 170 additions and 0 deletions
170
migrations/091_pax8_tables.sql
Normal file
170
migrations/091_pax8_tables.sql
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
-- PAX8 integration — Postgres schema.
|
||||||
|
--
|
||||||
|
-- Lays down the full PAX8 schema Phases 11-14 will populate and consume.
|
||||||
|
-- Schema-only migration (Phase 10) — no sync logic, no matching logic yet.
|
||||||
|
--
|
||||||
|
-- • Client companies -> pax8_companies
|
||||||
|
-- • Active seat/license subscriptions -> pax8_subscriptions
|
||||||
|
-- • Product/SKU catalog -> pax8_products
|
||||||
|
-- • Order headers (Invoice-shaped, see note below) -> pax8_orders
|
||||||
|
-- • Order line items (InvoiceItem-shaped) -> pax8_order_items
|
||||||
|
-- • PAX8 company <-> Autotask company conflicts -> pax8_company_match_review
|
||||||
|
--
|
||||||
|
-- NOTE on pax8_orders / pax8_order_items: table names stay "orders" per the
|
||||||
|
-- D-01 header/line-item design decision, but the column shapes below carry
|
||||||
|
-- PAX8 Invoice/InvoiceItem semantics (total, status, unit price) rather than
|
||||||
|
-- the bare Order/LineItem object fields — Phase 12's sync service sources
|
||||||
|
-- these from PAX8's `/invoices` resource, not `/orders`. Confirm the exact
|
||||||
|
-- field mapping against PAX8's live API response when Phase 12 is planned.
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 1. pax8_companies — PAX8 client companies
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- external_id is left nullable and separate from name: PAX8's own company id
|
||||||
|
-- is the primary key, but there is no confirmed stable secondary identifier
|
||||||
|
-- (domain, external ref) yet — RESEARCH-pax8-company-identifiers is still
|
||||||
|
-- open. Do not assume company name is the only field comparable to Autotask.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pax8_companies (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
external_id TEXT,
|
||||||
|
website TEXT,
|
||||||
|
status TEXT,
|
||||||
|
city TEXT,
|
||||||
|
state_or_province TEXT,
|
||||||
|
postal_code TEXT,
|
||||||
|
country TEXT,
|
||||||
|
raw_payload JSONB,
|
||||||
|
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pax8_companies_is_deleted ON pax8_companies(is_deleted);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 2. pax8_products — PAX8 product/SKU catalog
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- No "referenced only" constraint here on purpose: whether Phase 11 syncs
|
||||||
|
-- the full catalog or lazily populates rows as referenced by subscriptions
|
||||||
|
-- is left open (CONTEXT.md Claude's Discretion). The deprecated alternate-
|
||||||
|
-- SKU field upstream is not modeled.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pax8_products (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
sku TEXT,
|
||||||
|
vendor_sku TEXT,
|
||||||
|
name TEXT,
|
||||||
|
category TEXT,
|
||||||
|
raw_payload JSONB,
|
||||||
|
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pax8_products_is_deleted ON pax8_products(is_deleted);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 3. pax8_subscriptions — active seat/license subscriptions per company
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- pax8_company_id / product_id are intentionally soft refs (plain indexed
|
||||||
|
-- columns, not hard FKs) — sync insert order across companies/subscriptions/
|
||||||
|
-- products is not guaranteed within a single sync pass.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pax8_subscriptions (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
pax8_company_id UUID, -- soft ref -> pax8_companies(id)
|
||||||
|
product_id UUID, -- soft ref -> pax8_products(id)
|
||||||
|
quantity INTEGER,
|
||||||
|
billing_term TEXT,
|
||||||
|
status TEXT,
|
||||||
|
start_date TIMESTAMPTZ,
|
||||||
|
raw_payload JSONB,
|
||||||
|
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pax8_subscriptions_is_deleted ON pax8_subscriptions(is_deleted);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pax8_subscriptions_company ON pax8_subscriptions(pax8_company_id);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 4. pax8_orders — order/invoice header (D-01 header table)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Invoice-shaped: total/status/currency mirror PAX8's Invoice object per
|
||||||
|
-- RESEARCH Pitfall 1. No lookback-window column (D-02) — sync pulls full
|
||||||
|
-- order history on first sync in a later phase.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pax8_orders (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
pax8_company_id UUID, -- soft ref -> pax8_companies(id)
|
||||||
|
order_date TIMESTAMPTZ,
|
||||||
|
total NUMERIC(12,2),
|
||||||
|
status TEXT,
|
||||||
|
currency CHAR(3) NOT NULL DEFAULT 'USD',
|
||||||
|
raw_payload JSONB,
|
||||||
|
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pax8_orders_is_deleted ON pax8_orders(is_deleted);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pax8_orders_company ON pax8_orders(pax8_company_id);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 5. pax8_order_items — order/invoice line items (D-01 line table)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- InvoiceItem-shaped: unit_price/line_total mirror PAX8's InvoiceItem object
|
||||||
|
-- per RESEARCH Pitfall 1. Hard FK to pax8_orders — header/line relationship,
|
||||||
|
-- unlike the company/product soft refs above, because order_items are always
|
||||||
|
-- inserted together with (after) their parent order in a single sync pass.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pax8_order_items (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
order_id UUID NOT NULL REFERENCES pax8_orders(id) ON DELETE CASCADE,
|
||||||
|
product_id UUID, -- soft ref -> pax8_products(id)
|
||||||
|
quantity INTEGER,
|
||||||
|
unit_price NUMERIC(12,2),
|
||||||
|
line_total NUMERIC(12,2),
|
||||||
|
currency CHAR(3) NOT NULL DEFAULT 'USD',
|
||||||
|
raw_payload JSONB,
|
||||||
|
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pax8_order_items_is_deleted ON pax8_order_items(is_deleted);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pax8_order_items_order ON pax8_order_items(order_id);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 6. pax8_company_match_review — PAX8 <-> Autotask company conflicts queue
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Copied field-for-field from device_link_review (migration 080). Populated
|
||||||
|
-- by Phase 12's matching logic — this migration only creates the shape.
|
||||||
|
-- Admin picks the right Autotask company; the matcher does not auto-merge.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pax8_company_match_review (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
pax8_company_id UUID NOT NULL REFERENCES pax8_companies(id) ON DELETE CASCADE,
|
||||||
|
candidate_company_ids BIGINT[] NOT NULL,
|
||||||
|
match_confidences TEXT[] NOT NULL,
|
||||||
|
detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
resolved_at TIMESTAMPTZ,
|
||||||
|
resolved_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL,
|
||||||
|
resolved_to_company_id BIGINT REFERENCES companies(id) ON DELETE SET NULL,
|
||||||
|
resolution_note TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_pax8_company_match_review_unresolved
|
||||||
|
ON pax8_company_match_review(detected_at DESC) WHERE resolved_at IS NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_pax8_company_match_review_pax8_company
|
||||||
|
ON pax8_company_match_review(pax8_company_id);
|
||||||
|
|
||||||
|
-- Don't keep stale rows blocking new reviews: partial unique index enforces
|
||||||
|
-- "one open review per PAX8 company" rather than a blanket unique constraint.
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_pax8_company_match_review_open
|
||||||
|
ON pax8_company_match_review(pax8_company_id) WHERE resolved_at IS NULL;
|
||||||
|
|
||||||
|
COMMENT ON TABLE pax8_company_match_review IS
|
||||||
|
'PAX8 -> Autotask company-match conflicts queue: one row per PAX8 company that matches 2+ Autotask companies (or is otherwise ambiguous). Populated by Phase 12''s matching logic. Admin picks the right Autotask company; the matcher does not auto-merge.';
|
||||||
Loading…
Add table
Add a link
Reference in a new issue