Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9e0d20c
docs(pax8): design spec for Pax8 ordering
Jul 14, 2026
0987e80
docs(pax8): implementation plan for Pax8 ordering
Jul 14, 2026
3d2f612
feat(pax8): add pax8_orders + pax8_order_lines staged intent ledger
Jul 14, 2026
03560d2
fix(pax8): preserve tenant keys on linked record deletion
Jul 14, 2026
f33679e
fix(pax8): enforce order line billing and org integrity
Jul 14, 2026
1d035b4
feat(pax8): add order/subscription write methods to Pax8Client
Jul 14, 2026
3d50699
feat(pax8): draft order authoring service with commitment guards
Jul 14, 2026
08a9a43
fix(pax8): harden draft order authoring
Jul 14, 2026
770c059
fix(pax8): close order authoring ambiguity races
Jul 14, 2026
d9aa733
fix(pax8): lock order before line removal
Jul 14, 2026
e5e016f
feat(pax8): add no-blind-retry order submit pipeline
Jul 14, 2026
b333a80
fix(pax8): harden order submission reconciliation
Jul 14, 2026
7479c4b
fix(pax8): pin reconciliation evidence
Jul 14, 2026
00a213c
feat(pax8): add order routes
Jul 14, 2026
aa867f5
fix(pax8): classify order action audit outcomes
Jul 14, 2026
564608b
fix(pax8): audit authorized order failures
Jul 14, 2026
424c5f3
fix(pax8): enrich order outcome audits
Jul 14, 2026
f8d82a6
feat(pax8): stage orders on quote acceptance
Jul 14, 2026
9711eaf
fix(pax8): stop billing from stale subscription quantity
Jul 14, 2026
184ee9e
fix(pax8): require fresh quantity evidence for drift
Jul 14, 2026
fb6b146
feat(pax8): add organization ordering workspace
Jul 14, 2026
41bb412
fix(pax8): harden organization ordering workflow
Jul 14, 2026
13c16e4
fix(pax8): lock readiness checks with ordering writes
Jul 14, 2026
de9f16d
feat(pax8): surface staged orders on accepted quotes
Jul 14, 2026
518fb25
docs(pax8): document ordering and billing truth
Jul 14, 2026
257306c
fix(pax8): clarify quantity observation wording
Jul 14, 2026
75f7b61
fix(pax8): use shared hash state for order navigation
Jul 14, 2026
ae3b6cc
fix(pax8): harden ordering recovery UI
Jul 14, 2026
ab45be0
fix(pax8): align recovery affordances
Jul 14, 2026
d1aad17
fix(pax8): harden order authoring integrity
Jul 14, 2026
5b0d3a4
fix(pax8): close submit authorization races
Jul 14, 2026
d88b1a3
fix(pax8): reopen safe orders for restaging
Jul 14, 2026
0cef481
test(pax8): align restage error constructor
Jul 14, 2026
327d47f
fix(i18n): annotate Pax8 dynamic keys
Jul 14, 2026
d65e556
docs(pax8): align order authoring contract
Jul 14, 2026
070bb3c
fix(pax8): align UI recovery states
Jul 14, 2026
f3be6b6
docs(pax8): correct quantity sync guidance
Jul 14, 2026
5d1e6dd
docs(pax8): correct observation MFA requirements
Jul 14, 2026
deee93c
fix(pax8): require explicit billing quantity
Jul 14, 2026
78bda43
test(pax8): record feature verification
Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions apps/api/migrations/2026-07-13-a-pax8-ordering.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
-- Pax8 ordering: staged intent ledger.
-- Pax8 has no idempotency key and no order status field, so THIS TABLE — not
-- Pax8 — is the record of whether money was spent. A line is claimed
-- (submit_state='in_flight') in a committed txn before the HTTP call.
-- Partner-axis (RLS shape 3), matching the five existing pax8_* tables:
-- org_id is a linkage column, never the tenancy axis.

CREATE TABLE IF NOT EXISTS pax8_orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
integration_id UUID NOT NULL,
partner_id UUID NOT NULL REFERENCES partners(id),
org_id UUID NOT NULL,
pax8_company_id VARCHAR(64),
status VARCHAR(20) NOT NULL DEFAULT 'draft',
source VARCHAR(10) NOT NULL DEFAULT 'direct',
source_quote_id UUID REFERENCES quotes(id) ON DELETE SET NULL,
dedupe_key VARCHAR(120) NOT NULL,
pax8_order_id VARCHAR(64),
error TEXT,
created_by UUID REFERENCES users(id),
submitted_by UUID REFERENCES users(id),
submitted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT pax8_orders_status_chk CHECK (status IN (
'draft','awaiting_details','ready','submitting','completed','partially_failed','failed','cancelled')),
CONSTRAINT pax8_orders_source_chk CHECK (source IN ('direct','quote')),
CONSTRAINT pax8_orders_integration_partner_fkey
FOREIGN KEY (integration_id, partner_id)
REFERENCES pax8_integrations(id, partner_id) ON DELETE CASCADE,
CONSTRAINT pax8_orders_org_partner_fkey
FOREIGN KEY (org_id, partner_id)
REFERENCES organizations(id, partner_id) ON DELETE CASCADE
);

