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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,41 @@ in this development slice and does not yet claim remote identity validation.
Environment credentials take precedence over the active profile for automation.
An explicit `--profile <name>` takes precedence over environment credentials.

## Customers

Manage Customers through the Platform API with an organization server key and
an explicit project ID:

```shell
polymorfa customer status --project project_123
polymorfa customer enable \
--project project_123 \
--idempotency-key enable-customers-project-123

polymorfa customer create \
--project project_123 \
--name Ada \
--phone +15551234567 \
--external-id crm_456 \
--idempotency-key create-customer-crm-456

polymorfa customer pairing-link create customer_123 \
--project project_123 \
--method qr \
--method phone \
--idempotency-key pair-customer-123
```

The `customer` commands cover project enablement, list filters and cursors,
profile creation and updates, archive and restore, assigned Numbers, recent
events, pairing-link creation/listing/revocation, and confirmed Number
transfers. Mutations that require retry identity also require
`--idempotency-key`. Archive, pairing-link revocation, and Number transfer
require explicit confirmation with `--yes`.

Pairing-link creation prints the one-time URL in human output. Store it when it
is returned. An idempotent replay returns the link record without the URL.

## Groups

The first curated Messaging resource commands are read-only and require an
Expand Down
27 changes: 27 additions & 0 deletions docs/command-research/customers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Customer command contract

The Customer command family maps the complete public Customers surface in the
Platform contract. Every request uses an organization server API key and an
explicit project ID.

| Command | Platform operation |
| ------------------------------ | -------------------------------------------------------------------- |
| `customer status` | Check project enablement |
| `customer enable` | Enable Customers and assign existing Numbers to the default Customer |
| `customer list` | List and filter Customer summaries with cursor metadata |
| `customer create` | Create a Customer with optional name, phone, and external ID |
| `customer view` | Retrieve one Customer |
| `customer update` | Update or clear Customer profile fields |
| `customer archive` | Archive a non-default Customer |
| `customer restore` | Restore an archived Customer |
| `customer number list` | List assigned Numbers |
| `customer event list` | List recent lifecycle events |
| `customer pairing-link create` | Create a one-time pairing URL |
| `customer pairing-link list` | List pairing-link records |
| `customer pairing-link revoke` | Revoke a pairing link |
| `customer number transfer` | Transfer a Number from a confirmed source Customer |

