Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
37ebe06
feat(pax8): optional claim-time verification hook for approved submit…
Oct 31, 2026
3053164
feat(ai-tools): pinned four-eyes Pax8 submit and supervised reconcile…
Oct 31, 2026
b19fa0c
test(pax8): integration proof for approved order submits (#6785)
Oct 31, 2026
062c191
feat(ai-tools): W15 Pax8 approval copy, docs and submit golden prompt…
Oct 31, 2026
91aab6c
Merge remote-tracking branch 'origin/main' into feature/6754-ai-full-…
Oct 31, 2026
1ffe12c
fix(ai-tools): W15 PR B review follow-ups — preserve Pax8 evidence an…
Oct 31, 2026
ba49d45
fix(ai-tools): W15 PR B CI follow-ups — restore linked-line preview v…
Oct 31, 2026
9bf0b11
Merge remote-tracking branch 'origin/main' into HEAD
Oct 31, 2026
cbff742
fix(migrations): rename pax8 order-line baseline migration to sort af…
Oct 31, 2026
30fdf15
chore(migrations): move pax8 baseline migration to slot 141000 (avoid…
Oct 31, 2026
bb1b7cd
chore(release-notes): move entry to the consolidated notes PR (#10832)
Nov 1, 2026
c1ab022
Merge remote-tracking branch 'origin/main' into HEAD
Nov 1, 2026
c93bc31
Merge origin/main into feature/6754-ai-full-control/wave-6785-b
Nov 2, 2026
067623c
chore(migrations): move unshipped migration after main's newest (2027…
Nov 2, 2026
18763d7
Merge remote-tracking branch 'origin/main' into HEAD
Nov 2, 2026
131c2c1
fix(web): picker labels for manage_pax8_orders submit (catalog label …
Nov 2, 2026
97f3b0c
chore(migrations): move unshipped migration after main's newest (2027…
Nov 2, 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
14 changes: 14 additions & 0 deletions .github/scripts/ci-area-gating.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -793,3 +793,17 @@ for (const file of ['pax8OrderPreview', 'aiToolSchemasPax8', 'aiToolsPax8', 'aiT
assert.equal(out.app, 'true');
});
}


// W15 PR B (#6785): the pinned money-surface digest and its real-DB proof.
for (const file of [
'apps/api/src/services/actionIntents/pax8OrderDigest.ts',
'apps/api/src/__tests__/integration/aiToolsPax8OrderSubmitScope.integration.test.ts',
]) {
test(`W15 PR B ${file} runs API and integration checks`, () => {
const out = classifyPrGate([file]);
assert.deepEqual(areasOf(out), expectAreas(['api']));
assert.equal(out.integration, 'true');
assert.equal(out.app, 'true');
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Preserve the approved billing baseline for every line action across reconciliation.
ALTER TABLE pax8_order_lines DROP CONSTRAINT IF EXISTS pax8_order_lines_authorized_baseline_chk;
ALTER TABLE pax8_order_lines ADD CONSTRAINT pax8_order_lines_authorized_baseline_chk
CHECK (authorized_baseline_quantity IS NULL OR authorized_baseline_quantity >= 0);

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: fixture.partner.id,
orderId: fixture.order.id,
actorUserId: fixture.user.id,
mode: { kind: 'rest' },
})).rejects.toMatchObject({ status: 422, message: expect.stringContaining('ready') });

expect(createClient).not.toHaveBeenCalled();
Expand Down Expand Up @@ -236,6 +237,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: fixture.partner.id,
orderId: fixture.order.id,
actorUserId: fixture.user.id,
mode: { kind: 'rest' },
})).rejects.toMatchObject({ status: 422, message: expect.stringContaining('future') });

expect(createClient).not.toHaveBeenCalled();
Expand Down Expand Up @@ -272,6 +274,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: fixture.partner.id,
orderId: fixture.order.id,
actorUserId: fixture.user.id,
mode: { kind: 'rest' },
})).rejects.toMatchObject({ status: 409, message: expect.stringContaining('no longer matches') });

expect(client.cancelSubscription).not.toHaveBeenCalled();
Expand All @@ -291,6 +294,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: fixture.partner.id,
orderId: fixture.order.id,
actorUserId: fixture.user.id,
mode: { kind: 'rest' },
})).rejects.toMatchObject({ status: 422, message: expect.stringContaining('active') });

expect(client.createOrder).not.toHaveBeenCalled();
Expand All @@ -305,6 +309,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: fixture.partner.id,
orderId: fixture.order.id,
actorUserId: fixture.user.id,
mode: { kind: 'rest' },
})).rejects.toMatchObject({ status: 409, message: expect.stringContaining('changed') });

