Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2453,3 +2453,67 @@ 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 <project-uuid> --json
polymorfa billing priority list --json
```

Changes require `billing:manage` and Pay-As-You-Go. Supply the observed revision:

```bash
polymorfa billing limits set <number-uuid> --scope number --credits 1000 --expected-revision 0
polymorfa billing limits set <project-uuid> --scope project --unlimited --expected-revision 1
polymorfa billing priority set <number-uuid> --scope number --priority 10 --expected-revision 0
polymorfa billing priority reorder --scope number --project <project-uuid> --ids <first-uuid>,<second-uuid> --expected-revision 1
```

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.
This source follows the financial-control contract at API commit
`ba09326dd3c6688f05320c1f7685aeb0290a0ed2`.

Read or update one resource without loading the team directory:

```bash
polymorfa billing controls get <customer-uuid> --scope customer --json
polymorfa billing controls set <customer-uuid> --scope customer --credits 1500 --priority 25 --expected-budget-revision 0 --expected-priority-revision 0
polymorfa quicklink create --project <project-uuid> --limit-credits 250 --priority 30 --json
```

The combined save is atomic and requires both observed revisions. Project
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 <project-uuid>
polymorfa billing priority reorder --scope resource --project <project-uuid> --ids number:<number-uuid>,customer:<customer-uuid> --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:<number-uuid>", "customer:<customer-uuid>"]
```

```bash
polymorfa billing priority reorder --scope resource --project <project-uuid> --ids-file order.json --expected-revision 5
```

Use either `--ids` or `--ids-file`. Funding order never bypasses a spending cap.
24 changes: 24 additions & 0 deletions src/commands/billing/controls/get.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
38 changes: 38 additions & 0 deletions src/commands/billing/controls/set.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
15 changes: 15 additions & 0 deletions src/commands/billing/limits/list.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const {flags} = await this.parse(LimitsList)
const result = await getLimits(
await this.platformClient(flags),
flags.project,
)
this.writeValue(result, renderLimits(result.data), flags)
}
}
41 changes: 41 additions & 0 deletions src/commands/billing/limits/set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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', 'customer', '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<void> {
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' | 'customer' | 'number',
args.id,
value,
flags['expected-revision'],
)
this.writeValue(result, 'Spending limit saved.', flags)
}
}
19 changes: 19 additions & 0 deletions src/commands/billing/priority/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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, customer and number funding priorities.'
static override description =
'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<void> {
const {flags} = await this.parse(PriorityList)
const result = await getPriorities(
await this.platformClient(flags),
flags.project,
)
this.writeValue(result, renderPriorities(result.data), flags)
}
}
51 changes: 51 additions & 0 deletions src/commands/billing/priority/reorder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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, 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', 'resource'],
required: true,
}),
ids: Flags.string({
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<void> {
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' | 'resource',
ids,
flags['expected-revision'],
flags.project,
)
this.writeValue(result, 'Priority order saved.', flags)
}
}
30 changes: 30 additions & 0 deletions src/commands/billing/priority/set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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', 'customer', 'number'],
required: true,
}),
priority: Flags.integer({required: true, min: 0, max: 1000000}),
'expected-revision': Flags.integer({required: true, min: 0}),
}
async run(): Promise<void> {
const {args, flags} = await this.parse(PrioritySet)
const result = await setPriority(
await this.platformClient(flags),
flags.scope as 'project' | 'customer' | 'number',
args.id,
flags.priority,
flags['expected-revision'],
)
this.writeValue(result, 'Priority saved.', flags)
}
}
24 changes: 24 additions & 0 deletions src/commands/quicklink/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.',
Expand Down Expand Up @@ -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')
Expand Down
Loading