-- The idempotency guard. A concurrent submit of the same intent loses this race.
CREATE UNIQUE INDEX IF NOT EXISTS pax8_orders_dedupe_key_uq
ON pax8_orders(partner_id, dedupe_key);
CREATE INDEX IF NOT EXISTS pax8_orders_partner_idx ON pax8_orders(partner_id);
CREATE INDEX IF NOT EXISTS pax8_orders_org_idx ON pax8_orders(org_id);
CREATE INDEX IF NOT EXISTS pax8_orders_status_idx ON pax8_orders(partner_id, status);
CREATE INDEX IF NOT EXISTS pax8_orders_quote_idx ON pax8_orders(source_quote_id);
-- Target for the order_lines composite FK. Including org_id prevents an order
-- line from pointing at another customer under the same MSP partner.
CREATE UNIQUE INDEX IF NOT EXISTS pax8_orders_id_partner_org_idx
ON pax8_orders(id, partner_id, org_id);

CREATE TABLE IF NOT EXISTS pax8_order_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL,
partner_id UUID NOT NULL REFERENCES partners(id),
org_id UUID NOT NULL,
action VARCHAR(20) NOT NULL,
submit_state VARCHAR(20) NOT NULL DEFAULT 'pending',
pax8_product_id VARCHAR(64),
catalog_item_id UUID,
billing_term VARCHAR(20),
commitment_term_id VARCHAR(64),
quantity NUMERIC(12,2),
provisioning_details JSONB NOT NULL DEFAULT '[]'::jsonb,
target_subscription_id VARCHAR(64),
cancel_date DATE,
result_subscription_id VARCHAR(64),
contract_line_id UUID,
source_quote_line_id UUID,
error TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT pax8_order_lines_action_chk CHECK (action IN (
'new_subscription','change_quantity','cancel')),
CONSTRAINT pax8_order_lines_state_chk CHECK (submit_state IN (
'pending','in_flight','succeeded','failed','needs_reconcile')),
CONSTRAINT pax8_order_lines_billing_term_chk CHECK (
billing_term IS NULL OR billing_term IN (
'Monthly','Annual','2-Year','3-Year','One-Time','Trial','Activation')),
-- Each action carries a different payload; enforce the shape rather than
-- trusting the service layer. A cancel with a quantity is a bug, not data.
CONSTRAINT pax8_order_lines_action_payload_chk CHECK (
(action = 'new_subscription'
AND pax8_product_id IS NOT NULL AND billing_term IS NOT NULL
AND quantity IS NOT NULL AND quantity > 0 AND target_subscription_id IS NULL)
OR (action = 'change_quantity'
AND target_subscription_id IS NOT NULL AND quantity IS NOT NULL AND quantity >= 0)
OR (action = 'cancel'
AND target_subscription_id IS NOT NULL AND quantity IS NULL)
),
CONSTRAINT pax8_order_lines_order_partner_org_fkey
FOREIGN KEY (order_id, partner_id, org_id)
REFERENCES pax8_orders(id, partner_id, org_id) ON DELETE CASCADE,
CONSTRAINT pax8_order_lines_org_partner_fkey
FOREIGN KEY (org_id, partner_id)
REFERENCES organizations(id, partner_id) ON DELETE CASCADE,
CONSTRAINT pax8_order_lines_catalog_item_partner_fkey
FOREIGN KEY (catalog_item_id, partner_id)
REFERENCES catalog_items(id, partner_id) ON DELETE SET NULL (catalog_item_id),
CONSTRAINT pax8_order_lines_contract_line_org_fkey
FOREIGN KEY (contract_line_id, org_id)
REFERENCES contract_lines(id, org_id) ON DELETE SET NULL (contract_line_id)
);

CREATE INDEX IF NOT EXISTS pax8_order_lines_order_idx ON pax8_order_lines(order_id);
CREATE INDEX IF NOT EXISTS pax8_order_lines_partner_idx ON pax8_order_lines(partner_id);
CREATE INDEX IF NOT EXISTS pax8_order_lines_org_idx ON pax8_order_lines(org_id);
CREATE INDEX IF NOT EXISTS pax8_order_lines_contract_line_idx ON pax8_order_lines(contract_line_id);
-- Finds lines stranded mid-flight (crash between claim and result).
CREATE INDEX IF NOT EXISTS pax8_order_lines_inflight_idx
ON pax8_order_lines(submit_state) WHERE submit_state IN ('in_flight','needs_reconcile');

