432 lines
17 KiB
TypeScript
432 lines
17 KiB
TypeScript
import { InjectQueue } from "@nestjs/bullmq";
|
|
import { BadRequestException, HttpException, HttpStatus, Injectable, InternalServerErrorException } from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import { Queue } from "bullmq";
|
|
// eslint-disable-next-line import/no-named-as-default
|
|
import Decimal from "decimal.js";
|
|
import { FastifyReply } from "fastify";
|
|
import { DataSource, Not, QueryRunner } from "typeorm";
|
|
|
|
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
|
import { CommonMessage, PaymentMessage, UserMessage, WalletMessage } from "../../../common/enums/message.enum";
|
|
import { User } from "../../users/entities/user.entity";
|
|
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
|
import { WalletsService } from "../../wallets/providers/wallets.service";
|
|
import { PAYMENT } from "../constants";
|
|
import { CreateBankAccountDto } from "../DTO/create-bankaccount.dto";
|
|
import { GatewayDepositDto, TransferDepositDto } from "../DTO/deposit-wallet.dto";
|
|
import { PaymentTransactionQueryDto } from "../DTO/payment-transaction-query.dto";
|
|
import { SearchTransactionQueryDto } from "../DTO/search-transaction-query.dto";
|
|
import { UpdateBankAccountDto } from "../DTO/update-bankaccount.dto";
|
|
import { RejectDepositRequestDto } from "../DTO/update-deposit-request-status.dto";
|
|
import { VerifyQueryDto } from "../DTO/verify-payment.dto";
|
|
import { BankAccount } from "../entities/bank-account.entity";
|
|
import { DepositRequest } from "../entities/deposit-request.entity";
|
|
import { PaymentGateway } from "../entities/payment-gateway.entity";
|
|
import { Payment } from "../entities/payment.entity";
|
|
import { DepositRequestStatus } from "../enums/deposit-request-status.enum";
|
|
import { PaymentStatus } from "../enums/payment-status.enum";
|
|
import { PaymentGatewayFactory } from "../factories/payment.factory";
|
|
import { BankAccountsRepository } from "../repositories/bank-accounts.repository";
|
|
import { DepositRequestsRepository } from "../repositories/deposit-requests.repository";
|
|
import { PaymentGatewaysRepository } from "../repositories/payment-gateway.repository";
|
|
import { PaymentsRepository } from "../repositories/payments.repository";
|
|
import { GatewayType } from "../types/gateway.type";
|
|
|
|
@Injectable()
|
|
export class PaymentsService {
|
|
constructor(
|
|
@InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME) private readonly paymentQueue: Queue,
|
|
private readonly configService: ConfigService,
|
|
private readonly gatewayFactory: PaymentGatewayFactory,
|
|
private readonly paymentGatewaysRepository: PaymentGatewaysRepository,
|
|
private readonly paymentsRepository: PaymentsRepository,
|
|
private readonly bankAccountsRepository: BankAccountsRepository,
|
|
private readonly walletsService: WalletsService,
|
|
private readonly depositRequestsRepository: DepositRequestsRepository,
|
|
private dataSource: DataSource,
|
|
) {}
|
|
|
|
//*********************************** */
|
|
async chargeWalletWithGateway(chargeDto: GatewayDepositDto, userId: string) {
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
try {
|
|
const { amount, gatewayId } = chargeDto;
|
|
|
|
const user = await queryRunner.manager.findOneBy(User, { id: userId });
|
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
|
|
|
const paymentGateway = await queryRunner.manager.findOneBy(PaymentGateway, { id: gatewayId });
|
|
if (!paymentGateway) throw new BadRequestException(PaymentMessage.PAYMENT_GATEWAY_NOT_FOUND);
|
|
|
|
const gatewayData = await this.processPayment(paymentGateway.name, amount, WalletMessage.DEPOSIT_WALLET_IPG, user.email, user.phone);
|
|
|
|
const payment = await this.createGatewayPaymentForUser(user, amount, gatewayData.reference, paymentGateway.id, queryRunner);
|
|
|
|
await queryRunner.commitTransaction();
|
|
return {
|
|
payment,
|
|
...gatewayData,
|
|
};
|
|
} catch (error) {
|
|
await queryRunner.rollbackTransaction();
|
|
if (error instanceof HttpException) throw error;
|
|
throw new InternalServerErrorException(WalletMessage.ERROR_IN_CHARGE_WALLET);
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
|
|
//*********************************** */
|
|
async chargeWalletWithTransfer(depositDto: TransferDepositDto, userId: string) {
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
const { amount, bankAccountId, method, transferReceiptUrl } = depositDto;
|
|
try {
|
|
const bankAccount = await queryRunner.manager.findOneBy(BankAccount, { id: bankAccountId });
|
|
if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND);
|
|
|
|
const user = await queryRunner.manager.findOneBy(User, { id: userId });
|
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
|
|
|
// if (method === TransferType.GATEWAY) throw new BadRequestException(PaymentMessage.TRANSFER_METHOD_NOT_ALLOWED);
|
|
|
|
//
|
|
const depositRequest = queryRunner.manager.create(DepositRequest, {
|
|
bankAccount,
|
|
user,
|
|
receiptUrl: transferReceiptUrl,
|
|
amount,
|
|
type: method,
|
|
});
|
|
|
|
await queryRunner.manager.save(DepositRequest, depositRequest);
|
|
|
|
await queryRunner.commitTransaction();
|
|
return {
|
|
message: PaymentMessage.DEPOSIT_CREATED,
|
|
depositId: depositRequest.id,
|
|
};
|
|
} catch (error) {
|
|
await queryRunner.rollbackTransaction();
|
|
|
|
throw error;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
//*********************************** */
|
|
//*********************************** */
|
|
async processPayment(provider: GatewayType, amount: number, description: string, email?: string, mobile?: string) {
|
|
const paymentGateway = this.gatewayFactory.getPaymentGateway(provider);
|
|
return paymentGateway.processPayment({ amount, description, email, mobile });
|
|
}
|
|
|
|
//*********************************** */
|
|
//*********************************** */
|
|
async verifyPayment(gateway: GatewayType, queryDto: VerifyQueryDto, rep: FastifyReply) {
|
|
const frontUrl = new URL(this.configService.getOrThrow<string>("SITE_URL"));
|
|
frontUrl.pathname = "/transactions";
|
|
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
try {
|
|
const paymentGateway = this.gatewayFactory.getPaymentGateway(gateway);
|
|
const payment = await queryRunner.manager.findOne(Payment, {
|
|
where: { reference: queryDto.Authority },
|
|
relations: ["user"],
|
|
});
|
|
|
|
if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF);
|
|
if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE);
|
|
|
|
if (queryDto.Status !== "OK") {
|
|
await this.handleFailedPayment(payment.id, queryRunner);
|
|
await queryRunner.commitTransaction();
|
|
frontUrl.searchParams.append("status", PaymentStatus.FAILED);
|
|
frontUrl.searchParams.append("id", payment.id);
|
|
frontUrl.searchParams.append("date", payment.createdAt.toISOString());
|
|
frontUrl.searchParams.append("amount", payment.amount.toString());
|
|
|
|
return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString());
|
|
}
|
|
|
|
const verifyData = await paymentGateway.verifyPayment({ reference: queryDto.Authority, amount: payment.amount });
|
|
|
|
if (verifyData.code === 100) {
|
|
await this.handleSuccessfulPayment(payment.id, payment.user.id, payment.amount, `${verifyData.ref_id}`, queryRunner);
|
|
await queryRunner.commitTransaction();
|
|
frontUrl.searchParams.append("status", PaymentStatus.COMPLETED);
|
|
frontUrl.searchParams.append("id", payment.id);
|
|
frontUrl.searchParams.append("date", payment.createdAt.toISOString());
|
|
frontUrl.searchParams.append("amount", payment.amount.toString());
|
|
} else if (verifyData.code === 101) {
|
|
await queryRunner.commitTransaction();
|
|
frontUrl.searchParams.append("status", PaymentStatus.PENDING);
|
|
frontUrl.searchParams.append("id", payment.id);
|
|
frontUrl.searchParams.append("date", payment.createdAt.toISOString());
|
|
frontUrl.searchParams.append("amount", payment.amount.toString());
|
|
} else {
|
|
await this.handleFailedPayment(payment.id, queryRunner);
|
|
await queryRunner.commitTransaction();
|
|
frontUrl.searchParams.append("status", PaymentStatus.FAILED);
|
|
frontUrl.searchParams.append("id", payment.id);
|
|
frontUrl.searchParams.append("date", payment.createdAt.toISOString());
|
|
frontUrl.searchParams.append("amount", payment.amount.toString());
|
|
}
|
|
|
|
console.log(frontUrl.toString());
|
|
return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString());
|
|
} catch (error) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw error;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
|
|
//*********************************** */
|
|
//*********************************** */
|
|
async createGatewayPaymentForUser(user: User, amount: number, reference: string, gatewayId: string, queryRunner: QueryRunner) {
|
|
//
|
|
const payment = queryRunner.manager.create(Payment, {
|
|
amount,
|
|
reference,
|
|
user,
|
|
paymentGateway: { id: gatewayId },
|
|
});
|
|
//
|
|
await queryRunner.manager.save(Payment, payment);
|
|
this.paymentQueue.add(PAYMENT.PAYMENT_START, { payment }, { delay: 1000 });
|
|
|
|
return payment;
|
|
}
|
|
//*********************************** */
|
|
//*********************************** */
|
|
async getAvailableGateways() {
|
|
const paymentGateways = await this.paymentGatewaysRepository.find({ where: { isActive: true } });
|
|
return { paymentGateways };
|
|
}
|
|
//*********************************** */
|
|
//*********************************** */
|
|
async getPaymentWithReference(reference: string) {
|
|
const payment = await this.paymentsRepository.findOneWithReference(reference);
|
|
if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF);
|
|
|
|
return { payment };
|
|
}
|
|
|
|
//*********************************** */
|
|
//*********************************** */
|
|
async handleFailedPayment(paymentId: string, queryRunner: QueryRunner) {
|
|
await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.CANCELLED });
|
|
}
|
|
|
|
//*********************************** */
|
|
async handleSuccessfulPayment(paymentId: string, userId: string, amount: Decimal, transactionId: string, queryRunner: QueryRunner) {
|
|
await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.COMPLETED, transactionId });
|
|
const transaction = await this.walletsService.createPaymentTransaction(userId, amount, queryRunner);
|
|
return transaction;
|
|
}
|
|
|
|
//*********************************** */
|
|
async addBankAccount(createDto: CreateBankAccountDto) {
|
|
const existCardNumber = await this.bankAccountsRepository.findOneBy({ cardNumber: createDto.cardNumber });
|
|
if (existCardNumber) throw new BadRequestException(PaymentMessage.CARD_NUMBER_EXIST);
|
|
//
|
|
const existIBan = await this.bankAccountsRepository.findOneBy({ IBan: createDto.IBan });
|
|
if (existIBan) throw new BadRequestException(PaymentMessage.IBAN_EXIST);
|
|
//
|
|
const bankAccount = this.bankAccountsRepository.create({ ...createDto });
|
|
await this.bankAccountsRepository.save(bankAccount);
|
|
|
|
return {
|
|
message: CommonMessage.CREATED,
|
|
bankAccount,
|
|
};
|
|
}
|
|
//*********************************** */
|
|
async updateBankAccount(updateDto: UpdateBankAccountDto, bankAccountId: string) {
|
|
const bankAccount = await this.bankAccountsRepository.findOneBy({ id: bankAccountId });
|
|
if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND);
|
|
|
|
if (updateDto.cardNumber) {
|
|
const existCardNumber = await this.bankAccountsRepository.findOneBy({ cardNumber: updateDto.cardNumber, id: Not(bankAccountId) });
|
|
if (existCardNumber) throw new BadRequestException(PaymentMessage.CARD_NUMBER_EXIST);
|
|
}
|
|
//
|
|
if (updateDto.IBan) {
|
|
const existIBan = await this.bankAccountsRepository.findOneBy({ IBan: updateDto.IBan, id: Not(bankAccountId) });
|
|
if (existIBan) throw new BadRequestException(PaymentMessage.IBAN_EXIST);
|
|
}
|
|
|
|
await this.bankAccountsRepository.save({ ...bankAccount, ...updateDto });
|
|
}
|
|
//*********************************** */
|
|
async getBankAccounts(isAdmin: boolean) {
|
|
let bankAccounts: BankAccount[];
|
|
|
|
if (isAdmin) {
|
|
bankAccounts = await this.bankAccountsRepository.find({ where: { isActive: true } });
|
|
} else {
|
|
bankAccounts = await this.bankAccountsRepository.find();
|
|
}
|
|
|
|
return {
|
|
bankAccounts,
|
|
};
|
|
}
|
|
|
|
//*********************************** */
|
|
async getBankAccountById(bankAccountId: string) {
|
|
const bankAccount = await this.bankAccountsRepository.findOneBy({ id: bankAccountId });
|
|
if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND);
|
|
|
|
return {
|
|
bankAccount,
|
|
};
|
|
}
|
|
|
|
// async deleteBankAccount()
|
|
//*********************************** */
|
|
async getTransactions(queryDto: SearchTransactionQueryDto) {
|
|
return this.walletsService.getTransactionsForAdmin(queryDto);
|
|
}
|
|
|
|
//*********************************** */
|
|
async getDepositRequests(queryDto: PaymentTransactionQueryDto) {
|
|
const { limit, skip } = PaginationUtils(queryDto);
|
|
const { type } = queryDto;
|
|
|
|
const queryBuilder = this.depositRequestsRepository
|
|
.createQueryBuilder("depositRequest")
|
|
.leftJoinAndSelect("depositRequest.user", "user")
|
|
.leftJoinAndSelect("depositRequest.bankAccount", "bankAccount")
|
|
.orderBy("depositRequest.createdAt", "DESC")
|
|
.skip(skip)
|
|
.take(limit);
|
|
|
|
if (type) {
|
|
queryBuilder.andWhere("depositRequest.type = :type", { type });
|
|
}
|
|
|
|
const [depositRequests, count] = await queryBuilder.getManyAndCount();
|
|
|
|
return {
|
|
depositRequests,
|
|
count,
|
|
paginate: true,
|
|
};
|
|
}
|
|
//*********************************** */
|
|
|
|
async getDepositGatewayPayment(queryDto: PaginationDto) {
|
|
const { limit, skip } = PaginationUtils(queryDto);
|
|
|
|
const queryBuilder = this.paymentsRepository
|
|
.createQueryBuilder("payment")
|
|
.leftJoinAndSelect("payment.user", "user")
|
|
.leftJoinAndSelect("payment.paymentGateway", "paymentGateway")
|
|
.orderBy("payment.createdAt", "DESC")
|
|
.skip(skip)
|
|
.take(limit);
|
|
|
|
const [payments, count] = await queryBuilder.getManyAndCount();
|
|
|
|
return {
|
|
payments,
|
|
count,
|
|
paginate: true,
|
|
};
|
|
}
|
|
|
|
//*********************************** */
|
|
|
|
async getDepositRequestById(depositId: string) {
|
|
const depositRequest = await this.depositRequestsRepository.findOneBy({ id: depositId });
|
|
if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND);
|
|
|
|
return {
|
|
depositRequest,
|
|
};
|
|
}
|
|
|
|
//*********************************** */
|
|
async approverDepositRequest(depositId: string) {
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
try {
|
|
// const depositRequest = await queryRunner.manager.findOne(DepositRequest, {
|
|
// where: { id: depositId },
|
|
// lock: { mode: "pessimistic_write" },
|
|
// });
|
|
|
|
const depositRequest = await queryRunner.manager
|
|
.createQueryBuilder(DepositRequest, "deposit")
|
|
.innerJoinAndSelect("deposit.user", "user")
|
|
.where("deposit.id = :id", { id: depositId })
|
|
.setLock("pessimistic_write")
|
|
.getOne();
|
|
|
|
if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND);
|
|
|
|
if (depositRequest.status === DepositRequestStatus.APPROVED) throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_APPROVED);
|
|
|
|
await queryRunner.manager.update(DepositRequest, { id: depositRequest.id }, { status: DepositRequestStatus.APPROVED });
|
|
|
|
await this.walletsService.createDepositTransaction(depositRequest.amount, depositRequest.user.id, queryRunner);
|
|
|
|
await queryRunner.commitTransaction();
|
|
return {
|
|
message: PaymentMessage.DEPOSIT_APPROVED,
|
|
depositRequest: depositRequest.id,
|
|
};
|
|
} catch (error) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw error;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
|
|
//*********************************** */
|
|
async rejectDepositRequest(depositId: string, rejectDto: RejectDepositRequestDto) {
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
try {
|
|
const depositRequest = await queryRunner.manager.findOne(DepositRequest, {
|
|
where: { id: depositId },
|
|
lock: { mode: "pessimistic_write" },
|
|
});
|
|
if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND);
|
|
|
|
if (depositRequest.status === DepositRequestStatus.REJECTED) throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_REJECTED);
|
|
|
|
await queryRunner.manager.update(
|
|
DepositRequest,
|
|
{ id: depositRequest.id },
|
|
{ status: DepositRequestStatus.REJECTED, comment: rejectDto.comment },
|
|
);
|
|
|
|
await queryRunner.commitTransaction();
|
|
|
|
return {
|
|
message: PaymentMessage.DEPOSIT_REJECTED,
|
|
depositRequest,
|
|
};
|
|
} catch (error) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw error;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
}
|