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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ beta feature.

```shell
polymorfa cloud catalog list --waba WABA_ID --graph-version v26.0 --limit 25
polymorfa cloud catalog products --waba WABA_ID --catalog CATALOG_ID --graph-version v26.0 --limit 25
polymorfa cloud template list --session number_123
polymorfa cloud template get order_update --session number_123 --language en_US
polymorfa cloud template create --session number_123 --input template.json --idempotency-key tpl-order-update
Expand Down Expand Up @@ -497,6 +498,11 @@ Credential reads use `sessions:read`; reauthorization uses `quicklink:manage`.
`cloud catalog list` reads linked product-catalog IDs and names with `sessions:read`
and Graph access. Pass the returned opaque cursor using `--after` for another
page. Catalog visibility does not grant merchant ownership or permission to sell.
`cloud catalog products` reads only IDs, optional retailer IDs, names and
availability from one catalog linked to the authenticated WABA. Pass its
`paging.cursors.after` value as `--after` to read the next page. The API rejects
unlinked catalogs and simulated Numbers. This read does not send a product or
establish that the merchant may sell it.

Reauthorization creates a QuickLink for a stopped or disconnected standalone
Official Number. It never stops the Number or opens the link automatically.
Expand Down
17 changes: 17 additions & 0 deletions docs/internal/2026-09-26-catalog-product-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Catalog product inventory command gate

`cloud catalog products` consumes the typed
`MessagingClient.cloudCatalogs.listProducts` method from the SDK follow-up to
PR #313. The exact API source is `polymorfa/polymorfa` PR #380 at
`bbfb2f4af3a8e1289074d9b7d0ed058b96881410`; its Graph OpenAPI copy is
`docs/api-reference/openapi.graph.json` with SHA-256
`b9d3778f17a90fd0c58e0cb1b3f6ef689661980fa83ad7565bc55ae4ab93fab4`.
The API checks live WABA/project authority and a linked catalog before reading
Meta's `/{catalogId}/products` edge. The response carries only the projected
fields and opaque cursors, never Meta paging URLs.

The CLI remains on its older exact SDK registry version. Merge the upstream
API and SDK PR chains, publish and verify the SDK artifact, then pin that exact
version and regenerate the CLI lockfile before CLI merge or release. No live
catalog permissions, product visibility, deployed path or customer access were
verified here. No release flag or Admin capability changes.
56 changes: 56 additions & 0 deletions src/commands/cloud/catalog/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {Flags} from '@oclif/core'
import {PolymorfaConfigurationError} from '@polymorfa/sdk'
import {BaseCommand} from '../../../base-command.js'

export default class CloudCatalogProducts extends BaseCommand {
static override description =
'List products in a catalog linked to an Official API WABA (beta). Requires sessions:read and Graph access.'

static override flags = {
waba: Flags.string({
description: 'WhatsApp Business Account ID.',
required: true,
}),
catalog: Flags.string({
description: 'Numeric product catalog ID linked to the WABA.',
required: true,
}),
'graph-version': Flags.string({
description: 'Meta Graph version, for example v26.0.',
required: true,
}),
limit: Flags.integer({
description: 'Page size from 1 to 100; API default is 25.',
min: 1,
max: 100,
}),
after: Flags.string({
description: 'Opaque cursor from the previous response.',
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(CloudCatalogProducts)
if (
!flags.waba.trim() ||
!/^[0-9]+$/.test(flags.catalog) ||
!flags['graph-version'].trim()
)
throw new PolymorfaConfigurationError(
'Provide a WABA ID, numeric catalog ID, and Graph version.',
)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = await client.cloudCatalogs.listProducts(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin an SDK version that provides listProducts

On every clean install and build of this commit, client.cloudCatalogs.listProducts is unavailable because both package.json and package-lock.json still pin @polymorfa/sdk to 0.1.0-dev.20260926005918, which predates this method (as the newly added internal gate document also acknowledges). Consequently TypeScript cannot compile the command, so the SDK dependency and lockfile must be updated to the published version containing listProducts before adding this invocation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid publication gate. The cloudCatalogs.listProducts method exists in SDK PR #314 and this CLI PR is deliberately stacked as source-ready consumer code. The only registry SDK pinned by package.json/lockfile predates it; changing to an unpublished version would make npm ci fail. We validated the CLI source against a local package built from SDK #316 (which includes #314): typecheck, build, and full tests passed. We will update the exact dependency and regenerated lockfile only after the SDK chain merges and its published version/integrity are verified; this CLI PR must not merge before then.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved: this branch now merges CLI dev, which pins @polymorfa/sdk 0.1.0-dev.20261001124422 (published after polymorfa/sdks #314 merged). That version exports cloudCatalogs.listProducts; typecheck and build pass against it.

flags.waba,
flags.catalog,
{
version: flags['graph-version'],
...(flags.limit === undefined ? {} : {limit: flags.limit}),
...(flags.after === undefined ? {} : {after: flags.after}),
},
)
this.writeValue(response, JSON.stringify(response.data, null, 2), flags)
}
}
33 changes: 33 additions & 0 deletions test/commands/cloud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,39 @@ describe('Official API commands', () => {
expect(result.stdout).not.toContain('"success":true')
})

test('lists linked catalog products with an exact cursor', async () => {
const data = {
data: [{id: '111', retailer_id: 'sku-1', name: 'Mug'}],
paging: {cursors: {after: 'next+/='}},
}
server = await startHttpServer({body: data})
const result = await credential(
() =>
runCommand(
'cloud catalog products --waba 1234 --catalog 98765432109876543210 --graph-version v26.0 --limit 10 --after next+/= --json',
),
'project',
)
expect(result.error).toBeUndefined()
expect((JSON.parse(result.stdout) as {data: unknown}).data).toEqual(data)
expect(server.requests).toHaveLength(1)
expect(server.requests[0]).toMatchObject({
method: 'GET',
url: '/graph/whatsapp/v26.0/1234/product_catalogs/98765432109876543210/products?limit=10&after=next%2B%2F%3D',
})
})

test('rejects a non-numeric catalog ID before any request', async () => {
server = await startHttpServer({body: {data: []}})
const result = await credential(() =>
runCommand(
'cloud catalog products --waba 1234 --catalog ../x --graph-version v26.0 --json',
),
)
expect(result.error).toBeDefined()
expect(server.requests).toHaveLength(0)
})

async function credential<T>(
run: () => Promise<T>,
kind: 'api' | 'project' | 'client' = 'api',
Expand Down