ALTER TABLE pax8_orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE pax8_orders FORCE ROW LEVEL SECURITY;
ALTER TABLE pax8_order_lines ENABLE ROW LEVEL SECURITY;
ALTER TABLE pax8_order_lines FORCE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS breeze_partner_isolation_select ON pax8_orders;
DROP POLICY IF EXISTS breeze_partner_isolation_insert ON pax8_orders;
DROP POLICY IF EXISTS breeze_partner_isolation_update ON pax8_orders;
DROP POLICY IF EXISTS breeze_partner_isolation_delete ON pax8_orders;
CREATE POLICY breeze_partner_isolation_select ON pax8_orders
FOR SELECT USING (public.breeze_has_partner_access(partner_id));
CREATE POLICY breeze_partner_isolation_insert ON pax8_orders
FOR INSERT WITH CHECK (public.breeze_has_partner_access(partner_id));
CREATE POLICY breeze_partner_isolation_update ON pax8_orders
FOR UPDATE USING (public.breeze_has_partner_access(partner_id))
WITH CHECK (public.breeze_has_partner_access(partner_id));
CREATE POLICY breeze_partner_isolation_delete ON pax8_orders
FOR DELETE USING (public.breeze_has_partner_access(partner_id));

DROP POLICY IF EXISTS breeze_partner_isolation_select ON pax8_order_lines;
DROP POLICY IF EXISTS breeze_partner_isolation_insert ON pax8_order_lines;
DROP POLICY IF EXISTS breeze_partner_isolation_update ON pax8_order_lines;
DROP POLICY IF EXISTS breeze_partner_isolation_delete ON pax8_order_lines;
CREATE POLICY breeze_partner_isolation_select ON pax8_order_lines
FOR SELECT USING (public.breeze_has_partner_access(partner_id));
CREATE POLICY breeze_partner_isolation_insert ON pax8_order_lines
FOR INSERT WITH CHECK (public.breeze_has_partner_access(partner_id));
CREATE POLICY breeze_partner_isolation_update ON pax8_order_lines
FOR UPDATE USING (public.breeze_has_partner_access(partner_id))
WITH CHECK (public.breeze_has_partner_access(partner_id));
CREATE POLICY breeze_partner_isolation_delete ON pax8_order_lines
FOR DELETE USING (public.breeze_has_partner_access(partner_id));
41 changes: 41 additions & 0 deletions apps/api/migrations/2026-07-14-pax8-direct-draft-uniqueness.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- Keep at most one mutable direct Pax8 order per partner/customer. Quote
-- staging intentionally remains outside this index so multiple accepted quotes
-- may wait for fulfillment details at the same time.
DO $$
DECLARE
cleaned_count INTEGER;
BEGIN
WITH ranked AS (
SELECT
id,
ROW_NUMBER() OVER (
PARTITION BY partner_id, org_id
ORDER BY created_at, id
) AS ordinal
FROM pax8_orders
WHERE source = 'direct'
AND status IN ('draft', 'awaiting_details')
), cleaned AS (
UPDATE pax8_orders AS duplicate
SET
status = 'cancelled',
error = CONCAT_WS(
E'\n',
NULLIF(duplicate.error, ''),
'Cancelled duplicate mutable direct draft during 2026-07-14 uniqueness migration.'
),
updated_at = NOW()
FROM ranked
WHERE duplicate.id = ranked.id
AND ranked.ordinal > 1
RETURNING duplicate.id
)
SELECT COUNT(*) INTO cleaned_count FROM cleaned;

RAISE WARNING 'cancelled % duplicate mutable direct Pax8 draft order(s)', cleaned_count;
END $$;

CREATE UNIQUE INDEX IF NOT EXISTS pax8_orders_one_mutable_direct_per_org_uq
ON pax8_orders(partner_id, org_id)
WHERE source = 'direct'
AND status IN ('draft', 'awaiting_details');
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Pax8 omits Subscription.quantity in some responses. The client normalizes
-- that absence to 0.00 for wire compatibility; retain the evidence bit so a
-- synthesized zero can never be mistaken for real billing drift.
ALTER TABLE pax8_subscription_snapshots
ADD COLUMN IF NOT EXISTS quantity_known boolean NOT NULL DEFAULT false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Preserve the Breeze manual quantity against which a direct Pax8 quantity
-- change was authorized. Submit must match this value under lock before any
-- vendor write so a later billing edit cannot invert increase/decrease policy.

ALTER TABLE pax8_order_lines
ADD COLUMN IF NOT EXISTS authorized_baseline_quantity NUMERIC(12,2);

DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'pax8_order_lines_authorized_baseline_chk'
AND conrelid = 'pax8_order_lines'::regclass
) THEN
ALTER TABLE pax8_order_lines
ADD CONSTRAINT pax8_order_lines_authorized_baseline_chk CHECK (
authorized_baseline_quantity IS NULL
OR (action = 'change_quantity' AND authorized_baseline_quantity >= 0)
);
END IF;
END $$;
Loading
Loading