import * as crypto from 'crypto'; import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { type PaymentProvider } from '@prisma/client'; import { type IPaymentGateway, type CreatePaymentUrlParams, type CreatePaymentUrlResult, type CallbackVerifyResult, type RefundParams, type RefundResult, } from './payment-gateway.interface'; @Injectable() export class MomoService implements IPaymentGateway { private readonly logger = new Logger(MomoService.name); readonly provider: PaymentProvider = 'MOMO'; private readonly partnerCode: string; private readonly accessKey: string; private readonly secretKey: string; private readonly endpoint: string; constructor(private readonly config: ConfigService) { this.partnerCode = this.config.getOrThrow('MOMO_PARTNER_CODE'); this.accessKey = this.config.getOrThrow('MOMO_ACCESS_KEY'); this.secretKey = this.config.getOrThrow('MOMO_SECRET_KEY'); this.endpoint = this.config.get('MOMO_ENDPOINT', 'https://test-payment.momo.vn/v2/gateway/api'); } async createPaymentUrl(params: CreatePaymentUrlParams): Promise { const requestId = crypto.randomUUID(); const requestType = 'payWithMethod'; const extraData = ''; const autoCapture = true; const lang = 'vi'; const amount = params.amountVND.toString(); const rawSignature = [ `accessKey=${this.accessKey}`, `amount=${amount}`, `extraData=${extraData}`, `ipnUrl=${params.returnUrl}`, `orderId=${params.orderId}`, `orderInfo=${params.description}`, `partnerCode=${this.partnerCode}`, `redirectUrl=${params.returnUrl}`, `requestId=${requestId}`, `requestType=${requestType}`, ].join('&'); const signature = crypto .createHmac('sha256', this.secretKey) .update(rawSignature) .digest('hex'); const body = { partnerCode: this.partnerCode, partnerName: 'GoodGo', storeId: 'GoodGo', requestId, amount: Number(amount), orderId: params.orderId, orderInfo: params.description, redirectUrl: params.returnUrl, ipnUrl: params.returnUrl, lang, requestType, autoCapture, extraData, signature, }; try { const response = await fetch(`${this.endpoint}/create`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const result = await response.json() as { resultCode: number; payUrl: string }; if (result.resultCode !== 0) { throw new Error(`MoMo create payment failed: resultCode=${result.resultCode}`); } this.logger.log(`MoMo payment URL created for order ${params.orderId}`); return { paymentUrl: result.payUrl, providerTxId: params.orderId, }; } catch (error) { this.logger.error(`MoMo createPaymentUrl error: ${error}`); throw error; } } verifyCallback(data: Record): CallbackVerifyResult { const orderId = data['orderId'] ?? ''; const providerTxId = data['transId'] ?? ''; const resultCode = data['resultCode']; const receivedSignature = data['signature'] ?? ''; const rawSignature = [ `accessKey=${this.accessKey}`, `amount=${data['amount']}`, `extraData=${data['extraData'] ?? ''}`, `message=${data['message'] ?? ''}`, `orderId=${orderId}`, `orderInfo=${data['orderInfo'] ?? ''}`, `orderType=${data['orderType'] ?? ''}`, `partnerCode=${this.partnerCode}`, `payType=${data['payType'] ?? ''}`, `requestId=${data['requestId'] ?? ''}`, `responseTime=${data['responseTime'] ?? ''}`, `resultCode=${resultCode}`, `transId=${providerTxId}`, ].join('&'); const expectedSignature = crypto .createHmac('sha256', this.secretKey) .update(rawSignature) .digest('hex'); const isValid = receivedSignature.length > 0 && receivedSignature.length === expectedSignature.length && crypto.timingSafeEqual(Buffer.from(receivedSignature, 'hex'), Buffer.from(expectedSignature, 'hex')); const isSuccess = isValid && resultCode === '0'; this.logger.log( `MoMo callback verified: orderId=${orderId}, valid=${isValid}, success=${isSuccess}`, ); return { isValid, orderId, providerTxId, isSuccess, rawData: data, }; } async refund(params: RefundParams): Promise { const requestId = crypto.randomUUID(); const amount = params.amountVND.toString(); const rawSignature = [ `accessKey=${this.accessKey}`, `amount=${amount}`, `description=${params.reason}`, `orderId=${requestId}`, `partnerCode=${this.partnerCode}`, `requestId=${requestId}`, `transId=${params.providerTxId}`, ].join('&'); const signature = crypto .createHmac('sha256', this.secretKey) .update(rawSignature) .digest('hex'); const body = { partnerCode: this.partnerCode, orderId: requestId, requestId, amount: Number(amount), transId: Number(params.providerTxId), lang: 'vi', description: params.reason, signature, }; try { const response = await fetch(`${this.endpoint}/refund`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const result = await response.json() as { resultCode: number }; const success = result.resultCode === 0; this.logger.log( `MoMo refund ${success ? 'successful' : 'failed'} for ${params.providerTxId}`, ); return { success, refundTxId: success ? requestId : null, }; } catch (error) { this.logger.error(`MoMo refund error: ${error}`); return { success: false, refundTxId: null }; } } }