expect(createClient).not.toHaveBeenCalled();
Expand Down Expand Up @@ -338,6 +343,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: fixture.partner.id,
orderId: fixture.order.id,
actorUserId: fixture.user.id,
mode: { kind: 'rest' },
});
expect(restagedClient.updateSubscriptionQuantity)
.toHaveBeenCalledWith('subscription-cancel', 25);
Expand All @@ -354,6 +360,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: fixture.partner.id,
orderId: fixture.order.id,
actorUserId: fixture.user.id,
mode: { kind: 'rest' },
})).rejects.toMatchObject({ status: 409, message: expect.stringContaining('baseline') });
expect(client.updateSubscriptionQuantity).not.toHaveBeenCalled();
const [orderAfter] = await withSystemDbAccessContext(() => db.select()
Expand All @@ -374,6 +381,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: fixture.partner.id,
orderId: fixture.order.id,
actorUserId: fixture.user.id,
mode: { kind: 'rest' },
});

expect(client.updateSubscriptionQuantity).toHaveBeenCalledWith('subscription-cancel', 15);
Expand Down Expand Up @@ -406,6 +414,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: patchWins.partner.id,
orderId: patchWins.order.id,
actorUserId: patchWins.user.id,
mode: { kind: 'rest' },
});
await new Promise((resolve) => setTimeout(resolve, 25));
expect(patchWinsClient.createOrder).not.toHaveBeenCalled();
Expand All @@ -428,6 +437,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: claimWins.partner.id,
orderId: claimWins.order.id,
actorUserId: claimWins.user.id,
mode: { kind: 'rest' },
});
await expect(updateOrderLine({
partnerId: claimWins.partner.id,
Expand All @@ -441,7 +451,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
const fixture = await seedOrder();
const client = successfulClient();
const service = serviceWithClient(client);
const input = { partnerId: fixture.partner.id, orderId: fixture.order.id, actorUserId: fixture.user.id };
const input = { partnerId: fixture.partner.id, orderId: fixture.order.id, actorUserId: fixture.user.id, mode: { kind: 'rest' as const } };

const results = await Promise.allSettled([service.submitOrder(input), service.submitOrder(input)]);
const failureMessages = results.flatMap((result) => result.status === 'rejected'
Expand Down Expand Up @@ -471,6 +481,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: rejectedFixture.partner.id,
orderId: rejectedFixture.order.id,
actorUserId: rejectedFixture.user.id,
mode: { kind: 'rest' },
});

const [contractLine] = await withSystemDbAccessContext(() =>
Expand All @@ -488,6 +499,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: cancelFixture.partner.id,
orderId: cancelFixture.order.id,
actorUserId: cancelFixture.user.id,
mode: { kind: 'rest' },
});
const [cancelContractLine] = await withSystemDbAccessContext(() =>
db.select().from(contractLines).where(eq(contractLines.id, cancelFixture.contractLine.id)));
Expand All @@ -505,6 +517,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: timeoutFixture.partner.id,
orderId: timeoutFixture.order.id,
actorUserId: timeoutFixture.user.id,
mode: { kind: 'rest' },
});
expect(timeoutResult.lines[0]?.submitState).toBe('needs_reconcile');
await withSystemDbAccessContext(() => db.update(pax8CompanyMappings)
Expand Down Expand Up @@ -535,7 +548,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
pax8OrderId: 'captured-parent',
submittedAt: capturedAt,
}).where(eq(pax8Orders.id, parentGuardFixture.order.id));
await db.update(pax8OrderLines).set({ submitState: 'needs_reconcile' })
await db.update(pax8OrderLines).set({ submitState: 'needs_reconcile', authorizedBaselineQuantity: '0.00' })
.where(eq(pax8OrderLines.id, parentGuardFixture.line.id));
});
const parentGuardBundle = await pax8OrderSubmitRepository.loadReconcileOrder({
Expand Down Expand Up @@ -574,7 +587,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
resultSubscriptionId: 'captured-subscription',
}],
'captured-parent',
)).resolves.toEqual({ resolved: 1, stillUnknown: 0 });
)).resolves.toEqual(expect.objectContaining({ resolved: 1, stillUnknown: 0, billingConflicts: [] }));

