feat: add OTP whatsapp notification

pull/144/head 1.6.3-alpha.1
shancheas 2025-06-04 15:32:05 +07:00
parent 63e43a7ba0
commit 36b6ee733f
4 changed files with 117 additions and 12 deletions

View File

@ -4,6 +4,7 @@ import { VerificationModel } from '../models/verification.model';
import { BookingVerification } from '../../domain/entities/booking-verification.entity';
import { UnprocessableEntityException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { WhatsappService } from 'src/services/whatsapp/whatsapp.service';
export class VerificationService {
constructor(
@InjectRepository(VerificationModel)
@ -25,21 +26,25 @@ export class VerificationService {
async register(data: BookingVerification) {
const currentTime = Math.floor(Date.now()); // current time in seconds
if (
data.created_at &&
currentTime - data.created_at > this.expiredTimeRegister
) {
throw new UnprocessableEntityException('Please try again in 1 minute');
}
// Generate a 4 digit OTP code
data.code = Math.floor(1000 + Math.random() * 9000).toString();
data.tried = 0;
data.updated_at = currentTime;
const otpCode = Math.floor(1000 + Math.random() * 9000).toString();
let verification = await this.verificationRepository.findOne({
where: { phone_number: data.phone_number },
});
if (
verification.updated_at &&
currentTime - verification.updated_at < this.expiredTimeRegister
) {
throw new UnprocessableEntityException('Please try again in 1 minute');
}
data.code = otpCode;
data.tried = 0;
data.updated_at = currentTime;
if (verification) {
// Update existing record
verification = this.verificationRepository.merge(verification, data);
@ -47,7 +52,15 @@ export class VerificationService {
// Create new record
verification = this.verificationRepository.create(data);
}
return this.verificationRepository.save(verification);
const payload = await this.verificationRepository.save(verification);
const notificationService = new WhatsappService();
notificationService.sendOtpNotification({
phone: data.phone_number,
code: otpCode,
});
return payload;
}
async findByPhoneNumber(phoneNumber: string) {

View File

@ -50,6 +50,7 @@ export class BookingOrderController {
@Get(':id')
async get(@Param('id') transactionId: string) {
const data = await this.serviceData.getOneByOptions({
relations: ['items'],
where: { id: transactionId },
});
@ -60,15 +61,49 @@ export class BookingOrderController {
invoice_code,
status,
id,
items,
} = data;
const usageItems = items.map((item) => {
const {
id,
item_id,
item_name,
item_price,
item_category_name,
total_price,
total_net_price,
qty,
qty_remaining,
} = item;
return {
id,
item_id,
item_name,
item_price,
item_category_name,
total_price,
total_net_price,
qty,
qty_remaining,
};
});
// Mask customer_phone with * and keep last 4 numbers
let maskedCustomerPhone = customer_phone;
if (typeof customer_phone === 'string' && customer_phone.length > 4) {
const last4 = customer_phone.slice(-4);
maskedCustomerPhone = '*'.repeat(customer_phone.length - 4) + last4;
}
return {
customer_name,
customer_phone,
customer_phone: maskedCustomerPhone,
booking_date,
invoice_code,
status,
id,
items: usageItems,
};
}
}

View File

@ -2,7 +2,7 @@ export const WHATSAPP_BUSINESS_API_URL =
process.env.WHATSAPP_BUSINESS_API_URL ?? 'https://graph.facebook.com/';
export const WHATSAPP_BUSINESS_VERSION =
process.env.WHATSAPP_BUSINESS_VERSION ?? 'v21.0';
process.env.WHATSAPP_BUSINESS_VERSION ?? 'v22.0';
export const WHATSAPP_BUSINESS_QUEUE_URL =
process.env.WHATSAPP_BUSINESS_QUEUE_URL ?? 'auth/login';

View File

@ -30,6 +30,19 @@ export class WhatsappService {
const response = await axios(config);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
console.error('Axios error response:', {
status: error.response.status,
data: error.response.data,
headers: error.response.headers,
});
} else if (error.request) {
console.error('Axios error request:', error.request);
} else {
console.error('Axios error message:', error.message);
}
}
Logger.error(error);
apm?.captureError(error);
return null;
@ -105,6 +118,50 @@ export class WhatsappService {
);
}
async sendOtpNotification(data: { phone: string; code: string }) {
// Compose the WhatsApp message payload for OTP using Facebook WhatsApp API
const payload = {
messaging_product: 'whatsapp',
to: data.phone, // recipient's phone number in international format
type: 'template',
template: {
name: 'booking_otp', // Make sure this template is approved in WhatsApp Business Manager
language: {
code: 'id', // or 'en' if you want English
},
components: [
{
type: 'body',
parameters: [
{
type: 'text',
text: parseInt(data.code), // OTP code
},
],
},
{
type: 'button',
sub_type: 'url',
index: '0',
parameters: [
{
type: 'text',
text: `${data.code}`,
},
],
},
],
},
};
const response = await this.sendMessage(payload);
if (response) {
Logger.log(
`OTP notification for code ${data.code} sent to ${data.phone}`,
);
}
}
async queueProcess(data: WhatsappQueue) {
const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`;
const payload = {