refactor: the whole paymanet service
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { InjectQueue } from "@nestjs/bullmq";
|
||||
import { BadRequestException, HttpException, HttpStatus, Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||
import { BadRequestException, HttpException, HttpStatus, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Queue } from "bullmq";
|
||||
import dayjs from "dayjs";
|
||||
import Decimal from "decimal.js";
|
||||
import { FastifyReply } from "fastify";
|
||||
import { FastifyReply as FRply } from "fastify";
|
||||
import { DataSource, Not, QueryRunner } from "typeorm";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
@@ -30,17 +30,19 @@ 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 { TransferType } from "../enums/payment-type.enum";
|
||||
import { PaymentGatewayFactory } from "../factories/payment.factory";
|
||||
import { IVerifyPayment } from "../interfaces/IPayment";
|
||||
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 {
|
||||
private readonly logger = new Logger(PaymentsService.name);
|
||||
constructor(
|
||||
@InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME) private readonly paymentQueue: Queue,
|
||||
@InjectQueue(PAYMENT.QUEUE_NAME) private readonly paymentQueue: Queue,
|
||||
private readonly notificationsService: NotificationsService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly gatewayFactory: PaymentGatewayFactory,
|
||||
@@ -53,19 +55,17 @@ export class PaymentsService {
|
||||
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 user = await this.getUserById(userId, queryRunner);
|
||||
const paymentGateway = await this.getPaymentGatewayById(gatewayId, queryRunner);
|
||||
|
||||
const gatewayData = await this.processPayment(paymentGateway.name, amount, WalletMessage.DEPOSIT_WALLET_IPG, user.email, user.phone);
|
||||
|
||||
@@ -84,32 +84,20 @@ export class PaymentsService {
|
||||
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 { amount, bankAccountId, method, transferReceiptUrl } = depositDto;
|
||||
|
||||
const user = await queryRunner.manager.findOneBy(User, { id: userId });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
const { bankAccount } = await this.getBankAccountById(bankAccountId);
|
||||
const user = await this.getUserById(userId, queryRunner);
|
||||
|
||||
// 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);
|
||||
const depositRequest = this.createDepositRequest(bankAccount, user, transferReceiptUrl, amount, method, queryRunner);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return {
|
||||
@@ -118,101 +106,267 @@ export class PaymentsService {
|
||||
};
|
||||
} 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 = "/payment";
|
||||
async verifyPayment(gateway: GatewayType, queryDto: VerifyQueryDto, rep: FRply) {
|
||||
const frontUrl = this.buildFrontendRedirectUrl();
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const paymentGateway = this.gatewayFactory.getPaymentGateway(gateway);
|
||||
const payment = await this.getPaymentByReference(queryDto.Authority, queryRunner);
|
||||
|
||||
if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE);
|
||||
|
||||
if (queryDto.Status !== "OK") await this.handleFailedPaymentRedirect(payment, queryRunner, rep, frontUrl);
|
||||
|
||||
const verifyData = await paymentGateway.verifyPayment({
|
||||
reference: queryDto.Authority,
|
||||
amount: payment.amount,
|
||||
});
|
||||
|
||||
return await this.handlePaymentVerificationWithResponse(verifyData, payment, rep, frontUrl, queryRunner);
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
//===============================================
|
||||
|
||||
async handlePaymentVerificationWithResponse(verifyData: IVerifyPayment, payment: Payment, rp: FRply, frontUrl: URL, qryRnr: QueryRunner) {
|
||||
const result = await this.processVerificationResult(verifyData, payment, qryRnr);
|
||||
|
||||
this.addPaymentParamsToUrl(frontUrl, result.payment, result.status);
|
||||
|
||||
if ("additionalParams" in result) {
|
||||
Object.entries(result.additionalParams).forEach(([key, value]) => {
|
||||
frontUrl.searchParams.append(key, String(value));
|
||||
});
|
||||
}
|
||||
|
||||
await qryRnr.commitTransaction();
|
||||
|
||||
return rp.status(HttpStatus.FOUND).redirect(frontUrl.toString());
|
||||
}
|
||||
//===============================================
|
||||
|
||||
async processVerificationResult(verifyData: IVerifyPayment, payment: Payment, qryRnr: QueryRunner) {
|
||||
if (verifyData.code === 100) {
|
||||
return this.processSuccessfulVerification(payment, verifyData.ref_id, qryRnr);
|
||||
} else if (verifyData.code === 101) {
|
||||
return { status: PaymentStatus.PENDING, payment };
|
||||
} else {
|
||||
await this.handleFailedPayment(payment.id, qryRnr);
|
||||
return { status: PaymentStatus.FAILED, payment };
|
||||
}
|
||||
}
|
||||
//===============================================
|
||||
private async processSuccessfulVerification(payment: Payment, refId: number, queryRunner: QueryRunner) {
|
||||
// Regular wallet charge
|
||||
const { wallet } = await this.handleSuccessfulPayment(payment, payment.user.id, payment.amount, refId, queryRunner);
|
||||
|
||||
//for future use
|
||||
const additionalParams: Record<string, string> = {};
|
||||
|
||||
//
|
||||
await this.notificationsService.createWalletChargeNotification(
|
||||
payment.user.id,
|
||||
{
|
||||
amount: new Decimal(payment.amount).toNumber(),
|
||||
balance: new Decimal(wallet.balance).toNumber(),
|
||||
date: payment.createdAt,
|
||||
reason: WalletMessage.DEPOSIT_WALLET_IPG,
|
||||
userPhone: payment.user.phone,
|
||||
userEmail: payment.user.email,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
return {
|
||||
status: PaymentStatus.COMPLETED,
|
||||
payment,
|
||||
additionalParams,
|
||||
};
|
||||
}
|
||||
//===============================================
|
||||
|
||||
async createGatewayPaymentForUser(usr: User, amt: number, ref_Id: string, gatewayId: string, qryRnr: QueryRunner) {
|
||||
const payment = qryRnr.manager.create(Payment, {
|
||||
amount: amt,
|
||||
reference: ref_Id,
|
||||
user: usr,
|
||||
paymentGateway: { id: gatewayId },
|
||||
});
|
||||
|
||||
await qryRnr.manager.save(Payment, payment);
|
||||
this.logger.log(`Created payment ${payment.id} with reference ${ref_Id}`);
|
||||
|
||||
await this.addPaymentReminderToQueue(payment);
|
||||
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(payment: Payment, userId: string, amount: Decimal, transactionId: number, queryRunner: QueryRunner) {
|
||||
await queryRunner.manager.update(
|
||||
Payment,
|
||||
{ id: payment.id },
|
||||
{ status: PaymentStatus.COMPLETED, transactionId: transactionId.toString() },
|
||||
);
|
||||
|
||||
await this.handleReferralReward(payment, queryRunner);
|
||||
return this.walletsService.createPaymentTransaction(userId, amount, payment, queryRunner);
|
||||
}
|
||||
|
||||
//===============================================
|
||||
|
||||
async addBankAccount(createDto: CreateBankAccountDto) {
|
||||
await this.validateBankAccountDetails(createDto);
|
||||
|
||||
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);
|
||||
|
||||
await this.validateBankAccountUpdate(updateDto, bankAccountId);
|
||||
await this.bankAccountsRepository.save({ ...bankAccount, ...updateDto });
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATED,
|
||||
};
|
||||
}
|
||||
//===============================================
|
||||
|
||||
async getBankAccounts(isAdmin: boolean) {
|
||||
const bankAccounts = isAdmin
|
||||
? await this.bankAccountsRepository.find({ where: { isActive: true } })
|
||||
: 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 getTransactions(queryDto: SearchTransactionQueryDto) {
|
||||
return this.walletsService.getTransactionsForAdmin(queryDto);
|
||||
}
|
||||
//===============================================
|
||||
|
||||
async getDepositRequests(queryDto: PaymentTransactionQueryDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
const { type } = queryDto;
|
||||
|
||||
const queryBuilder = this.buildDepositRequestsQuery(skip, limit, type);
|
||||
const [depositRequests, count] = await queryBuilder.getManyAndCount();
|
||||
|
||||
return {
|
||||
depositRequests,
|
||||
count,
|
||||
paginate: true,
|
||||
};
|
||||
}
|
||||
//===============================================
|
||||
|
||||
async getDepositGatewayPayment(queryDto: PaginationDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.buildDepositGatewayPaymentsQuery(skip, 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 paymentGateway = this.gatewayFactory.getPaymentGateway(gateway);
|
||||
// const payment = await queryRunner.manager.findOne(Payment, {
|
||||
// where: { reference: queryDto.Authority },
|
||||
// relations: ["user"],
|
||||
// });
|
||||
const depositRequest = await this.getDepositRequestWithLock(depositId, queryRunner);
|
||||
|
||||
const payment = await queryRunner.manager
|
||||
.createQueryBuilder(Payment, "payment")
|
||||
.innerJoinAndSelect("payment.user", "user")
|
||||
.where("payment.reference = :reference", { reference: queryDto.Authority })
|
||||
.setLock("pessimistic_write")
|
||||
.getOne();
|
||||
|
||||
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());
|
||||
if (depositRequest.status === DepositRequestStatus.APPROVED) {
|
||||
throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_APPROVED);
|
||||
}
|
||||
|
||||
const verifyData = await paymentGateway.verifyPayment({ reference: queryDto.Authority, amount: payment.amount });
|
||||
await this.updateDepositRequestStatus(depositRequest.id, DepositRequestStatus.APPROVED, null, queryRunner);
|
||||
|
||||
if (verifyData.code === 100) {
|
||||
const { wallet } = await this.handleSuccessfulPayment(
|
||||
payment,
|
||||
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());
|
||||
//
|
||||
await this.notificationsService.createWalletChargeNotification(
|
||||
payment.user.id,
|
||||
{
|
||||
amount: new Decimal(payment.amount).toNumber(),
|
||||
balance: new Decimal(wallet.balance).toNumber(),
|
||||
date: payment.createdAt,
|
||||
reason: WalletMessage.DEPOSIT_WALLET_IPG,
|
||||
userPhone: payment.user.phone,
|
||||
userEmail: payment.user.email,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
} 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());
|
||||
}
|
||||
await this.walletsService.createDepositTransaction(depositRequest.user.id, depositRequest.amount, depositRequest, queryRunner);
|
||||
|
||||
return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString());
|
||||
await queryRunner.commitTransaction();
|
||||
return {
|
||||
message: PaymentMessage.DEPOSIT_APPROVED,
|
||||
depositRequest: depositRequest.id,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
@@ -221,43 +375,49 @@ export class PaymentsService {
|
||||
}
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
//*********************************** */
|
||||
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 });
|
||||
//===============================================
|
||||
async rejectDepositRequest(depositId: string, rejectDto: RejectDepositRequestDto) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
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);
|
||||
try {
|
||||
const depositRequest = await queryRunner.manager.findOne(DepositRequest, {
|
||||
where: { id: depositId },
|
||||
lock: { mode: "pessimistic_write" },
|
||||
});
|
||||
|
||||
return { payment };
|
||||
}
|
||||
if (!depositRequest) {
|
||||
throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND);
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
//*********************************** */
|
||||
async handleFailedPayment(paymentId: string, queryRunner: QueryRunner) {
|
||||
await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.CANCELLED });
|
||||
if (depositRequest.status === DepositRequestStatus.REJECTED) {
|
||||
throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_REJECTED);
|
||||
}
|
||||
|
||||
await this.updateDepositRequestStatus(depositRequest.id, DepositRequestStatus.REJECTED, rejectDto.comment, queryRunner);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return {
|
||||
message: PaymentMessage.DEPOSIT_REJECTED,
|
||||
depositRequest,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
//----------------------------------------
|
||||
//-private methods
|
||||
//----------------------------------------
|
||||
|
||||
private buildFrontendRedirectUrl(): URL {
|
||||
const frontUrl = new URL(this.configService.getOrThrow<string>("SITE_URL"));
|
||||
frontUrl.pathname = "/payment";
|
||||
return frontUrl;
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
private async isFirstPurchase(userId: string, queryRunner: QueryRunner) {
|
||||
const paymentCount = await queryRunner.manager.count(Payment, {
|
||||
@@ -266,7 +426,7 @@ export class PaymentsService {
|
||||
return paymentCount === 0;
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
//===============================================
|
||||
|
||||
private async handleReferralReward(payment: Payment, queryRunner: QueryRunner) {
|
||||
const isFirstPurchase = await this.isFirstPurchase(payment.user.id, queryRunner);
|
||||
@@ -293,87 +453,100 @@ export class PaymentsService {
|
||||
referral.completedAt = dayjs().toDate();
|
||||
await queryRunner.manager.save(referral);
|
||||
}
|
||||
//*********************************** */
|
||||
//===============================================
|
||||
|
||||
async handleSuccessfulPayment(payment: Payment, userId: string, amount: Decimal, transactionId: string, queryRunner: QueryRunner) {
|
||||
await queryRunner.manager.update(Payment, { id: payment.id }, { status: PaymentStatus.COMPLETED, transactionId });
|
||||
const result = await this.walletsService.createPaymentTransaction(userId, amount, payment, queryRunner);
|
||||
private async handleFailedPaymentRedirect(payment: Payment, queryRunner: QueryRunner, rep: FRply, frontUrl: URL) {
|
||||
await this.handleFailedPayment(payment.id, queryRunner);
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// Handle referral reward if applicable
|
||||
await this.handleReferralReward(payment, queryRunner);
|
||||
this.addPaymentParamsToUrl(frontUrl, payment, PaymentStatus.FAILED);
|
||||
|
||||
return result;
|
||||
return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString());
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
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,
|
||||
};
|
||||
//===============================================
|
||||
private addPaymentParamsToUrl(url: URL, payment: Payment, status: PaymentStatus) {
|
||||
url.searchParams.append("status", status);
|
||||
url.searchParams.append("id", payment.id);
|
||||
url.searchParams.append("date", payment.createdAt.toISOString());
|
||||
url.searchParams.append("amount", payment.amount.toString());
|
||||
}
|
||||
//*********************************** */
|
||||
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) });
|
||||
private async getUserById(userId: string, queryRunner: QueryRunner): Promise<User> {
|
||||
const user = await queryRunner.manager.findOneBy(User, { id: userId });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return user;
|
||||
}
|
||||
//===============================================
|
||||
|
||||
private async getPaymentGatewayById(gatewayId: string, queryRunner: QueryRunner): Promise<PaymentGateway> {
|
||||
const paymentGateway = await queryRunner.manager.findOneBy(PaymentGateway, { id: gatewayId });
|
||||
if (!paymentGateway) throw new BadRequestException(PaymentMessage.PAYMENT_GATEWAY_NOT_FOUND);
|
||||
return paymentGateway;
|
||||
}
|
||||
|
||||
//===============================================
|
||||
private createDepositRequest(bnkAcc: BankAccount, usr: User, recUrl: string, amt: number, typ: TransferType, qryRnr: QueryRunner) {
|
||||
const depositRequest = qryRnr.manager.create(DepositRequest, {
|
||||
bankAccount: bnkAcc,
|
||||
user: usr,
|
||||
receiptUrl: recUrl,
|
||||
amount: amt,
|
||||
type: typ,
|
||||
});
|
||||
|
||||
qryRnr.manager.save(DepositRequest, depositRequest);
|
||||
return depositRequest;
|
||||
}
|
||||
//===============================================
|
||||
|
||||
private async getPaymentByReference(reference: string, queryRunner: QueryRunner): Promise<Payment> {
|
||||
const payment = await queryRunner.manager
|
||||
.createQueryBuilder(Payment, "payment")
|
||||
.innerJoinAndSelect("payment.user", "user")
|
||||
.where("payment.reference = :reference", { reference })
|
||||
.setLock("pessimistic_write")
|
||||
.getOne();
|
||||
|
||||
if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF);
|
||||
return payment;
|
||||
}
|
||||
|
||||
//===============================================
|
||||
private async validateBankAccountDetails(accountDetails: { cardNumber?: string; IBan?: string }) {
|
||||
if (accountDetails.cardNumber) {
|
||||
const existCardNumber = await this.bankAccountsRepository.findOneBy({ cardNumber: accountDetails.cardNumber });
|
||||
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 (accountDetails.IBan) {
|
||||
const existIBan = await this.bankAccountsRepository.findOneBy({ IBan: accountDetails.IBan });
|
||||
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();
|
||||
//===============================================
|
||||
private async validateBankAccountUpdate(updateDto: UpdateBankAccountDto, bankAccountId: string) {
|
||||
if (updateDto.cardNumber) {
|
||||
const existCardNumber = await this.bankAccountsRepository.findOneBy({
|
||||
cardNumber: updateDto.cardNumber,
|
||||
id: Not(bankAccountId),
|
||||
});
|
||||
if (existCardNumber) throw new BadRequestException(PaymentMessage.CARD_NUMBER_EXIST);
|
||||
}
|
||||
|
||||
return {
|
||||
bankAccounts,
|
||||
};
|
||||
if (updateDto.IBan) {
|
||||
const existIBan = await this.bankAccountsRepository.findOneBy({
|
||||
IBan: updateDto.IBan,
|
||||
id: Not(bankAccountId),
|
||||
});
|
||||
if (existIBan) throw new BadRequestException(PaymentMessage.IBAN_EXIST);
|
||||
}
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
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;
|
||||
|
||||
//===============================================
|
||||
private buildDepositRequestsQuery(skip: number, limit: number, type?: TransferType) {
|
||||
const queryBuilder = this.depositRequestsRepository
|
||||
.createQueryBuilder("depositRequest")
|
||||
.leftJoinAndSelect("depositRequest.user", "user")
|
||||
@@ -386,117 +559,53 @@ export class PaymentsService {
|
||||
queryBuilder.andWhere("depositRequest.type = :type", { type });
|
||||
}
|
||||
|
||||
const [depositRequests, count] = await queryBuilder.getManyAndCount();
|
||||
|
||||
return {
|
||||
depositRequests,
|
||||
count,
|
||||
paginate: true,
|
||||
};
|
||||
return queryBuilder;
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getDepositGatewayPayment(queryDto: PaginationDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.paymentsRepository
|
||||
//===============================================
|
||||
private buildDepositGatewayPaymentsQuery(skip: number, limit: number) {
|
||||
return 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,
|
||||
};
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
//===============================================
|
||||
private async getDepositRequestWithLock(depositId: string, queryRunner: QueryRunner) {
|
||||
const depositRequest = await queryRunner.manager
|
||||
.createQueryBuilder(DepositRequest, "deposit")
|
||||
.innerJoinAndSelect("deposit.user", "user")
|
||||
.where("deposit.id = :id", { id: depositId })
|
||||
.setLock("pessimistic_write")
|
||||
.getOne();
|
||||
|
||||
async getDepositRequestById(depositId: string) {
|
||||
const depositRequest = await this.depositRequestsRepository.findOneBy({ id: depositId });
|
||||
if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND);
|
||||
|
||||
return {
|
||||
depositRequest,
|
||||
};
|
||||
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.user.id, depositRequest.amount, depositRequest, queryRunner);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return {
|
||||
message: PaymentMessage.DEPOSIT_APPROVED,
|
||||
depositRequest: depositRequest.id,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
//===============================================
|
||||
private async updateDepositRequestStatus(depositId: string, status: DepositRequestStatus, comment: string | null, qryRnr: QueryRunner) {
|
||||
const updateData: Partial<DepositRequest> = { status };
|
||||
if (comment !== null) {
|
||||
updateData.comment = comment;
|
||||
}
|
||||
|
||||
await qryRnr.manager.update(DepositRequest, { id: depositId }, updateData);
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
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();
|
||||
}
|
||||
//===============================================
|
||||
private async addPaymentReminderToQueue(payment: Payment) {
|
||||
this.paymentQueue.add(
|
||||
PAYMENT.START,
|
||||
{ payment },
|
||||
{
|
||||
delay: PAYMENT.START_DELAY,
|
||||
priority: PAYMENT.JOB_PRIORITY,
|
||||
attempts: PAYMENT.JOB_ATTEMPTS,
|
||||
backoff: { type: "exponential", delay: PAYMENT.JOB_BACKOFF },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user