Skip to content

Commit fce3e55

Browse files
committed
feat: send pix button
1 parent 9f39ec2 commit fce3e55

File tree

3 files changed

+133
-6
lines changed

3 files changed

+133
-6
lines changed

src/api/dto/sendMessage.dto.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,21 @@ export class SendAudioDto extends Metadata {
9494
audio: string;
9595
}
9696

97-
export type TypeButton = 'reply' | 'copy' | 'url' | 'call';
97+
export type TypeButton = 'reply' | 'copy' | 'url' | 'call' | 'pix';
98+
99+
export type KeyType = 'phone' | 'email' | 'cpf' | 'cnpj' | 'random';
98100

99101
export class Button {
100102
type: TypeButton;
101-
displayText: string;
103+
displayText?: string;
102104
id?: string;
103105
url?: string;
104106
copyCode?: string;
105107
phoneNumber?: string;
108+
currency?: string;
109+
name?: string;
110+
keyType?: KeyType;
111+
key?: string;
106112
}
107113

108114
export class SendButtonsDto extends Metadata {

src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { HandleLabelDto, LabelDto } from '@api/dto/label.dto';
3333
import {
3434
Button,
3535
ContactMessage,
36+
KeyType,
3637
MediaMessage,
3738
Options,
3839
SendAudioDto,
@@ -1408,12 +1409,12 @@ export class BaileysStartupService extends ChannelStartupService {
14081409
});
14091410

14101411
const existingChat = await this.prismaRepository.chat.findFirst({
1411-
where: { instanceId: this.instanceId, remoteJid: message.key.remoteJid },
1412+
where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
14121413
});
14131414

14141415
if (!!existingChat) {
14151416
const chatToInsert = {
1416-
remoteJid: message.key.remoteJid,
1417+
remoteJid: message.remoteJid,
14171418
instanceId: this.instanceId,
14181419
name: message.pushName || '',
14191420
unreadMessages: 0,
@@ -2831,6 +2832,15 @@ export class BaileysStartupService extends ChannelStartupService {
28312832
);
28322833
}
28332834

2835+
private generateRandomId(length = 11) {
2836+
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
2837+
let result = '';
2838+
for (let i = 0; i < length; i++) {
2839+
result += characters.charAt(Math.floor(Math.random() * characters.length));
2840+
}
2841+
return result;
2842+
}
2843+
28342844
private toJSONString(button: Button): string {
28352845
const toString = (obj: any) => JSON.stringify(obj);
28362846

@@ -2844,6 +2854,49 @@ export class BaileysStartupService extends ChannelStartupService {
28442854
url: button.url,
28452855
merchant_url: button.url,
28462856
}),
2857+
pix: () =>
2858+
toString({
2859+
currency: button.currency,
2860+
total_amount: {
2861+
value: 0,
2862+
offset: 100
2863+
},
2864+
reference_id: this.generateRandomId(),
2865+
type: "physical-goods",
2866+
order: {
2867+
status: "pending",
2868+
subtotal: {
2869+
value: 0,
2870+
offset: 100
2871+
},
2872+
order_type: "ORDER",
2873+
items: [
2874+
{
2875+
name: "",
2876+
amount: {
2877+
value: 0,
2878+
offset: 100
2879+
},
2880+
quantity: 0,
2881+
sale_amount: {
2882+
value: 0,
2883+
offset: 100
2884+
}
2885+
}
2886+
]
2887+
},
2888+
payment_settings: [
2889+
{
2890+
type: "pix_static_code",
2891+
pix_static_code: {
2892+
merchant_name: button.name,
2893+
key: button.key,
2894+
key_type: this.mapKeyType.get(button.keyType)
2895+
}
2896+
}
2897+
],
2898+
share_payment_status: false
2899+
}),
28472900
};
28482901

28492902
return json[button.type]?.() || '';
@@ -2854,9 +2907,73 @@ export class BaileysStartupService extends ChannelStartupService {
28542907
['copy', 'cta_copy'],
28552908
['url', 'cta_url'],
28562909
['call', 'cta_call'],
2910+
['pix', 'payment_info'],
2911+
]);
2912+
2913+
private readonly mapKeyType = new Map<KeyType, string>([
2914+
['phone', 'PHONE'],
2915+
['email', 'EMAIL'],
2916+
['cpf', 'CPF'],
2917+
['cnpj', 'CNPJ'],
2918+
['random', 'EVP'],
28572919
]);
28582920

28592921
public async buttonMessage(data: SendButtonsDto) {
2922+
if (data.buttons.length === 0) {
2923+
throw new BadRequestException('At least one button is required');
2924+
}
2925+
2926+
const hasReplyButtons = data.buttons.some(btn => btn.type === 'reply');
2927+
2928+
const hasPixButton = data.buttons.some(btn => btn.type === 'pix');
2929+
2930+
const hasOtherButtons = data.buttons.some(btn => btn.type !== 'reply' && btn.type !== 'pix');
2931+
2932+
if (hasReplyButtons) {
2933+
if (data.buttons.length > 3) {
2934+
throw new BadRequestException('Maximum of 3 reply buttons allowed');
2935+
}
2936+
if (hasOtherButtons) {
2937+
throw new BadRequestException('Reply buttons cannot be mixed with other button types');
2938+
}
2939+
}
2940+
2941+
if (hasPixButton) {
2942+
if (data.buttons.length > 1) {
2943+
throw new BadRequestException('Only one PIX button is allowed');
2944+
}
2945+
if (hasOtherButtons) {
2946+
throw new BadRequestException('PIX button cannot be mixed with other button types');
2947+
}
2948+
2949+
const message: proto.IMessage = {
2950+
viewOnceMessage: {
2951+
message: {
2952+
interactiveMessage: {
2953+
nativeFlowMessage: {
2954+
buttons: [{
2955+
name: this.mapType.get('pix'),
2956+
buttonParamsJson: this.toJSONString(data.buttons[0]),
2957+
}],
2958+
messageParamsJson: JSON.stringify({
2959+
from: 'api',
2960+
templateId: v4(),
2961+
}),
2962+
},
2963+
},
2964+
},
2965+
},
2966+
};
2967+
2968+
return await this.sendMessageWithTyping(data.number, message, {
2969+
delay: data?.delay,
2970+
presence: 'composing',
2971+
quoted: data?.quoted,
2972+
mentionsEveryOne: data?.mentionsEveryOne,
2973+
mentioned: data?.mentioned,
2974+
});
2975+
}
2976+
28602977
const generate = await (async () => {
28612978
if (data?.thumbnailUrl) {
28622979
return await this.prepareMediaMessage({

src/validate/message.schema.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,14 +413,18 @@ export const buttonsMessageSchema: JSONSchema7 = {
413413
properties: {
414414
type: {
415415
type: 'string',
416-
enum: ['reply', 'copy', 'url', 'call'],
416+
enum: ['reply', 'copy', 'url', 'call', 'pix'],
417417
},
418418
displayText: { type: 'string' },
419419
id: { type: 'string' },
420420
url: { type: 'string' },
421421
phoneNumber: { type: 'string' },
422+
currency: { type: 'string' },
423+
name: { type: 'string' },
424+
keyType: { type: 'string', enum: ['phone', 'email', 'cpf', 'cnpj', 'random'] },
425+
key: { type: 'string' },
422426
},
423-
required: ['type', 'displayText'],
427+
required: ['type'],
424428
...isNotEmpty('id', 'url', 'phoneNumber'),
425429
},
426430
},

0 commit comments

Comments
 (0)