Skip to content

Commit

Permalink
feat: send pix button
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidsonGomes committed Oct 29, 2024
1 parent 9f39ec2 commit fce3e55
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 6 deletions.
10 changes: 8 additions & 2 deletions src/api/dto/sendMessage.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,21 @@ export class SendAudioDto extends Metadata {
audio: string;
}

export type TypeButton = 'reply' | 'copy' | 'url' | 'call';
export type TypeButton = 'reply' | 'copy' | 'url' | 'call' | 'pix';

export type KeyType = 'phone' | 'email' | 'cpf' | 'cnpj' | 'random';

export class Button {
type: TypeButton;
displayText: string;
displayText?: string;
id?: string;
url?: string;
copyCode?: string;
phoneNumber?: string;
currency?: string;
name?: string;
keyType?: KeyType;
key?: string;
}

export class SendButtonsDto extends Metadata {
Expand Down
121 changes: 119 additions & 2 deletions src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { HandleLabelDto, LabelDto } from '@api/dto/label.dto';
import {
Button,
ContactMessage,
KeyType,
MediaMessage,
Options,
SendAudioDto,
Expand Down Expand Up @@ -1408,12 +1409,12 @@ export class BaileysStartupService extends ChannelStartupService {
});

const existingChat = await this.prismaRepository.chat.findFirst({
where: { instanceId: this.instanceId, remoteJid: message.key.remoteJid },
where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
});

if (!!existingChat) {
const chatToInsert = {
remoteJid: message.key.remoteJid,
remoteJid: message.remoteJid,
instanceId: this.instanceId,
name: message.pushName || '',
unreadMessages: 0,
Expand Down Expand Up @@ -2831,6 +2832,15 @@ export class BaileysStartupService extends ChannelStartupService {
);
}

private generateRandomId(length = 11) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}

private toJSONString(button: Button): string {
const toString = (obj: any) => JSON.stringify(obj);

Expand All @@ -2844,6 +2854,49 @@ export class BaileysStartupService extends ChannelStartupService {
url: button.url,
merchant_url: button.url,
}),
pix: () =>
toString({
currency: button.currency,
total_amount: {
value: 0,
offset: 100
},
reference_id: this.generateRandomId(),
type: "physical-goods",
order: {
status: "pending",
subtotal: {
value: 0,
offset: 100
},
order_type: "ORDER",
items: [
{
name: "",
amount: {
value: 0,
offset: 100
},
quantity: 0,
sale_amount: {
value: 0,
offset: 100
}
}
]
},
payment_settings: [
{
type: "pix_static_code",
pix_static_code: {
merchant_name: button.name,
key: button.key,
key_type: this.mapKeyType.get(button.keyType)
}
}
],
share_payment_status: false
}),
};

return json[button.type]?.() || '';
Expand All @@ -2854,9 +2907,73 @@ export class BaileysStartupService extends ChannelStartupService {
['copy', 'cta_copy'],
['url', 'cta_url'],
['call', 'cta_call'],
['pix', 'payment_info'],
]);

private readonly mapKeyType = new Map<KeyType, string>([
['phone', 'PHONE'],
['email', 'EMAIL'],
['cpf', 'CPF'],
['cnpj', 'CNPJ'],
['random', 'EVP'],
]);

public async buttonMessage(data: SendButtonsDto) {
if (data.buttons.length === 0) {
throw new BadRequestException('At least one button is required');
}

const hasReplyButtons = data.buttons.some(btn => btn.type === 'reply');

const hasPixButton = data.buttons.some(btn => btn.type === 'pix');

const hasOtherButtons = data.buttons.some(btn => btn.type !== 'reply' && btn.type !== 'pix');

if (hasReplyButtons) {
if (data.buttons.length > 3) {
throw new BadRequestException('Maximum of 3 reply buttons allowed');
}
if (hasOtherButtons) {
throw new BadRequestException('Reply buttons cannot be mixed with other button types');
}
}

if (hasPixButton) {
if (data.buttons.length > 1) {
throw new BadRequestException('Only one PIX button is allowed');
}
if (hasOtherButtons) {
throw new BadRequestException('PIX button cannot be mixed with other button types');
}

const message: proto.IMessage = {
viewOnceMessage: {
message: {
interactiveMessage: {
nativeFlowMessage: {
buttons: [{
name: this.mapType.get('pix'),
buttonParamsJson: this.toJSONString(data.buttons[0]),
}],
messageParamsJson: JSON.stringify({
from: 'api',
templateId: v4(),
}),
},
},
},
},
};

return await this.sendMessageWithTyping(data.number, message, {
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
});
}

const generate = await (async () => {
if (data?.thumbnailUrl) {
return await this.prepareMediaMessage({
Expand Down
8 changes: 6 additions & 2 deletions src/validate/message.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,14 +413,18 @@ export const buttonsMessageSchema: JSONSchema7 = {
properties: {
type: {
type: 'string',
enum: ['reply', 'copy', 'url', 'call'],
enum: ['reply', 'copy', 'url', 'call', 'pix'],
},
displayText: { type: 'string' },
id: { type: 'string' },
url: { type: 'string' },
phoneNumber: { type: 'string' },
currency: { type: 'string' },
name: { type: 'string' },
keyType: { type: 'string', enum: ['phone', 'email', 'cpf', 'cnpj', 'random'] },
key: { type: 'string' },
},
required: ['type', 'displayText'],
required: ['type'],
...isNotEmpty('id', 'url', 'phoneNumber'),
},
},
Expand Down

0 comments on commit fce3e55

Please sign in to comment.