From 6d096cf26e868fc36f02d58b9d69eeb1da9e1759 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 26 Sep 2026 23:03:09 +0200 Subject: [PATCH 1/4] feat(cli): list linked catalog products --- README.md | 6 +++ .../2026-09-26-catalog-product-inventory.md | 17 ++++++ src/commands/cloud/catalog/products.ts | 52 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 docs/internal/2026-09-26-catalog-product-inventory.md create mode 100644 src/commands/cloud/catalog/products.ts diff --git a/README.md b/README.md index 69fa4fe..a47b43d 100644 --- a/README.md +++ b/README.md @@ -385,6 +385,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 @@ -414,6 +415,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. diff --git a/docs/internal/2026-09-26-catalog-product-inventory.md b/docs/internal/2026-09-26-catalog-product-inventory.md new file mode 100644 index 0000000..0ea185f --- /dev/null +++ b/docs/internal/2026-09-26-catalog-product-inventory.md @@ -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. diff --git a/src/commands/cloud/catalog/products.ts b/src/commands/cloud/catalog/products.ts new file mode 100644 index 0000000..c49553a --- /dev/null +++ b/src/commands/cloud/catalog/products.ts @@ -0,0 +1,52 @@ +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 { + 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( + 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) + } +} From fd4c6a5fd0b0397da6b156dd265015f9f766b410 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 26 Sep 2026 23:33:56 +0200 Subject: [PATCH 2/4] style(cli): format catalog products command --- src/commands/cloud/catalog/products.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/cloud/catalog/products.ts b/src/commands/cloud/catalog/products.ts index c49553a..79c1cd4 100644 --- a/src/commands/cloud/catalog/products.ts +++ b/src/commands/cloud/catalog/products.ts @@ -31,7 +31,11 @@ export default class CloudCatalogProducts extends BaseCommand { async run(): Promise { const {flags} = await this.parse(CloudCatalogProducts) - if (!flags.waba.trim() || !/^[0-9]+$/.test(flags.catalog) || !flags['graph-version'].trim()) + 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.', ) From fb8d4d9104f03bf2b2f0d808cec32a688f42e423 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Thu, 1 Oct 2026 16:15:35 +0300 Subject: [PATCH 3/4] Test linked catalog product listing --- test/commands/cloud.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/commands/cloud.test.ts b/test/commands/cloud.test.ts index 882a930..116612e 100644 --- a/test/commands/cloud.test.ts +++ b/test/commands/cloud.test.ts @@ -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).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( run: () => Promise, kind: 'api' | 'project' | 'client' = 'api', From 9410bbc6431286196710409b87c2481791ff2fa3 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Thu, 1 Oct 2026 16:22:18 +0300 Subject: [PATCH 4/4] Type the parsed catalog output in tests --- test/commands/cloud.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/cloud.test.ts b/test/commands/cloud.test.ts index 116612e..3bafb10 100644 --- a/test/commands/cloud.test.ts +++ b/test/commands/cloud.test.ts @@ -412,7 +412,7 @@ describe('Official API commands', () => { 'project', ) expect(result.error).toBeUndefined() - expect(JSON.parse(result.stdout).data).toEqual(data) + expect((JSON.parse(result.stdout) as {data: unknown}).data).toEqual(data) expect(server.requests).toHaveLength(1) expect(server.requests[0]).toMatchObject({ method: 'GET',