payme-integration
Version:
NestJS integration module for Payme payment system
393 lines (359 loc) • 12.4 kB
text/typescript
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;
}
}