The CLI preserves `Idempotency-Key` headers for every operation that requires
them. It also keeps list cursor metadata in JSON output. Human output masks
phone numbers returned by list and Number endpoints; full Customer retrieval
returns the API response unchanged under `--json`.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"dependencies": {
"@napi-rs/keyring": "1.3.0",
"@oclif/core": "^4.13.5",
"@polymorfa/sdk": "github:polymorfa/sdks#b4d5f3c8ce51bb0c171264eec8961797314cb172"
"@polymorfa/sdk": "github:polymorfa/sdks#bf5969e68eda2c0cf5541076bd2c55eb51f09f13"
},
"devDependencies": {
"@eslint/js": "^9.39.5",
Expand Down Expand Up @@ -109,6 +109,18 @@
"contact": {
"description": "Inspect contacts and manage the contact blocklist."
},
"customer": {
"description": "Manage project Customers, pairing links, and assigned Numbers."
},
"customer:event": {
"description": "Inspect Customer lifecycle events."
},
"customer:number": {
"description": "Inspect and transfer Customer Numbers."
},
"customer:pairing-link": {
"description": "Create, inspect, and revoke Customer pairing links."
},
"group": {
"description": "Inspect and manage Messaging groups."
},
Expand Down
38 changes: 38 additions & 0 deletions src/commands/customer/archive.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 {
archiveCustomer,
renderCustomer,
requireCustomerProject,
} from '../../services/customers.js'

export default class CustomerArchive extends BaseCommand {
static override args = {
customer: Args.string({description: 'Customer ID.', required: true}),
}

static override description = 'Archive a non-default Customer.'

static override flags = {
'idempotency-key': Flags.string({
description: 'Identify this archive attempt for safe retries.',
required: true,
}),
yes: Flags.boolean({
description: 'Confirm Customer archival.',
required: true,
}),
}

async run(): Promise<void> {
const {args, flags} = await this.parse(CustomerArchive)
const result = await archiveCustomer(
await this.platformClient(flags),
args.customer,
{projectId: requireCustomerProject(flags.project)},
{idempotencyKey: flags['idempotency-key']},
)
this.writeValue(result, renderCustomer(result.data), flags)
}
}
41 changes: 41 additions & 0 deletions src/commands/customer/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {Flags} from '@oclif/core'

import {BaseCommand} from '../../base-command.js'
import {
createCustomer,
renderCustomer,
requireCustomerProject,
} from '../../services/customers.js'

export default class CustomerCreate extends BaseCommand {
static override description = 'Create a Customer.'

static override flags = {
'external-id': Flags.string({description: 'Caller-defined Customer ID.'}),
'idempotency-key': Flags.string({
description: 'Identify this create attempt for safe retries.',
required: true,
}),
name: Flags.string({description: 'Customer name.'}),
phone: Flags.string({
description: 'Customer phone number in E.164 format.',
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(CustomerCreate)
const result = await createCustomer(
await this.platformClient(flags),
{
projectId: requireCustomerProject(flags.project),
...(flags.name === undefined ? {} : {name: flags.name}),
...(flags.phone === undefined ? {} : {phone: flags.phone}),
...(flags['external-id'] === undefined
? {}
: {externalCustomerId: flags['external-id']}),
},
{idempotencyKey: flags['idempotency-key']},
)
this.writeValue(result, renderCustomer(result.data), flags)
}
}
33 changes: 33 additions & 0 deletions src/commands/customer/enable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {Flags} from '@oclif/core'

import {BaseCommand} from '../../base-command.js'
import {
enableCustomers,
requireCustomerProject,
} from '../../services/customers.js'

export default class CustomerEnable extends BaseCommand {
static override description =
'Enable Customers and assign existing Numbers to the default Customer.'

static override flags = {
'idempotency-key': Flags.string({
description: 'Identify this enablement attempt for safe retries.',
required: true,
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(CustomerEnable)
const result = await enableCustomers(
await this.platformClient(flags),
requireCustomerProject(flags.project),
{idempotencyKey: flags['idempotency-key']},
)
this.writeValue(
result,
`Enabled Customers and assigned ${result.data.migratedNumberCount} existing Numbers.`,
flags,
)
}
}
37 changes: 37 additions & 0 deletions src/commands/customer/event/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {Args, Flags} from '@oclif/core'

import {BaseCommand} from '../../../base-command.js'
import {
listCustomerEvents,
renderCustomerEvents,
requireCustomerProject,
} from '../../../services/customers.js'

export default class CustomerEventList extends BaseCommand {
static override args = {
customer: Args.string({description: 'Customer ID.', required: true}),
}

static override description = 'List recent events for a Customer.'

static override flags = {
limit: Flags.integer({
description: 'Maximum events to return.',
max: 100,
min: 1,
}),
}

async run(): Promise<void> {
const {args, flags} = await this.parse(CustomerEventList)
const result = await listCustomerEvents(
await this.platformClient(flags),
args.customer,
{
projectId: requireCustomerProject(flags.project),
...(flags.limit === undefined ? {} : {limit: flags.limit}),
},
)
this.writeValue(result, renderCustomerEvents(result.data), flags)
}
}
63 changes: 63 additions & 0 deletions src/commands/customer/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {Flags} from '@oclif/core'

import {BaseCommand} from '../../base-command.js'
import {
listCustomers,
renderCustomers,
requireCustomerProject,
} from '../../services/customers.js'

export default class CustomerList extends BaseCommand {
static override description = 'List Customers in one project.'

static override flags = {
cursor: Flags.string({description: 'Continue from this cursor.'}),
default: Flags.string({
description: 'Filter by default-Customer status.',
options: ['true', 'false'],
}),
'has-numbers': Flags.string({
description: 'Filter by whether a Customer has assigned Numbers.',
options: ['true', 'false'],
}),
limit: Flags.integer({
description: 'Maximum Customers to return.',
max: 100,
min: 1,
}),
'needs-attention': Flags.string({
description: 'Filter by whether a Customer needs attention.',
options: ['true', 'false'],
}),
search: Flags.string({description: 'Search Customer profile fields.'}),
status: Flags.string({
description: 'Filter by Customer status.',
options: ['active', 'archiving', 'archived', 'all'],
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(CustomerList)
const result = await listCustomers(await this.platformClient(flags), {
projectId: requireCustomerProject(flags.project),
...(flags.cursor === undefined ? {} : {cursor: flags.cursor}),
...(flags.limit === undefined ? {} : {limit: flags.limit}),
...(flags.search === undefined ? {} : {search: flags.search}),
...(flags.status === undefined
? {}
: {
status: flags.status as 'active' | 'archiving' | 'archived' | 'all',
}),
...(flags.default === undefined
? {}
: {isDefault: flags.default === 'true'}),
...(flags['has-numbers'] === undefined
? {}
: {hasNumbers: flags['has-numbers'] === 'true'}),
...(flags['needs-attention'] === undefined
? {}
: {needsAttention: flags['needs-attention'] === 'true'}),
})
this.writeValue(result, renderCustomers(result.data), flags)
}
}
26 changes: 26 additions & 0 deletions src/commands/customer/number/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Args} from '@oclif/core'

import {BaseCommand} from '../../../base-command.js'
import {
listCustomerNumbers,
renderCustomerNumbers,
requireCustomerProject,
} from '../../../services/customers.js'

export default class CustomerNumberList extends BaseCommand {
static override args = {
customer: Args.string({description: 'Customer ID.', required: true}),
}

static override description = 'List Numbers assigned to a Customer.'

async run(): Promise<void> {
const {args, flags} = await this.parse(CustomerNumberList)
const result = await listCustomerNumbers(
await this.platformClient(flags),
args.customer,
requireCustomerProject(flags.project),
)
this.writeValue(result, renderCustomerNumbers(result.data), flags)
}
}
Loading