diff --git a/.env.example b/.env.example index 95edd4e..b999d91 100644 --- a/.env.example +++ b/.env.example @@ -16,4 +16,14 @@ DB_NAME= FEATURE_FLAG= #AHAMOVE INTEGRATION -AHAMOVE_TOKEN= \ No newline at end of file +AHAMOVE_TOKEN= + +#MOMO INTEGRATION +MOMO_PARTNER_CODE= +MOMO_ACCESS_KEY= +MOMO_SECRETKEY= +MOMO_REDIRECT_HOST= +MOMO_REDIRECT_URL= +MOMO_REQUEST_TYPE= +MOMO_BASE_URL= +MOMO_MAX_RETRIES= \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 9409967..4447635 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { "singleQuote": true, "trailingComma": "all", - "printWidth": 80 + "printWidth": 80, + "endOfLine": "auto" } diff --git a/package.json b/package.json index e782d62..1c520af 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@nestjs/typeorm": "^10.0.1", "@nestjs/websockets": "^10.3.3", "axios": "^1.6.2", + "axios-retry": "^4.0.0", "flagsmith-nodejs": "^3.2.0", "joi": "^17.12.1", "mysql2": "^3.6.5", diff --git a/src/app.module.ts b/src/app.module.ts index 874590e..0a5c8f2 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,8 @@ import { CommonModule } from './feature/common/common.module'; import { CartModule } from './feature/cart/cart.module'; import { RatingAndReviewModule } from './feature/rating-and-review/rating-and-review.module'; import { AhamoveModule } from './dependency/ahamove/ahamove.module'; +import { InvoiceStatusHistoryModule } from './feature/invoice-status-history/invoice-status-history.module'; +import { MomoModule } from './dependency/momo/momo.module'; import { OrderModule } from './feature/order/order.module'; import { HealthCheckController } from './healthcheck/health-check.controller'; @@ -50,6 +52,8 @@ import { HealthCheckController } from './healthcheck/health-check.controller'; CartModule, RatingAndReviewModule, AhamoveModule, + InvoiceStatusHistoryModule, + MomoModule, OrderModule, ], controllers: [AppController, HealthCheckController], diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 9909ca6..59d9644 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -7,6 +7,17 @@ export default () => ({ password: process.env.DB_PASSWORD, name: process.env.DB_NAME, }, + momo: { + partnerCode: process.env.MOMO_PARTNER_CODE || '', + accessKey: process.env.MOMO_ACCESS_KEY || '', + secretkey: process.env.MOMO_SECRETKEY || '', + redirectHost: process.env.MOMO_REDIRECT_HOST || 'https://api.2all.com.vn', + redirectUrl: + process.env.MOMO_REDIRECT_URL || 'https://www.2all.com.vn/order/detail', + requestType: process.env.MOMO_REQUEST_TYPE || 'captureWallet', + baseUrl: process.env.MOMO_BASE_URL || 'https://test-payment.momo.vn', + maximumRetry: process.env.MOMO_MAX_RETRIES || 1, + }, featureFlag: process.env.FEATURE_FLAG || '', ahamoveToken: process.env.AHAMOVE_TOKEN, planningDay: 7, // the restaurant will plan new cooking schedule every Saturday (last until the end of the day) diff --git a/src/database/migrations/1708402970486-TableAhamoveOrder.ts b/src/database/migrations/1708402970486-TableAhamoveOrder.ts index 81c9028..7058ea0 100644 --- a/src/database/migrations/1708402970486-TableAhamoveOrder.ts +++ b/src/database/migrations/1708402970486-TableAhamoveOrder.ts @@ -5,8 +5,12 @@ export class TableAhamoveOrder1708402970486 implements MigrationInterface { await queryRunner.query( `CREATE TABLE \`Ahamove_Order\` (\`id\` varchar(36) NOT NULL, \`service_id\` varchar(255) NOT NULL, \`path\` json NOT NULL, \`requests\` json NOT NULL, \`payment_method\` varchar(255) NOT NULL, \`total_pay\` int NOT NULL, \`order_time\` int NOT NULL, \`promo_code\` varchar(255) NULL, \`remarks\` varchar(255) NOT NULL, \`admin_note\` varchar(255) NOT NULL, \`route_optimized\` tinyint NOT NULL, \`idle_until\` int NOT NULL, \`items\` json NOT NULL, \`package_detail\` json NOT NULL, \`group_service_id\` varchar(255) NULL, \`group_requests\` varchar(255) NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`, ); - await queryRunner.query(`ALTER TABLE \`Ahamove_Order\` ADD \`order_id\` varchar(255) NULL`); - await queryRunner.query(`ALTER TABLE \`Ahamove_Order\` ADD \`response\` json NOT NULL`); + await queryRunner.query( + `ALTER TABLE \`Ahamove_Order\` ADD \`order_id\` varchar(255) NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`Ahamove_Order\` ADD \`response\` json NOT NULL`, + ); } public async down(queryRunner: QueryRunner): Promise { diff --git a/src/database/migrations/1709005940772-add-momo-transactions.ts b/src/database/migrations/1709005940772-add-momo-transactions.ts new file mode 100644 index 0000000..18aa6a0 --- /dev/null +++ b/src/database/migrations/1709005940772-add-momo-transactions.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddMomoTransactions1709005940772 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`MomoTransaction\` (\`id\` int NOT NULL AUTO_INCREMENT, \`partnerCode\` varchar(50) NOT NULL, \`requestId\` varchar(50) NOT NULL, \`amount\` decimal(10,2) NOT NULL, \`orderId\` varchar(50) NOT NULL, \`transId\` bigint NOT NULL, \`responseTime\` bigint NOT NULL, \`orderInfo\` varchar(255) NOT NULL, \`type\` varchar(10) NOT NULL, \`resultCode\` int NOT NULL, \`redirectUrl\` varchar(255) NOT NULL, \`ipnUrl\` varchar(255) NOT NULL, \`extraData\` text NOT NULL, \`requestType\` varchar(50) NOT NULL, \`signature\` varchar(255) NOT NULL, \`lang\` varchar(2) NOT NULL DEFAULT 'en', PRIMARY KEY (\`id\`)) ENGINE=InnoDB`, + ); + await queryRunner.query(`ALTER TABLE \`MomoTransaction\` + ADD COLUMN \`payUrl\` TEXT NULL AFTER \`lang\`, + ADD COLUMN \`message\` TEXT NULL AFTER \`payUrl\`;`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE \`MomoTransaction\``); + } +} diff --git a/src/dependency/ahamove/ahamove.module.ts b/src/dependency/ahamove/ahamove.module.ts index 91be752..f939353 100644 --- a/src/dependency/ahamove/ahamove.module.ts +++ b/src/dependency/ahamove/ahamove.module.ts @@ -6,10 +6,16 @@ import { AhamoveController } from './ahamove.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AhamoveOrderEntity } from 'src/entity/ahamove-order.entity'; import { AhamoveOrderHookEntity } from 'src/entity/ahamove-order-hook.entity'; +import { InvoiceStatusHistoryModule } from 'src/feature/invoice-status-history/invoice-status-history.module'; import { OrderModule } from 'src/feature/order/order.module'; @Module({ - imports: [HttpModule, ConfigModule, TypeOrmModule.forFeature([AhamoveOrderEntity, AhamoveOrderHookEntity]), OrderModule], + imports: [ + HttpModule, + ConfigModule, + TypeOrmModule.forFeature([AhamoveOrderEntity, AhamoveOrderHookEntity]), + InvoiceStatusHistoryModule, + ], providers: [AhamoveService], exports: [AhamoveService], controllers: [AhamoveController], diff --git a/src/dependency/ahamove/ahamove.service.ts b/src/dependency/ahamove/ahamove.service.ts index 5efbf48..751c960 100644 --- a/src/dependency/ahamove/ahamove.service.ts +++ b/src/dependency/ahamove/ahamove.service.ts @@ -1,5 +1,12 @@ import { HttpService } from '@nestjs/axios'; -import { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger, OnModuleInit } from '@nestjs/common'; +import { + BadRequestException, + Inject, + Injectable, + InternalServerErrorException, + Logger, + OnModuleInit, +} from '@nestjs/common'; import { AxiosRequestConfig } from 'axios'; import { firstValueFrom, lastValueFrom } from 'rxjs'; import { Coordinate } from 'src/type'; @@ -7,7 +14,9 @@ import { FlagsmithService } from '../flagsmith/flagsmith.service'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; import { AhamoveOrder, PostAhaOrderRequest } from './dto/ahamove.dto'; -import postAhaOrderRequestSchema, { coordinateListSchema } from './schema/ahamove.schema'; +import postAhaOrderRequestSchema, { + coordinateListSchema, +} from './schema/ahamove.schema'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { AhamoveOrderEntity } from 'src/entity/ahamove-order.entity'; @@ -32,13 +41,18 @@ export class AhamoveService implements OnModuleInit { private readonly httpService: HttpService, @Inject('FLAGSMITH_SERVICE') private flagService: FlagsmithService, private configService: ConfigService, - @InjectRepository(AhamoveOrderEntity) private ahamoveOrder: Repository, - @InjectRepository(AhamoveOrderHookEntity) private ahamoveOrderHook: Repository, - private readonly orderService: OrderService, + @InjectRepository(AhamoveOrderEntity) + private ahamoveOrder: Repository, + @InjectRepository(AhamoveOrderHookEntity) + private ahamoveOrderHook: Repository, ) { - this.AHA_MOVE_BASE_URL = configService.get('ahamove.baseUrl') || 'https://apistg.ahamove.com/api'; - this.AHA_MOVE_API_KEY = configService.get('ahamove.apiKey') || '7bbc5c69e7237f267e97f81237a717c387f13bdb'; - this.AHA_MOVE_USERNAME = configService.get('ahamove.username') || '2ALL Admin'; + this.AHA_MOVE_BASE_URL = + configService.get('ahamove.baseUrl') || 'https://apistg.ahamove.com'; + this.AHA_MOVE_API_KEY = + configService.get('ahamove.apiKey') || + '7bbc5c69e7237f267e97f81237a717c387f13bdb'; + this.AHA_MOVE_USERNAME = + configService.get('ahamove.username') || '2ALL Admin'; this.AHA_MOVE_MOBILE = configService.get('ahamove.mobile') || '84905005248'; } @@ -48,14 +62,14 @@ export class AhamoveService implements OnModuleInit { async getAhamoveAccessToken() { let data = JSON.stringify({ - mobile: '84905005248', - name: '2ALL Admin', - api_key: '7bbc5c69e7237f267e97f81237a717c387f13bdb', + mobile: this.AHA_MOVE_MOBILE, + name: this.AHA_MOVE_USERNAME, + api_key: this.AHA_MOVE_API_KEY, }); let config = { method: 'post', - url: `${this.AHA_MOVE_BASE_URL}/v3/partner/account/register`, + url: `${this.AHA_MOVE_BASE_URL}/api/v3/partner/account/register`, headers: { 'Content-Type': 'application/json', }, @@ -75,7 +89,10 @@ export class AhamoveService implements OnModuleInit { }); } - async estimateTimeAndDistance(startingPoint: Coordinate, destination: Coordinate) { + async estimateTimeAndDistance( + startingPoint: Coordinate, + destination: Coordinate, + ) { try { const data: any = JSON.stringify({ order_time: 0, @@ -135,7 +152,9 @@ export class AhamoveService implements OnModuleInit { const { error, value } = await coordinateListSchema.validate(coordinates); if (error) { this.logger.warn('Bad coordinates: ' + coordinates); - throw new BadRequestException(error?.details.map((x) => x.message).join(', ')); + throw new BadRequestException( + error?.details.map((x) => x.message).join(', '), + ); } const startingPoint = coordinates[0]; const destination = coordinates[1]; @@ -168,7 +187,7 @@ export class AhamoveService implements OnModuleInit { const config: AxiosRequestConfig = { method: 'post', maxBodyLength: Infinity, - url: `${this.AHA_MOVE_BASE_URL}/v3/partner/order/estimate`, + url: `${this.AHA_MOVE_BASE_URL}/api/v3/partner/order/estimate`, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.AHA_MOVE_TOKEN}`, @@ -179,7 +198,9 @@ export class AhamoveService implements OnModuleInit { const response = await axios.request(config); return response.data; } catch (error) { - this.logger.error('Error occurred while call to get estimate: ' + JSON.stringify(error)); + this.logger.error( + 'Error occurred while call to get estimate: ' + JSON.stringify(error), + ); throw new InternalServerErrorException(error); } } @@ -194,7 +215,7 @@ export class AhamoveService implements OnModuleInit { let config = { method: 'post', maxBodyLength: Infinity, - url: `${this.AHA_MOVE_BASE_URL}/v3/partner/order/create`, + url: `${this.AHA_MOVE_BASE_URL}/api/v3/partner/order/create`, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.AHA_MOVE_TOKEN}`, @@ -206,17 +227,24 @@ export class AhamoveService implements OnModuleInit { // update order id and response from ahamove orderRequest.order_id = data.order_id; orderRequest.response = data; - const result = await this.ahamoveOrder.save(AhamoveMapper.fromDTOtoEntity(orderRequest)); + const result = await this.ahamoveOrder.save( + AhamoveMapper.fromDTOtoEntity(orderRequest), + ); this.logger.verbose('created ahamove order', JSON.stringify(result)); return data; } catch (error) { - this.logger.error('An error occurred while calling post ahamove order', JSON.stringify(error)); + this.logger.error( + 'An error occurred while calling post ahamove order', + JSON.stringify(error), + ); throw new InternalServerErrorException('An error occurred'); } } async getAhamoveOrderByOrderId(orderId: string): Promise { - const result = await this.ahamoveOrder.findOne({ where: { order_id: orderId } }); + const result = await this.ahamoveOrder.findOne({ + where: { order_id: orderId }, + }); return result; } @@ -225,7 +253,10 @@ export class AhamoveService implements OnModuleInit { return result; } - async #buildAhamoveRequest(order: PostAhaOrderRequest, service_type: string): Promise { + async #buildAhamoveRequest( + order: PostAhaOrderRequest, + service_type: string, + ): Promise { try { await postAhaOrderRequestSchema.validate(order); } catch (error) { @@ -253,4 +284,24 @@ export class AhamoveService implements OnModuleInit { } return result; } + + async cancelAhamoveOrder(orderId, cancelReasonMessage) { + cancelReasonMessage = + cancelReasonMessage || 'supplier_sender_ask_to_return_the_package'; + let config = { + method: 'get', + maxBodyLength: Infinity, + url: `${this.AHA_MOVE_BASE_URL}/v1/order/cancel?token=${this.AHA_MOVE_TOKEN}&order_id=${orderId}&comment=${cancelReasonMessage}`, + headers: { + 'cache-control': 'no-cache', + }, + }; + try { + const result = await axios.request(config); + return result; + } catch (error) { + this.logger.error('An error occurred ', JSON.stringify(error)); + throw new InternalServerErrorException(error?.message); + } + } } diff --git a/src/dependency/ahamove/schema/ahamove.schema.ts b/src/dependency/ahamove/schema/ahamove.schema.ts index d5292dc..b7615aa 100644 --- a/src/dependency/ahamove/schema/ahamove.schema.ts +++ b/src/dependency/ahamove/schema/ahamove.schema.ts @@ -53,5 +53,7 @@ const coordinateSchema = Joi.object({ long: Joi.number().required(), }); -export const coordinateListSchema = Joi.array().items(coordinateSchema).required(); +export const coordinateListSchema = Joi.array() + .items(coordinateSchema) + .required(); export default postAhaOrderRequestSchema; diff --git a/src/dependency/momo/momo.controller.ts b/src/dependency/momo/momo.controller.ts new file mode 100644 index 0000000..0bf3b50 --- /dev/null +++ b/src/dependency/momo/momo.controller.ts @@ -0,0 +1,21 @@ +import { Controller, Logger } from '@nestjs/common'; +import { MomoService } from './momo.service'; +import { MessagePattern } from '@nestjs/microservices'; +import { MomoRequestDTO } from './momo.dto'; + +@Controller('momo') +export class MomoController { + private readonly logger = new Logger(MomoController.name); + + constructor(private readonly momoService: MomoService) {} + + @MessagePattern({ cmd: 'create_momo_payment' }) + async sendMomoPaymentRequest(payload: MomoRequestDTO) { + return this.momoService.sendMomoPaymentRequest(payload); + } + + @MessagePattern({ cmd: 'momo_payment_ipn_callback' }) + async handleMomoCallback(payload: any) { + return this.momoService.handleMoMoIpnCallBack(payload); + } +} diff --git a/src/dependency/momo/momo.dto.ts b/src/dependency/momo/momo.dto.ts new file mode 100644 index 0000000..d6cf02c --- /dev/null +++ b/src/dependency/momo/momo.dto.ts @@ -0,0 +1,16 @@ +export interface MomoRequestDTO { + invoiceId: number; +} + +export interface MomoRequest { + accessKey: string; + amount: string; + extraData: string; + ipnUrl: string; + orderId: string; + orderInfo: string; + partnerCode: string; + redirectUrl: string; + requestId: string; + requestType: string; +} diff --git a/src/dependency/momo/momo.module.ts b/src/dependency/momo/momo.module.ts new file mode 100644 index 0000000..3634afe --- /dev/null +++ b/src/dependency/momo/momo.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { MomoService } from './momo.service'; +import { MomoController } from './momo.controller'; +import { ConfigModule } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { MomoTransaction } from 'src/entity/momo-transaction.entity'; +import { InvoiceStatusHistoryModule } from 'src/feature/invoice-status-history/invoice-status-history.module'; +import { Invoice } from 'src/entity/invoice.entity'; +import { InvoiceStatusHistory } from 'src/entity/invoice-history-status.entity'; +import { OrderModule } from 'src/feature/order/order.module'; + +@Module({ + imports: [ + ConfigModule, + TypeOrmModule.forFeature([MomoTransaction, Invoice, InvoiceStatusHistory]), + InvoiceStatusHistoryModule, + OrderModule, + ], + providers: [MomoService], + controllers: [MomoController], +}) +export class MomoModule {} diff --git a/src/dependency/momo/momo.service.ts b/src/dependency/momo/momo.service.ts new file mode 100644 index 0000000..e929662 --- /dev/null +++ b/src/dependency/momo/momo.service.ts @@ -0,0 +1,260 @@ +import { + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import axios, { AxiosError } from 'axios'; +import { MomoTransaction } from 'src/entity/momo-transaction.entity'; +import { Repository } from 'typeorm'; +import { MomoRequestDTO } from './momo.dto'; +const crypto = require('crypto'); +import axiosRetry from 'axios-retry'; +import { InvoiceStatusHistory } from 'src/entity/invoice-history-status.entity'; +import { v4 as uuidv4 } from 'uuid'; +import { InvoiceHistoryStatusEnum, OrderStatus } from 'src/enum'; +import { Invoice } from 'src/entity/invoice.entity'; +import { OrderService } from 'src/feature/order/order.service'; +import { InvoiceStatusHistoryService } from 'src/feature/invoice-status-history/invoice-status-history.service'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class MomoService { + partnerCode = ''; + accessKey = ''; + secretkey = ''; + redirectHost = ''; + redirectUrl = ''; + ipnUrl = ''; + requestType = ''; + baseUrl = ''; + maximumRetry = 0; + + private readonly logger = new Logger(MomoService.name); + constructor( + @InjectRepository(MomoTransaction) + private momoRepo: Repository, + @InjectRepository(Invoice) private invoiceRepo: Repository, + @InjectRepository(InvoiceStatusHistory) + private orderStatusHistoryRepo: Repository, + @InjectRepository(InvoiceStatusHistory) + private invoiceHistoryStatusRepo: Repository, + private readonly orderService: OrderService, + private readonly invoiceStatusHistoryService: InvoiceStatusHistoryService, + private configService: ConfigService, + ) { + this.partnerCode = configService.get('momo.partnerCode'); + this.accessKey = configService.get('momo.accessKey'); + this.secretkey = configService.get('momo.secretkey'); + this.redirectHost = configService.get('momo.redirectHost'); + this.redirectUrl = configService.get('momo.redirectUrl'); + this.requestType = configService.get('momo.requestType'); + this.baseUrl = configService.get('momo.baseUrl'); + this.maximumRetry = configService.get('momo.maximumRetry'); + this.ipnUrl = `${this.redirectHost}/momo/momo-ipn-callback`; + } + + async sendMomoPaymentRequest(request: MomoRequestDTO) { + const currentInvoice = await this.invoiceRepo.findOne({ + where: { invoice_id: request.invoiceId }, + }); + if (!currentInvoice) { + throw new InternalServerErrorException('Invoice not found'); + } + const requestId = uuidv4(); + const orderId = requestId; + const momoSignatureObj = { + accessKey: this.accessKey, + amount: currentInvoice.total_amount, + extraData: currentInvoice.description, + ipnUrl: this.ipnUrl, + orderId: orderId, + orderInfo: currentInvoice.description, + partnerCode: this.partnerCode, + redirectUrl: this.redirectUrl, + requestId: requestId, + requestType: this.requestType, + }; + const rawSignature = this.createSignature(momoSignatureObj); + const signature = crypto + .createHmac('sha256', this.secretkey) + .update(rawSignature) + .digest('hex'); + const requestBody = JSON.stringify({ + partnerCode: this.partnerCode, + accessKey: this.accessKey, + requestId: requestId, + amount: currentInvoice.total_amount, + extraData: currentInvoice.description, + ipnUrl: this.ipnUrl, + orderId: orderId, + orderInfo: currentInvoice.description, + redirectUrl: this.redirectUrl, + requestType: this.requestType, + signature: signature, + lang: 'en', + }); + + const options = { + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(requestBody), + }, + method: 'post', + url: `${this.baseUrl}/v2/gateway/api/create`, + data: requestBody, + }; + // Create an Axios instance + const axiosInstance = axios.create(); + + // Configure retries + axiosRetry(axiosInstance, { + retries: this.maximumRetry, + retryDelay: (retryCount) => { + return retryCount * 1000; // wait 1s before retry + }, + retryCondition: (error: AxiosError) => { + return error.isAxiosError; + }, + onRetry: (retryCount, error, requestConfig) => { + this.logger.warn( + `Attempt ${retryCount}: Retrying request to ${requestConfig.url}`, + ); + }, + }); + + const latestInvoiceStatus = await this.invoiceHistoryStatusRepo.findOne({ + where: { invoice_id: currentInvoice.invoice_id }, + order: { created_at: 'DESC' }, + }); + if ( + latestInvoiceStatus && + latestInvoiceStatus.status_id === 'NEW' && + !currentInvoice.payment_order_id + ) { + this.logger.log( + 'currentInvoice for momo payment order: ', + currentInvoice, + ); + return axiosInstance + .request(options) + .then(async (response) => { + const momoOrderResult = response.data; + if ( + momoOrderResult.resultCode === 0 || + momoOrderResult.resultCode === 9000 + ) { + const momoResult = { + ...momoOrderResult, + requestId: requestId, + partnerCode: this.partnerCode, + extraData: currentInvoice.description, + ipnUrl: this.ipnUrl, + orderId: orderId, + orderInfo: currentInvoice.description, + redirectUrl: this.redirectUrl, + requestType: this.requestType, + signature: signature, + type: 'request', + lang: 'en', + }; + await this.momoRepo.save(momoResult); + if (momoResult.resultCode === 0) { + // const isPaymentOrderIdExist = + // !currentInvoice.payment_order_id || + // currentInvoice.payment_order_id == '' + // ? false + // : true; + + // Update field payment_order_id of table Invoice with requestId + await this.invoiceRepo.update(currentInvoice.invoice_id, { + payment_order_id: requestId, + }); + + //Insert a record into table 'Invoice_Status_History' + const momoInvoiceStatusHistory = new InvoiceStatusHistory(); + momoInvoiceStatusHistory.status_id = + InvoiceHistoryStatusEnum.PENDING; + momoInvoiceStatusHistory.status_history_id = uuidv4(); + momoInvoiceStatusHistory.invoice_id = currentInvoice.invoice_id; + // if (!isPaymentOrderIdExist) { + momoInvoiceStatusHistory.note = `momo request ${momoResult.requestId} for payment`; + // } else { + // momoInvoiceStatusHistory.note = `update new momo request ${momoResult.requestId} for payment`; + // } + await this.orderStatusHistoryRepo.insert( + momoInvoiceStatusHistory, + ); + } + } + // return momoOrderResult; + return { + invoiceId: currentInvoice.invoice_id, + amount: momoOrderResult.amount, + payUrl: momoOrderResult.payUrl, + }; + }) + .catch(async (error) => { + this.logger.error( + 'An error occurred when create momo request', + JSON.stringify(error), + ); + await this.orderService.cancelOrder(currentInvoice.order_id, { + isMomo: true, + }); + throw new InternalServerErrorException(); + }); + } else if ( + latestInvoiceStatus && + latestInvoiceStatus.status_id === 'PENDING' && + currentInvoice.payment_order_id + ) { + const currentMomoTransaction = await this.momoRepo.findOne({ + where: { requestId: currentInvoice.payment_order_id, type: 'request' }, + }); + return { + // orderId: currentMomoTransaction?.orderId, + // requestId: currentMomoTransaction?.requestId, + invoiceId: currentInvoice.invoice_id, + amount: currentMomoTransaction.amount, + // responseTime: currentMomoTransaction.responseTime, + // message: currentMomoTransaction.message, + // resultCode: currentMomoTransaction.resultCode, + payUrl: currentMomoTransaction.payUrl, + }; + } else { + throw new InternalServerErrorException( + 'cannot create momo payment order with the invoice', + ); + } + } + + async handleMoMoIpnCallBack(payload) { + try { + await this.momoRepo.save(payload); + await this.invoiceStatusHistoryService.updateInvoiceHistoryFromMomoWebhook( + payload, + ); + return { message: 'OK' }; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(); + } + } + + createSignature({ + accessKey, + amount, + extraData, + ipnUrl, + orderId, + orderInfo, + partnerCode, + redirectUrl, + requestId, + requestType, + }) { + const rawSignature = `accessKey=${accessKey}&amount=${amount}&extraData=${extraData}&ipnUrl=${ipnUrl}&orderId=${orderId}&orderInfo=${orderInfo}&partnerCode=${partnerCode}&redirectUrl=${redirectUrl}&requestId=${requestId}&requestType=${requestType}`; + return rawSignature; + } +} diff --git a/src/entity/driver-status-log.entity.ts b/src/entity/driver-status-log.entity.ts new file mode 100644 index 0000000..62e615e --- /dev/null +++ b/src/entity/driver-status-log.entity.ts @@ -0,0 +1,35 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Order } from './order.entity'; +import { Driver } from './driver.entity'; + +@Entity('Driver_Status_Log') +export class DriverStatusLog { + @PrimaryGeneratedColumn('uuid') + log_id: string; + + @Column() + order_id: number; + + @Column({ nullable: true }) + driver_id: number | null; + + @Column('text', { nullable: true }) + note: string | null; + + @Column('bigint') + logged_at: number; + + @ManyToOne(() => Order) + @JoinColumn({ name: 'order_id' }) + order: Order; + + @ManyToOne(() => Driver, { nullable: true }) + @JoinColumn({ name: 'driver_id' }) + driver: Driver | null; +} diff --git a/src/entity/driver.entity.ts b/src/entity/driver.entity.ts new file mode 100644 index 0000000..9318d73 --- /dev/null +++ b/src/entity/driver.entity.ts @@ -0,0 +1,48 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from 'typeorm'; + +@Entity('Driver') +export class Driver { + @PrimaryGeneratedColumn() + driver_id: number; + + @Column({ length: 255 }) + name: string; + + @Column({ length: 25, nullable: true }) + phone_number: string | null; + + @Column({ length: 255, nullable: true }) + email: string | null; + + @Column({ length: 255, nullable: true }) + vehicle: string | null; + + @Column({ length: 48, nullable: true }) + license_plates: string | null; + + @Column({ + type: 'enum', + enum: ['AHAMOVE', 'ONWHEEL'], + nullable: true, + }) + type: 'AHAMOVE' | 'ONWHEEL' | null; + + @Column({ length: 255, nullable: true }) + reference_id: string; + + @Column({ type: 'int', nullable: true }) + profile_image: number; + + @CreateDateColumn({ + type: 'datetime', + nullable: false, + unique: false, + default: () => 'CURRENT_TIMESTAMP', + }) + public created_at: Date; +} diff --git a/src/entity/invoice-history-status.entity.ts b/src/entity/invoice-history-status.entity.ts new file mode 100644 index 0000000..eec6363 --- /dev/null +++ b/src/entity/invoice-history-status.entity.ts @@ -0,0 +1,39 @@ +import { + Entity, + PrimaryColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, +} from 'typeorm'; +import { Invoice } from './invoice.entity'; + +@Entity('Invoice_Status_History') +export class InvoiceStatusHistory { + @PrimaryColumn({ + type: 'varchar', + length: 36, + }) + status_history_id: string; + + @Column({ + type: 'int', + }) + invoice_id: number; + + @Column({ + type: 'varchar', + length: 64, + nullable: true, + }) + status_id: string | null; + + @Column({ + type: 'text', + nullable: true, + }) + note: string | null; + + @Column({ type: 'bigint', nullable: false }) + created_at: number; +} diff --git a/src/entity/invoice.entity.ts b/src/entity/invoice.entity.ts new file mode 100644 index 0000000..063ad78 --- /dev/null +++ b/src/entity/invoice.entity.ts @@ -0,0 +1,62 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from 'typeorm'; + +@Entity('Invoice') +export class Invoice { + @PrimaryGeneratedColumn() + invoice_id: number; + + @Column({ + type: 'int', + }) + payment_method: number; + + @Column({ + type: 'int', + }) + total_amount: number; + + @Column({ + type: 'int', + }) + tax_amount: number; + + @Column({ + type: 'int', + default: 0, + }) + discount_amount: number; + + @Column({ + type: 'text', + nullable: true, + }) + description: string | null; + + @Column({ + type: 'int', + }) + order_id: number; + + @Column({ + type: 'int', + }) + currency: number; + + @Column({ + type: 'varchar', + length: 255, + nullable: true, + }) + payment_order_id: string | null; + + @CreateDateColumn({ + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP', + }) + created_at: Date; +} diff --git a/src/entity/momo-transaction.entity.ts b/src/entity/momo-transaction.entity.ts new file mode 100644 index 0000000..a0bde86 --- /dev/null +++ b/src/entity/momo-transaction.entity.ts @@ -0,0 +1,58 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity('MomoTransaction') +export class MomoTransaction { + @PrimaryGeneratedColumn() + id: number; + + @Column({ length: 50 }) + partnerCode: string; + + @Column({ length: 50 }) + requestId: string; + + @Column('decimal', { precision: 10, scale: 2 }) + amount: number; + + @Column({ length: 50 }) + orderId: string; + + @Column({ type: 'bigint', nullable: true }) + transId: number; + + @Column({ type: 'bigint', nullable: true }) + responseTime: number; + + @Column({ length: 255, nullable: true }) + orderInfo: string; + + @Column({ length: 10, nullable: true }) + type: string; // request / response + + @Column({ nullable: true }) + resultCode: number; + + @Column({ nullable: true }) + redirectUrl: string; + + @Column({ nullable: true }) + ipnUrl: string; + + @Column('text', { nullable: true }) + extraData: string; + + @Column({ length: 50, nullable: true }) + requestType: string; + + @Column({ length: 255, nullable: true }) + signature: string; + + @Column('text', { nullable: true }) + payUrl: string; + + @Column('text', { nullable: true }) + message: string; + + @Column({ default: 'en', nullable: true }) + lang: string; +} diff --git a/src/entity/order-sku.entity.ts b/src/entity/order-sku.entity.ts index 7976708..a96a352 100644 --- a/src/entity/order-sku.entity.ts +++ b/src/entity/order-sku.entity.ts @@ -1,12 +1,4 @@ -import { - Entity, - CreateDateColumn, - PrimaryGeneratedColumn, - Column, - JoinColumn, - ManyToOne, - OneToOne, -} from 'typeorm'; +import { Entity, CreateDateColumn, PrimaryGeneratedColumn, Column, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; import { Unit } from './unit.entity'; import { SKU } from './sku.entity'; import { FoodRating } from './food-rating.entity'; diff --git a/src/entity/order-status-log.entity.ts b/src/entity/order-status-log.entity.ts new file mode 100644 index 0000000..a24fd9b --- /dev/null +++ b/src/entity/order-status-log.entity.ts @@ -0,0 +1,20 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { OrderStatus } from 'src/enum'; + +@Entity('Order_Status_Log') +export class OrderStatusLog { + @PrimaryGeneratedColumn('uuid') + log_id: string; + + @Column({ type: 'int' }) + order_id: number; + + @Column({ type: 'varchar', length: 64 }) + order_status_id: OrderStatus; + + @Column('text', { nullable: true }) + note: string | null; + + @Column('bigint') + logged_at: number; +} diff --git a/src/entity/order.entity.ts b/src/entity/order.entity.ts index afdfb98..ba41172 100644 --- a/src/entity/order.entity.ts +++ b/src/entity/order.entity.ts @@ -1,5 +1,4 @@ -import { OrderStatus } from 'src/enum'; -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; @Entity({ name: 'Order' }) export class Order { @@ -15,8 +14,8 @@ export class Order { @Column({ type: 'int', nullable: false }) address_id: number; - @Column({ type: 'int', nullable: true }) - driver_id: number; + // @Column({ type: 'int', nullable: true }) + // driver_id: number; @Column({ type: 'int', nullable: false }) order_total: number; @@ -48,52 +47,14 @@ export class Order { @Column({ type: 'tinyint', nullable: false, default: '0' }) is_preorder: boolean; - @Column({ type: 'int', nullable: false }) - payment_method: number; - @Column({ type: 'bigint', nullable: true }) expected_arrival_time: number; @Column({ type: 'varchar', length: 255, nullable: true }) delivery_order_id: string; - //TODO Add other relations... - @Column({ - type: 'enum', - enum: OrderStatus, - default: OrderStatus.NEW, - }) - order_status_id: OrderStatus; - - @Column({ type: 'bigint', nullable: true }) - confirm_time: number; - - @Column({ type: 'bigint', nullable: true }) - processing_time: number; - - @Column({ type: 'bigint', nullable: true }) - driver_accept_time: number; - - @Column({ type: 'bigint', nullable: true }) - driver_cancel_time: number; - - @Column({ type: 'bigint', nullable: true }) - ready_time: number; - - @Column({ type: 'bigint', nullable: true }) - pickup_time: number; - - @Column({ type: 'bigint', nullable: true }) - completed_time: number; - - @Column({ type: 'bigint', nullable: true }) - fail_time: number; - - @Column({ type: 'bigint', nullable: true }) - return_time: number; - - @Column({ type: 'bigint', nullable: true }) - cancel_time: number; + @Column({ type: 'varchar', length: 255, nullable: true }) + driver_note: string; @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) created_at: Date; diff --git a/src/entity/urgent-action-needed.entity.ts b/src/entity/urgent-action-needed.entity.ts new file mode 100644 index 0000000..a84b855 --- /dev/null +++ b/src/entity/urgent-action-needed.entity.ts @@ -0,0 +1,26 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from 'typeorm'; + +@Entity('Urgent_Action_Needed') +export class UrgentActionNeeded { + @PrimaryGeneratedColumn() + issue_id: number; + + @Column('text', { nullable: true }) + description: string | null; + + @Column({ type: 'varchar', length: 128, nullable: true }) + solved_by: string | null; + + @CreateDateColumn({ + type: 'datetime', + nullable: false, + unique: false, + default: () => 'CURRENT_TIMESTAMP', + }) + public created_at: Date; +} diff --git a/src/enum/index.ts b/src/enum/index.ts index ba22cad..3da39b5 100644 --- a/src/enum/index.ts +++ b/src/enum/index.ts @@ -39,6 +39,17 @@ export enum OrderStatus { NEW = 'NEW', PROCESSING = 'PROCESSING', READY = 'READY', + STUCK = 'STUCK', +} + +export enum InvoiceHistoryStatusEnum { + PAID = 'PAID', + PENDING = 'PENDING', + FAILED = 'FAILED', + CANCELLED = 'CANCELLED', + COMPLETED = 'COMPLETED', + REFUNDED = 'REFUNDED', + STARTED = 'STARTED', } export enum CouponCreatorType { diff --git a/src/feature/invoice-status-history/invoice-status-history.module.ts b/src/feature/invoice-status-history/invoice-status-history.module.ts new file mode 100644 index 0000000..de317a2 --- /dev/null +++ b/src/feature/invoice-status-history/invoice-status-history.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { InvoiceStatusHistoryService } from './invoice-status-history.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { InvoiceStatusHistory } from 'src/entity/invoice-history-status.entity'; +import { Invoice } from 'src/entity/invoice.entity'; +import { OrderModule } from '../order/order.module'; + +@Module({ + providers: [InvoiceStatusHistoryService], + imports: [ + TypeOrmModule.forFeature([InvoiceStatusHistory, Invoice]), + OrderModule, + ], + controllers: [], + exports: [InvoiceStatusHistoryService], +}) +export class InvoiceStatusHistoryModule {} diff --git a/src/feature/invoice-status-history/invoice-status-history.service.ts b/src/feature/invoice-status-history/invoice-status-history.service.ts new file mode 100644 index 0000000..df3d85b --- /dev/null +++ b/src/feature/invoice-status-history/invoice-status-history.service.ts @@ -0,0 +1,98 @@ +import { + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { InvoiceStatusHistory } from 'src/entity/invoice-history-status.entity'; +import { Invoice } from 'src/entity/invoice.entity'; +import { Order } from 'src/entity/order.entity'; +import { InvoiceHistoryStatusEnum, OrderStatus } from 'src/enum'; +import { Repository } from 'typeorm'; +import { v4 as uuidv4 } from 'uuid'; +import { OrderService } from '../order/order.service'; + +@Injectable() +export class InvoiceStatusHistoryService { + private readonly logger = new Logger(InvoiceStatusHistoryService.name); + constructor( + @InjectRepository(Invoice) private invoiceRepo: Repository, + @InjectRepository(InvoiceStatusHistory) + private invoiceHistoryStatusRepo: Repository, + private readonly orderService: OrderService, + ) {} + + async updateInvoiceHistoryFromMomoWebhook(webhookData): Promise { + try { + const requestId = webhookData.requestId; + this.logger.log( + `receiving data from webhook to ${requestId} with ${JSON.stringify( + webhookData, + )}`, + ); + const currentInvoice = await this.invoiceRepo.findOne({ + where: { payment_order_id: requestId }, + }); + console.log('????', currentInvoice); + + if (currentInvoice) { + const updateData = this.convertMomoResultCode( + webhookData.resultCode, + webhookData.message, + ); + this.logger.log( + `Updating data from webhook to ${requestId} with ${JSON.stringify( + updateData, + )}`, + ); + const momoInvoiceStatusHistory = new InvoiceStatusHistory(); + momoInvoiceStatusHistory.invoice_id = currentInvoice.invoice_id; + momoInvoiceStatusHistory.status_id = updateData.status_id; + momoInvoiceStatusHistory.note = updateData.note; + momoInvoiceStatusHistory.status_history_id = uuidv4(); + await this.invoiceHistoryStatusRepo.save(momoInvoiceStatusHistory); + if (updateData.status_id === InvoiceHistoryStatusEnum.FAILED) { + await this.orderService.cancelOrder(currentInvoice.order_id, { + isMomo: true, + }); + } + return { message: 'Order updated successfully' }; + } + return { message: 'Order not existed' }; + } catch (error) { + this.logger.error( + `An error occurred while updating momo callback: ${error.message}`, + ); + throw new InternalServerErrorException(); + } + } + private convertMomoResultCode(code, message) { + const isFinal = (x) => { + return x === 0 || x === 99 || (x >= 1001 && x <= 4100); + }; + if (isFinal(code)) { + if (code === 0) { + return { + status_id: InvoiceHistoryStatusEnum.PAID, + }; + } else if (code !== 9000) { + return { + status_id: InvoiceHistoryStatusEnum.FAILED, + note: message, + }; + } + } else { + if (code === 9000) { + return { + status_id: InvoiceHistoryStatusEnum.PENDING, + note: message, + }; + } else if (code !== 0) { + return { + status_id: InvoiceHistoryStatusEnum.PENDING, + note: message, + }; + } + } + } +} diff --git a/src/feature/order/order.module.ts b/src/feature/order/order.module.ts index f8eb72a..6348a5f 100644 --- a/src/feature/order/order.module.ts +++ b/src/feature/order/order.module.ts @@ -1,12 +1,28 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { OrderService } from './order.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Order } from 'src/entity/order.entity'; import { OrderController } from './order.controller'; +import { InvoiceStatusHistory } from 'src/entity/invoice-history-status.entity'; +import { AhamoveModule } from 'src/dependency/ahamove/ahamove.module'; +import { OrderStatusLog } from 'src/entity/order-status-log.entity'; +import { DriverStatusLog } from 'src/entity/driver-status-log.entity'; +import { UrgentActionNeeded } from 'src/entity/urgent-action-needed.entity'; +import { Driver } from 'src/entity/driver.entity'; @Module({ providers: [OrderService], - imports: [TypeOrmModule.forFeature([Order])], + imports: [ + TypeOrmModule.forFeature([ + Order, + InvoiceStatusHistory, + OrderStatusLog, + DriverStatusLog, + UrgentActionNeeded, + Driver, + ]), + forwardRef(() => AhamoveModule), + ], controllers: [OrderController], exports: [OrderService], }) diff --git a/src/feature/order/order.service.ts b/src/feature/order/order.service.ts index 2bdc214..309b3dd 100644 --- a/src/feature/order/order.service.ts +++ b/src/feature/order/order.service.ts @@ -3,6 +3,7 @@ import { InternalServerErrorException, Logger, } from '@nestjs/common'; +import { InvoiceStatusHistory } from 'src/entity/invoice-history-status.entity'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { Order } from 'src/entity/order.entity'; import { @@ -13,7 +14,12 @@ import { } from 'src/enum'; import { EntityManager, Repository } from 'typeorm'; import { GetApplicationFeeResponse } from './dto/get-application-fee-response.dto'; +import { AhamoveService } from 'src/dependency/ahamove/ahamove.service'; import { PaymentOption } from 'src/entity/payment-option.entity'; +import { OrderStatusLog } from 'src/entity/order-status-log.entity'; +import { DriverStatusLog } from 'src/entity/driver-status-log.entity'; +import { Driver } from 'src/entity/driver.entity'; +import { UrgentActionNeeded } from 'src/entity/urgent-action-needed.entity'; import { CouponAppliedItem, MoneyType } from 'src/type'; import { Restaurant } from 'src/entity/restaurant.entity'; import { CustomRpcException } from 'src/exceptions/custom-rpc.exception'; @@ -25,6 +31,17 @@ export class OrderService { private readonly logger = new Logger(OrderService.name); constructor( @InjectRepository(Order) private orderRepo: Repository, + @InjectRepository(InvoiceStatusHistory) + private orderStatusHistoryRepo: Repository, + @InjectRepository(OrderStatusLog) + private orderStatusLogRepo: Repository, + @InjectRepository(DriverStatusLog) + private driverStatusLogRepo: Repository, + @InjectRepository(Driver) + private driverRepo: Repository, + @InjectRepository(UrgentActionNeeded) + private urgentActionNeededRepo: Repository, + private ahamoveService: AhamoveService, @InjectEntityManager() private entityManager: EntityManager, ) {} @@ -39,49 +56,21 @@ export class OrderService { const currentOrder = await this.orderRepo.findOne({ where: { delivery_order_id: orderId }, }); + const latestOrderStatus = await this.orderStatusLogRepo.findOne({ + where: { order_id: orderId }, + order: { logged_at: 'DESC' }, + }); + const latestDriverStatus = await this.driverStatusLogRepo.findOne({ + where: { order_id: orderId }, + order: { logged_at: 'DESC' }, + }); if (currentOrder) { - const { - status, - cancel_time, - sub_status, - pickup_time, - complete_time, - fail_time, - return_time, - processing_time, - accept_time, - } = webhookData; - const ahavemoveData = { - status, - accept_time, - cancel_time, - pickup_time, - complete_time, - fail_time, - return_time, - sub_status, - processing_time, - path_status: '', - }; - ahavemoveData.path_status = webhookData?.path?.status; - const orderData = { - order_pickup_time: currentOrder.pickup_time, - is_preorder: currentOrder.is_preorder, - ready_time: currentOrder.ready_time, - }; - const updateData = this.getOrderStatusBaseOnAhahaStatus( - orderData, - ahavemoveData, + await this.handleOrderFlowBaseOnAhahaStatus( + currentOrder, + latestOrderStatus?.order_status_id, + latestDriverStatus?.driver_id, + webhookData, ); - this.logger.log( - `Updating data from webhook to ${orderId} with ${JSON.stringify( - updateData, - )}`, - ); - await this.orderRepo.update(currentOrder.order_id, { - ...currentOrder, - ...updateData, - }); return { message: 'Order updated successfully' }; } return { message: 'Order not existed' }; @@ -92,91 +81,193 @@ export class OrderService { throw new InternalServerErrorException(); } } - private getOrderStatusBaseOnAhahaStatus( - { order_pickup_time, is_preorder, ready_time }, - { - status, - sub_status, - path_status, - accept_time, - cancel_time, - pickup_time, - complete_time, - fail_time, - return_time, - processing_time, - }, + private async handleOrderFlowBaseOnAhahaStatus( + order: Order, + latestOrderStatus: OrderStatus, + latestDriverId, + webhookData, ) { - let data = {}; - switch (status) { + const PATH_STATUS = webhookData?.path?.status; + const SUB_STATUS = webhookData?.sub_status; + + const orderStatusLog = new OrderStatusLog(); + orderStatusLog.order_id = order.order_id; + switch (webhookData.status) { case 'ASSIGNING': - if (!is_preorder) { - data = { - order_status_id: OrderStatus.PROCESSING, - processing_time, - }; - } else if (is_preorder) { - data = {}; + if ( + latestOrderStatus !== OrderStatus.PROCESSING && + latestOrderStatus !== OrderStatus.READY + ) { + const action = new UrgentActionNeeded(); + action.description = `Order <${order.order_id}>: Ahavmove order <${webhookData._id}> is assigning but the order status is neither PROCESSING nor READY'. Need action from admin !!!`; + await this.urgentActionNeededRepo.save(action); + break; + } + + if (webhookData.rebroadcast_comment) { + if (latestDriverId) { + //Driver_Status_Log + const driverStatusLog = new DriverStatusLog(); + driverStatusLog.driver_id = latestDriverId; + driverStatusLog.order_id = order.order_id; + driverStatusLog.note = webhookData.rebroadcast_comment; + await this.driverStatusLogRepo.save(driverStatusLog); + } else { + this.logger.error( + `Order <${order.order_id}>: Ahavmove order <${webhookData._id}> is rebroadcasting but the order didnot have any driver infomation before`, + ); + } + } else { + //do nothing } break; case 'ACCEPTED': - data = { - driver_accept_time: accept_time, - }; + if ( + latestOrderStatus !== OrderStatus.PROCESSING && + latestOrderStatus !== OrderStatus.READY + ) { + const action = new UrgentActionNeeded(); + action.description = `Order <${order.order_id}>: Ahavmove order <${webhookData._id}> has been accepted by the driver but the order status is neither PROCESSING nor READY'. Need to check the ISSUE !!!`; + await this.urgentActionNeededRepo.save(action); + break; + } + + if (latestDriverId) { + this.logger.error( + `Order <${order.order_id}>: Ahavmove order <${webhookData._id}> get a driver but the order has been assigned to other driver. Need to check the ISSUE !!!`, + ); + } + + const driverType = + webhookData.service_id === 'VNM-PARTNER-2ALL' ? 'ONWHEEL' : 'AHAMOVE'; + const currentDriver = await this.driverRepo.findOne({ + where: { reference_id: webhookData.supplier_id, type: driverType }, + }); + let driverId = currentDriver.driver_id; + if (currentDriver) { + currentDriver.license_plates = webhookData.supplier_plate_number; + currentDriver.phone_number = webhookData.supplier_id; + currentDriver.name = webhookData.supplier_name; + await this.driverRepo.save(currentDriver); + } else { + const newDriver = new Driver(); + newDriver.license_plates = webhookData.supplier_plate_number; + newDriver.phone_number = webhookData.supplier_id; + newDriver.name = webhookData.supplier_name; + newDriver.type = driverType; + newDriver.reference_id = webhookData.supplier_id; + const result = await this.driverRepo.save(newDriver); + driverId = result.driver_id; + } + const driverStatusLog = new DriverStatusLog(); + driverStatusLog.driver_id = driverId; + driverStatusLog.order_id = order.order_id; + await this.driverStatusLogRepo.save(driverStatusLog); + break; case 'CANCELLED': - data = { - driver_cancel_time: cancel_time, - }; + // //Order_Status_Log with status STUCK + // orderStatusLog.order_status_id = OrderStatus.STUCK; + // await this.orderStatusLogRepo.save(orderStatusLog); + //Urgent_Action_Needed + const action = new UrgentActionNeeded(); + action.description = `Order <${order.order_id}>: Ahavmove order <${webhookData._id}> got cancel with comment '<${webhookData.cancel_comment}>'. Need action from admin !!!`; + await this.urgentActionNeededRepo.save(action); break; - // case 'CANCELLED': - // data = { - // cancel_time, - // }; - // break; case 'IN PROCESS': - if (path_status === 'FAILED') { - data = {}; + // path status is failed => do nothing + if (PATH_STATUS === OrderStatus.FAILED) { + break; + } + // order_status == PROCESSING + // Order_Status_Log with READY and DELIVERING + if (latestOrderStatus === OrderStatus.PROCESSING) { + orderStatusLog.order_status_id = OrderStatus.READY; + await this.orderStatusLogRepo.save(orderStatusLog); + orderStatusLog.order_status_id = OrderStatus.DELIVERING; + await this.orderStatusLogRepo.save(orderStatusLog); + } else if (latestOrderStatus === OrderStatus.READY) { + // order_status == READY + // Order_Status_Log with DELIVERING + orderStatusLog.order_status_id = OrderStatus.COMPLETED; + await this.orderStatusLogRepo.save(orderStatusLog); } else { - if (pickup_time) { - data = { - order_status_id: OrderStatus.DELIVERING, - pickup_time: pickup_time, - ready_time: !ready_time ? pickup_time : ready_time, // if ready_time is null or equal = 0 - }; - } else if (!pickup_time) { - data = {}; - } + //Urgent_Action_Needed + const action = new UrgentActionNeeded(); + action.description = `Order <${order.order_id}>: Food has been picked up BUT the status is neither PROCESSING nor READY. Need action from admin !!!`; + await this.urgentActionNeededRepo.save(action); } break; case 'COMPLETED': - if (path_status === 'FAILED') { - data = { - order_status_id: OrderStatus.FAILED, - fail_time, - }; - } else if (path_status === 'RETURNED') { - data = { - return_time, - }; - } else if (sub_status === 'COMPLETED') { - data = { - order_status_id: OrderStatus.COMPLETED, - completed_time: complete_time, - }; - } else { - // data = { - // order_status_id: OrderStatus.COMPLETED, - // completed_time: complete_time, - // }; - data = {}; + if (SUB_STATUS === 'RETURNED') { + break; + } + if (latestOrderStatus !== OrderStatus.DELIVERING) { + const action = new UrgentActionNeeded(); + action.description = `Order <${order.order_id}>: Ahavmove order <${webhookData._id}> is completed but the order status is not DELIVERING'. Need action from admin !!!`; + await this.urgentActionNeededRepo.save(action); + break; + } + if (PATH_STATUS === 'FAILED') { + // Order_Status_Log with FAILED + orderStatusLog.order_status_id = OrderStatus.FAILED; + await this.orderStatusLogRepo.save(orderStatusLog); + } else if (PATH_STATUS === 'COMPLETED') { + // Order_Status_Log with COMPLETED + orderStatusLog.order_status_id = OrderStatus.COMPLETED; + await this.orderStatusLogRepo.save(orderStatusLog); } break; default: this.logger.log('The value does not match any case'); break; } - return data; + } + + async cancelOrder(order_id, source) { + const currentOrder = await this.orderRepo.findOne({ + where: { order_id: order_id }, + }); + const isMomo = source?.isMomo; + this.logger.debug('canceling Order', JSON.stringify(currentOrder)); + if (!currentOrder) { + this.logger.warn('The order status is not existed'); + return; + } + if (isMomo) { + const latestOrderStatus = await this.orderStatusLogRepo.findOne({ + where: { order_id: order_id }, + order: { logged_at: 'DESC' }, + }); + if ( + latestOrderStatus?.order_status_id === OrderStatus.NEW || + latestOrderStatus?.order_status_id === OrderStatus.IDLE + ) { + try { + if (currentOrder.delivery_order_id) { + //TODO: cancel delivery + await this.ahamoveService.cancelAhamoveOrder( + currentOrder.delivery_order_id, + 'momo payment request failed', + ); + } + } catch (error) { + this.logger.error( + 'An error occurred while cancel delivery', + JSON.stringify(error), + ); + } finally { + // UPDATE ORDER STATUS + const orderStatusLog = new OrderStatusLog(); + orderStatusLog.logged_at = new Date().getTime(); + orderStatusLog.order_status_id = OrderStatus.CANCELLED; + orderStatusLog.note = 'momo payment has been failed'; + await this.orderStatusLogRepo.save(orderStatusLog); + } + } else { + this.logger.warn('The order status is not valid to cancel'); + } + } } async getApplicationFeeFromEndPoint( diff --git a/src/ormconfig.ts b/src/ormconfig.ts index 0c11083..5192c33 100644 --- a/src/ormconfig.ts +++ b/src/ormconfig.ts @@ -1,8 +1,8 @@ -import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { DataSourceOptions } from 'typeorm'; +import 'dotenv/config'; function ormConfig(): DataSourceOptions { - console.log(__dirname + '/migrations/**/*{.ts,.js}'); + console.log('process.env.BACKEND_ENV', process.env.BACKEND_ENV); const commonConf = { SYNCRONIZE: true, diff --git a/yarn.lock b/yarn.lock index d7dbe2c..9741d8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1601,6 +1601,13 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +axios-retry@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-4.0.0.tgz#d5cb8ea1db18e05ce6f08aa5fe8b2663bba48e60" + integrity sha512-F6P4HVGITD/v4z9Lw2mIA24IabTajvpDZmKa6zq/gGwn57wN5j1P3uWrAV0+diqnW6kTM2fTqmWNfgYWGmMuiA== + dependencies: + is-retry-allowed "^2.2.0" + axios@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" @@ -3298,6 +3305,11 @@ is-property@^1.0.2: resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== +is-retry-allowed@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" + integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"