const nullParentFixture = await seedOrder();
await withSystemDbAccessContext(async () => {
Expand All @@ -583,7 +596,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
pax8CompanyId: 'company-1',
submittedAt: capturedAt,
}).where(eq(pax8Orders.id, nullParentFixture.order.id));
await db.update(pax8OrderLines).set({ submitState: 'needs_reconcile' })
await db.update(pax8OrderLines).set({ submitState: 'needs_reconcile', authorizedBaselineQuantity: '0.00' })
.where(eq(pax8OrderLines.id, nullParentFixture.line.id));
});
const nullParentBundle = await pax8OrderSubmitRepository.loadReconcileOrder({
Expand All @@ -599,7 +612,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
resultSubscriptionId: 'matched-subscription',
}],
'matched-parent',
)).resolves.toEqual({ resolved: 1, stillUnknown: 0 });
)).resolves.toEqual(expect.objectContaining({ resolved: 1, stillUnknown: 0, billingConflicts: [] }));
const [nullParentOrder] = await withSystemDbAccessContext(() => db.select()
.from(pax8Orders).where(eq(pax8Orders.id, nullParentFixture.order.id)));
expect(nullParentOrder?.pax8OrderId).toBe('matched-parent');
Expand All @@ -617,18 +630,31 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
};
});
const rollbackService = serviceWithClient(rollbackClient);
await expect(rollbackService.submitOrder({
const missingLineResult = await rollbackService.submitOrder({
partnerId: rollbackFixture.partner.id,
orderId: rollbackFixture.order.id,
actorUserId: rollbackFixture.user.id,
})).rejects.toMatchObject({ status: 409 });
mode: { kind: 'rest' },
});
expect(missingLineResult).toMatchObject({
kind: 'executed',
status: 'partially_failed',
lines: [{ lineId: rollbackFixture.line.id, submitState: 'succeeded', error: expect.stringContaining('billing_conflict:line_missing:') }],
billingConflicts: [{ lineId: rollbackFixture.line.id, reason: 'line_missing' }],
});
const rollbackState = await withSystemDbAccessContext(async () => {
const [order] = await db.select().from(pax8Orders).where(eq(pax8Orders.id, rollbackFixture.order.id));
const [line] = await db.select().from(pax8OrderLines).where(eq(pax8OrderLines.id, rollbackFixture.line.id));
return { order, line };
const [contractLine] = await db.select().from(contractLines).where(eq(contractLines.id, rollbackFixture.contractLine.id));
return { order, line, contractLine };
});
expect(rollbackState.order?.status).toBe('submitting');
expect(rollbackState.line?.submitState).toBe('in_flight');
expect(rollbackState.order?.status).toBe('partially_failed');
expect(rollbackState.order?.pax8OrderId).toBe('pax-order-rollback');
expect(rollbackState.line?.submitState).toBe('succeeded');
expect(rollbackState.line?.resultSubscriptionId).toBe('subscription-rollback');
expect(rollbackState.line?.error).toContain('billing_conflict:line_missing:');
expect(rollbackState.contractLine).toBeUndefined(); // billing must not be recreated after deletion
expect(rollbackClient.createOrder).toHaveBeenCalledTimes(2); // one dry run and one vendor send

const duplicateFixture = await seedOrder({ action: 'cancel' });
await withSystemDbAccessContext(() => db.insert(pax8OrderLines).values({
Expand All @@ -646,6 +672,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: duplicateFixture.partner.id,
orderId: duplicateFixture.order.id,
actorUserId: duplicateFixture.user.id,
mode: { kind: 'rest' },
})).rejects.toMatchObject({ status: 422 });
expect(duplicateClient.createOrder).not.toHaveBeenCalled();
expect(duplicateClient.cancelSubscription).not.toHaveBeenCalled();
Expand All @@ -656,6 +683,7 @@ describe('Pax8 submit pipeline (real Postgres)', () => {
partnerId: wrongPartner.id,
orderId: duplicateFixture.order.id,
actorUserId: duplicateFixture.user.id,
mode: { kind: 'rest' },
})).rejects.toMatchObject({ status: 404 });
expect(isolatedClient.createOrder).not.toHaveBeenCalled();

