@kuralle-agents/messaging-meta 0.8.0 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ npm install @kuralle-agents/messaging-meta @kuralle-agents/messaging
12
12
 
13
13
  Provides production-ready clients for Meta's three messaging platforms, built on a shared Graph API foundation with retry logic, rate limiting, and unified error handling.
14
14
 
15
- - **WhatsApp** — full API coverage: text (auto-split at 4096 chars), media, templates, interactive buttons, list messages, CTA buttons, WhatsApp Flows, reactions, locations, contacts.
15
+ - **WhatsApp** — full API coverage: text (auto-split at 4096 chars), media, templates, interactive buttons, list messages, CTA buttons, WhatsApp Flows, reactions, locations, contacts, commerce (single/multi-product, catalog, and address messages, plus inbound order parsing).
16
16
  - **Messenger** — button templates, generic templates (carousel), quick replies, sender actions, user profile lookups.
17
17
  - **Instagram** — text (auto-split at 1000 bytes), quick replies, carousels, private replies to comments, ice breakers, message tags.
18
18
  - **Main barrel** — `GraphAPIClient`, `BaseMetaClient`, `verifySignature`, `normalizeWebhook`, error classes.
@@ -66,6 +66,35 @@ const instagram = createInstagramClient({
66
66
  });
67
67
  ```
68
68
 
69
+ ### WhatsApp commerce
70
+
71
+ Share catalog products and receive orders without leaving WhatsApp:
72
+
73
+ ```typescript
74
+ import { parseInboundOrder } from '@kuralle-agents/messaging-meta/whatsapp';
75
+
76
+ // Single product, multi-product (max 10 sections / 30 products), catalog, address request
77
+ await whatsapp.sendProduct(to, { catalogId, productRetailerId: 'sku-1', body: { text: 'Check this out' } });
78
+ await whatsapp.sendProductList(to, {
79
+ header: { type: 'text', text: 'Bestsellers' },
80
+ body: { text: 'Pick your favorites' },
81
+ catalogId,
82
+ sections: [{ title: 'Cakes', productRetailerIds: ['sku-1', 'sku-2'] }],
83
+ });
84
+ await whatsapp.sendCatalog(to, { body: { text: 'Browse our catalog' } });
85
+ await whatsapp.sendAddressRequest(to, { body: { text: 'Where should we deliver?' }, country: 'IN' });
86
+
87
+ // Inbound orders (webhook message type "order") arrive typed:
88
+ whatsapp.onMessage(async (msg) => {
89
+ const order = parseInboundOrder(msg);
90
+ if (order) {
91
+ // order.catalog_id, order.product_items[].{product_retailer_id, quantity, item_price, currency}
92
+ }
93
+ });
94
+ ```
95
+
96
+ Address submissions arrive as `nfm_reply` interactive messages — use `parseInboundAddress(msg)` for a typed accessor. Address messages are country-gated by Meta (currently India only).
97
+
69
98
  ### Webhook verification without a client
70
99
 
71
100
  ```typescript
@@ -81,7 +110,7 @@ const events = normalizeWebhook(JSON.parse(rawBody));
81
110
  | Import path | Contents |
82
111
  |---|---|
83
112
  | `@kuralle-agents/messaging-meta` | `GraphAPIClient`, `BaseMetaClient`, errors, `verifySignature`, `normalizeWebhook`, `MessengerClient`, `InstagramClient` |
84
- | `@kuralle-agents/messaging-meta/whatsapp` | `WhatsAppClient`, `createWhatsAppClient`, templates, flows, format converter |
113
+ | `@kuralle-agents/messaging-meta/whatsapp` | `WhatsAppClient`, `createWhatsAppClient`, templates, flows, commerce (`parseInboundOrder`, `parseInboundAddress`), format converter |
85
114
  | `@kuralle-agents/messaging-meta/messenger` | `MessengerClient`, `createMessengerClient`, format converter |
86
115
  | `@kuralle-agents/messaging-meta/instagram` | `InstagramClient`, `createInstagramClient`, ice breakers, format converter |
87
116
  | `@kuralle-agents/messaging-meta/webhooks` | `verifySignature`, `normalizeWebhook` only |
@@ -83,12 +83,24 @@ export interface NormalizedMessage {
83
83
  nfm_reply?: {
84
84
  name?: string;
85
85
  response_json: string;
86
+ body?: string;
86
87
  };
87
88
  };
88
89
  button?: {
89
90
  text: string;
90
91
  payload: string;
91
92
  };
