From e6bd4a2752e73a0f8d6673d2a741f000198c219c Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 3 Oct 2026 11:24:37 +0300 Subject: [PATCH 1/5] feat(billing): add limits and funding priority commands --- README.md | 26 +++ src/commands/billing/limits/list.ts | 15 ++ src/commands/billing/limits/set.ts | 38 +++++ src/commands/billing/priority/list.ts | 15 ++ src/commands/billing/priority/reorder.ts | 27 ++++ src/commands/billing/priority/set.ts | 27 ++++ src/services/billing-controls.ts | 194 +++++++++++++++++++++++ test/commands/billing-controls.test.ts | 130 +++++++++++++++ 8 files changed, 472 insertions(+) create mode 100644 src/commands/billing/limits/list.ts create mode 100644 src/commands/billing/limits/set.ts create mode 100644 src/commands/billing/priority/list.ts create mode 100644 src/commands/billing/priority/reorder.ts create mode 100644 src/commands/billing/priority/set.ts create mode 100644 src/services/billing-controls.ts create mode 100644 test/commands/billing-controls.test.ts diff --git a/README.md b/README.md index d83f6f0b..ca0d9a28 100644 --- a/README.md +++ b/README.md @@ -2453,3 +2453,29 @@ project. Human output labels unmeasured duration as `unknown`. `calls view` reads one stored call. Its JSON output includes bounded event history, participant and connection lifetimes, and allowlisted diagnostics. It does not retrieve media or webhook deliveries. + +### Spending limits and funding priority + +With an organization key carrying `billing:read`, inspect current controls: + +```bash +polymorfa billing limits list --project --json +polymorfa billing priority list --json +``` + +Changes require `billing:manage` and Pay-As-You-Go. Supply the observed revision: + +```bash +polymorfa billing limits set --scope number --credits 1000 --expected-revision 0 +polymorfa billing limits set --scope project --unlimited --expected-revision 1 +polymorfa billing priority set --scope number --priority 10 --expected-revision 0 +polymorfa billing priority reorder --scope number --project --ids , --expected-revision 1 +``` + +A reorder includes every active project or every real number in the selected +project, highest first. A stale or incomplete order fails without partial +changes. Monthly limits reset at 00:00 UTC on the first day; zero blocks new +charges and null removes the limit. Existing paid service keeps its window. +Project and client credentials cannot use these financial controls. Commands +use the pinned SDK's authenticated raw transport; no unpublished SDK method is +required. CLI publication and API deployment remain separate release gates. diff --git a/src/commands/billing/limits/list.ts b/src/commands/billing/limits/list.ts new file mode 100644 index 00000000..0e2a2f2b --- /dev/null +++ b/src/commands/billing/limits/list.ts @@ -0,0 +1,15 @@ +import {BaseCommand} from '../../../base-command.js' +import {getLimits, renderLimits} from '../../../services/billing-controls.js' +export default class LimitsList extends BaseCommand { + static override summary = 'Read monthly project and number spending limits.' + static override description = + 'Requires an organization key with billing:read. Use --project to include its number limits. Months reset on the first day at 00:00 UTC.' + async run(): Promise { + const {flags} = await this.parse(LimitsList) + const result = await getLimits( + await this.platformClient(flags), + flags.project, + ) + this.writeValue(result, renderLimits(result.data), flags) + } +} diff --git a/src/commands/billing/limits/set.ts b/src/commands/billing/limits/set.ts new file mode 100644 index 00000000..3ca5a803 --- /dev/null +++ b/src/commands/billing/limits/set.ts @@ -0,0 +1,38 @@ +import {Args, Flags} from '@oclif/core' +import {BaseCommand} from '../../../base-command.js' +import {setLimit, limitValue} from '../../../services/billing-controls.js' +export default class LimitsSet extends BaseCommand { + static override summary = 'Set or remove a monthly spending limit.' + static override description = + 'Requires Pay-As-You-Go and an organization key with billing:manage. Zero blocks new charges; already-paid service continues. A stale observed revision fails without overwriting newer settings.' + static override args = { + id: Args.string({required: true, description: 'Project or number UUID.'}), + } + static override flags = { + scope: Flags.string({options: ['project', 'number'], required: true}), + credits: Flags.string({ + description: 'Monthly Polymorfa credits, up to six decimal places.', + }), + unlimited: Flags.boolean({ + description: 'Remove the limit.', + exclusive: ['credits'], + }), + 'expected-revision': Flags.integer({ + required: true, + min: 0, + description: 'Revision from billing limits list.', + }), + } + async run(): Promise { + const {args, flags} = await this.parse(LimitsSet) + const value = limitValue(flags.credits, flags.unlimited) + const result = await setLimit( + await this.platformClient(flags), + flags.scope as 'project' | 'number', + args.id, + value, + flags['expected-revision'], + ) + this.writeValue(result, 'Spending limit saved.', flags) + } +} diff --git a/src/commands/billing/priority/list.ts b/src/commands/billing/priority/list.ts new file mode 100644 index 00000000..dbccaca7 --- /dev/null +++ b/src/commands/billing/priority/list.ts @@ -0,0 +1,15 @@ +import {BaseCommand} from '../../../base-command.js' +import { + getPriorities, + renderPriorities, +} from '../../../services/billing-controls.js' +export default class PriorityList extends BaseCommand { + static override summary = 'Read project and number funding priorities.' + static override description = + 'Requires an organization key with billing:read. Higher project priority takes precedence, followed by number priority. Includes the revision used for changes.' + async run(): Promise { + const {flags} = await this.parse(PriorityList) + const result = await getPriorities(await this.platformClient(flags)) + this.writeValue(result, renderPriorities(result.data), flags) + } +} diff --git a/src/commands/billing/priority/reorder.ts b/src/commands/billing/priority/reorder.ts new file mode 100644 index 00000000..28a8a050 --- /dev/null +++ b/src/commands/billing/priority/reorder.ts @@ -0,0 +1,27 @@ +import {Flags} from '@oclif/core' +import {BaseCommand} from '../../../base-command.js' +import {reorderPriorities} from '../../../services/billing-controls.js' +export default class PriorityReorder extends BaseCommand { + static override summary = 'Save a complete funding order atomically.' + static override description = + 'Requires Pay-As-You-Go and an organization key with billing:manage. Pass every active project, or every real number in --project, highest first. Incomplete or stale orders fail without partial changes.' + static override flags = { + scope: Flags.string({options: ['project', 'number'], required: true}), + ids: Flags.string({ + required: true, + description: 'Comma-separated UUIDs, highest first.', + }), + 'expected-revision': Flags.integer({required: true, min: 0}), + } + async run(): Promise { + const {flags} = await this.parse(PriorityReorder) + const result = await reorderPriorities( + await this.platformClient(flags), + flags.scope as 'project' | 'number', + flags.ids.split(',').map((id) => id.trim()), + flags['expected-revision'], + flags.project, + ) + this.writeValue(result, 'Priority order saved.', flags) + } +} diff --git a/src/commands/billing/priority/set.ts b/src/commands/billing/priority/set.ts new file mode 100644 index 00000000..91d4b0ca --- /dev/null +++ b/src/commands/billing/priority/set.ts @@ -0,0 +1,27 @@ +import {Args, Flags} from '@oclif/core' +import {BaseCommand} from '../../../base-command.js' +import {setPriority} from '../../../services/billing-controls.js' +export default class PrioritySet extends BaseCommand { + static override summary = 'Set a project or number funding priority.' + static override description = + 'Requires Pay-As-You-Go and an organization key with billing:manage. Changes future renewal protection and recovery; paid windows keep their expiry.' + static override args = { + id: Args.string({required: true, description: 'Project or number UUID.'}), + } + static override flags = { + scope: Flags.string({options: ['project', 'number'], required: true}), + priority: Flags.integer({required: true, min: 0, max: 1000000}), + 'expected-revision': Flags.integer({required: true, min: 0}), + } + async run(): Promise { + const {args, flags} = await this.parse(PrioritySet) + const result = await setPriority( + await this.platformClient(flags), + flags.scope as 'project' | 'number', + args.id, + flags.priority, + flags['expected-revision'], + ) + this.writeValue(result, 'Priority saved.', flags) + } +} diff --git a/src/services/billing-controls.ts b/src/services/billing-controls.ts new file mode 100644 index 00000000..b0f1f9d2 --- /dev/null +++ b/src/services/billing-controls.ts @@ -0,0 +1,194 @@ +import { + type ClientInstance, + PolymorfaValidationError, + PolymorfaServerError, +} from '@polymorfa/sdk' +import {z} from 'zod' + +const uuid = z.uuid() +const revision = z.number().int().min(0).max(2147483646) +const priority = z.number().int().min(0).max(1000000) +export const billingScope = z.enum(['project', 'number']) +const row = z.object({id: uuid, name: z.string(), priority}) +const priorities = z.object({ + revision, + projects: z.array(row), + numbers: z.array(row.extend({projectId: uuid})), +}) +const credits = z.number().finite().nonnegative() +const limits = z.object({ + checkedAt: z.iso.datetime(), + periodStart: z.iso.datetime(), + periodEnd: z.iso.datetime(), + todayCredits: credits, + monthCredits: credits, + daily: z.array(z.object({date: z.iso.date(), credits})), + budgets: z.array( + z.object({ + scope: billingScope, + resourceId: uuid, + projectId: uuid, + name: z.string(), + limitCredits: credits.nullable(), + spentCredits: credits, + reservedCredits: credits, + revision, + }), + ), +}) +export type BillingPriorities = z.infer +export type BillingLimits = z.infer +export function validate(schema: z.ZodType, value: unknown): T { + const parsed = schema.safeParse(value) + if (!parsed.success) + throw new PolymorfaValidationError( + 'Invalid billing control. Use UUIDs and an observed revision.', + {code: 'invalid_billing_control'}, + ) + return parsed.data +} +export function observedRevision(value: number): number { + return validate(revision, value) +} +export function resourceId(value: string): string { + return validate(uuid, value).toLowerCase() +} +export function priorityValue(value: number): number { + return validate(priority, value) +} +export function limitValue( + value: string | undefined, + unlimited: boolean, +): number | null { + if (unlimited && value === undefined) return null + if ( + unlimited || + value === undefined || + !/^\d+(\.\d{1,6})?$/.test(value) || + Number(value) > 1000000 + ) + throw new PolymorfaValidationError( + 'Use --credits 0–1000000 (up to six decimal places) or --unlimited.', + {code: 'invalid_billing_control'}, + ) + return Number(value) +} +async function request( + client: ClientInstance<'organization'>, + method: 'GET' | 'PUT', + path: string, + schema: z.ZodType, + body?: unknown, + projectId?: string, +) { + const response = await client.raw.request<{data: unknown}>({ + method, + path, + ...(body === undefined ? {} : {body}), + ...(projectId === undefined + ? {} + : {query: {projectId: resourceId(projectId)}}), + }) + const parsed = schema.safeParse(response.data.data) + if (!parsed.success) + throw new PolymorfaServerError('Billing response is invalid.', { + code: 'invalid_response', + status: response.metadata.status, + }) + return {data: parsed.data, metadata: response.metadata} +} +export function getLimits( + client: ClientInstance<'organization'>, + projectId?: string, +) { + return request( + client, + 'GET', + '/platform/billing/limits', + limits, + undefined, + projectId, + ) +} +export function setLimit( + client: ClientInstance<'organization'>, + scope: 'project' | 'number', + id: string, + credits: number | null, + expectedRevision: number, +) { + return request( + client, + 'PUT', + `/platform/billing/limits/${validate(billingScope, scope)}/${resourceId(id)}`, + z.object({saved: z.boolean()}), + { + limitCredits: credits, + expectedRevision: observedRevision(expectedRevision), + }, + ) +} +export function getPriorities(client: ClientInstance<'organization'>) { + return request(client, 'GET', '/platform/billing/priorities', priorities) +} +export function setPriority( + client: ClientInstance<'organization'>, + scope: 'project' | 'number', + id: string, + value: number, + expectedRevision: number, +) { + return request( + client, + 'PUT', + `/platform/billing/priorities/${validate(billingScope, scope)}/${resourceId(id)}`, + priorities, + { + priority: priorityValue(value), + expectedRevision: observedRevision(expectedRevision), + }, + ) +} +export function reorderPriorities( + client: ClientInstance<'organization'>, + scope: 'project' | 'number', + ids: string[], + expectedRevision: number, + projectId?: string, +) { + validate(billingScope, scope) + const resourceIds = ids.map(resourceId) + if ( + new Set(resourceIds).size !== resourceIds.length || + (scope === 'number' && !projectId) || + (scope === 'project' && projectId) + ) + throw new PolymorfaValidationError( + 'Use unique IDs; --project is required only for number order.', + {code: 'invalid_billing_control'}, + ) + return request(client, 'PUT', '/platform/billing/priorities', priorities, { + scope, + resourceIds, + expectedRevision: observedRevision(expectedRevision), + ...(projectId ? {projectId: resourceId(projectId)} : {}), + }) +} +export function renderLimits(data: BillingLimits): string { + return data.budgets + .map( + (b) => + `${b.scope}\t${b.resourceId}\t${b.name}\t${b.spentCredits + b.reservedCredits}/${b.limitCredits ?? 'unlimited'} credits\trevision ${b.revision}`, + ) + .join('\n') +} +export function renderPriorities(data: BillingPriorities): string { + return [ + `Revision: ${data.revision}`, + ...data.projects.map((p) => `project\t${p.id}\t${p.name}\t${p.priority}`), + ...data.numbers.map( + (n) => + `number\t${n.id}\t${n.name}\t${n.priority}\tproject ${n.projectId}`, + ), + ].join('\n') +} diff --git a/test/commands/billing-controls.test.ts b/test/commands/billing-controls.test.ts new file mode 100644 index 00000000..f0ace40b --- /dev/null +++ b/test/commands/billing-controls.test.ts @@ -0,0 +1,130 @@ +import {runCommand} from '@oclif/test' +import {afterEach, expect, test} from 'vitest' +import { + startHttpSequenceServer, + withEnvironment, + type TestHttpServer, +} from '../helpers/http-server.js' +const key = + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +const project = '22222222-2222-4222-8222-222222222222', + number = '33333333-3333-4333-8333-333333333333' +const priorities = { + revision: 5, + projects: [{id: project, name: 'Support', priority: 1}], + numbers: [{id: number, projectId: project, name: 'Desk', priority: 2}], +} +let server: TestHttpServer +async function run(args: string[]) { + return withEnvironment( + { + POLYMORFA_API_KEY: key, + POLYMORFA_BASE_URL: server.baseUrl, + POLYMORFA_CLIENT_TOKEN: undefined, + POLYMORFA_PROJECT_TOKEN: undefined, + }, + () => runCommand(args), + ) +} +afterEach(async () => { + await server?.close() +}) +test('reads priorities and saves a complete number order with the observed revision', async () => { + server = await startHttpSequenceServer([ + {body: {success: true, data: priorities}}, + {body: {success: true, data: priorities}}, + ]) + expect( + (await run(['billing', 'priority', 'list', '--json'])).error, + ).toBeUndefined() + expect( + ( + await run([ + 'billing', + 'priority', + 'reorder', + '--scope', + 'number', + '--project', + project, + '--ids', + number, + '--expected-revision', + '4', + '--json', + ]) + ).error, + ).toBeUndefined() + expect(server.requests[0]?.url).toBe('/platform/billing/priorities') + expect(JSON.parse(server.requests[1]!.body)).toEqual({ + scope: 'number', + projectId: project, + resourceIds: [number], + expectedRevision: 4, + }) +}) +test('removes a cap explicitly and blocks malformed controls before sending a request', async () => { + server = await startHttpSequenceServer([ + {body: {success: true, data: {saved: true}}}, + ]) + expect( + ( + await run([ + 'billing', + 'limits', + 'set', + number, + '--scope', + 'number', + '--unlimited', + '--expected-revision', + '0', + '--json', + ]) + ).error, + ).toBeUndefined() + expect(JSON.parse(server.requests[0]!.body)).toEqual({ + limitCredits: null, + expectedRevision: 0, + }) + const invalid = await run([ + 'billing', + 'limits', + 'set', + number, + '--scope', + 'number', + '--credits', + '0.0000001', + '--expected-revision', + '0', + ]) + expect(invalid.error).toBeDefined() + expect(server.requests).toHaveLength(1) +}) +test('reports a revision conflict without retrying or claiming success', async () => { + server = await startHttpSequenceServer([ + { + status: 409, + body: { + success: false, + error: {code: 'conflict', message: 'priority changed; reload'}, + }, + }, + ]) + const result = await run([ + 'billing', + 'priority', + 'set', + project, + '--scope', + 'project', + '--priority', + '9', + '--expected-revision', + '0', + ]) + expect(result.error).toBeDefined() + expect(result.stdout).not.toContain('Priority saved') + expect(server.requests).toHaveLength(1) +}) From f80b46543c6eb6987ec6ae422fc7d4353136d380 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 3 Oct 2026 11:31:10 +0300 Subject: [PATCH 2/5] test(billing): match the public response envelope --- test/commands/billing-controls.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/commands/billing-controls.test.ts b/test/commands/billing-controls.test.ts index f0ace40b..869ef06b 100644 --- a/test/commands/billing-controls.test.ts +++ b/test/commands/billing-controls.test.ts @@ -31,8 +31,8 @@ afterEach(async () => { }) test('reads priorities and saves a complete number order with the observed revision', async () => { server = await startHttpSequenceServer([ - {body: {success: true, data: priorities}}, - {body: {success: true, data: priorities}}, + {body: {data: priorities}}, + {body: {data: priorities}}, ]) expect( (await run(['billing', 'priority', 'list', '--json'])).error, @@ -64,9 +64,7 @@ test('reads priorities and saves a complete number order with the observed revis }) }) test('removes a cap explicitly and blocks malformed controls before sending a request', async () => { - server = await startHttpSequenceServer([ - {body: {success: true, data: {saved: true}}}, - ]) + server = await startHttpSequenceServer([{body: {data: {saved: true}}}]) expect( ( await run([ From b7b914deb98849d3f71d4034bf3089e9b5e15bb0 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 3 Oct 2026 13:41:16 +0300 Subject: [PATCH 3/5] Add native resource controls and number creation budget flags --- README.md | 17 +++++- src/commands/billing/controls/get.ts | 24 +++++++++ src/commands/billing/controls/set.ts | 38 ++++++++++++++ src/commands/billing/limits/set.ts | 7 ++- src/commands/billing/priority/reorder.ts | 7 ++- src/commands/billing/priority/set.ts | 7 ++- src/commands/quicklink/create.ts | 24 +++++++++ src/services/billing-controls.ts | 57 +++++++++++++++++--- src/services/quicklinks.ts | 26 ++++++++++ test/commands/billing-controls.test.ts | 66 ++++++++++++++++++++++++ test/commands/quicklinks.test.ts | 43 +++++++++++++++ 11 files changed, 303 insertions(+), 13 deletions(-) create mode 100644 src/commands/billing/controls/get.ts create mode 100644 src/commands/billing/controls/set.ts diff --git a/README.md b/README.md index ca0d9a28..0c0eb935 100644 --- a/README.md +++ b/README.md @@ -2472,10 +2472,25 @@ polymorfa billing priority set --scope number --priority 10 --expe polymorfa billing priority reorder --scope number --project --ids , --expected-revision 1 ``` -A reorder includes every active project or every real number in the selected +A reorder includes every active project, customer or real number in the selected project, highest first. A stale or incomplete order fails without partial changes. Monthly limits reset at 00:00 UTC on the first day; zero blocks new charges and null removes the limit. Existing paid service keeps its window. Project and client credentials cannot use these financial controls. Commands use the pinned SDK's authenticated raw transport; no unpublished SDK method is required. CLI publication and API deployment remain separate release gates. + + +Read or update one resource without loading the team directory: + +```bash +polymorfa billing controls get --scope customer --json +polymorfa billing controls set --scope customer --credits 1500 --priority 25 --expected-budget-revision 0 --expected-priority-revision 0 +polymorfa quicklink create --project --limit-credits 250 --priority 30 --json +``` + +The combined save is atomic and requires both observed revisions. Project +priority wins first, then customer priority, then number priority. A customer's +assigned numbers share its monthly cap. Creation controls apply to a real new +number before pairing admission; testing and supplementary connections reject +these fields. Omit them to keep the default unlimited cap and priority zero. diff --git a/src/commands/billing/controls/get.ts b/src/commands/billing/controls/get.ts new file mode 100644 index 00000000..8bd6d067 --- /dev/null +++ b/src/commands/billing/controls/get.ts @@ -0,0 +1,24 @@ +import {Args, Flags} from '@oclif/core' +import {BaseCommand} from '../../../base-command.js' +import {getResourceControls} from '../../../services/billing-controls.js' +export default class ControlsGet extends BaseCommand { + static override summary = 'Read one resource limit and priority.' + static override args = { + id: Args.string({required: true, description: 'Resource UUID.'}), + } + static override flags = { + scope: Flags.string({ + required: true, + options: ['project', 'customer', 'number'], + }), + } + async run() { + const {args, flags} = await this.parse(ControlsGet) + const result = await getResourceControls( + await this.platformClient(flags), + flags.scope as 'project' | 'customer' | 'number', + args.id, + ) + this.writeValue(result, JSON.stringify(result.data), flags) + } +} diff --git a/src/commands/billing/controls/set.ts b/src/commands/billing/controls/set.ts new file mode 100644 index 00000000..e1303ae2 --- /dev/null +++ b/src/commands/billing/controls/set.ts @@ -0,0 +1,38 @@ +import {Args, Flags} from '@oclif/core' +import {BaseCommand} from '../../../base-command.js' +import { + setResourceControls, + limitValue, +} from '../../../services/billing-controls.js' +export default class ControlsSet extends BaseCommand { + static override summary = 'Atomically save one resource limit and priority.' + static override args = { + id: Args.string({required: true, description: 'Resource UUID.'}), + } + static override flags = { + scope: Flags.string({ + required: true, + options: ['project', 'customer', 'number'], + }), + credits: Flags.string(), + unlimited: Flags.boolean({exclusive: ['credits']}), + priority: Flags.integer({required: true, min: 0, max: 1000000}), + 'expected-budget-revision': Flags.integer({required: true, min: 0}), + 'expected-priority-revision': Flags.integer({required: true, min: 0}), + } + async run() { + const {args, flags} = await this.parse(ControlsSet) + const result = await setResourceControls( + await this.platformClient(flags), + flags.scope as 'project' | 'customer' | 'number', + args.id, + { + limitCredits: limitValue(flags.credits, flags.unlimited), + priority: flags.priority, + expectedBudgetRevision: flags['expected-budget-revision'], + expectedPriorityRevision: flags['expected-priority-revision'], + }, + ) + this.writeValue(result, 'Limits and priority saved.', flags) + } +} diff --git a/src/commands/billing/limits/set.ts b/src/commands/billing/limits/set.ts index 3ca5a803..9e321787 100644 --- a/src/commands/billing/limits/set.ts +++ b/src/commands/billing/limits/set.ts @@ -9,7 +9,10 @@ export default class LimitsSet extends BaseCommand { id: Args.string({required: true, description: 'Project or number UUID.'}), } static override flags = { - scope: Flags.string({options: ['project', 'number'], required: true}), + scope: Flags.string({ + options: ['project', 'customer', 'number'], + required: true, + }), credits: Flags.string({ description: 'Monthly Polymorfa credits, up to six decimal places.', }), @@ -28,7 +31,7 @@ export default class LimitsSet extends BaseCommand { const value = limitValue(flags.credits, flags.unlimited) const result = await setLimit( await this.platformClient(flags), - flags.scope as 'project' | 'number', + flags.scope as 'project' | 'customer' | 'number', args.id, value, flags['expected-revision'], diff --git a/src/commands/billing/priority/reorder.ts b/src/commands/billing/priority/reorder.ts index 28a8a050..ba362a3d 100644 --- a/src/commands/billing/priority/reorder.ts +++ b/src/commands/billing/priority/reorder.ts @@ -6,7 +6,10 @@ export default class PriorityReorder extends BaseCommand { static override description = 'Requires Pay-As-You-Go and an organization key with billing:manage. Pass every active project, or every real number in --project, highest first. Incomplete or stale orders fail without partial changes.' static override flags = { - scope: Flags.string({options: ['project', 'number'], required: true}), + scope: Flags.string({ + options: ['project', 'customer', 'number'], + required: true, + }), ids: Flags.string({ required: true, description: 'Comma-separated UUIDs, highest first.', @@ -17,7 +20,7 @@ export default class PriorityReorder extends BaseCommand { const {flags} = await this.parse(PriorityReorder) const result = await reorderPriorities( await this.platformClient(flags), - flags.scope as 'project' | 'number', + flags.scope as 'project' | 'customer' | 'number', flags.ids.split(',').map((id) => id.trim()), flags['expected-revision'], flags.project, diff --git a/src/commands/billing/priority/set.ts b/src/commands/billing/priority/set.ts index 91d4b0ca..4058883e 100644 --- a/src/commands/billing/priority/set.ts +++ b/src/commands/billing/priority/set.ts @@ -9,7 +9,10 @@ export default class PrioritySet extends BaseCommand { id: Args.string({required: true, description: 'Project or number UUID.'}), } static override flags = { - scope: Flags.string({options: ['project', 'number'], required: true}), + scope: Flags.string({ + options: ['project', 'customer', 'number'], + required: true, + }), priority: Flags.integer({required: true, min: 0, max: 1000000}), 'expected-revision': Flags.integer({required: true, min: 0}), } @@ -17,7 +20,7 @@ export default class PrioritySet extends BaseCommand { const {args, flags} = await this.parse(PrioritySet) const result = await setPriority( await this.platformClient(flags), - flags.scope as 'project' | 'number', + flags.scope as 'project' | 'customer' | 'number', args.id, flags.priority, flags['expected-revision'], diff --git a/src/commands/quicklink/create.ts b/src/commands/quicklink/create.ts index 345efe7e..a06f7768 100644 --- a/src/commands/quicklink/create.ts +++ b/src/commands/quicklink/create.ts @@ -2,6 +2,7 @@ import {PolymorfaConfigurationError} from '@polymorfa/sdk' import {Flags} from '@oclif/core' import {readJsonInput} from '../../api/request.js' import {BaseCommand} from '../../base-command.js' +import {limitValue} from '../../services/billing-controls.js' import {createQuickLink} from '../../services/quicklinks.js' export default class QuickLinkCreate extends BaseCommand { @@ -23,6 +24,15 @@ export default class QuickLinkCreate extends BaseCommand { options: ['linked_devices', 'official_api'] as const, description: 'Missing connection to add.', }), + 'limit-credits': Flags.string({ + description: + 'Initial monthly limit in credits; team billing:manage credential required.', + }), + priority: Flags.integer({ + min: 0, + max: 1000000, + description: 'Initial number funding priority.', + }), input: Flags.string({ description: 'JSON request file or - for stdin. Use configuration for connection, phone, history and simulation.', @@ -51,6 +61,20 @@ export default class QuickLinkCreate extends BaseCommand { ) body[key] = value } + if (flags['limit-credits'] !== undefined || flags.priority !== undefined) { + if (body.billingControls !== undefined) + throw new PolymorfaConfigurationError( + 'Use either input billingControls or financial flags.', + 'input', + ) + body.billingControls = { + limitCredits: + flags['limit-credits'] === undefined + ? null + : limitValue(flags['limit-credits'], false), + priority: flags.priority ?? 0, + } + } if ( body.purpose === 'add_connection' && (!body.session || !body.addConnection || body.connectionGoal === 'single') diff --git a/src/services/billing-controls.ts b/src/services/billing-controls.ts index b0f1f9d2..2a0003ef 100644 --- a/src/services/billing-controls.ts +++ b/src/services/billing-controls.ts @@ -8,11 +8,12 @@ import {z} from 'zod' const uuid = z.uuid() const revision = z.number().int().min(0).max(2147483646) const priority = z.number().int().min(0).max(1000000) -export const billingScope = z.enum(['project', 'number']) +export const billingScope = z.enum(['project', 'customer', 'number']) const row = z.object({id: uuid, name: z.string(), priority}) const priorities = z.object({ revision, projects: z.array(row), + customers: z.array(row.extend({projectId: uuid})), numbers: z.array(row.extend({projectId: uuid})), }) const credits = z.number().finite().nonnegative() @@ -112,7 +113,7 @@ export function getLimits( } export function setLimit( client: ClientInstance<'organization'>, - scope: 'project' | 'number', + scope: 'project' | 'customer' | 'number', id: string, credits: number | null, expectedRevision: number, @@ -133,7 +134,7 @@ export function getPriorities(client: ClientInstance<'organization'>) { } export function setPriority( client: ClientInstance<'organization'>, - scope: 'project' | 'number', + scope: 'project' | 'customer' | 'number', id: string, value: number, expectedRevision: number, @@ -151,7 +152,7 @@ export function setPriority( } export function reorderPriorities( client: ClientInstance<'organization'>, - scope: 'project' | 'number', + scope: 'project' | 'customer' | 'number', ids: string[], expectedRevision: number, projectId?: string, @@ -160,11 +161,11 @@ export function reorderPriorities( const resourceIds = ids.map(resourceId) if ( new Set(resourceIds).size !== resourceIds.length || - (scope === 'number' && !projectId) || + (scope !== 'project' && !projectId) || (scope === 'project' && projectId) ) throw new PolymorfaValidationError( - 'Use unique IDs; --project is required only for number order.', + 'Use unique IDs; --project is required for customer or number order.', {code: 'invalid_billing_control'}, ) return request(client, 'PUT', '/platform/billing/priorities', priorities, { @@ -192,3 +193,47 @@ export function renderPriorities(data: BillingPriorities): string { ), ].join('\n') } + +const resourceControls = z.object({ + budget: limits.shape.budgets.element, + priority, + priorityRevision: revision, +}) +export function getResourceControls( + client: ClientInstance<'organization'>, + scope: 'project' | 'customer' | 'number', + id: string, +) { + return request( + client, + 'GET', + `/platform/billing/controls/${validate(billingScope, scope)}/${resourceId(id)}`, + resourceControls, + ) +} +export function setResourceControls( + client: ClientInstance<'organization'>, + scope: 'project' | 'customer' | 'number', + id: string, + input: { + limitCredits: number | null + priority: number + expectedBudgetRevision: number + expectedPriorityRevision: number + }, +) { + return request( + client, + 'PUT', + `/platform/billing/controls/${validate(billingScope, scope)}/${resourceId(id)}`, + resourceControls, + { + limitCredits: input.limitCredits, + priority: priorityValue(input.priority), + expectedBudgetRevision: observedRevision(input.expectedBudgetRevision), + expectedPriorityRevision: observedRevision( + input.expectedPriorityRevision, + ), + }, + ) +} diff --git a/src/services/quicklinks.ts b/src/services/quicklinks.ts index 7b2c73c2..224c3039 100644 --- a/src/services/quicklinks.ts +++ b/src/services/quicklinks.ts @@ -12,6 +12,7 @@ export async function createQuickLink( ) const body = input as Record const allowed = new Set([ + 'billingControls', 'projectId', 'purpose', 'connectionGoal', @@ -31,6 +32,31 @@ export async function createQuickLink( 'Input projectId conflicts with --project.', 'project', ) + if (body.billingControls !== undefined) { + const financial = body.billingControls as Record + if ( + !financial || + typeof financial !== 'object' || + Array.isArray(financial) || + Object.keys(financial).some( + (key) => !['limitCredits', 'priority'].includes(key), + ) || + (financial.limitCredits !== null && + (typeof financial.limitCredits !== 'number' || + !/^\d+(\.\d{1,6})?$/.test(String(financial.limitCredits)) || + financial.limitCredits > 1000000)) || + typeof financial.priority !== 'number' || + !Number.isInteger(financial.priority) || + financial.priority < 0 || + financial.priority > 1000000 || + body.purpose === 'add_connection' || + (body.configuration as {testing?: unknown} | undefined)?.testing + ) + throw new PolymorfaConfigurationError( + 'Billing controls require a real new number, credits and numeric priority.', + 'input', + ) + } const response = await client.quickLinks.create({ ...body, ...(projectId ? {projectId} : {}), diff --git a/test/commands/billing-controls.test.ts b/test/commands/billing-controls.test.ts index 869ef06b..9b90d1e4 100644 --- a/test/commands/billing-controls.test.ts +++ b/test/commands/billing-controls.test.ts @@ -12,6 +12,7 @@ const project = '22222222-2222-4222-8222-222222222222', const priorities = { revision: 5, projects: [{id: project, name: 'Support', priority: 1}], + customers: [], numbers: [{id: number, projectId: project, name: 'Desk', priority: 2}], } let server: TestHttpServer @@ -126,3 +127,68 @@ test('reports a revision conflict without retrying or claiming success', async ( expect(result.stdout).not.toContain('Priority saved') expect(server.requests).toHaveLength(1) }) + +test('reads one customer and saves both observed revisions', async () => { + const state = { + budget: { + scope: 'customer', + resourceId: number, + projectId: project, + name: 'Acme', + limitCredits: 100, + spentCredits: 0, + reservedCredits: 0, + revision: 4, + }, + priority: 10, + priorityRevision: 5, + } + server = await startHttpSequenceServer([ + {body: {data: state}}, + {body: {data: state}}, + ]) + expect( + ( + await run([ + 'billing', + 'controls', + 'get', + number, + '--scope', + 'customer', + '--json', + ]) + ).error, + ).toBeUndefined() + expect( + ( + await run([ + 'billing', + 'controls', + 'set', + number, + '--scope', + 'customer', + '--credits', + '200', + '--priority', + '40', + '--expected-budget-revision', + '4', + '--expected-priority-revision', + '5', + '--json', + ]) + ).error, + ).toBeUndefined() + expect(server.requests.map((r) => [r.method, r.url])).toEqual([ + ['GET', `/platform/billing/controls/customer/${number}`], + ['PUT', `/platform/billing/controls/customer/${number}`], + ]) + expect(JSON.parse(server.requests[1]!.body)).toEqual({ + limitCredits: 200, + priority: 40, + expectedBudgetRevision: 4, + expectedPriorityRevision: 5, + }) +}) diff --git a/test/commands/quicklinks.test.ts b/test/commands/quicklinks.test.ts index d47e77d8..c05894a5 100644 --- a/test/commands/quicklinks.test.ts +++ b/test/commands/quicklinks.test.ts @@ -196,3 +196,46 @@ test('reports completed QuickLink cancellation after a synchronous response', as function exitCode(error: unknown): number | undefined { return (error as {oclif?: {exit?: number}} | undefined)?.oclif?.exit } + +test('includes initial number financial controls and rejects invalid precision before transport', async () => { + const server = await startHttpSequenceServer([ + { + body: { + success: true, + data: { + id: 'ql_budget', + url: 'https://example.test/connect/budget', + session: 'reserved', + expiresAt: null, + }, + }, + }, + ]) + try { + await withEnvironment( + { + POLYMORFA_API_KEY: apiKey, + POLYMORFA_BASE_URL: server.baseUrl, + POLYMORFA_CLIENT_TOKEN: undefined, + POLYMORFA_PROJECT_TOKEN: undefined, + }, + async () => { + const result = await runCommand( + `quicklink create --project ${projectId} --limit-credits 250 --priority 30 --json`, + ) + expect(result.error, result.stderr).toBeUndefined() + expect(JSON.parse(server.requests[0]!.body)).toMatchObject({ + projectId, + billingControls: {limitCredits: 250, priority: 30}, + }) + const invalid = await runCommand( + `quicklink create --project ${projectId} --limit-credits 0.0000001 --priority 30 --json`, + ) + expect(invalid.error).toBeDefined() + expect(server.requests).toHaveLength(1) + }, + ) + } finally { + await server.close() + } +}) From 80ff9a43c44eeb85ca7478acf91a6ea5834d27a6 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 3 Oct 2026 14:56:16 +0300 Subject: [PATCH 4/5] feat(billing): support mixed funding orders from JSON files Align funding-control requests with API source ba09326dd3c6688f05320c1f7685aeb0290a0ed2. Add project-filtered reads, customer output and mixed resource ordering through the published SDK raw transport. Test 2,000-resource file input and reject malformed, missing, duplicate or conflicting inputs before HTTP. --- README.md | 28 +++- src/commands/billing/priority/list.ts | 10 +- src/commands/billing/priority/reorder.ts | 33 ++++- src/services/billing-controls.ts | 47 +++++- test/commands/billing-controls.test.ts | 177 +++++++++++++++++++++++ 5 files changed, 281 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0c0eb935..ab4b2be1 100644 --- a/README.md +++ b/README.md @@ -2479,6 +2479,8 @@ charges and null removes the limit. Existing paid service keeps its window. Project and client credentials cannot use these financial controls. Commands use the pinned SDK's authenticated raw transport; no unpublished SDK method is required. CLI publication and API deployment remain separate release gates. +This source follows the financial-control contract at API commit +`ba09326dd3c6688f05320c1f7685aeb0290a0ed2`. Read or update one resource without loading the team directory: @@ -2490,7 +2492,29 @@ polymorfa quicklink create --project --limit-credits 250 --priori ``` The combined save is atomic and requires both observed revisions. Project -priority wins first, then customer priority, then number priority. A customer's -assigned numbers share its monthly cap. Creation controls apply to a real new +priority wins first. Numbers inherit customer priority; a higher number +priority overrides it within the project. A customer's assigned numbers share +its monthly cap. Creation controls apply to a real new number before pairing admission; testing and supplementary connections reject these fields. Omit them to keep the default unlimited cap and priority zero. + +Read one project's funding controls and save its combined customer and number +order: + +```bash +polymorfa billing priority list --project +polymorfa billing priority reorder --scope resource --project --ids number:,customer: --expected-revision 5 +``` + +Include every active customer and real number, highest priority first. For +thousands of resources, put the complete order in a JSON file: + +```json +["number:", "customer:"] +``` + +```bash +polymorfa billing priority reorder --scope resource --project --ids-file order.json --expected-revision 5 +``` + +Use either `--ids` or `--ids-file`. Funding order never bypasses a spending cap. diff --git a/src/commands/billing/priority/list.ts b/src/commands/billing/priority/list.ts index dbccaca7..de5a123e 100644 --- a/src/commands/billing/priority/list.ts +++ b/src/commands/billing/priority/list.ts @@ -4,12 +4,16 @@ import { renderPriorities, } from '../../../services/billing-controls.js' export default class PriorityList extends BaseCommand { - static override summary = 'Read project and number funding priorities.' + static override summary = + 'Read project, customer and number funding priorities.' static override description = - 'Requires an organization key with billing:read. Higher project priority takes precedence, followed by number priority. Includes the revision used for changes.' + 'Requires an organization key with billing:read. Project priority takes precedence. Numbers inherit customer priority; a higher number priority overrides it. Use --project for its funding controls. Includes the revision used for changes.' async run(): Promise { const {flags} = await this.parse(PriorityList) - const result = await getPriorities(await this.platformClient(flags)) + const result = await getPriorities( + await this.platformClient(flags), + flags.project, + ) this.writeValue(result, renderPriorities(result.data), flags) } } diff --git a/src/commands/billing/priority/reorder.ts b/src/commands/billing/priority/reorder.ts index ba362a3d..7d9aff3f 100644 --- a/src/commands/billing/priority/reorder.ts +++ b/src/commands/billing/priority/reorder.ts @@ -1,27 +1,48 @@ +import {readFile} from 'node:fs/promises' +import {PolymorfaValidationError} from '@polymorfa/sdk' import {Flags} from '@oclif/core' import {BaseCommand} from '../../../base-command.js' import {reorderPriorities} from '../../../services/billing-controls.js' export default class PriorityReorder extends BaseCommand { static override summary = 'Save a complete funding order atomically.' static override description = - 'Requires Pay-As-You-Go and an organization key with billing:manage. Pass every active project, or every real number in --project, highest first. Incomplete or stale orders fail without partial changes.' + 'Requires Pay-As-You-Go and an organization key with billing:manage. Pass every active project, customer or real number for its scope. Resource scope mixes all customers and numbers in --project, highest first. Incomplete or stale orders fail without partial changes.' static override flags = { scope: Flags.string({ - options: ['project', 'customer', 'number'], + options: ['project', 'customer', 'number', 'resource'], required: true, }), ids: Flags.string({ - required: true, - description: 'Comma-separated UUIDs, highest first.', + exclusive: ['ids-file'], + description: + 'UUIDs, highest first. For resource scope use customer:UUID or number:UUID.', + }), + 'ids-file': Flags.string({ + exclusive: ['ids'], + description: 'JSON array of ordered IDs; supports large lists.', }), 'expected-revision': Flags.integer({required: true, min: 0}), } async run(): Promise { const {flags} = await this.parse(PriorityReorder) + let ids: unknown = flags.ids?.split(',').map((id) => id.trim()) + if (flags['ids-file']) { + try { + ids = JSON.parse(await readFile(flags['ids-file'], 'utf8')) + } catch { + throw new PolymorfaValidationError( + 'Could not read an ordered JSON array from --ids-file.', + ) + } + } + if (!Array.isArray(ids) || !ids.every((id) => typeof id === 'string')) + throw new PolymorfaValidationError( + 'Use --ids or --ids-file with a complete ordered list.', + ) const result = await reorderPriorities( await this.platformClient(flags), - flags.scope as 'project' | 'customer' | 'number', - flags.ids.split(',').map((id) => id.trim()), + flags.scope as 'project' | 'customer' | 'number' | 'resource', + ids, flags['expected-revision'], flags.project, ) diff --git a/src/services/billing-controls.ts b/src/services/billing-controls.ts index 2a0003ef..8062a6ec 100644 --- a/src/services/billing-controls.ts +++ b/src/services/billing-controls.ts @@ -129,8 +129,18 @@ export function setLimit( }, ) } -export function getPriorities(client: ClientInstance<'organization'>) { - return request(client, 'GET', '/platform/billing/priorities', priorities) +export function getPriorities( + client: ClientInstance<'organization'>, + projectId?: string, +) { + return request( + client, + 'GET', + '/platform/billing/priorities', + priorities, + undefined, + projectId, + ) } export function setPriority( client: ClientInstance<'organization'>, @@ -152,14 +162,41 @@ export function setPriority( } export function reorderPriorities( client: ClientInstance<'organization'>, - scope: 'project' | 'customer' | 'number', + scope: 'project' | 'customer' | 'number' | 'resource', ids: string[], expectedRevision: number, projectId?: string, ) { + if (scope === 'resource') { + if (!projectId) + throw new PolymorfaValidationError( + '--project is required for customer and number funding controls.', + ) + const resources = ids.map((value) => { + const [kind, id, ...extra] = value.split(':') + if ((kind !== 'customer' && kind !== 'number') || !id || extra.length) + throw new PolymorfaValidationError( + 'Use customer:UUID or number:UUID, highest first.', + ) + return {scope: kind, resourceId: resourceId(id)} + }) + if ( + resources.length > 1000000 || + new Set(resources.map((r) => `${r.scope}:${r.resourceId}`)).size !== + resources.length + ) + throw new PolymorfaValidationError('Use unique resources.') + return request(client, 'PUT', '/platform/billing/priorities', priorities, { + scope, + projectId: resourceId(projectId), + resources, + expectedRevision: observedRevision(expectedRevision), + }) + } validate(billingScope, scope) const resourceIds = ids.map(resourceId) if ( + resourceIds.length > 1000000 || new Set(resourceIds).size !== resourceIds.length || (scope !== 'project' && !projectId) || (scope === 'project' && projectId) @@ -187,6 +224,10 @@ export function renderPriorities(data: BillingPriorities): string { return [ `Revision: ${data.revision}`, ...data.projects.map((p) => `project\t${p.id}\t${p.name}\t${p.priority}`), + ...data.customers.map( + (c) => + `customer\t${c.id}\t${c.name}\t${c.priority}\tproject ${c.projectId}`, + ), ...data.numbers.map( (n) => `number\t${n.id}\t${n.name}\t${n.priority}\tproject ${n.projectId}`, diff --git a/test/commands/billing-controls.test.ts b/test/commands/billing-controls.test.ts index 9b90d1e4..1841264b 100644 --- a/test/commands/billing-controls.test.ts +++ b/test/commands/billing-controls.test.ts @@ -1,3 +1,6 @@ +import {mkdir, mkdtemp, rm, writeFile} from 'node:fs/promises' +import {homedir} from 'node:os' +import {join} from 'node:path' import {runCommand} from '@oclif/test' import {afterEach, expect, test} from 'vitest' import { @@ -16,6 +19,18 @@ const priorities = { numbers: [{id: number, projectId: project, name: 'Desk', priority: 2}], } let server: TestHttpServer +let directory: string | undefined +async function orderFile(content: string) { + const parent = join( + homedir(), + '.polymorfa-agent-work/project-billing-dashboard/cli-test-artifacts', + ) + await mkdir(parent, {recursive: true}) + directory ??= await mkdtemp(join(parent, 'priority-order-')) + const path = join(directory, 'order.json') + await writeFile(path, content) + return path +} async function run(args: string[]) { return withEnvironment( { @@ -29,6 +44,10 @@ async function run(args: string[]) { } afterEach(async () => { await server?.close() + if (directory) { + await rm(directory, {recursive: true, force: true}) + directory = undefined + } }) test('reads priorities and saves a complete number order with the observed revision', async () => { server = await startHttpSequenceServer([ @@ -192,3 +211,161 @@ test('reads one customer and saves both observed revisions', async () => { expectedPriorityRevision: 5, }) }) + +test('reads a selected project and saves mixed customer/number funding controls', async () => { + server = await startHttpSequenceServer([ + {body: {data: priorities}}, + {body: {data: priorities}}, + ]) + expect( + (await run(['billing', 'priority', 'list', '--project', project, '--json'])) + .error, + ).toBeUndefined() + expect( + ( + await run([ + 'billing', + 'priority', + 'reorder', + '--scope', + 'resource', + '--project', + project, + '--ids', + `number:${number},customer:${project}`, + '--expected-revision', + '5', + '--json', + ]) + ).error, + ).toBeUndefined() + expect(server.requests[0]?.url).toBe( + `/platform/billing/priorities?projectId=${project}`, + ) + expect(JSON.parse(server.requests[1]!.body)).toEqual({ + scope: 'resource', + projectId: project, + resources: [ + {scope: 'number', resourceId: number}, + {scope: 'customer', resourceId: project}, + ], + expectedRevision: 5, + }) + expect( + ( + await run([ + 'billing', + 'priority', + 'reorder', + '--scope', + 'resource', + '--project', + project, + '--ids', + `number:${number},number:${number}`, + '--expected-revision', + '5', + ]) + ).error, + ).toBeDefined() + expect(server.requests).toHaveLength(2) +}) + +test('renders customers as well as projects and numbers in human output', async () => { + server = await startHttpSequenceServer([ + { + body: { + data: { + ...priorities, + customers: [ + {id: project, projectId: project, name: 'Acme', priority: 3}, + ], + }, + }, + }, + ]) + const result = await run([ + 'billing', + 'priority', + 'list', + '--project', + project, + ]) + expect(result.error).toBeUndefined() + expect(result.stdout).toContain('Revision: 5') + expect(result.stdout).toContain(`customer\t${project}\tAcme\t3`) + expect(result.stdout).toContain(`number\t${number}\tDesk\t2`) + expect(server.requests[0]?.url).toBe( + `/platform/billing/priorities?projectId=${project}`, + ) +}) + +test('loads a complete 2,000-resource order from a file into one revision-guarded request', async () => { + const resources = Array.from({length: 2000}, (_, index) => ({ + scope: index % 2 === 0 ? 'customer' : 'number', + resourceId: `00000000-0000-4000-8000-${(index + 1).toString(16).padStart(12, '0')}`, + })) + const file = await orderFile( + JSON.stringify(resources.map((row) => `${row.scope}:${row.resourceId}`)), + ) + server = await startHttpSequenceServer([{body: {data: priorities}}]) + const result = await run([ + 'billing', + 'priority', + 'reorder', + '--scope', + 'resource', + '--project', + project, + '--ids-file', + file, + '--expected-revision', + '5', + '--json', + ]) + expect(result.error).toBeUndefined() + expect(server.requests).toHaveLength(1) + expect(JSON.parse(server.requests[0]!.body)).toEqual({ + scope: 'resource', + projectId: project, + resources, + expectedRevision: 5, + }) +}) + +test('rejects malformed, missing, duplicate, or conflicting file inputs before HTTP', async () => { + server = await startHttpSequenceServer([{body: {data: priorities}}]) + const args = [ + 'billing', + 'priority', + 'reorder', + '--scope', + 'resource', + '--project', + project, + '--expected-revision', + '5', + ] + for (const content of [ + '{not json', + JSON.stringify({ids: [`number:${number}`]}), + JSON.stringify([5]), + JSON.stringify([`number:${number}`, `number:${number}`]), + JSON.stringify([`project:${project}`]), + ]) { + const file = await orderFile(content) + const result = await run([...args, '--ids-file', file]) + expect(result.error, content).toBeDefined() + expect(result.stdout).not.toContain('Priority order saved.') + } + const file = await orderFile(JSON.stringify([`number:${number}`])) + expect( + (await run([...args, '--ids-file', `${file}.missing`])).error, + ).toBeDefined() + expect((await run(args)).error).toBeDefined() + expect( + (await run([...args, '--ids-file', file, '--ids', `number:${number}`])) + .error, + ).toBeDefined() + expect(server.requests).toHaveLength(0) +}) From 3b2ddde09ee1f1981538d719801382806562c0e0 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 3 Oct 2026 23:37:26 +0300 Subject: [PATCH 5/5] style(docs): format billing controls README --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ab4b2be1..49a22a44 100644 --- a/README.md +++ b/README.md @@ -2482,7 +2482,6 @@ required. CLI publication and API deployment remain separate release gates. This source follows the financial-control contract at API commit `ba09326dd3c6688f05320c1f7685aeb0290a0ed2`. - Read or update one resource without loading the team directory: ```bash