From d4d605d16887bf02204dd67779ca3ccc5f9b0d0e Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 9 Jun 2025 16:55:55 +0700 Subject: [PATCH] feat: add whatsapp notification for booking created --- package.json | 2 + .../data/services/verification.service.ts | 2 + .../order/infrastructure/order.controller.ts | 17 +++- .../midtrans-transaction-callback.handler.ts | 30 ++++--- .../whatsapp/entity/booking.entity.ts | 7 ++ src/services/whatsapp/whatsapp.constant.ts | 8 ++ src/services/whatsapp/whatsapp.service.ts | 81 +++++++++++++++++++ yarn.lock | 45 ++++++++++- 8 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 src/services/whatsapp/entity/booking.entity.ts diff --git a/package.json b/package.json index 9b6f5a4..ef1f732 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "pdfmake": "^0.2.10", "pg": "^8.11.5", "plop": "^4.0.1", + "qrcode": "^1.5.4", "reflect-metadata": "^0.2.0", "rxjs": "^7.5.0", "typeorm": "^0.3.20", @@ -71,6 +72,7 @@ "@types/express": "^4.17.13", "@types/jest": "29.5.12", "@types/node": "^20.12.13", + "@types/qrcode": "^1.5.5", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", diff --git a/src/modules/booking-online/authentication/data/services/verification.service.ts b/src/modules/booking-online/authentication/data/services/verification.service.ts index acbc186..69a5540 100644 --- a/src/modules/booking-online/authentication/data/services/verification.service.ts +++ b/src/modules/booking-online/authentication/data/services/verification.service.ts @@ -25,6 +25,7 @@ export class VerificationService { } async register(data: BookingVerification) { + const isProduction = process.env.NODE_ENV === 'true'; const currentTime = Math.floor(Date.now()); // current time in seconds // Generate a 4 digit OTP code @@ -35,6 +36,7 @@ export class VerificationService { }); if ( + isProduction && verification.updated_at && currentTime - verification.updated_at < this.expiredTimeRegister ) { diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index 9a31e01..6b87ca2 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Public } from 'src/core/guards'; import { TransactionDto } from './dto/booking-order.dto'; @@ -7,6 +7,9 @@ import { TransactionDataService } from 'src/modules/transaction/transaction/data import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { MidtransService } from 'src/modules/configuration/midtrans/data/services/midtrans.service'; import { CreateBookingManager } from '../domain/usecases/managers/create-booking.manager'; +import * as QRCode from 'qrcode'; +import { Gate } from 'src/core/response/domain/decorators/pagination.response'; +import { Response } from 'express'; @ApiTags('Booking Order') @Controller('v1/booking') @@ -106,4 +109,16 @@ export class BookingOrderController { items: usageItems, }; } + + @Gate() + @Get('qrcode/:id') + async getQRcode(@Param('id') id: string, @Res() res: Response) { + console.log(QRCode); + const qrData = id; + const data = await QRCode.toDataURL(qrData); + res.setHeader('Content-Type', 'image/png'); + const base64Data = data.split(',')[1]; + const buffer = Buffer.from(base64Data, 'base64'); + res.send(buffer); + } } diff --git a/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts b/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts index a441d4b..35f9214 100644 --- a/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts +++ b/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts @@ -11,6 +11,9 @@ import { TransactionChangeStatusEvent } from '../../entities/event/transaction-c import * as _ from 'lodash'; import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { generateInvoiceCodeHelper } from '../managers/helpers/generate-invoice-code.helper'; +import { TransactionType } from '../../../constants'; +import { WhatsappService } from 'src/services/whatsapp/whatsapp.service'; +import * as moment from 'moment'; @EventsHandler(MidtransCallbackEvent) export class MidtransCallbackHandler @@ -22,7 +25,6 @@ export class MidtransCallbackHandler ) {} async handle(event: MidtransCallbackEvent) { - console.log('callbak mid', event); const data_id = event.data.id; const data = event.data.data; let old_data = undefined; @@ -58,16 +60,22 @@ export class MidtransCallbackHandler .manager.connection.createQueryRunner(); await this.dataService.create(queryRunner, TransactionModel, transaction); - console.log('update change status to tr', { - id: data_id, - old: old_data, - data: { ...data, status: transaction.status }, - user: BLANK_USER, - description: 'Midtrans Callback', - module: TABLE_NAME.TRANSACTION, - op: OPERATION.UPDATE, - }); - console.log({ data, old_data }); + + if ( + transaction.status === STATUS.SETTLED && + transaction.type === TransactionType.ONLINE + ) { + const whatsappService = new WhatsappService(); + const formattedDate = moment(transaction.booking_date); + const payload = { + id: transaction.id, + phone: transaction.customer_phone, + code: transaction.invoice_code, + name: transaction.customer_name, + time: formattedDate.valueOf(), + }; + await whatsappService.bookingCreated(payload); + } this.eventBus.publish( new TransactionChangeStatusEvent({ diff --git a/src/services/whatsapp/entity/booking.entity.ts b/src/services/whatsapp/entity/booking.entity.ts new file mode 100644 index 0000000..90f5b95 --- /dev/null +++ b/src/services/whatsapp/entity/booking.entity.ts @@ -0,0 +1,7 @@ +export interface WhatsappBookingCreate { + id: string; + phone: string; + code: string; + name: string; + time: number; +} diff --git a/src/services/whatsapp/whatsapp.constant.ts b/src/services/whatsapp/whatsapp.constant.ts index cc04926..2b8f472 100644 --- a/src/services/whatsapp/whatsapp.constant.ts +++ b/src/services/whatsapp/whatsapp.constant.ts @@ -1,6 +1,14 @@ export const WHATSAPP_BUSINESS_API_URL = process.env.WHATSAPP_BUSINESS_API_URL ?? 'https://graph.facebook.com/'; +export const BOOKING_QR_URL = + process.env.BOOKING_QR_URL ?? + 'https://www.drupal.org/files/project-images/qrcode-module_0.png?'; + +export const BOOKING_TICKET_URL = + process.env.BOOKING_TICKET_URL ?? + 'https://booking.sky.eigen.co.id/app/ticket/'; + export const WHATSAPP_BUSINESS_VERSION = process.env.WHATSAPP_BUSINESS_VERSION ?? 'v22.0'; diff --git a/src/services/whatsapp/whatsapp.service.ts b/src/services/whatsapp/whatsapp.service.ts index 8d22282..cad1b36 100644 --- a/src/services/whatsapp/whatsapp.service.ts +++ b/src/services/whatsapp/whatsapp.service.ts @@ -4,6 +4,8 @@ import { } from 'src/modules/queue/domain/helpers/time.helper'; import { WhatsappQueue } from './entity/whatsapp-queue.entity'; import { + BOOKING_QR_URL, + BOOKING_TICKET_URL, WHATSAPP_BUSINESS_ACCESS_TOKEN, WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID, WHATSAPP_BUSINESS_API_URL, @@ -13,6 +15,8 @@ import { import axios from 'axios'; import { Logger } from '@nestjs/common'; import { apm } from 'src/core/apm'; +import { WhatsappBookingCreate } from './entity/booking.entity'; +import * as moment from 'moment'; export class WhatsappService { async sendMessage(data) { @@ -36,6 +40,7 @@ export class WhatsappService { status: error.response.status, data: error.response.data, headers: error.response.headers, + error: error.response.data?.error?.error_data, }); } else if (error.request) { console.error('Axios error request:', error.request); @@ -118,6 +123,82 @@ export class WhatsappService { ); } + async bookingCreated(data: WhatsappBookingCreate) { + const ticketUrl = `${BOOKING_TICKET_URL}${data.id}`; + const imageUrl = `${BOOKING_QR_URL}${data.id}`; + + const momentDate = moment(data.time); + const fallbackValue = momentDate.locale('id').format('dddd, DD MMMM YYYY'); + // const dayOfWeek = momentDate.day(); + // const dayOfMonth = momentDate.date(); + // const year = momentDate.year(); + // const month = momentDate.month() + 1; + // const hour = momentDate.hour(); + // const minute = momentDate.minute(); + + const payload = { + messaging_product: 'whatsapp', + to: phoneNumberOnly(data.phone), // recipient's phone number + type: 'template', + template: { + name: 'booking_online', + language: { + code: 'id', // language code + }, + components: [ + { + type: 'header', + parameters: [ + { + type: 'image', + image: { + link: imageUrl, + }, + }, + ], + }, + { + type: 'body', + parameters: [ + { + type: 'text', + parameter_name: 'customer', + text: data.name, // replace with name variable + }, + { + type: 'text', + parameter_name: 'booking_code', + text: data.code, // replace with queue_code variable + }, + { + type: 'text', + parameter_name: 'booking_date', + text: fallbackValue, + }, + ], + }, + { + type: 'button', + sub_type: 'url', + index: '0', + parameters: [ + { + type: 'text', + text: ticketUrl, // replace with dynamic URL + }, + ], + }, + ], + }, + }; + + const response = await this.sendMessage(payload); + if (response) + Logger.log( + `Notification register Booking for ${data.code} send to ${data.phone}`, + ); + } + async sendOtpNotification(data: { phone: string; code: string }) { // Compose the WhatsApp message payload for OTP using Facebook WhatsApp API const payload = { diff --git a/yarn.lock b/yarn.lock index ddf1626..a4abb7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1239,6 +1239,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== +"@types/qrcode@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac" + integrity sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg== + dependencies: + "@types/node" "*" + "@types/qs@*": version "6.9.15" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" @@ -2861,6 +2868,11 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +dijkstrajs@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" + integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -6363,6 +6375,11 @@ png-js@^1.0.0: resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d" integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g== +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -6464,6 +6481,15 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== +qrcode@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88" + integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg== + dependencies: + dijkstrajs "^1.0.1" + pngjs "^5.0.0" + yargs "^15.3.1" + qs@6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" @@ -8070,7 +8096,7 @@ yargs-parser@21.1.1, yargs-parser@^21.0.1, yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs-parser@^18.1.1: +yargs-parser@^18.1.1, yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== @@ -8108,6 +8134,23 @@ yargs@15.3.1: y18n "^4.0.0" yargs-parser "^18.1.1" +yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + yargs@^16.0.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"