93
+ /** Order placed from a catalog, single-, or multi-product message. */
94
+ order?: {
95
+ catalog_id: string;
96
+ text?: string;
97
+ product_items: Array<{
98
+ product_retailer_id: string;
99
+ quantity: number;
100
+ item_price: number;
101
+ currency: string;
102
+ }>;
103
+ };
92
104
  /** Quoted/replied-to message context. */
93
105
  context?: {
94
106
  message_id: string;
@@ -128,6 +128,8 @@ function normalizeWhatsAppWebhook(payload) {
128
128
  normalized.interactive = msg.interactive;
129
129
  if (msg.button)
130
130
  normalized.button = msg.button;
131
+ if (msg.order)
132
+ normalized.order = msg.order;
131
133
  if (msg.context)
132
134
  normalized.context = msg.context;
133
135
  if (msg.referral)
@@ -5,8 +5,9 @@
5
5
  *
6
6
  * Extends {@link BaseMetaClient} for shared webhook verification, payload
7
7
  * normalization, handler dispatch, and `/webhook` Hono sub-app. Templates,
8
- * Flows, phone-number management, CTA buttons, reactions, location, and
9
- * contact messages remain in this file as WhatsApp-specific surface.
8
+ * Flows, phone-number management, CTA buttons, commerce (product, catalog,
9
+ * and address messages), reactions, location, and contact messages remain
10
+ * in this file as WhatsApp-specific surface.
10
11
  *
11
12
  * @example
12
13
  * ```ts
@@ -24,7 +25,7 @@ import { type FormatConverter, type InboundMessage, type InteractiveMessage, typ
24
25
  import { BaseMetaClient } from '../base-client.js';
25
26
  import type { BaseMetaClientConfig } from '../base-client.js';
26
27
  import type { NormalizedMessage, NormalizedReaction, NormalizedStatus } from '../webhook/normalizer.js';
27
- import type { WhatsAppClientConfig, TemplateMessage, TemplateInfo, TemplateDefinition, TextOrTemplateOptions, ListMessage, ButtonMessage, CTAButtonMessage, FlowInteractiveInput, LocationPayload, ContactPayload, BusinessProfile, FlowDefinition, FlowInfo, FlowAssets } from './types.js';
28
+ import type { WhatsAppClientConfig, TemplateMessage, TemplateInfo, TemplateDefinition, TextOrTemplateOptions, ListMessage, ButtonMessage, CTAButtonMessage, FlowInteractiveInput, ProductMessage, ProductListMessage, CatalogMessage, AddressMessage, LocationPayload, ContactPayload, BusinessProfile, FlowDefinition, FlowInfo, FlowAssets } from './types.js';
28
29
  type WhatsAppInbound = NormalizedMessage;
29
30
  type WhatsAppOutbound = Record<string, unknown>;
30
31
  /** Internal config — base-client common fields threaded through the public config. */
@@ -34,8 +35,8 @@ export declare function createWhatsAppClient(config: WhatsAppClientConfig): What
34
35
  * Full-featured WhatsApp Cloud API client.
35
36
  *
36
37
  * Implements the `PlatformClient` interface via {@link BaseMetaClient}.
37
- * Layers WhatsApp-specific capabilities (templates, Flows, reactions, CTA
38
- * buttons, locations, contacts) on top.
38
+ * Layers WhatsApp-specific capabilities (templates, Flows, commerce,
39
+ * reactions, CTA buttons, locations, contacts) on top.
39
40
  */
40
41
  export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, WhatsAppOutbound, InternalWhatsAppConfig> {
41
42
  readonly platform: "whatsapp";
@@ -76,6 +77,40 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
76
77
  sendInteractiveButtons(to: string, msg: ButtonMessage): Promise<SendResult>;
77
78
  sendCTAButton(to: string, cta: CTAButtonMessage): Promise<SendResult>;
78
79
  sendInteractiveFlow(to: string, flow: FlowInteractiveInput): Promise<SendResult>;
80
+ /**
81
+ * Send a single-product message (`interactive.type: "product"`).
82
+ *
83
+ * Displays one catalog product with a **View** button; the user can open
84
+ * the Product Detail Page, add to cart, and send an order. Orders arrive
85
+ * as inbound `type: "order"` messages — see {@link parseInboundOrder}.
86
+ */
87
+ sendProduct(to: string, product: ProductMessage): Promise<SendResult>;
88
+ /**
89
+ * Send a multi-product message (`interactive.type: "product_list"`).
90
+ *
91
+ * Displays up to {@link MAX_PRODUCT_LIST_PRODUCTS} catalog products across
92
+ * up to {@link MAX_PRODUCT_LIST_SECTIONS} sections. Header and body are
93
+ * required by the Cloud API.
94
+ *
95
+ * @throws {MessagingError} `INVALID_PRODUCT_LIST` when the section or
96
+ * product limits are violated, or a section has no products.
97
+ */
98
+ sendProductList(to: string, list: ProductListMessage): Promise<SendResult>;
99
+ /**
100
+ * Send a catalog message (`interactive.type: "catalog_message"`).
101
+ *
102
+ * Displays a product thumbnail header, custom body text, and a
103
+ * **View catalog** button that opens the full catalog within WhatsApp.
104
+ */
105
+ sendCatalog(to: string, catalog: CatalogMessage): Promise<SendResult>;
106
+ /**
107
+ * Send an address request message (`interactive.type: "address_message"`).
108
+ *
109
+ * Country-gated by Meta — currently India-based businesses and their
110
+ * India customers only. The user's submission arrives as an inbound
111
+ * `nfm_reply` interactive message — see {@link parseInboundAddress}.
112
+ */
113
+ sendAddressRequest(to: string, address: AddressMessage): Promise<SendResult>;
79
114
  sendReaction(to: string, messageId: string, emoji: string): Promise<SendResult>;
80
115
  sendLocation(to: string, location: LocationPayload): Promise<SendResult>;
81
116
  sendContacts(to: string, contacts: ContactPayload[]): Promise<SendResult>;
@@ -103,6 +138,7 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
103
138
  protected toInboundMessage(msg: NormalizedMessage): InboundMessage;
104
139
  protected toStatusUpdate(status: NormalizedStatus): StatusUpdate;
105
140
  protected toReactionData(reaction: NormalizedReaction): ReactionData;
141
+ private validateProductList;
106
142
  private toSendResult;
107
143
  private mapMessageType;
108
144
  private extractTextFallback;
@@ -5,8 +5,9 @@
5
5
  *
6
6
  * Extends {@link BaseMetaClient} for shared webhook verification, payload
7
7
  * normalization, handler dispatch, and `/webhook` Hono sub-app. Templates,
8
- * Flows, phone-number management, CTA buttons, reactions, location, and
9
- * contact messages remain in this file as WhatsApp-specific surface.
8
+ * Flows, phone-number management, CTA buttons, commerce (product, catalog,
9
+ * and address messages), reactions, location, and contact messages remain
10
+ * in this file as WhatsApp-specific surface.
10
11
  *
11
12
  * @example
12
13
  * ```ts
@@ -24,6 +25,7 @@ import { MessagingError, WindowClosedError, FileHashDedupStrategy, UploadStrateg
24
25
  import { BaseMetaClient } from '../base-client.js';
25
26
  import { GraphAPIClient } from '../graph-api/client.js';
26
27
  import { SmartSplitter } from '../message-splitter.js';
28
+ import { MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
27
29
  import { WhatsAppFormatConverter } from './format.js';
28
30
  // ---------------------------------------------------------------------------
29
31
  // Factory
@@ -38,8 +40,8 @@ export function createWhatsAppClient(config) {
38
40
  * Full-featured WhatsApp Cloud API client.
39
41
  *
40
42
  * Implements the `PlatformClient` interface via {@link BaseMetaClient}.
41
- * Layers WhatsApp-specific capabilities (templates, Flows, reactions, CTA
42
- * buttons, locations, contacts) on top.
43
+ * Layers WhatsApp-specific capabilities (templates, Flows, commerce,
44
+ * reactions, CTA buttons, locations, contacts) on top.
43
45
  */
44
46
  export class WhatsAppClient extends BaseMetaClient {
45
47
  platform = 'whatsapp';
@@ -281,6 +283,124 @@ export class WhatsAppClient extends BaseMetaClient {
281
283
  return this.toSendResult(to, response);
282
284
  }
283
285
  // =========================================================================
286
+ // WhatsApp-specific — Commerce (products, catalog, addresses)
287
+ // =========================================================================
288
+ /**
289
+ * Send a single-product message (`interactive.type: "product"`).
290
+ *
291
+ * Displays one catalog product with a **View** button; the user can open
292
+ * the Product Detail Page, add to cart, and send an order. Orders arrive
293
+ * as inbound `type: "order"` messages — see {@link parseInboundOrder}.
294
+ */
295
+ async sendProduct(to, product) {
296
+ const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
297
+ messaging_product: 'whatsapp',
298
+ recipient_type: 'individual',
299
+ to,
300
+ type: 'interactive',
301
+ interactive: {
302
+ type: 'product',
303
+ body: product.body,
304
+ footer: product.footer,
305
+ action: {
306
+ catalog_id: product.catalogId,
307
+ product_retailer_id: product.productRetailerId,
308
+ },
309
+ },
310
+ });
311
+ return this.toSendResult(to, response);
312
+ }
313
+ /**
314
+ * Send a multi-product message (`interactive.type: "product_list"`).
315
+ *
316
+ * Displays up to {@link MAX_PRODUCT_LIST_PRODUCTS} catalog products across
317
+ * up to {@link MAX_PRODUCT_LIST_SECTIONS} sections. Header and body are
318
+ * required by the Cloud API.
319
+ *
320
+ * @throws {MessagingError} `INVALID_PRODUCT_LIST` when the section or
321
+ * product limits are violated, or a section has no products.
322
+ */
323
+ async sendProductList(to, list) {
324
+ this.validateProductList(list);
325
+ const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
326
+ messaging_product: 'whatsapp',
327
+ recipient_type: 'individual',
328
+ to,
329
+ type: 'interactive',
330
+ interactive: {
331
+ type: 'product_list',
332
+ header: list.header,
333
+ body: list.body,
334
+ footer: list.footer,
335
+ action: {
336
+ catalog_id: list.catalogId,
337
+ sections: list.sections.map((section) => ({
338
+ title: section.title,
339
+ product_items: section.productRetailerIds.map((id) => ({
340
+ product_retailer_id: id,
341
+ })),
342
+ })),
343
+ },
344
+ },
345
+ });
346
+ return this.toSendResult(to, response);
347
+ }
348
+ /**
349
+ * Send a catalog message (`interactive.type: "catalog_message"`).
350
+ *
351
+ * Displays a product thumbnail header, custom body text, and a
352
+ * **View catalog** button that opens the full catalog within WhatsApp.
353
+ */
354
+ async sendCatalog(to, catalog) {
355
+ const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
356
+ messaging_product: 'whatsapp',
357
+ recipient_type: 'individual',
358
+ to,
359
+ type: 'interactive',
360
+ interactive: {
361
+ type: 'catalog_message',
362
+ body: catalog.body,
363
+ footer: catalog.footer,
364
+ action: {
365
+ name: 'catalog_message',
366
+ parameters: catalog.thumbnailProductRetailerId
367
+ ? { thumbnail_product_retailer_id: catalog.thumbnailProductRetailerId }
368
+ : undefined,
369
+ },
370
+ },
371
+ });
372
+ return this.toSendResult(to, response);
373
+ }
374
+ /**
375
+ * Send an address request message (`interactive.type: "address_message"`).
376
+ *
377
+ * Country-gated by Meta — currently India-based businesses and their
378
+ * India customers only. The user's submission arrives as an inbound
379
+ * `nfm_reply` interactive message — see {@link parseInboundAddress}.
380
+ */
381
+ async sendAddressRequest(to, address) {
382
+ const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
383
+ messaging_product: 'whatsapp',
384
+ recipient_type: 'individual',
385
+ to,
386
+ type: 'interactive',
387
+ interactive: {
388
+ type: 'address_message',
389
+ body: address.body,
390
+ action: {
391
+ name: 'address_message',
392
+ parameters: {
393
+ country: address.country,
394
+ values: address.values,
395
+ saved_addresses: address.savedAddresses,
396
+ validation_errors: address.validationErrors,
397
+ },
398
+ },
399
+ },
400
+ });
401
+ return this.toSendResult(to, response);
402
+ }
403
+ // =========================================================================
284
404
  // WhatsApp-specific — Reactions, location, contacts