Expand Down
8 changes: 8 additions & 0 deletions apps/api/src/__tests__/parkedFanout.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ function discovered(): string[] {
'services/partnerGroupPreview.ts',
// W15 (#6785): explicitly classify this billing-only service as required by the wave contract.
'services/pax8OrderPreview.ts',
'services/actionIntents/pax8OrderDigest.ts',
]);
for (const file of [...JOB_FILES, ...SERVICE_FILES, ...EE_FILES]) {
const code = read(file);
Expand Down Expand Up @@ -212,6 +213,7 @@ function exemptByPattern(file: string): boolean {

/** Cannot reach a parked device, or must (removal and retention). */
const EXEMPT: Record<string, string> = {
'services/actionIntents/pax8OrderDigest.ts': 'W15 (#6785): one Pax8 order approval digest; billing evidence with no device access or fan-out',
'services/pax8OrderPreview.ts': 'W15 (#6785): read-only Pax8 order billing evidence; no device access or fan-out',
'services/webhookPayload/alertEnrichment.ts': 'enriches the webhook payload of one already-raised alert by that alert\'s own device_id; selects no targets',
'services/manualAssetOps.ts': 'manual (non-networked) inventory CRUD (#4622, lifted from routes/devices/manual.ts): reads agent devices only to resolve the ONE device id a link request names (same-org + same-site check); selects no fan-out targets and runs nothing',
Expand Down Expand Up @@ -495,6 +497,12 @@ describe('contract: background selectors leave parked devices out', () => {
expect(DEVICE_READ.test(read('services/pax8OrderPreview.ts'))).toBe(false);
});

it('W15 classifies the Pax8 order digest as billing evidence with no device fan-out', () => {
expect(found).toContain('services/actionIntents/pax8OrderDigest.ts');
expect(EXEMPT['services/actionIntents/pax8OrderDigest.ts']).toContain('no device');
expect(DEVICE_READ.test(read('services/actionIntents/pax8OrderDigest.ts'))).toBe(false);
});

it('discovers the expected shape of the tree (not vacuous)', () => {
expect(found.length).toBeGreaterThan(60);
expect(found).toContain('jobs/cisJobs.ts');
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/db/schema/pax8.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,13 @@ describe('Pax8 quantity authorization baseline', () => {
expect(migration).toMatch(/action = 'change_quantity'/i);
});
});


it('permits a durable approved billing baseline for every Pax8 line action', () => {
const migration = readFileSync(fileURLToPath(new URL(
'../../../migrations/2027-01-18-192000-pax8-order-line-baseline-any-action.sql', import.meta.url,
)), 'utf8');
expect(migration).toMatch(/DROP CONSTRAINT IF EXISTS pax8_order_lines_authorized_baseline_chk/i);
expect(migration).toMatch(/CHECK \(authorized_baseline_quantity IS NULL OR authorized_baseline_quantity >= 0\)/i);
expect(migration).not.toMatch(/action\s*=|\b(?:INSERT|UPDATE|DELETE)\b/i);
});
3 changes: 3 additions & 0 deletions apps/api/src/db/schema/pax8Orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export const pax8OrderLines = pgTable('pax8_order_lines', {
billingTerm: varchar('billing_term', { length: 20 }),
commitmentTermId: varchar('commitment_term_id', { length: 64 }),
quantity: numeric('quantity', { precision: 12, scale: 2 }),
// Linked contract line manual_quantity observed inside the claim transaction,
// for every action; null when no contract line is linked. Both submit and
// reconcile compare against this durable approved baseline before billing.
authorizedBaselineQuantity: numeric('authorized_baseline_quantity', { precision: 12, scale: 2 }),
provisioningDetails: jsonb('provisioning_details').notNull().default([]),
targetSubscriptionId: varchar('target_subscription_id', { length: 64 }),
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/middleware/automationPrincipal.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export const REVIEWED_AI_AGENT_SITES: Record<string, Classification> = {
// human-only; neither an agent nor a flow is a person who can own the row.
'services/aiToolsAuditBaselines.ts:if (isAutomationPrincipal(auth)) return automationRefusal(action);': 'automation',
"services/aiToolsPamRules.ts:if (isAutomationPrincipal(auth)) return jsonError('PAM policy is changed by a person, not an automated run', 'AGENT_REFUSED');": 'automation',
// Pax8 submit needs a human requester and approval surface; agents and flows
// are both refused before preparing a submit intent.
"services/aiToolsPax8Orders.ts:if (isAutomationPrincipal(auth)) return jsonError('A person must request Pax8 order submission', 'AGENT_HUMAN_ONLY');": 'automation',
'services/aiToolsTicketOpsShared.ts:return isAutomationPrincipal(auth);': 'automation',
// Same upstream-approval-instead-of-MFA rule for delivery-rail writes.
"services/aiToolsDelivery.ts:if (!isAutomationPrincipal(auth) && !hasSatisfiedMfa(auth)) fail(403, 'MFA required');": 'automation',
Expand Down
Loading
Loading