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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -686,3 +686,22 @@ The file contains `messages`, with up to 100 entries specifying `id`,
`senderPhone`, `text`, `timestamp` (Unix seconds), and `fromMe`. Senders must be
existing simulated numbers in the selected project. This command does not enable
hosted message storage.

## Prepaid billing responses

`polymorfa api /platform/billing --surface platform --json` preserves fractional
credit quantities. Fields ending in `Cents` in the prepaid balance and usage
responses are credits, not cash minor units.

A request denied for insufficient funding returns `payment_required`, preserves
the API error code and request ID, and exits with code 4. This is not a retryable
server error. Add credit or resolve the billing restriction in the Console before
retrying. HTTP 409 from an expired tier quote also requires reviewing a new quote;
the CLI never creates or confirms a replacement purchase automatically.

For a tier change, use the raw API command to create a quote with
`POST /platform/sessions/{sessionId}/tier-quotes`, review the returned charge,
then explicitly submit its `quoteId` with `PATCH /platform/sessions/{sessionId}`.
Read `GET /platform/sessions/{sessionId}/tier-quotes/{quoteId}` until the result is
applied or rejected. Store the full JSON request in a file and pass `--input` when
it contains numeric values or `null`; `--field` creates string values.
16 changes: 16 additions & 0 deletions src/errors/present.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PolymorfaConnectionError,
PolymorfaError,
PolymorfaNotFoundError,
PolymorfaPaymentRequiredError,
PolymorfaRateLimitError,
PolymorfaServerError,
PolymorfaTimeoutError,
Expand All @@ -22,6 +23,7 @@ export type ErrorCategory =
| 'internal'
| 'network'
| 'not_found'
| 'payment_required'
| 'rate_limit'
| 'server'
| 'timeout'
Expand All @@ -32,6 +34,8 @@ export interface PresentedError {
readonly exitCode: number
readonly message: string
readonly requestId?: string
readonly code?: string
readonly retryable?: boolean
}

export function presentError(
Expand All @@ -45,6 +49,14 @@ export function presentError(

return {
...classified,
...(error instanceof PolymorfaPaymentRequiredError
? {
retryable: false,
...(error.code === undefined
? {}
: {code: redact(error.code, secrets)}),
}
: {}),
message,
...(requestId === undefined ? {} : {requestId}),
}
Expand All @@ -53,6 +65,10 @@ export function presentError(
function classifyError(
error: unknown,
): Pick<PresentedError, 'category' | 'exitCode'> {
if (error instanceof PolymorfaPaymentRequiredError) {
return {category: 'payment_required', exitCode: 4}
}

if (error instanceof PolymorfaConfigurationError) {
return {category: 'configuration', exitCode: 2}
}
Expand Down
58 changes: 58 additions & 0 deletions test/commands/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,64 @@ describe('api', () => {
let server: TestHttpServer | undefined
const tempDirectories: string[] = []

test('preserves fractional prepaid balances in JSON output', async () => {
server = await startHttpServer({
body: {data: {balanceCents: 17.500001, preferredCurrency: 'USD'}},
})
const result = await withEnvironment(
{
POLYMORFA_API_KEY:
'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
POLYMORFA_BASE_URL: server.baseUrl,
POLYMORFA_CLIENT_TOKEN: undefined,
},
() => runCommand('api /platform/billing --surface platform --json'),
)
expect(result.error).toBeUndefined()
expect(JSON.parse(result.stdout)).toMatchObject({
data: {data: {balanceCents: 17.500001}},
})
expect(server.requests).toHaveLength(1)
})

test('reports insufficient credit as payment required without retrying the operation', async () => {
server = await startHttpServer({
status: 402,
headers: {'request-id': 'req_billing'},
body: {
error: {
type: 'permission_error',
code: 'entitlement_required',
message:
'The account needs the required entitlement for this action.',
param: null,
},
data: null,
docs: 'https://docs.polymorfa.com/api/errors#entitlement-required',
},
})
const result = await withEnvironment(
{
POLYMORFA_API_KEY:
'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
POLYMORFA_BASE_URL: server.baseUrl,
POLYMORFA_CLIENT_TOKEN: undefined,
},
() =>
runCommand(
'api /platform/sessions/number/start --surface platform --method POST --json',
),
)
expect(exitCode(result.error)).toBe(4)
expect(JSON.parse(result.stderr)).toMatchObject({
category: 'payment_required',
code: 'entitlement_required',
retryable: false,
requestId: 'req_billing',
})
expect(server.requests).toHaveLength(1)
})

afterEach(async () => {
await server?.close()
server = undefined
Expand Down
1 change: 1 addition & 0 deletions test/sdk/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ describe('createRawClient', () => {
expect(client.sessions).toHaveProperty('retrieve')
expect(client.sessions).toHaveProperty('update')
expect(client.sessions).toHaveProperty('quoteTierChange')
expect(client.sessions).toHaveProperty('retrieveTierChange')
expect(client.organizations).not.toHaveProperty('update')

expect(() =>
Expand Down