@kuralle-agents/messaging-meta 0.8.0 → 0.9.0

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.
@@ -41,6 +41,7 @@ export function normalizeWebhook(payload) {
41
41
  messages: [],
42
42
  statuses: [],
43
43
  reactions: [],
44
+ errors: [],
44
45
  };
45
46
  if (!payload || typeof payload !== 'object') {
46
47
  return result;
@@ -68,6 +69,7 @@ function normalizeWhatsAppWebhook(payload) {
68
69
  messages: [],
69
70
  statuses: [],
70
71
  reactions: [],
72
+ errors: [],
71
73
  };
72
74
  const entries = payload.entry ?? [];
73
75
  for (const entry of entries) {
@@ -128,12 +130,32 @@ function normalizeWhatsAppWebhook(payload) {
128
130
  normalized.interactive = msg.interactive;
129
131
  if (msg.button)
130
132
  normalized.button = msg.button;
131
- if (msg.context)
132
- normalized.context = msg.context;
133
+ if (msg.order)
134
+ normalized.order = msg.order;
135
+ if (msg.context) {
136
+ // Real WhatsApp webhooks send `context.{id, from}` (plus forwarding
137
+ // flags / referred_product) — normalize raw `id` to `message_id`.
138
+ const referred = msg.context.referred_product;
139
+ normalized.context = {
140
+ message_id: msg.context.id ?? '',
141
+ from: msg.context.from,
142
+ forwarded: msg.context.forwarded,
143
+ frequently_forwarded: msg.context.frequently_forwarded,
144
+ referred_product: referred?.catalog_id && referred.product_retailer_id
145
+ ? {
146
+ catalog_id: referred.catalog_id,
147
+ product_retailer_id: referred.product_retailer_id,
148
+ }
149
+ : undefined,
150
+ };
151
+ }
133
152
  if (msg.referral)
134
153
  normalized.referral = msg.referral;
135
154
  result.messages.push(normalized);
136
155
  }
156
+ for (const err of value.errors ?? []) {
157
+ result.errors.push(normalizeWebhookError(err, phoneNumberId));
158
+ }
137
159
  // --- Statuses ---
138
160
  for (const status of value.statuses ?? []) {
139
161
  const normalizedStatus = {
@@ -143,6 +165,11 @@ function normalizeWhatsAppWebhook(payload) {
143
165
  timestamp: status.timestamp ?? '',
144
166
  phoneNumberId,
145
167
  };
168
+ if (status.recipient_type)
169
+ normalizedStatus.recipient_type = status.recipient_type;
170
+ if (status.biz_opaque_callback_data) {
171
+ normalizedStatus.biz_opaque_callback_data = status.biz_opaque_callback_data;
172
+ }
146
173
  if (status.conversation) {
147
174
  normalizedStatus.conversation = status.conversation;
148
175
  }
@@ -172,6 +199,7 @@ function normalizePageWebhook(payload) {
172
199
  messages: [],
173
200
  statuses: [],
174
201
  reactions: [],
202
+ errors: [],
175
203
  };
176
204
  const entries = payload.entry ?? [];
177
205
  for (const entry of entries) {
@@ -214,6 +242,13 @@ function normalizePageWebhook(payload) {
214
242
  if (msg.reply_to?.mid) {
215
243
  normalized.context = { message_id: msg.reply_to.mid };
216
244
  }
245
+ if (msg.quick_reply?.payload) {
246
+ normalized.type = 'postback';
247
+ normalized.button = {
248
+ text: msg.text ?? '',
249
+ payload: msg.quick_reply.payload,
250
+ };
251
+ }
217
252
  result.messages.push(normalized);
218
253
  continue;
219
254
  }
@@ -286,15 +321,30 @@ function resolvePageMessageType(msg) {
286
321
  }
287
322
  return 'unknown';
288
323
  }
289
- /** Normalise a status string to the union type, defaulting to `"sent"`. */
324
+ /** Normalise a status string; unknown values pass through unchanged. */
290
325
  function normalizeStatusValue(raw) {
326
+ if (!raw)
327
+ return 'sent';
291
328
  switch (raw) {
292
329
  case 'sent':
293
330
  case 'delivered':
294
331
  case 'read':
295
332
  case 'failed':
333
+ case 'played':
296
334
  return raw;
297
335
  default:
298
- return 'sent';
336
+ return raw;
299
337
  }
300
338
  }
339
+ function normalizeWebhookError(err, phoneNumberId) {
340
+ return {
341
+ code: typeof err.code === 'number' ? err.code : 0,
342
+ title: typeof err.title === 'string' ? err.title : undefined,
343
+ message: typeof err.message === 'string' ? err.message : undefined,
344
+ error_data: err.error_data && typeof err.error_data === 'object'
345
+ ? err.error_data
346
+ : undefined,
347
+ href: typeof err.href === 'string' ? err.href : undefined,
348
+ phoneNumberId,
349
+ };
350
+ }
@@ -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, TemplateCreateResponse, 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";
@@ -60,8 +61,15 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
60
61
  sendMedia(to: string, media: MediaPayload): Promise<SendResult>;
61
62
  sendInteractive(to: string, msg: InteractiveMessage): Promise<SendResult>;
62
63
  sendRaw(to: string, payload: WhatsAppOutbound): Promise<SendResult>;
63
- markAsRead(messageId: string): Promise<void>;
64
- /** WhatsApp Cloud API does not support typing indicators — no-op. */
64
+ markAsRead(messageId: string, opts?: {
65
+ typing?: boolean;
66
+ }): Promise<void>;
67
+ /** Show a typing indicator for 25s (requires a recent inbound message id). */
68
+ sendTypingIndicatorFor(messageId: string): Promise<void>;
69
+ /**
70
+ * WhatsApp typing indicators are keyed by message ID, not recipient.
71
+ * Use {@link sendTypingIndicatorFor} with the inbound message id instead.
72
+ */
65
73
  sendTypingIndicator(_to: string): Promise<void>;
66
74
  sendTemplate(to: string, template: TemplateMessage): Promise<SendResult>;
67
75
  /**
@@ -76,13 +84,50 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
76
84
  sendInteractiveButtons(to: string, msg: ButtonMessage): Promise<SendResult>;
77
85
  sendCTAButton(to: string, cta: CTAButtonMessage): Promise<SendResult>;
78
86
  sendInteractiveFlow(to: string, flow: FlowInteractiveInput): Promise<SendResult>;
87
+ /**
88
+ * Send a single-product message (`interactive.type: "product"`).
89
+ *
90
+ * Displays one catalog product with a **View** button; the user can open
91
+ * the Product Detail Page, add to cart, and send an order. Orders arrive
92
+ * as inbound `type: "order"` messages — see {@link parseInboundOrder}.
93
+ */
94
+ sendProduct(to: string, product: ProductMessage): Promise<SendResult>;
95
+ /**
96
+ * Send a multi-product message (`interactive.type: "product_list"`).
97
+ *
98
+ * Displays up to {@link MAX_PRODUCT_LIST_PRODUCTS} catalog products across
99
+ * up to {@link MAX_PRODUCT_LIST_SECTIONS} sections. Header and body are
100
+ * required by the Cloud API.
101
+ *
102
+ * @throws {MessagingError} `INVALID_PRODUCT_LIST` when the section or
103
+ * product limits are violated, or a section has no products.
104
+ */
105
+ sendProductList(to: string, list: ProductListMessage): Promise<SendResult>;
106
+ /**
107
+ * Send a catalog message (`interactive.type: "catalog_message"`).
108
+ *
109
+ * Displays a product thumbnail header, custom body text, and a
110
+ * **View catalog** button that opens the full catalog within WhatsApp.
111
+ */
112
+ sendCatalog(to: string, catalog: CatalogMessage): Promise<SendResult>;
113
+ /**
114
+ * Send an address request message (`interactive.type: "address_message"`).
115
+ *
116
+ * Country-gated by Meta — currently India-based businesses and their
117
+ * India customers only. The user's submission arrives as an inbound
118
+ * `nfm_reply` interactive message — see {@link parseInboundAddress}.
119
+ */
120
+ sendAddressRequest(to: string, address: AddressMessage): Promise<SendResult>;
79
121
  sendReaction(to: string, messageId: string, emoji: string): Promise<SendResult>;
80
122
  sendLocation(to: string, location: LocationPayload): Promise<SendResult>;
81
123
  sendContacts(to: string, contacts: ContactPayload[]): Promise<SendResult>;
82
124
  readonly templates: {
83
125
  list: (wabaId: string) => Promise<TemplateInfo[]>;
84
- create: (wabaId: string, template: TemplateDefinition) => Promise<TemplateInfo>;
85
- delete: (wabaId: string, name: string) => Promise<void>;
126
+ create: (wabaId: string, template: TemplateDefinition) => Promise<TemplateCreateResponse>;
127
+ delete: (wabaId: string, opts: {
128
+ name?: string;
129
+ hsm_id?: string;
130
+ }) => Promise<void>;
86
131
  };
87
132
  readonly phoneNumbers: {
88
133
  requestCode: (phoneNumberId: string, method: "SMS" | "VOICE", language: string) => Promise<void>;
@@ -95,6 +140,7 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
95
140
  create: (wabaId: string, flow: FlowDefinition) => Promise<FlowInfo>;
96
141
  update: (flowId: string, flow: Partial<FlowDefinition>) => Promise<FlowInfo>;
97
142
  publish: (flowId: string) => Promise<void>;
143
+ deprecate: (flowId: string) => Promise<void>;
98
144
  delete: (flowId: string) => Promise<void>;
99
145
  getAssets: (flowId: string) => Promise<FlowAssets>;
100
146
  };
@@ -103,6 +149,7 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
103
149
  protected toInboundMessage(msg: NormalizedMessage): InboundMessage;
104
150
  protected toStatusUpdate(status: NormalizedStatus): StatusUpdate;
105
151
  protected toReactionData(reaction: NormalizedReaction): ReactionData;
152
+ private validateProductList;
106
153
  private toSendResult;
107
154
  private mapMessageType;
108
155
  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';
@@ -135,7 +137,7 @@ export class WhatsAppClient extends BaseMetaClient {
135
137
  footer: msg.footer ? { text: msg.footer } : undefined,
136
138
  flowId: msg.action.flowId,
137
139
  flowCta: 'Continue',
138
- flowToken: msg.action.flowToken ?? '',
140
+ flowToken: msg.action.flowToken,
139
141
  flowAction: 'navigate',
140
142
  flowActionPayload: msg.action.parameters,
141
143
  });
@@ -151,14 +153,25 @@ export class WhatsAppClient extends BaseMetaClient {
151
153
  });
152
154
  return this.toSendResult(to, response);
153
155
  }
154
- async markAsRead(messageId) {
155
- await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
156
+ async markAsRead(messageId, opts) {
157
+ const body = {
156
158
  messaging_product: 'whatsapp',
157
159
  status: 'read',
158
160
  message_id: messageId,
159
- });
161
+ };
162
+ if (opts?.typing) {
163
+ body.typing_indicator = { type: 'text' };
164
+ }
165
+ await this.graphApi.post(`${this.config.phoneNumberId}/messages`, body);
160
166
  }
