feat: whatsapp notification
parent
82e7879969
commit
44e74de315
|
@ -43,4 +43,7 @@ GOOGLE_CALENDAR_ID="326464ac296874c7121825f5ef2e2799baa90b51da240f0045aae22beec1
|
||||||
|
|
||||||
SUPERSET_URL=https://dashboard.weplayground.eigen.co.id
|
SUPERSET_URL=https://dashboard.weplayground.eigen.co.id
|
||||||
SUPERSET_ADMIN_USERNAME=admin
|
SUPERSET_ADMIN_USERNAME=admin
|
||||||
SUPERSET_ADMIN_PASSWORD=admin
|
SUPERSET_ADMIN_PASSWORD=admin
|
||||||
|
|
||||||
|
WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID=604883366037548
|
||||||
|
WHATSAPP_BUSINESS_ACCESS_TOKEN=EAAINOvRRiEEBO9yQsYDnYtjHZB7q1nZCwbBpRcxIGMDWajKZBtmWxNRKvPYkS95KQZBsZBOvSFyjiEg5CcCZBZBtaSZApxyV8fiA3cEyVwf7iVZBQP2YCTPRQZArMFeeXbO0uq5TGygmjsIz3M4YxcUHxPzKO4pKxIyxnzcoUZCqCSo1NqQSLVf3a0JyZAwgDXGL55dV
|
|
@ -40,4 +40,7 @@ GOOGLE_CALENDAR_ID="326464ac296874c7121825f5ef2e2799baa90b51da240f0045aae22beec1
|
||||||
|
|
||||||
SUPERSET_URL=https://dashboard.weplayground.eigen.co.id
|
SUPERSET_URL=https://dashboard.weplayground.eigen.co.id
|
||||||
SUPERSET_ADMIN_USERNAME=admin
|
SUPERSET_ADMIN_USERNAME=admin
|
||||||
SUPERSET_ADMIN_PASSWORD=admin
|
SUPERSET_ADMIN_PASSWORD=admin
|
||||||
|
|
||||||
|
WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID=604883366037548
|
||||||
|
WHATSAPP_BUSINESS_ACCESS_TOKEN=EAAINOvRRiEEBO9yQsYDnYtjHZB7q1nZCwbBpRcxIGMDWajKZBtmWxNRKvPYkS95KQZBsZBOvSFyjiEg5CcCZBZBtaSZApxyV8fiA3cEyVwf7iVZBQP2YCTPRQZArMFeeXbO0uq5TGygmjsIz3M4YxcUHxPzKO4pKxIyxnzcoUZCqCSo1NqQSLVf3a0JyZAwgDXGL55dV
|
|
@ -165,6 +165,15 @@ export class QueueService extends BaseDataService<QueueModel> {
|
||||||
super(repo);
|
super(repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async queueTicket(queueId: string) {
|
||||||
|
return this.repo.findOne({
|
||||||
|
relations: ['item', 'item.ticket'],
|
||||||
|
where: {
|
||||||
|
id: queueId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async queues(ids: string[]) {
|
async queues(ids: string[]) {
|
||||||
const start = moment().startOf('day').valueOf();
|
const start = moment().startOf('day').valueOf();
|
||||||
const end = moment().endOf('day').valueOf();
|
const end = moment().endOf('day').valueOf();
|
||||||
|
@ -225,6 +234,11 @@ export class QueueService extends BaseDataService<QueueModel> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<void> {
|
async updateItemQty(item_id: string, qty: number): Promise<void> {
|
||||||
const query = `UPDATE queue_items SET qty = qty - ${qty} WHERE id = '${item_id}'`;
|
const query = `UPDATE queue_items SET qty = qty - ${qty} WHERE id = '${item_id}'`;
|
||||||
this.dataSource.query(query);
|
this.dataSource.query(query);
|
||||||
|
|
|
@ -12,6 +12,11 @@ import {
|
||||||
ORDER_TYPE,
|
ORDER_TYPE,
|
||||||
QUEUE_STATUS,
|
QUEUE_STATUS,
|
||||||
} from 'src/core/strings/constants/base.constants';
|
} 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()
|
@Injectable()
|
||||||
export class QueueAdminOrchestrator {
|
export class QueueAdminOrchestrator {
|
||||||
|
@ -21,6 +26,7 @@ export class QueueAdminOrchestrator {
|
||||||
private indexManager: IndexQueueManager,
|
private indexManager: IndexQueueManager,
|
||||||
private callManager: CallQueueManager,
|
private callManager: CallQueueManager,
|
||||||
private doneManager: DoneQueueManager,
|
private doneManager: DoneQueueManager,
|
||||||
|
private readonly queueTimeFormula: QueueTimeFormula,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async index(params): Promise<PaginationResponse<Queue>> {
|
async index(params): Promise<PaginationResponse<Queue>> {
|
||||||
|
@ -43,4 +49,44 @@ export class QueueAdminOrchestrator {
|
||||||
await this.doneManager.execute();
|
await this.doneManager.execute();
|
||||||
return this.doneManager.getResult();
|
return this.doneManager.getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async job(): Promise<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { padCode } from 'src/modules/transaction/vip-code/domain/usecases/manage
|
||||||
import { QueueBucketReadService } from '../../data/services/queue-bucket';
|
import { QueueBucketReadService } from '../../data/services/queue-bucket';
|
||||||
import { ItemModel } from 'src/modules/item-related/item/data/models/item.model';
|
import { ItemModel } from 'src/modules/item-related/item/data/models/item.model';
|
||||||
import { QueueTimeFormula } from './formula/queue-time.formula';
|
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';
|
import * as moment from 'moment';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -22,6 +24,8 @@ export class RegisterQueueManager extends BaseCreateManager<QueueModel> {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private currentItemMaster;
|
||||||
|
|
||||||
async averageTime(): Promise<number> {
|
async averageTime(): Promise<number> {
|
||||||
const item = await this.getItemMaster();
|
const item = await this.getItemMaster();
|
||||||
return item.play_estimation;
|
return item.play_estimation;
|
||||||
|
@ -40,6 +44,7 @@ export class RegisterQueueManager extends BaseCreateManager<QueueModel> {
|
||||||
async beforeProcess(): Promise<void> {
|
async beforeProcess(): Promise<void> {
|
||||||
const vip = this.data.vip ?? false;
|
const vip = this.data.vip ?? false;
|
||||||
const item = await this.getItemMaster();
|
const item = await this.getItemMaster();
|
||||||
|
this.currentItemMaster = item;
|
||||||
const [, end] = await this.queueTime(item.item_queue_id);
|
const [, end] = await this.queueTime(item.item_queue_id);
|
||||||
|
|
||||||
const queueNumber = await this.bucketService.getQueue(
|
const queueNumber = await this.bucketService.getQueue(
|
||||||
|
@ -71,6 +76,21 @@ export class RegisterQueueManager extends BaseCreateManager<QueueModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async afterProcess(): Promise<void> {
|
async afterProcess(): Promise<void> {
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ import { SplitQueueManager } from './domain/usecases/split-queue.manager';
|
||||||
import { QueueTransactionCancelHandler } from './infrastructure/handlers/cancel-transaction.handler';
|
import { QueueTransactionCancelHandler } from './infrastructure/handlers/cancel-transaction.handler';
|
||||||
import { ItemQueueModel } from '../item-related/item-queue/data/models/item-queue.model';
|
import { ItemQueueModel } from '../item-related/item-queue/data/models/item-queue.model';
|
||||||
import { QueueTimeFormula } from './domain/usecases/formula/queue-time.formula';
|
import { QueueTimeFormula } from './domain/usecases/formula/queue-time.formula';
|
||||||
|
import { QueueJobController } from './infrastructure/controllers/queue-job.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -56,7 +57,7 @@ import { QueueTimeFormula } from './domain/usecases/formula/queue-time.formula';
|
||||||
),
|
),
|
||||||
CqrsModule,
|
CqrsModule,
|
||||||
],
|
],
|
||||||
controllers: [QueueController, QueueAdminController],
|
controllers: [QueueController, QueueAdminController, QueueJobController],
|
||||||
providers: [
|
providers: [
|
||||||
QueueOrchestrator,
|
QueueOrchestrator,
|
||||||
QueueAdminOrchestrator,
|
QueueAdminOrchestrator,
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export interface WhatsappQueue {
|
||||||
|
id: string;
|
||||||
|
phone: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
item_name: string;
|
||||||
|
time: number;
|
||||||
|
}
|
|
@ -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 ?? '';
|
|
@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue