Skip to content

Commit 97a314b

Browse files
committed
feat(messages): add sendProduct endpoint for WhatsApp Business catalog cards
1 parent 28ef9b7 commit 97a314b

4 files changed

Lines changed: 72 additions & 0 deletions

File tree

src/api/controllers/sendMessage.controller.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SendLocationDto,
88
SendMediaDto,
99
SendPollDto,
10+
SendProductDto,
1011
SendPtvDto,
1112
SendReactionDto,
1213
SendStatusDto,
@@ -101,6 +102,13 @@ export class SendMessageController {
101102
return await this.waMonitor.waInstances[instanceName].pollMessage(data);
102103
}
103104

105+
public async sendProduct({ instanceName }: InstanceDto, data: SendProductDto) {
106+
if (!isURL(data?.productImage) && !isBase64(data?.productImage)) {
107+
throw new BadRequestException('productImage must be a URL or base64 string');
108+
}
109+
return await this.waMonitor.waInstances[instanceName].productMessage(data);
110+
}
111+
104112
public async sendStatus({ instanceName }: InstanceDto, data: SendStatusDto, file?: any) {
105113
return await this.waMonitor.waInstances[instanceName].statusMessage(data, file);
106114
}

src/api/dto/sendMessage.dto.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,28 @@ export class SendReactionDto {
167167
key: proto.IMessageKey;
168168
reaction: string;
169169
}
170+
171+
export class SendProductDto extends Metadata {
172+
/** WhatsApp internal product id (from /business/getCatalog `id`) */
173+
productId: string;
174+
/** Business owner JID — `<phone>@s.whatsapp.net` of the catalog owner */
175+
businessOwnerJid: string;
176+
/** Product image — URL or base64 */
177+
productImage: string;
178+
/** Merchant-side retailer id (e.g. `BD3`). Optional. */
179+
retailerId?: string;
180+
/** Product title shown to recipients as a fallback. */
181+
title?: string;
182+
/** Product description shown as a fallback. */
183+
description?: string;
184+
/** ISO 4217 currency code (e.g. `ILS`, `USD`). Defaults to `USD`. */
185+
currencyCode?: string;
186+
/** Price × 1000 (e.g. 5500 ILS → `5500000`). */
187+
priceAmount1000?: number;
188+
/** Product landing URL. Optional. */
189+
url?: string;
190+
/** How many images the product has in the catalog. Defaults to 1. */
191+
productImageCount?: number;
192+
/** Optional caption sent alongside the product card. */
193+
caption?: string;
194+
}

src/api/routes/sendMessage.router.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SendLocationDto,
88
SendMediaDto,
99
SendPollDto,
10+
SendProductDto,
1011
SendPtvDto,
1112
SendReactionDto,
1213
SendStatusDto,
@@ -23,6 +24,7 @@ import {
2324
locationMessageSchema,
2425
mediaMessageSchema,
2526
pollMessageSchema,
27+
productMessageSchema,
2628
ptvMessageSchema,
2729
reactionMessageSchema,
2830
statusMessageSchema,
@@ -162,6 +164,16 @@ export class MessageRouter extends RouterBroker {
162164

163165
return res.status(HttpStatus.CREATED).json(response);
164166
})
167+
.post(this.routerPath('sendProduct'), ...guards, async (req, res) => {
168+
const response = await this.dataValidate<SendProductDto>({
169+
request: req,
170+
schema: productMessageSchema,
171+
ClassRef: SendProductDto,
172+
execute: (instance, data) => sendMessageController.sendProduct(instance, data),
173+
});
174+
175+
return res.status(HttpStatus.CREATED).json(response);
176+
})
165177
.post(this.routerPath('sendList'), ...guards, async (req, res) => {
166178
const response = await this.dataValidate<SendListDto>({
167179
request: req,

src/validate/message.schema.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,3 +447,30 @@ export const buttonsMessageSchema: JSONSchema7 = {
447447
},
448448
required: ['number'],
449449
};
450+
451+
export const productMessageSchema: JSONSchema7 = {
452+
$id: v4(),
453+
type: 'object',
454+
properties: {
455+
number: { ...numberDefinition },
456+
productId: { type: 'string', minLength: 1 },
457+
businessOwnerJid: {
458+
type: 'string',
459+
pattern: '^[0-9]+@s[.]whatsapp[.]net$',
460+
description: '"businessOwnerJid" must look like "<phone>@s.whatsapp.net"',
461+
},
462+
productImage: { type: 'string', minLength: 1 },
463+
retailerId: { type: 'string' },
464+
title: { type: 'string' },
465+
description: { type: 'string' },
466+
currencyCode: { type: 'string', minLength: 3, maxLength: 3 },
467+
priceAmount1000: { type: 'integer', minimum: 0 },
468+
url: { type: 'string' },
469+
productImageCount: { type: 'integer', minimum: 1 },
470+
caption: { type: 'string' },
471+
delay: { type: 'integer', description: 'Enter a value in milliseconds' },
472+
quoted: { ...quotedOptionsSchema },
473+
},
474+
required: ['number', 'productId', 'businessOwnerJid', 'productImage'],
475+
...isNotEmpty('number', 'productId', 'businessOwnerJid', 'productImage'),
476+
};

0 commit comments

Comments
 (0)