UNPKG

payme-integration

Version:

NestJS integration module for Payme payment system

393 lines (359 loc) 12.4 kB
import { Inject, Injectable } from '@nestjs/common'; import { CheckPerformTransactionRequestDto } from './dto/requests/check-perform-transaction-request.dto'; import { CheckPerformTransactionResponseDto } from './dto/responses/check-perform-transaction-response.dto'; import { CreateTransactionResponseDto } from './dto/responses/create-transaction-response.dto'; import { PerformTransactionRequestDto } from './dto/requests/perform-transaction-request.dto'; import { PerformTransactionResponseDto } from './dto/responses/perform-transaction-response.dto'; import { CancelTransactionRequestDto } from './dto/requests/cancel-transaction-request.dto'; import { CancelTransactionResponseDto } from './dto/responses/cancel-transaction-response.dto'; import { CheckTransactionResponseDto } from './dto/responses/check-transaction-response.dto'; import { GetStatementRequestDto } from './dto/requests/get-statement-request.dto'; import { GetStatementResponseDto, ITransactionResponseItem, } from './dto/responses/get-statement-response.dto'; import { TransactionMethods } from './types/transaction-methods'; import { CreateTransactionRequestDto } from './dto/requests/create-transaction-request.dto'; import { PaymeError, PaymeException } from '../errors'; import { DateTime } from 'luxon'; import { OrderStatus, TransactionCancellationReason, TransactionState, TransactionStatus, } from './types/enums'; import { InjectModel } from '@nestjs/mongoose'; import { PaymeTransaction, PaymeTransactionDocument, } from './schemas/payme-transaction.schema'; import { Model } from 'mongoose'; import { IPaymeStrategy } from './IPaymeStrategy'; import { RequestBody } from './dto/requests/request-body.dto'; import { CheckTransactionRequestDto } from './dto/requests/check-transaction-request.dto'; @Injectable() export class PaymeService { constructor( @Inject('PAYME_STRATEGY') private paymeStrategy: IPaymeStrategy, @InjectModel(PaymeTransaction.name) private readonly transactionModel: Model<PaymeTransactionDocument>, ) {} async handleTransactionMethods(reqBody: RequestBody) { const method = reqBody.method; switch (method) { case TransactionMethods.CheckPerformTransaction: return this.checkPerformTransaction( reqBody as CheckPerformTransactionRequestDto, ); case TransactionMethods.CreateTransaction: return this.createTransaction(reqBody as CreateTransactionRequestDto); case TransactionMethods.PerformTransaction: return this.performTransaction(reqBody as PerformTransactionRequestDto); case TransactionMethods.CancelTransaction: return this.cancelTransaction(reqBody as CancelTransactionRequestDto); case TransactionMethods.CheckTransaction: return this.checkTransaction(reqBody as CheckTransactionRequestDto); case TransactionMethods.GetStatement: return this.getStatement(reqBody as GetStatementRequestDto); default: throw new PaymeException(PaymeError.METHOD_NOT_FOUND); } } private async checkPerformTransaction( dto: CheckPerformTransactionRequestDto, ): Promise<CheckPerformTransactionResponseDto> { const order = await this.paymeStrategy.getOrderByAccount( dto.params.account, ); if (!order) { throw new PaymeException(PaymeError.ORDER_NOT_FOUND, dto.params.account); } if (order.status !== OrderStatus.ReadyForPayment) { throw new PaymeException(PaymeError.ORDER_IN_PROGRESS); } if (order.amount * 100 !== dto.params.amount) { throw new PaymeException(PaymeError.INVALID_AMOUNT); } return { result: { allow: true, }, id: dto.id, }; } private async createTransaction( dto: CreateTransactionRequestDto, ): Promise<CreateTransactionResponseDto> { const transId = dto.params.id; const transaction = await this.transactionModel .findOne({ trans_id: transId, }) .exec(); if (!transaction) { const pending_transaction = await this.transactionModel .findOne({ state: TransactionState.Pending, account: dto.params.account, }) .exec(); if (pending_transaction) { throw new PaymeException(PaymeError.TRANSACTION_ALREADY_CREATED); } await this.checkPerformTransaction({ id: dto.id, method: TransactionMethods.CheckPerformTransaction, params: { account: dto.params.account, amount: dto.params.amount, }, }); const newTransaction = await this.transactionModel.create({ state: TransactionState.Pending, trans_id: dto.params.id, amount: dto.params.amount, account: dto.params.account, create_time: new Date(), }); return { result: { transaction: newTransaction._id.toHexString(), create_time: newTransaction.create_time.getTime(), state: newTransaction.state, }, id: dto.id, }; } else { if (transaction.state !== TransactionState.Pending) { throw new PaymeException(PaymeError.INACTIVE_TRANSCACTION); } if (this.checkTransactionExpiration(transaction.create_time)) { transaction.state = TransactionState.PendingCanceled; transaction.reason = TransactionCancellationReason.TimeoutCancellation; transaction.cancel_time = new Date(); await transaction.save(); throw new PaymeException(PaymeError.COULD_NOT_PERFORM_TRANSACTION); } return { result: { transaction: transaction._id.toHexString(), create_time: transaction.create_time?.getTime() ?? 0, state: transaction.state, }, id: dto.id, }; } } private async performTransaction( dto: PerformTransactionRequestDto, ): Promise<PerformTransactionResponseDto> { const transaction = await this.transactionModel .findOne({ trans_id: dto.params.id, }) .exec(); if (!transaction) { throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND); } if (transaction.state === TransactionState.Pending) { const isTransactionExpired = this.checkTransactionExpiration( transaction.create_time, ); if (isTransactionExpired) { transaction.state = TransactionState.PendingCanceled; transaction.reason = TransactionCancellationReason.TimeoutCancellation; transaction.cancel_time = new Date(); await transaction.save(); throw new PaymeException(PaymeError.COULD_NOT_PERFORM_TRANSACTION); } else { const perform_time = new Date(); const updated_payment = await this.transactionModel .findOneAndUpdate( { trans_id: dto.params.id, }, { state: TransactionState.Paid, perform_time, status: TransactionStatus.PAID, }, { new: true, }, ) .exec(); if (!updated_payment) { throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND); } await this.paymeStrategy.successPayment(updated_payment); return { result: { transaction: updated_payment.id, perform_time: updated_payment.perform_time?.getTime() ?? 0, state: updated_payment.state, }, id: dto.id, }; } } else if (transaction.state === TransactionState.Paid) { return { result: { transaction: transaction.id, perform_time: transaction.perform_time?.getTime() ?? 0, state: transaction.state, }, id: dto.id, }; } else { throw new PaymeException(PaymeError.COULD_NOT_PERFORM_TRANSACTION); } } private async cancelTransaction( dto: CancelTransactionRequestDto, ): Promise<CancelTransactionResponseDto> { const transaction = await this.transactionModel .findOne({ trans_id: dto.params.id, }) .exec(); if (!transaction) { throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND); } if (transaction.state === TransactionState.Pending) { const cancel_transaction = await this.transactionModel .findOneAndUpdate( { _id: transaction._id, }, { state: TransactionState.PendingCanceled, reason: dto.params.reason, cancel_time: new Date(), status: TransactionStatus.CANCELED, }, { new: true }, ) .exec(); if (!cancel_transaction) { throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND); } return { result: { transaction: cancel_transaction._id.toHexString(), state: cancel_transaction.state, cancel_time: cancel_transaction?.cancel_time?.getTime() ?? 0, }, id: dto.id, }; } else if (transaction.state !== TransactionState.Paid) { return { result: { transaction: transaction._id.toHexString(), state: transaction.state, cancel_time: transaction.cancel_time?.getTime() ?? 0, }, id: dto.id, }; } else { const updated_transaction = await this.transactionModel .findOneAndUpdate( { trans_id: dto.params.id, }, { status: TransactionStatus.CANCELED, state: TransactionState.PaidCanceled, cancel_time: new Date(), reason: dto.params.reason, }, { new: true, }, ) .exec(); if (!updated_transaction) { throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND); } return { id: dto.id, result: { transaction: updated_transaction._id.toHexString(), state: updated_transaction.state, cancel_time: updated_transaction?.cancel_time?.getTime() ?? 0, }, }; } } private async checkTransaction( dto: CheckTransactionRequestDto, ): Promise<CheckTransactionResponseDto> { const transaction = await this.transactionModel .findOne({ trans_id: dto.params.id, }) .exec(); if (!transaction) { throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND); } return { result: { create_time: transaction.create_time?.getTime() ?? 0, perform_time: transaction.perform_time ? new Date(transaction.perform_time).getTime() : 0, cancel_time: transaction?.cancel_time ? new Date(transaction.cancel_time).getTime() : 0, transaction: transaction._id.toHexString(), state: transaction.state, reason: transaction.reason ?? null, }, id: dto.id, }; } private async getStatement( dto: GetStatementRequestDto, ): Promise<GetStatementResponseDto> { const transactions = await this.transactionModel .find({ create_time: { $gte: dto.params.from, $lte: dto.params.to, }, }) .exec(); const mappedTransactions = transactions.map( (transaction): ITransactionResponseItem => { return <ITransactionResponseItem>{ id: transaction._id.toHexString(), account: transaction.account, amount: transaction.amount, time: transaction?.create_time?.getTime() ?? 0, transaction: transaction.id, state: transaction.state, perform_time: transaction.perform_time?.getTime() ?? 0, cancel_time: transaction.cancel_time?.getTime() ?? 0, create_time: transaction.create_time?.getTime() ?? 0, reason: transaction.reason ?? null, }; }, ); // some logic return { result: { transactions: mappedTransactions, }, id: dto.id, }; } private checkTransactionExpiration(createdAt: Date) { const transactionCreatedAt = new Date(createdAt); const timeoutDuration = 720; const timeoutThreshold = DateTime.now() .minus({ minutes: timeoutDuration, }) .toJSDate(); return transactionCreatedAt < timeoutThreshold; } }