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

View File

@ -50,6 +50,7 @@ export class BookingOrderController {
@Get(':id') @Get(':id')
async get(@Param('id') transactionId: string) { async get(@Param('id') transactionId: string) {
const data = await this.serviceData.getOneByOptions({ const data = await this.serviceData.getOneByOptions({
relations: ['items'],
where: { id: transactionId }, where: { id: transactionId },
}); });
@ -60,15 +61,49 @@ export class BookingOrderController {
invoice_code, invoice_code,
status, status,
id, id,
items,
} = data; } = 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 { return {
customer_name, customer_name,
customer_phone, customer_phone: maskedCustomerPhone,
booking_date, booking_date,
invoice_code, invoice_code,
status, status,
id, 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/'; process.env.WHATSAPP_BUSINESS_API_URL ?? 'https://graph.facebook.com/';
export const WHATSAPP_BUSINESS_VERSION = 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 = export const WHATSAPP_BUSINESS_QUEUE_URL =
process.env.WHATSAPP_BUSINESS_QUEUE_URL ?? 'auth/login'; process.env.WHATSAPP_BUSINESS_QUEUE_URL ?? 'auth/login';

View File

@ -30,6 +30,19 @@ export class WhatsappService {
const response = await axios(config); const response = await axios(config);
return response.data; return response.data;
} catch (error) { } 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); Logger.error(error);
apm?.captureError(error); apm?.captureError(error);
return null; 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) { async queueProcess(data: WhatsappQueue) {
const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`; const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`;
const payload = { const payload = {