285
405
  // =========================================================================
286
406
  async sendReaction(to, messageId, emoji) {
@@ -490,6 +610,19 @@ export class WhatsAppClient extends BaseMetaClient {
490
610
  // =========================================================================
491
611
  // Private — conversion helpers
492
612
  // =========================================================================
613
+ validateProductList(list) {
614
+ if (list.sections.length === 0 || list.sections.length > MAX_PRODUCT_LIST_SECTIONS) {
615
+ throw new MessagingError(`Product list must have between 1 and ${MAX_PRODUCT_LIST_SECTIONS} sections (has ${list.sections.length})`, 'INVALID_PRODUCT_LIST', 'whatsapp');
616
+ }
617
+ const emptySection = list.sections.find((s) => s.productRetailerIds.length === 0);
618
+ if (emptySection) {
619
+ throw new MessagingError(`Product list section "${emptySection.title}" has no products`, 'INVALID_PRODUCT_LIST', 'whatsapp');
620
+ }
621
+ const totalProducts = list.sections.reduce((sum, s) => sum + s.productRetailerIds.length, 0);
622
+ if (totalProducts > MAX_PRODUCT_LIST_PRODUCTS) {
623
+ throw new MessagingError(`Product list exceeds maximum of ${MAX_PRODUCT_LIST_PRODUCTS} products across all sections (has ${totalProducts})`, 'INVALID_PRODUCT_LIST', 'whatsapp');
624
+ }
625
+ }
493
626
  toSendResult(to, response) {
494
627
  return {
495
628
  messageId: response.messages[0]?.id ?? '',
@@ -527,6 +660,8 @@ export class WhatsAppClient extends BaseMetaClient {
527
660
  return msg.interactive.button_reply.title;
528
661
  if (msg.interactive?.list_reply)
529
662
  return msg.interactive.list_reply.title;
663
+ if (msg.order?.text)
664
+ return msg.order.text;
530
665
  if (msg.location) {
531
666
  return msg.location.name ?? `${msg.location.latitude},${msg.location.longitude}`;
532
667
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @module whatsapp/commerce
3
+ *
4
+ * Helpers and limits for WhatsApp commerce messages.
5
+ *
6
+ * Commerce messages let you share products from a Meta catalog and receive
7
+ * orders directly in WhatsApp. The send methods live on the
8
+ * {@link WhatsAppClient} (`sendProduct`, `sendProductList`, `sendCatalog`,
9
+ * `sendAddressRequest`); this module provides the inbound typed accessors.
10
+ *
11
+ * @see https://developers.facebook.com/documentation/business-messaging/whatsapp/catalogs/share-products
12
+ * @see https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/order
13
+ */
14
+ import type { InboundMessage } from '@kuralle-agents/messaging';
15
+ import type { WhatsAppInboundOrder, WhatsAppInboundAddress } from './types.js';
16
+ export type { ProductMessage, ProductListMessage, ProductSection, CatalogMessage, AddressMessage, WhatsAppAddressValues, WhatsAppSavedAddress, WhatsAppOrderItem, WhatsAppInboundOrder, WhatsAppInboundAddress, } from './types.js';
17
+ /** Maximum number of sections in a multi-product message. */
18
+ export declare const MAX_PRODUCT_LIST_SECTIONS = 10;
19
+ /** Maximum number of products across all sections of a multi-product message. */
20
+ export declare const MAX_PRODUCT_LIST_PRODUCTS = 30;
21
+ /**
22
+ * Extract the typed order from an inbound message, when the user placed an
23
+ * order from a catalog, single-, or multi-product message.
24
+ *
25
+ * The generic `InboundMessage` type has no `order` field, so the order
26
+ * travels on `message.raw` (the normalized WhatsApp webhook message). This
27
+ * accessor returns it typed.
28
+ *
29
+ * @param message - An inbound message from the WhatsApp client.
30
+ * @returns The order, or `undefined` if the message is not an order.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * client.onMessage(async (msg) => {
35
+ * const order = parseInboundOrder(msg);
36
+ * if (order) {
37
+ * console.log(`Order against catalog ${order.catalog_id}:`);
38
+ * for (const item of order.product_items) {
39
+ * console.log(` ${item.quantity}x ${item.product_retailer_id} @ ${item.item_price} ${item.currency}`);
40
+ * }
41
+ * }
42
+ * });
43
+ * ```
44
+ */
45
+ export declare function parseInboundOrder(message: InboundMessage): WhatsAppInboundOrder | undefined;
46
+ /**
47
+ * Extract the typed address submission from an inbound message, when the
48
+ * user replied to an address message (`sendAddressRequest`).
49
+ *
50
+ * Address replies arrive as `interactive.type === "nfm_reply"` messages with
51
+ * `nfm_reply.name === "address_message"`; the address fields are JSON-encoded
52
+ * in `nfm_reply.response_json`.
53
+ *
54
+ * @param message - An inbound message from the WhatsApp client.
55
+ * @returns The parsed address, or `undefined` if the message is not an
56
+ * address submission (or the payload is malformed).
57
+ */
58
+ export declare function parseInboundAddress(message: InboundMessage): WhatsAppInboundAddress | undefined;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @module whatsapp/commerce
3
+ *
4
+ * Helpers and limits for WhatsApp commerce messages.
5
+ *
6
+ * Commerce messages let you share products from a Meta catalog and receive
7
+ * orders directly in WhatsApp. The send methods live on the
8
+ * {@link WhatsAppClient} (`sendProduct`, `sendProductList`, `sendCatalog`,
9
+ * `sendAddressRequest`); this module provides the inbound typed accessors.
10
+ *
11
+ * @see https://developers.facebook.com/documentation/business-messaging/whatsapp/catalogs/share-products
12
+ * @see https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/order
13
+ */
14
+ // ---------------------------------------------------------------------------
15
+ // Limits (per Meta Cloud API docs)
16
+ // ---------------------------------------------------------------------------
17
+ /** Maximum number of sections in a multi-product message. */
18
+ export const MAX_PRODUCT_LIST_SECTIONS = 10;
19
+ /** Maximum number of products across all sections of a multi-product message. */
20
+ export const MAX_PRODUCT_LIST_PRODUCTS = 30;
21
+ // ---------------------------------------------------------------------------
22
+ // Inbound typed accessors
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Extract the typed order from an inbound message, when the user placed an
26
+ * order from a catalog, single-, or multi-product message.
27
+ *
28
+ * The generic `InboundMessage` type has no `order` field, so the order
29
+ * travels on `message.raw` (the normalized WhatsApp webhook message). This
30
+ * accessor returns it typed.
31
+ *
32
+ * @param message - An inbound message from the WhatsApp client.
33
+ * @returns The order, or `undefined` if the message is not an order.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * client.onMessage(async (msg) => {
38
+ * const order = parseInboundOrder(msg);
39
+ * if (order) {
40
+ * console.log(`Order against catalog ${order.catalog_id}:`);
41
+ * for (const item of order.product_items) {
42
+ * console.log(` ${item.quantity}x ${item.product_retailer_id} @ ${item.item_price} ${item.currency}`);
43
+ * }
44
+ * }
45
+ * });
46
+ * ```
47
+ */
48
+ export function parseInboundOrder(message) {
49
+ const raw = message.raw;
50
+ const order = raw?.order;
51
+ if (!order || typeof order.catalog_id !== 'string' || !Array.isArray(order.product_items)) {
52
+ return undefined;
53
+ }
54
+ return order;
55
+ }
56
+ /**
57
+ * Extract the typed address submission from an inbound message, when the
58
+ * user replied to an address message (`sendAddressRequest`).
59
+ *
60
+ * Address replies arrive as `interactive.type === "nfm_reply"` messages with
61
+ * `nfm_reply.name === "address_message"`; the address fields are JSON-encoded
62
+ * in `nfm_reply.response_json`.
63
+ *
64
+ * @param message - An inbound message from the WhatsApp client.
65
+ * @returns The parsed address, or `undefined` if the message is not an
66
+ * address submission (or the payload is malformed).
67
+ */
68
+ export function parseInboundAddress(message) {
69
+ const raw = message.raw;
70
+ const nfm = raw?.interactive?.nfm_reply;
71
+ if (!nfm || nfm.name !== 'address_message' || !nfm.response_json) {
72
+ return undefined;
73
+ }
74
+ try {
75
+ const parsed = JSON.parse(nfm.response_json);
76
+ if (parsed !== null &&
77
+ typeof parsed === 'object' &&
78
+ !Array.isArray(parsed) &&
79
+ typeof parsed.values === 'object' &&
80
+ parsed.values !== null) {
81
+ return parsed;
82
+ }
83
+ return undefined;
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
@@ -30,9 +30,10 @@
30
30
  * @packageDocumentation
31
31
  */
32
32
  export { WhatsAppClient, createWhatsAppClient } from './client.js';
33
- export type { WhatsAppClientConfig, WhatsAppThreadId, WhatsAppSendResponse, TemplateMessage, TemplateLanguage, TemplateComponent, TemplateParameter, MediaObject, ListMessage, ListSection, ListRow, ButtonMessage, ReplyButton, CTAButtonMessage, FlowInteractiveInput, LocationPayload, ContactPayload, BusinessProfile, TemplateDefinition, TemplateDefinitionComponent, TemplateInfo, FlowDefinition, FlowInfo, FlowAssets, WhatsAppMediaResponse, TextOrTemplateOptions, } from './types.js';
33
+ export type { WhatsAppClientConfig, WhatsAppThreadId, WhatsAppSendResponse, TemplateMessage, TemplateLanguage, TemplateComponent, TemplateParameter, MediaObject, ListMessage, ListSection, ListRow, ButtonMessage, ReplyButton, CTAButtonMessage, FlowInteractiveInput, ProductMessage, ProductListMessage, ProductSection, CatalogMessage, AddressMessage, WhatsAppAddressValues, WhatsAppSavedAddress, WhatsAppOrderItem, WhatsAppInboundOrder, WhatsAppInboundAddress, LocationPayload, ContactPayload, BusinessProfile, TemplateDefinition, TemplateDefinitionComponent, TemplateInfo, FlowDefinition, FlowInfo, FlowAssets, WhatsAppMediaResponse, TextOrTemplateOptions, } from './types.js';
34
34
  export { buildTemplateSendPayload, buildTemplatePayload, mapOutboundTemplateComponents, } from './templates.js';
35
35
  export type { TypedTemplateConfig, RawTemplateComponents, } from './templates.js';
36
36
  export { WhatsAppFormatConverter } from './format.js';
37
37
  export { splitMessage } from './split.js';
38
38
  export { generateFlowToken, buildFlowInput } from './flows.js';
39
+ export { parseInboundOrder, parseInboundAddress, MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
@@ -39,3 +39,5 @@ export { WhatsAppFormatConverter } from './format.js';
39
39
  export { splitMessage } from './split.js';
40
40
  // ─── Flows ──────────────────────────────────────────────────────────────
41
41
  export { generateFlowToken, buildFlowInput } from './flows.js';
42
+ // ─── Commerce ───────────────────────────────────────────────────────────
43
+ export { parseInboundOrder, parseInboundAddress, MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
@@ -279,6 +279,173 @@ export interface FlowInteractiveInput {
279
279
  /** Optional initial data for the flow. */
280
280
  flowActionPayload?: Record<string, unknown>;
281
281
  }
282
+ /**
283
+ * A single-product interactive message (`interactive.type: "product"`).
284
+ *
285
+ * Displays one product from your catalog with a product image, title,
286
+ * price, and a **View** button that opens the Product Detail Page.
287
+ */
288
+ export interface ProductMessage {
289
+ /** Optional body text (max 1024 chars). */
290
+ body?: {
291
+ text: string;
292
+ };
293
+ /** Optional footer text (max 60 chars). */
294
+ footer?: {
295
+ text: string;
296
+ };
297
+ /** Meta product catalog ID. */
298
+ catalogId: string;
299
+ /** Product SKU (labeled **Content ID** in Commerce Manager). */
300
+ productRetailerId: string;
301
+ }
302
+ /**
303
+ * A multi-product interactive message (`interactive.type: "product_list"`).
304
+ *
305
+ * Displays up to 30 products from your catalog, organized into up to
306
+ * 10 sections. Header and body are required by the Cloud API.
307
+ */
308
+ export interface ProductListMessage {
309
+ /** Required header (text only for product lists). */
310
+ header: {
311
+ type: 'text';
312
+ text: string;
313
+ };
314
+ /** Body text (required, max 1024 chars). */
315
+ body: {
316
+ text: string;
317
+ };
318
+ /** Optional footer text (max 60 chars). */
319
+ footer?: {
320
+ text: string;
321
+ };
322
+ /** Meta product catalog ID. */
323
+ catalogId: string;
324
+ /** Product sections (max 10; max 30 products across all sections). */
325
+ sections: ProductSection[];
326
+ }
327
+ /** A section within a multi-product message. */
328
+ export interface ProductSection {
329
+ /** Section title. */
330
+ title: string;
331
+ /** Product SKUs in this section (Content IDs in Commerce Manager). */
332
+ productRetailerIds: string[];
333
+ }
334
+ /**
335
+ * A catalog interactive message (`interactive.type: "catalog_message"`).
336
+ *
337
+ * Displays a product thumbnail header image, custom body text, and a
338
+ * **View catalog** button that opens your full catalog within WhatsApp.
339
+ */
340
+ export interface CatalogMessage {
341
+ /** Body text (required, max 1024 chars). */
342
+ body: {
343
+ text: string;
344
+ };
345
+ /** Optional footer text (max 60 chars). */
346
+ footer?: {
347
+ text: string;
348
+ };
349
+ /**
350
+ * Optional product SKU whose image is used as the header thumbnail.
351
+ * When omitted, the first item in the catalog is used.
352
+ */
353
+ thumbnailProductRetailerId?: string;
354
+ }
355
+ /**
356
+ * Address field values used in address messages.
357
+ *
358
+ * Field names mirror the Cloud API wire format. Currently only available
359
+ * for India-based businesses and their India customers.
360
+ */
361
+ export interface WhatsAppAddressValues {
362
+ /** Recipient name. */
363
+ name?: string;
364
+ /** Recipient phone number. */
365
+ phone_number?: string;
366
+ /** Pin code (India, max 6 chars). */
367
+ in_pin_code?: string;
368
+ /** Flat/house number. */
369
+ house_number?: string;
370
+ /** Floor number. */
371
+ floor_number?: string;
372
+ /** Tower number. */
373
+ tower_number?: string;
374
+ /** Building/apartment name. */
375
+ building_name?: string;
376
+ /** Street address. */
377
+ address?: string;
378
+ /** Landmark/area. */
379
+ landmark_area?: string;
380
+ /** City. */
381
+ city?: string;
382
+ /** State. */
383
+ state?: string;
384
+ }
385
+ /** A previously saved address offered to the user in an address message. */
386
+ export interface WhatsAppSavedAddress {
387
+ /** Identifier returned as `saved_address_id` when the user selects it. */
388
+ id: string;
389
+ /** The saved address field values. */
390
+ value: WhatsAppAddressValues;
391
+ }
392
+ /**
393
+ * An address request interactive message (`interactive.type: "address_message"`).
394
+ *
395
+ * Country-gated: only available for India-based businesses and their
396
+ * India customers. The user's submission arrives as an inbound
397
+ * `interactive.type: "nfm_reply"` message — see {@link parseInboundAddress}.
398
+ */
399
+ export interface AddressMessage {
400
+ /** Body text (required). */
401
+ body: {
402
+ text: string;
403
+ };
404
+ /** ISO country code (required by the API, e.g. `"IN"`). */
405
+ country: string;
406
+ /** Optional prefilled address field values. */
407
+ values?: WhatsAppAddressValues;
408
+ /** Optional saved addresses the user can pick instead of typing. */
409
+ savedAddresses?: WhatsAppSavedAddress[];
410
+ /**
411
+ * Optional per-field validation errors. WhatsApp blocks submission
412
+ * until the flagged fields are corrected.
413
+ */
414
+ validationErrors?: Partial<Record<keyof WhatsAppAddressValues, string>>;
415
+ }
416
+ /** A line item in an inbound WhatsApp order (wire format). */
417
+ export interface WhatsAppOrderItem {
418
+ /** Product SKU (Content ID in Commerce Manager). */
419
+ product_retailer_id: string;
420
+ /** Quantity ordered. */
421
+ quantity: number;
422
+ /** Unit price of the item. */
423
+ item_price: number;
424
+ /** Catalog currency code (e.g. `"USD"`). */
425
+ currency: string;
426
+ }
427
+ /**
428
+ * An inbound order placed by a user from a catalog, single-, or
429
+ * multi-product message (webhook message `type: "order"`).
430
+ */
431
+ export interface WhatsAppInboundOrder {
432
+ /** Meta product catalog ID the order was placed against. */
433
+ catalog_id: string;
434
+ /** Text accompanying the order. */
435
+ text?: string;
436
+ /** Ordered line items. */
437
+ product_items: WhatsAppOrderItem[];
438
+ }
439
+ /**
440
+ * A parsed inbound address submission (the `nfm_reply.response_json`
441
+ * of an address message reply).
442
+ */
443
+ export interface WhatsAppInboundAddress {
444
+ /** ID of the saved address the user selected, when applicable. */
445
+ saved_address_id?: string;
446
+ /** Address fields entered or confirmed by the user. */
447
+ values: WhatsAppAddressValues;
448
+ }
282
449
  /** A location payload for sending a map pin. */
283
450
  export interface LocationPayload {
284
451
  /** Latitude in decimal degrees. */
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/kuralle/kuralle-agents.git",
7
7
  "directory": "packages/kuralle-messaging-meta"
8
8
  },
9
- "version": "0.8.0",
9
+ "version": "0.8.5",
10
10
  "description": "Meta platform clients (WhatsApp, Messenger, Instagram) for Kuralle messaging",
11
11
  "type": "module",
12
12
  "main": "dist/index.js",
@@ -41,8 +41,8 @@
41
41
  "README.md"
42
42
  ],
43
43
  "dependencies": {
44
- "@kuralle-agents/http-client": "0.8.0",
45
- "@kuralle-agents/messaging": "0.8.0"
44
+ "@kuralle-agents/http-client": "0.8.5",
45
+ "@kuralle-agents/messaging": "0.8.5"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "hono": "^4.12.0"
@@ -58,9 +58,9 @@
58
58
  "redis": "^4.7.0",
59
59
  "typescript": "^5.8.2",
60
60
  "zod": "^4.0.0",
61
- "@kuralle-agents/core": "0.8.0",
62
- "@kuralle-agents/engagement": "0.8.0",
63
- "@kuralle-agents/hono-server": "0.8.0"
61
+ "@kuralle-agents/core": "0.8.5",
62
+ "@kuralle-agents/hono-server": "0.8.5",
63
+ "@kuralle-agents/engagement": "0.8.5"
64
64
  },
65
65
  "scripts": {
66
66
  "prebuild": "rm -rf dist",