161
- /** WhatsApp Cloud API does not support typing indicators no-op. */
167
+ /** Show a typing indicator for 25s (requires a recent inbound message id). */
168
+ async sendTypingIndicatorFor(messageId) {
169
+ await this.markAsRead(messageId, { typing: true });
170
+ }
171
+ /**
172
+ * WhatsApp typing indicators are keyed by message ID, not recipient.
173
+ * Use {@link sendTypingIndicatorFor} with the inbound message id instead.
174
+ */
162
175
  async sendTypingIndicator(_to) {
163
176
  // intentionally empty
164
177
  }
@@ -197,6 +210,10 @@ export class WhatsAppClient extends BaseMetaClient {
197
210
  // WhatsApp-specific — Interactive messages
198
211
  // =========================================================================
199
212
  async sendListMessage(to, list) {
213
+ const totalRows = list.sections.reduce((sum, section) => sum + section.rows.length, 0);
214
+ if (totalRows > 10) {
215
+ throw new MessagingError(`List message exceeds maximum of 10 rows across all sections (has ${totalRows})`, 'INVALID_LIST_MESSAGE', 'whatsapp');
216
+ }
200
217
  const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
201
218
  messaging_product: 'whatsapp',
202
219
  recipient_type: 'individual',
@@ -256,6 +273,16 @@ export class WhatsAppClient extends BaseMetaClient {
256
273
  return this.toSendResult(to, response);
257
274
  }
258
275
  async sendInteractiveFlow(to, flow) {
276
+ const parameters = {
277
+ flow_message_version: '3',
278
+ flow_id: flow.flowId,
279
+ flow_cta: flow.flowCta,
280
+ flow_action: flow.flowAction,
281
+ flow_action_payload: flow.flowActionPayload,
282
+ };
283
+ if (flow.flowToken) {
284
+ parameters.flow_token = flow.flowToken;
285
+ }
259
286
  const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
260
287
  messaging_product: 'whatsapp',
261
288
  recipient_type: 'individual',
@@ -267,13 +294,124 @@ export class WhatsAppClient extends BaseMetaClient {
267
294
  footer: flow.footer,
268
295
  action: {
269
296
  name: 'flow',
297
+ parameters,
298
+ },
299
+ },
300
+ });
301
+ return this.toSendResult(to, response);
302
+ }
303
+ // =========================================================================
304
+ // WhatsApp-specific — Commerce (products, catalog, addresses)
305
+ // =========================================================================
306
+ /**
307
+ * Send a single-product message (`interactive.type: "product"`).
308
+ *
309
+ * Displays one catalog product with a **View** button; the user can open
310
+ * the Product Detail Page, add to cart, and send an order. Orders arrive
311
+ * as inbound `type: "order"` messages — see {@link parseInboundOrder}.
312
+ */
313
+ async sendProduct(to, product) {
314
+ const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
315
+ messaging_product: 'whatsapp',
316
+ recipient_type: 'individual',
317
+ to,
318
+ type: 'interactive',
319
+ interactive: {
320
+ type: 'product',
321
+ body: product.body,
322
+ footer: product.footer,
323
+ action: {
324
+ catalog_id: product.catalogId,
325
+ product_retailer_id: product.productRetailerId,
326
+ },
327
+ },
328
+ });
329
+ return this.toSendResult(to, response);
330
+ }
331
+ /**
332
+ * Send a multi-product message (`interactive.type: "product_list"`).
333
+ *
334
+ * Displays up to {@link MAX_PRODUCT_LIST_PRODUCTS} catalog products across
335
+ * up to {@link MAX_PRODUCT_LIST_SECTIONS} sections. Header and body are
336
+ * required by the Cloud API.
337
+ *
338
+ * @throws {MessagingError} `INVALID_PRODUCT_LIST` when the section or
339
+ * product limits are violated, or a section has no products.
340
+ */
341
+ async sendProductList(to, list) {
342
+ this.validateProductList(list);
343
+ const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
344
+ messaging_product: 'whatsapp',
345
+ recipient_type: 'individual',
346
+ to,
347
+ type: 'interactive',
348
+ interactive: {
349
+ type: 'product_list',
350
+ header: list.header,
351
+ body: list.body,
352
+ footer: list.footer,
353
+ action: {
354
+ catalog_id: list.catalogId,
355
+ sections: list.sections.map((section) => ({
356
+ title: section.title,
357
+ product_items: section.productRetailerIds.map((id) => ({
358
+ product_retailer_id: id,
359
+ })),
360
+ })),
361
+ },
362
+ },
363
+ });
364
+ return this.toSendResult(to, response);
365
+ }
366
+ /**
367
+ * Send a catalog message (`interactive.type: "catalog_message"`).
368
+ *
369
+ * Displays a product thumbnail header, custom body text, and a
370
+ * **View catalog** button that opens the full catalog within WhatsApp.
371
+ */
372
+ async sendCatalog(to, catalog) {
373
+ const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
374
+ messaging_product: 'whatsapp',
375
+ recipient_type: 'individual',
376
+ to,
377
+ type: 'interactive',
378
+ interactive: {
379
+ type: 'catalog_message',
380
+ body: catalog.body,
381
+ footer: catalog.footer,
382
+ action: {
383
+ name: 'catalog_message',
384
+ parameters: catalog.thumbnailProductRetailerId
385
+ ? { thumbnail_product_retailer_id: catalog.thumbnailProductRetailerId }
386
+ : undefined,
387
+ },
388
+ },
389
+ });
390
+ return this.toSendResult(to, response);
391
+ }
392
+ /**
393
+ * Send an address request message (`interactive.type: "address_message"`).
394
+ *
395
+ * Country-gated by Meta — currently India-based businesses and their
396
+ * India customers only. The user's submission arrives as an inbound
397
+ * `nfm_reply` interactive message — see {@link parseInboundAddress}.
398
+ */
399
+ async sendAddressRequest(to, address) {
400
+ const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
401
+ messaging_product: 'whatsapp',
402
+ recipient_type: 'individual',
403
+ to,
404
+ type: 'interactive',
405
+ interactive: {
406
+ type: 'address_message',
407
+ body: address.body,
408
+ action: {
409
+ name: 'address_message',
270
410
  parameters: {
271
- flow_message_version: '3',
272
- flow_id: flow.flowId,
273
- flow_cta: flow.flowCta,
274
- flow_token: flow.flowToken,
275
- flow_action: flow.flowAction,
276
- flow_action_payload: flow.flowActionPayload,
411
+ country: address.country,
412
+ values: address.values,
413
+ saved_addresses: address.savedAddresses,
414
+ validation_errors: address.validationErrors,
277
415
  },
278
416
  },
279
417
  },
@@ -318,17 +456,31 @@ export class WhatsAppClient extends BaseMetaClient {
318
456
  // =========================================================================
319
457
  templates = {
320
458
  list: async (wabaId) => {
321
- const result = await this.graphApi.get(`${wabaId}/message_templates`);
322
- return result.data.map(mapListTemplateRow);
459
+ const all = [];
460
+ let endpoint = `${wabaId}/message_templates`;
461
+ let extraParams;
462
+ for (let page = 0; page < 50; page++) {
463
+ const result = await this.graphApi.get(endpoint, extraParams);
464
+ all.push(...result.data.map(mapListTemplateRow));
465
+ const next = result.paging?.next;
466
+ if (!next)
467
+ break;
468
+ const parsed = parseGraphPagingNext(next);
469
+ endpoint = parsed.endpoint;
470
+ extraParams = parsed.params;
471
+ }
472
+ return all;
323
473
  },
324
474
  create: async (wabaId, template) => {
325
475
  return this.graphApi.post(`${wabaId}/message_templates`, template);
326
476
  },
327
- delete: async (wabaId, name) => {
328
- await this.graphApi.post(`${wabaId}/message_templates`, {
329
- name,
330
- _method: 'DELETE',
331
- });
477
+ delete: async (wabaId, opts) => {
478
+ const params = {};
479
+ if (opts.name)
480
+ params.name = opts.name;
481
+ if (opts.hsm_id)
482
+ params.hsm_id = opts.hsm_id;
483
+ await this.graphApi.delete(`${wabaId}/message_templates`, { params });
332
484
  },
333
485
  };
334
486
  // =========================================================================
@@ -374,8 +526,11 @@ export class WhatsAppClient extends BaseMetaClient {
374
526
  publish: async (flowId) => {
375
527
  await this.graphApi.post(`${flowId}/publish`, {});
376
528
  },
529
+ deprecate: async (flowId) => {
530
+ await this.graphApi.post(`${flowId}/deprecate`, {});
531
+ },
377
532
  delete: async (flowId) => {
378
- await this.graphApi.post(flowId, { status: 'DEPRECATED' });
533
+ await this.graphApi.delete(flowId);
379
534
  },
380
535
  getAssets: async (flowId) => {
381
536
  return this.graphApi.get(`${flowId}/assets`);
@@ -442,7 +597,10 @@ export class WhatsAppClient extends BaseMetaClient {
442
597
  }
443
598
  : undefined,
444
599
  context: msg.context
445
- ? { messageId: msg.context.message_id, from: msg.context.from }
600
+ ? {
601
+ messageId: msg.context.message_id,
602
+ from: msg.context.from,
603
+ }
446
604
  : undefined,
447
605
  raw: msg,
448
606
  };
@@ -474,6 +632,7 @@ export class WhatsAppClient extends BaseMetaClient {
474
632
  ? {
475
633
  model: status.pricing.pricing_model,
476
634
  category: status.pricing.category,
635
+ ...(status.pricing.type ? { type: status.pricing.type } : {}),
477
636
  }
478
637
  : undefined,
479
638
  raw: status,
@@ -490,6 +649,19 @@ export class WhatsAppClient extends BaseMetaClient {
490
649
  // =========================================================================
491
650
  // Private — conversion helpers
492
651
  // =========================================================================
652
+ validateProductList(list) {
653
+ if (list.sections.length === 0 || list.sections.length > MAX_PRODUCT_LIST_SECTIONS) {
654
+ throw new MessagingError(`Product list must have between 1 and ${MAX_PRODUCT_LIST_SECTIONS} sections (has ${list.sections.length})`, 'INVALID_PRODUCT_LIST', 'whatsapp');
655
+ }
656
+ const emptySection = list.sections.find((s) => s.productRetailerIds.length === 0);
657
+ if (emptySection) {
658
+ throw new MessagingError(`Product list section "${emptySection.title}" has no products`, 'INVALID_PRODUCT_LIST', 'whatsapp');
659
+ }
660
+ const totalProducts = list.sections.reduce((sum, s) => sum + s.productRetailerIds.length, 0);
661
+ if (totalProducts > MAX_PRODUCT_LIST_PRODUCTS) {
662
+ throw new MessagingError(`Product list exceeds maximum of ${MAX_PRODUCT_LIST_PRODUCTS} products across all sections (has ${totalProducts})`, 'INVALID_PRODUCT_LIST', 'whatsapp');
663
+ }
664
+ }
493
665
  toSendResult(to, response) {
494
666
  return {
495
667
  messageId: response.messages[0]?.id ?? '',
@@ -527,6 +699,8 @@ export class WhatsAppClient extends BaseMetaClient {
527
699
  return msg.interactive.button_reply.title;
528
700
  if (msg.interactive?.list_reply)
529
701
  return msg.interactive.list_reply.title;
702
+ if (msg.order?.text)
703
+ return msg.order.text;
530
704
  if (msg.location) {
531
705
  return msg.location.name ?? `${msg.location.latitude},${msg.location.longitude}`;
532
706
  }
@@ -582,6 +756,17 @@ function mapListTemplateRow(raw) {
582
756
  paused,
583
757
  };
584
758
  }
759
+ function parseGraphPagingNext(nextUrl) {
760
+ const url = new URL(nextUrl);
761
+ const parts = url.pathname.split('/').filter(Boolean);
762
+ const endpoint = parts.slice(1).join('/');
763
+ const params = {};
764
+ for (const [key, value] of url.searchParams.entries()) {
765
+ if (key !== 'access_token')
766
+ params[key] = value;
767
+ }
768
+ return { endpoint, params: Object.keys(params).length > 0 ? params : undefined };
769
+ }
585
770
  // ---------------------------------------------------------------------------
586
771
  // Utility
587
772
  // ---------------------------------------------------------------------------
@@ -0,0 +1,63 @@
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, WhatsAppInboundProductInquiry } from './types.js';
16
+ export type { ProductMessage, ProductListMessage, ProductSection, CatalogMessage, AddressMessage, WhatsAppAddressValues, WhatsAppSavedAddress, WhatsAppOrderItem, WhatsAppInboundOrder, WhatsAppInboundAddress, WhatsAppInboundProductInquiry, } 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;
59
+ /**
60
+ * Extract a product inquiry from an inbound message when the user replied
61
+ * in the context of a catalog product message (`context.referred_product`).
62
+ */
63
+ export declare function parseProductInquiry(message: InboundMessage): WhatsAppInboundProductInquiry | undefined;