From 44e74de315801786abc9f03ff6a2f748a391a60f Mon Sep 17 00:00:00 2001 From: shancheas Date: Sat, 21 Dec 2024 04:01:54 +0700 Subject: [PATCH] feat: whatsapp notification --- env/env.development | 5 +- env/env.production | 5 +- .../queue/data/services/queue.service.ts | 14 ++ .../queue/domain/queue-admin.orchestrator.ts | 46 +++++ .../domain/usecases/register-queue.manager.ts | 20 +++ .../controllers/queue-job.controller.ts | 23 +++ src/modules/queue/queue.module.ts | 3 +- .../whatsapp/entity/whatsapp-queue.entity.ts | 8 + src/services/whatsapp/whatsapp.constant.ts | 14 ++ src/services/whatsapp/whatsapp.helper.ts | 62 +++++++ src/services/whatsapp/whatsapp.service.ts | 158 ++++++++++++++++++ 11 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 src/modules/queue/infrastructure/controllers/queue-job.controller.ts create mode 100644 src/services/whatsapp/entity/whatsapp-queue.entity.ts create mode 100644 src/services/whatsapp/whatsapp.constant.ts create mode 100644 src/services/whatsapp/whatsapp.helper.ts create mode 100644 src/services/whatsapp/whatsapp.service.ts diff --git a/env/env.development b/env/env.development index 4fb5ad3..1e241d4 100644 --- a/env/env.development +++ b/env/env.development @@ -43,4 +43,7 @@ GOOGLE_CALENDAR_ID="326464ac296874c7121825f5ef2e2799baa90b51da240f0045aae22beec1 SUPERSET_URL=https://dashboard.weplayground.eigen.co.id SUPERSET_ADMIN_USERNAME=admin -SUPERSET_ADMIN_PASSWORD=admin \ No newline at end of file +SUPERSET_ADMIN_PASSWORD=admin + +WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID=604883366037548 +WHATSAPP_BUSINESS_ACCESS_TOKEN=EAAINOvRRiEEBO9yQsYDnYtjHZB7q1nZCwbBpRcxIGMDWajKZBtmWxNRKvPYkS95KQZBsZBOvSFyjiEg5CcCZBZBtaSZApxyV8fiA3cEyVwf7iVZBQP2YCTPRQZArMFeeXbO0uq5TGygmjsIz3M4YxcUHxPzKO4pKxIyxnzcoUZCqCSo1NqQSLVf3a0JyZAwgDXGL55dV \ No newline at end of file diff --git a/env/env.production b/env/env.production index 5557402..fce7b16 100644 --- a/env/env.production +++ b/env/env.production @@ -40,4 +40,7 @@ GOOGLE_CALENDAR_ID="326464ac296874c7121825f5ef2e2799baa90b51da240f0045aae22beec1 SUPERSET_URL=https://dashboard.weplayground.eigen.co.id SUPERSET_ADMIN_USERNAME=admin -SUPERSET_ADMIN_PASSWORD=admin \ No newline at end of file +SUPERSET_ADMIN_PASSWORD=admin + +WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID=604883366037548 +WHATSAPP_BUSINESS_ACCESS_TOKEN=EAAINOvRRiEEBO9yQsYDnYtjHZB7q1nZCwbBpRcxIGMDWajKZBtmWxNRKvPYkS95KQZBsZBOvSFyjiEg5CcCZBZBtaSZApxyV8fiA3cEyVwf7iVZBQP2YCTPRQZArMFeeXbO0uq5TGygmjsIz3M4YxcUHxPzKO4pKxIyxnzcoUZCqCSo1NqQSLVf3a0JyZAwgDXGL55dV \ No newline at end of file diff --git a/src/modules/queue/data/services/queue.service.ts b/src/modules/queue/data/services/queue.service.ts index bb03a82..dbdd946 100644 --- a/src/modules/queue/data/services/queue.service.ts +++ b/src/modules/queue/data/services/queue.service.ts @@ -165,6 +165,15 @@ export class QueueService extends BaseDataService { super(repo); } + async queueTicket(queueId: string) { + return this.repo.findOne({ + relations: ['item', 'item.ticket'], + where: { + id: queueId, + }, + }); + } + async queues(ids: string[]) { const start = moment().startOf('day').valueOf(); const end = moment().endOf('day').valueOf(); @@ -225,6 +234,11 @@ export class QueueService extends BaseDataService { }); } + async updateLastNotification(queue_id: string, time: number) { + const query = `UPDATE queues SET last_notification = ${time} WHERE id = '${queue_id}'`; + this.dataSource.query(query); + } + async updateItemQty(item_id: string, qty: number): Promise { const query = `UPDATE queue_items SET qty = qty - ${qty} WHERE id = '${item_id}'`; this.dataSource.query(query); diff --git a/src/modules/queue/domain/queue-admin.orchestrator.ts b/src/modules/queue/domain/queue-admin.orchestrator.ts index 30d6b4d..96d6214 100644 --- a/src/modules/queue/domain/queue-admin.orchestrator.ts +++ b/src/modules/queue/domain/queue-admin.orchestrator.ts @@ -12,6 +12,11 @@ import { ORDER_TYPE, QUEUE_STATUS, } from 'src/core/strings/constants/base.constants'; +import { QueueTimeFormula } from './usecases/formula/queue-time.formula'; +import * as moment from 'moment'; +import { timeIsBefore, toTime } from './helpers/time.helper'; +import { WhatsappService } from 'src/services/whatsapp/whatsapp.service'; +import { WhatsappQueue } from 'src/services/whatsapp/entity/whatsapp-queue.entity'; @Injectable() export class QueueAdminOrchestrator { @@ -21,6 +26,7 @@ export class QueueAdminOrchestrator { private indexManager: IndexQueueManager, private callManager: CallQueueManager, private doneManager: DoneQueueManager, + private readonly queueTimeFormula: QueueTimeFormula, ) {} async index(params): Promise> { @@ -43,4 +49,44 @@ export class QueueAdminOrchestrator { await this.doneManager.execute(); return this.doneManager.getResult(); } + + async job(): Promise { + const notification = new WhatsappService(); + const itemMasters = await this.dataService.allQueue(); + const currentTime = moment().valueOf(); + + for (const queueItem of itemMasters) { + const queueTimes = await this.queueTimeFormula.items(queueItem.id); + if (!queueItem.use_notification) continue; + + for (const queueId in queueTimes) { + const callTime = queueTimes[queueId]; + + if (timeIsBefore(currentTime, callTime, queueItem.call_preparation)) { + const queueTicket = await this.service.queueTicket(queueId); + const payload: WhatsappQueue = { + id: queueId, + phone: queueTicket.item.ticket.phone, + code: queueTicket.code, + name: queueTicket.item.ticket.customer, + item_name: queueItem.name, + time: callTime, + }; + + console.log({ + currentTime: toTime(currentTime), + callTime: toTime(callTime), + last_notification: toTime(queueTicket.last_notification), + queueId, + }); + + const call_preparation = queueItem.call_preparation * 60 * 1000; + if (queueTicket.last_notification < currentTime - call_preparation) { + await notification.queueProcess(payload); + this.service.updateLastNotification(queueId, currentTime); + } + } + } + } + } } diff --git a/src/modules/queue/domain/usecases/register-queue.manager.ts b/src/modules/queue/domain/usecases/register-queue.manager.ts index 90ec7d6..f942322 100644 --- a/src/modules/queue/domain/usecases/register-queue.manager.ts +++ b/src/modules/queue/domain/usecases/register-queue.manager.ts @@ -11,6 +11,8 @@ import { padCode } from 'src/modules/transaction/vip-code/domain/usecases/manage import { QueueBucketReadService } from '../../data/services/queue-bucket'; import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; import { QueueTimeFormula } from './formula/queue-time.formula'; +import { WhatsappService } from 'src/services/whatsapp/whatsapp.service'; +import { WhatsappQueue } from 'src/services/whatsapp/entity/whatsapp-queue.entity'; import * as moment from 'moment'; @Injectable() @@ -22,6 +24,8 @@ export class RegisterQueueManager extends BaseCreateManager { super(); } + private currentItemMaster; + async averageTime(): Promise { const item = await this.getItemMaster(); return item.play_estimation; @@ -40,6 +44,7 @@ export class RegisterQueueManager extends BaseCreateManager { async beforeProcess(): Promise { const vip = this.data.vip ?? false; const item = await this.getItemMaster(); + this.currentItemMaster = item; const [, end] = await this.queueTime(item.item_queue_id); const queueNumber = await this.bucketService.getQueue( @@ -71,6 +76,21 @@ export class RegisterQueueManager extends BaseCreateManager { } async afterProcess(): Promise { + const notificationService = new WhatsappService(); + const item = this.currentItemMaster ?? (await this.getItemMaster()); + const queueTicket = await this.dataService.queueTicket(this.result.id); + + const payload: WhatsappQueue = { + id: this.result.id, + phone: queueTicket.item.ticket.phone, + code: this.result.code, + name: queueTicket.item.ticket.customer, + item_name: item.name, + time: this.result.time, + }; + notificationService.queueRegister(payload); + const currentTime = moment().valueOf(); + this.dataService.updateLastNotification(this.result.id, currentTime); return; } diff --git a/src/modules/queue/infrastructure/controllers/queue-job.controller.ts b/src/modules/queue/infrastructure/controllers/queue-job.controller.ts new file mode 100644 index 0000000..006b834 --- /dev/null +++ b/src/modules/queue/infrastructure/controllers/queue-job.controller.ts @@ -0,0 +1,23 @@ +import { Controller, Logger, Post } from '@nestjs/common'; + +import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; + +import { QueueAdminOrchestrator } from '../../domain/queue-admin.orchestrator'; +import { Public } from 'src/core/guards'; +// import { Cron } from '@nestjs/schedule'; + +@ApiTags(`Queue Admin`) +@Controller(`v1/${MODULE_NAME.QUEUE}-job`) +@ApiBearerAuth('JWT') +@Public(true) +export class QueueJobController { + constructor(private orchestrator: QueueAdminOrchestrator) {} + + // @Cron('*/1 * * * *') + @Post('queues/notification') + async call() { + Logger.log('call preparation'); + return this.orchestrator.job(); + } +} diff --git a/src/modules/queue/queue.module.ts b/src/modules/queue/queue.module.ts index a117956..e0c96d3 100644 --- a/src/modules/queue/queue.module.ts +++ b/src/modules/queue/queue.module.ts @@ -37,6 +37,7 @@ import { SplitQueueManager } from './domain/usecases/split-queue.manager'; import { QueueTransactionCancelHandler } from './infrastructure/handlers/cancel-transaction.handler'; import { ItemQueueModel } from '../item-related/item-queue/data/models/item-queue.model'; import { QueueTimeFormula } from './domain/usecases/formula/queue-time.formula'; +import { QueueJobController } from './infrastructure/controllers/queue-job.controller'; @Module({ imports: [ @@ -56,7 +57,7 @@ import { QueueTimeFormula } from './domain/usecases/formula/queue-time.formula'; ), CqrsModule, ], - controllers: [QueueController, QueueAdminController], + controllers: [QueueController, QueueAdminController, QueueJobController], providers: [ QueueOrchestrator, QueueAdminOrchestrator, diff --git a/src/services/whatsapp/entity/whatsapp-queue.entity.ts b/src/services/whatsapp/entity/whatsapp-queue.entity.ts new file mode 100644 index 0000000..0b169e7 --- /dev/null +++ b/src/services/whatsapp/entity/whatsapp-queue.entity.ts @@ -0,0 +1,8 @@ +export interface WhatsappQueue { + id: string; + phone: string; + code: string; + name: string; + item_name: string; + time: number; +} diff --git a/src/services/whatsapp/whatsapp.constant.ts b/src/services/whatsapp/whatsapp.constant.ts new file mode 100644 index 0000000..0f568ca --- /dev/null +++ b/src/services/whatsapp/whatsapp.constant.ts @@ -0,0 +1,14 @@ +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'; + +export const WHATSAPP_BUSINESS_QUEUE_URL = + process.env.WHATSAPP_BUSINESS_QUEUE_URL ?? 'auth/login'; + +export const WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID = + process.env.WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID ?? ''; + +export const WHATSAPP_BUSINESS_ACCESS_TOKEN = + process.env.WHATSAPP_BUSINESS_ACCESS_TOKEN ?? ''; diff --git a/src/services/whatsapp/whatsapp.helper.ts b/src/services/whatsapp/whatsapp.helper.ts new file mode 100644 index 0000000..2429d0f --- /dev/null +++ b/src/services/whatsapp/whatsapp.helper.ts @@ -0,0 +1,62 @@ +export function getTextMessageInput(recipient, text) { + return JSON.stringify({ + messaging_product: 'whatsapp', + preview_url: false, + recipient_type: 'individual', + to: recipient, + type: 'text', + text: { + body: text, + }, + }); +} + +export function getTemplatedMessageInput(recipient, movie, seats) { + return JSON.stringify({ + messaging_product: 'whatsapp', + to: recipient, + type: 'template', + template: { + name: 'sample_movie_ticket_confirmation', + language: { + code: 'en_US', + }, + components: [ + { + type: 'header', + parameters: [ + { + type: 'image', + image: { + link: movie.thumbnail, + }, + }, + ], + }, + { + type: 'body', + parameters: [ + { + type: 'text', + text: movie.title, + }, + { + type: 'date_time', + date_time: { + fallback_value: movie.time, + }, + }, + { + type: 'text', + text: movie.venue, + }, + { + type: 'text', + text: seats, + }, + ], + }, + ], + }, + }); +} diff --git a/src/services/whatsapp/whatsapp.service.ts b/src/services/whatsapp/whatsapp.service.ts new file mode 100644 index 0000000..3328586 --- /dev/null +++ b/src/services/whatsapp/whatsapp.service.ts @@ -0,0 +1,158 @@ +import { + phoneNumberOnly, + toTime, +} from 'src/modules/queue/domain/helpers/time.helper'; +import { WhatsappQueue } from './entity/whatsapp-queue.entity'; +import { + WHATSAPP_BUSINESS_ACCESS_TOKEN, + WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID, + WHATSAPP_BUSINESS_API_URL, + WHATSAPP_BUSINESS_QUEUE_URL, + WHATSAPP_BUSINESS_VERSION, +} from './whatsapp.constant'; +import axios from 'axios'; +import { Logger } from '@nestjs/common'; + +export class WhatsappService { + async sendMessage(data) { + const config = { + method: 'post', + url: `${WHATSAPP_BUSINESS_API_URL}/${WHATSAPP_BUSINESS_VERSION}/${WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID}/messages`, + headers: { + Authorization: `Bearer ${WHATSAPP_BUSINESS_ACCESS_TOKEN}`, + 'Content-Type': 'application/json', + }, + data: data, + }; + + const response = await axios(config); + return response.data; + } + + async queueRegister(data: WhatsappQueue) { + const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`; + const payload = { + messaging_product: 'whatsapp', + to: phoneNumberOnly(data.phone), // recipient's phone number + type: 'template', + template: { + name: 'queue_created', + language: { + code: 'id', // language code + }, + components: [ + { + type: 'header', + parameters: [ + { + parameter_name: 'queue_code', + type: 'text', + text: data.code, // replace with queue_code variable + }, + ], + }, + { + type: 'body', + parameters: [ + { + parameter_name: 'name', + type: 'text', + text: data.name, // replace with name variable + }, + { + parameter_name: 'item_name', + type: 'text', + text: data.item_name, // replace with item_name variable + }, + { + parameter_name: 'queue_code', + type: 'text', + text: data.code, // replace with queue_code variable + }, + { + parameter_name: 'queue_time', + type: 'text', + text: toTime(data.time), // replace with queue_time variable + }, + ], + }, + { + type: 'button', + sub_type: 'url', + index: '0', + parameters: [ + { + type: 'text', + text: queueUrl, // replace with dynamic URL + }, + ], + }, + ], + }, + }; + + await this.sendMessage(payload); + Logger.log(`Notification register for ${data.code} send to ${data.phone}`); + } + + async queueProcess(data: WhatsappQueue) { + const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`; + const payload = { + messaging_product: 'whatsapp', + to: data.phone, // recipient's phone number + type: 'template', + template: { + name: 'queue_process', + language: { + code: 'id', // language code + }, + components: [ + { + type: 'header', + parameters: [ + { + parameter_name: 'queue_code', + type: 'text', + text: data.item_name, // replace with queue_code variable + }, + ], + }, + { + type: 'body', + parameters: [ + { + parameter_name: 'name', + type: 'text', + text: data.name, // replace with name variable + }, + { + parameter_name: 'queue_code', + type: 'text', + text: data.code, // replace with queue_code variable + }, + { + parameter_name: 'queue_time', + type: 'text', + text: toTime(data.time), // replace with queue_time variable + }, + ], + }, + { + type: 'button', + sub_type: 'url', + index: '0', + parameters: [ + { + type: 'text', + text: queueUrl, // replace with dynamic URL + }, + ], + }, + ], + }, + }; + + await this.sendMessage(payload); + Logger.log(`Notification process for ${data.code} send to ${data.phone}`); + } +}