chore: normalize the payment and payment transaction

This commit is contained in:
mahyargdz
2025-02-06 13:08:51 +03:30
parent f3f47a80a1
commit e216937dd9
23 changed files with 547 additions and 90 deletions
+7
View File
@@ -46,3 +46,10 @@ BUCKET_ACCESS_KEY=
BUCKET_SECRET_KEY=
BUCKET_UPLOAD_URL=https://dsc.storage.iran.liara.space
IPG_TYPE=sandbox
ZARINPAL_MERCHANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
SITE_URL=http://localhost:4000/payment
ZARINPAL_API_URL=zarinpal.com/pg
CALLBACK_URL=http://localhost:4000/payments/verify
+8 -6
View File
@@ -1,18 +1,20 @@
import { Logger } from "@nestjs/common";
import { connectionSource } from "./connection";
import { seedAdmin } from "./seeders/admin.seeder";
import { seedNotifSettings } from "./seeders/notif-setting.seeder";
import { seedRole } from "./seeders/role.seeder";
// import { seedAdmin } from "./seeders/admin.seeder";
// import { seedNotifSettings } from "./seeders/notif-setting.seeder";
import { seedPaymentGateways } from "./seeders/payment-gateway.seeder";
// import { seedRole } from "./seeders/role.seeder";
const logger = new Logger("Seeder");
export const runSeeder = async () => {
await connectionSource.initialize();
logger.log("start seeding database");
await seedRole(connectionSource, logger);
await seedAdmin(connectionSource, logger);
await seedNotifSettings(connectionSource, logger);
// await seedRole(connectionSource, logger);
// await seedAdmin(connectionSource, logger);
// await seedNotifSettings(connectionSource, logger);
await seedPaymentGateways(connectionSource, logger);
logger.log("seeding completed");
process.exit(0);
@@ -0,0 +1,29 @@
import { Logger } from "@nestjs/common";
import { DataSource } from "typeorm";
import { PaymentGateway } from "../../src/modules/payments/entities/payment-gateway.entity";
import { GatewayEnum } from "../../src/modules/payments/enums/gateway.enum";
const paymentGateways: Partial<PaymentGateway>[] = [
{
name: GatewayEnum.ZARINPAL,
nameFa: "زرین پال",
logoUrl: "https://www.zarinpal.com/docs/logo/fa-dark.svg",
description: "درگاه پرداخت زرین پال",
},
];
export const seedPaymentGateways = async (dataSource: DataSource, logger: Logger) => {
try {
const paymentGatewayRepo = dataSource.getRepository(PaymentGateway);
for (const paymentGateway of paymentGateways) {
const pg = paymentGatewayRepo.create(paymentGateway);
await paymentGatewayRepo.save(pg);
}
logger.log("pg seeded successfully");
} catch (error) {
logger.error("error in seeding pg", error);
process.exit(1);
}
};
+22
View File
@@ -196,6 +196,9 @@ export const enum WalletMessage {
BANK_ACCOUNT_ID_SHOULD_BE_UUID = "شناسه حساب بانکی باید یک UUID معتبر باشد",
ERROR_IN_CHARGE_WALLET = "خطا در شارژ کیف پول",
DEPOSIT_WALLET_IPG = "شارژ کیف پول از طریق درگاه",
TRANSFER_METHOD_REQUIRED = "روش انتفال اجباری است",
GATEWAY_ID_SHOULD_BE_UUID = "آیدی درگاه پرداخت باید UUID باشد",
// DEPOSIT_WITH_TRANSFER = "DEPOSIT_WITH_TRANSFER",
}
export const enum PaymentMessage {
@@ -203,6 +206,25 @@ export const enum PaymentMessage {
ERROR_IN_PROCESS_PAYMENT = "خطا در پردازش پرداخت",
ERROR_IN_VERIFY_PAYMENT = "خطا در تایید پرداخت",
PAYMENT_NOT_FOUND_WITH_REF = "پرداختی با این شناسه یافت نشد",
CARD_NUMBER_REQUIRED = "شماره کارت الزامی است",
CARD_NUMBER_NUMBER_STRING = "شماره کارت باید شامل اعداد باشد",
CARD_NUMBER_LENGTH = "شماره کارت باید ۱۶ رقم باشد",
IBAN_REQUIRED = "شماره شبا الزامی است",
IBAN_NUMBER_STRING = "شماره شبا باید شامل اعداد باشد",
IBAN_INVALID = "شماره شبا معتبر نیست",
BANK_NAME_REQUIRED = "نام بانک الزامی است",
BANK_NAME_STRING = "نام بانک باید شامل حروف باشد",
BANK_NAME_LENGTH = "نام بانک باید بین ۲ تا ۱۰۰ کاراکتر باشد",
ACCOUNT_OWNER_NAME_REQUIRED = "نام صاحب حساب الزامی است",
ACCOUNT_OWNER_NAME_STRING = "نام صاحب حساب باید شامل حروف باشد",
ACCOUNT_OWNER_NAME_LENGTH = "نام صاحب حساب باید بین ۳ تا ۱۵۰ کاراکتر باشد",
IS_ACTIVE_REQUIRED = "وضعیت فعال بودن حساب الزامی است",
IS_ACTIVE_BOOLEAN = "وضعیت فعال بودن حساب باید true یا false باشد",
CARD_NUMBER_EXIST = "شماره کارت تکراری می‌باشد",
IBAN_EXIST = "شماره شبا تکراری می‌باشد",
BANK_ACCOUNT_NOT_FOUND = "حساب بانکی پیدا نشد",
DEPOSIT_CREATED = "درخواست واریز ثبت شد",
VALIDATED_BEFORE = "این پرداخت قبلا تایید شده است",
}
export const enum SettingMessageEnum {
@@ -0,0 +1,36 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsBoolean, IsNotEmpty, IsNumberString, IsString, Length } from "class-validator";
import { PaymentMessage } from "../../../common/enums/message.enum";
export class CreateBankAccountDto {
@IsNotEmpty({ message: PaymentMessage.CARD_NUMBER_REQUIRED })
@IsNumberString({ no_symbols: true }, { message: PaymentMessage.CARD_NUMBER_NUMBER_STRING })
@Length(16, 16, { message: PaymentMessage.CARD_NUMBER_LENGTH })
@ApiProperty({ description: "card number", example: "1234567812345678" })
cardNumber: string;
@IsNotEmpty({ message: PaymentMessage.IBAN_REQUIRED })
@IsNumberString({ no_symbols: true }, { message: PaymentMessage.IBAN_NUMBER_STRING })
// @IsIBAN({ message: PaymentMessage.IBAN_INVALID })
@Length(24, 24, { message: PaymentMessage.IBAN_INVALID })
@ApiProperty({ description: "IBAN number", example: "123456789012345678901234" })
IBan: string;
@IsNotEmpty({ message: PaymentMessage.BANK_NAME_REQUIRED })
@IsString({ message: PaymentMessage.BANK_NAME_STRING })
@Length(2, 100, { message: PaymentMessage.BANK_NAME_LENGTH })
@ApiProperty({ description: "bank name", example: "Example Bank" })
bankName: string;
@IsNotEmpty({ message: PaymentMessage.ACCOUNT_OWNER_NAME_REQUIRED })
@IsString({ message: PaymentMessage.ACCOUNT_OWNER_NAME_STRING })
@Length(3, 150, { message: PaymentMessage.ACCOUNT_OWNER_NAME_LENGTH })
@ApiProperty({ description: "account owner name", example: "John Doe" })
accountOwnerName: string;
@IsNotEmpty({ message: PaymentMessage.IS_ACTIVE_REQUIRED })
@IsBoolean({ message: PaymentMessage.IS_ACTIVE_BOOLEAN })
@ApiProperty({ description: "specify that bank account is active", example: true })
isActive: boolean;
}
+17 -11
View File
@@ -2,7 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
import { IsEnum, IsInt, IsNotEmpty, IsUUID, IsUrl, Min } from "class-validator";
import { WalletMessage } from "../../../common/enums/message.enum";
import { GatewayEnum } from "../enums/gateway.enum";
import { TransferMethod } from "../enums/payment-type.enum";
export class DepositDto {
@IsNotEmpty({ message: WalletMessage.AMOUNT_REQUIRED })
@@ -13,24 +13,30 @@ export class DepositDto {
}
export class GatewayDepositDto extends DepositDto {
// @IsNotEmpty({ message: WalletMessage.GATEWAY_REQUIRED })
// @IsEnum(GatewayEnum)
// @ApiProperty({ description: "Gateway to use", example: "zarinpal" })
// gateway: GatewayEnum;
@IsNotEmpty({ message: WalletMessage.GATEWAY_REQUIRED })
@IsEnum(GatewayEnum)
@ApiProperty({ description: "Gateway to use", example: "zarinpal" })
gateway: GatewayEnum;
@IsUUID("4", { message: WalletMessage.GATEWAY_ID_SHOULD_BE_UUID })
@ApiProperty({ description: "Gateway id to pay", example: "e6fdce2a-9f91-47c4-8561-48368fc275b5" })
gatewayId: string;
}
export class CardToCardDepositDto extends DepositDto {
export class TransferDepositDto extends DepositDto {
@IsNotEmpty({ message: WalletMessage.RECEIPT_URL_REQUIRED })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: WalletMessage.RECEIPT_URL_SHOULD_BE_URL })
@ApiProperty({ description: "the " })
@ApiProperty({ description: "the transferReceipt url", example: "https://example.com/image.png" })
transferReceiptUrl: string;
@IsNotEmpty({ message: WalletMessage.BANK_ACCOUNT_ID_REQUIRED })
@IsUUID("4", { message: WalletMessage.BANK_ACCOUNT_ID_SHOULD_BE_UUID })
@ApiProperty({ description: "bank account id", example: "e6fdce2a-9f91-47c4-8561-48368fc275b5" })
bankAccountId: string;
}
// @IsNotEmpty({ message: WalletMessage.DEPOSIT_TYPE_REQUIRED })
// @IsEnum(DepositType)
// @ApiProperty({ description: "Deposit type", example: DepositType.GATEWAY })
// depositType: DepositType;
@IsNotEmpty({ message: WalletMessage.TRANSFER_METHOD_REQUIRED })
@IsEnum(TransferMethod)
@ApiProperty({ description: "transfer method", example: TransferMethod.CARD_TO_CARD, enum: TransferMethod })
method: TransferMethod;
}
@@ -0,0 +1,27 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsDateString, IsEnum, IsOptional, IsString } from "class-validator";
import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { PaymentStatus } from "../enums/payment-status.enum";
export class SearchTransactionQueryDto extends PaginationDto {
@IsOptional()
@IsString()
@ApiPropertyOptional({ description: "user name or family or ...", example: "mamad" })
user?: string;
@IsOptional()
@IsDateString()
@ApiPropertyOptional({ description: "the date string", example: "2024-01-25" })
date?: string;
@IsOptional()
@IsEnum(PaymentStatus)
@ApiPropertyOptional({ enum: PaymentStatus, description: "payment status", example: PaymentStatus.PENDING })
status?: PaymentStatus;
@IsOptional()
@IsString()
@ApiPropertyOptional({ description: "search quey", example: "harchi" })
q?: string;
}
@@ -0,0 +1,5 @@
import { PartialType } from "@nestjs/swagger";
import { CreateBankAccountDto } from "./create-bankaccount.dto";
export class UpdateBankAccountDto extends PartialType(CreateBankAccountDto) {}
@@ -1,14 +1,15 @@
import { Column, Entity } from "typeorm";
import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm";
import { DepositRequest } from "./deposit-request.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
@Entity()
export class BankAccount extends BaseEntity {
@Column({ type: "varchar", length: 100, unique: true, nullable: true })
cardNumber: string;
cardNumber?: string;
@Column({ type: "varchar", length: 100, unique: true, nullable: true })
shebaNumber: string;
IBan?: string;
@Column({ type: "varchar", length: 100, nullable: false })
bankName: string;
@@ -18,4 +19,10 @@ export class BankAccount extends BaseEntity {
@Column({ default: true })
isActive: boolean;
@OneToMany(() => DepositRequest, (depositRequest) => depositRequest.bankAccount)
depositRequests: DepositRequest[];
@DeleteDateColumn()
deletedAt: Date;
}
@@ -0,0 +1,17 @@
import { Column, Entity, ManyToOne } from "typeorm";
import { BankAccount } from "./bank-account.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
@Entity()
export class DepositRequest extends BaseEntity {
@ManyToOne(() => BankAccount, (bankAccount) => bankAccount.depositRequests, { nullable: false, onDelete: "RESTRICT" })
bankAccount: BankAccount;
@ManyToOne(() => User, { nullable: false, onDelete: "RESTRICT" })
user: User;
@Column({ type: "varchar", length: 250, nullable: false })
receiptUrl: string;
}
@@ -0,0 +1,22 @@
import { Column, Entity } from "typeorm";
import { BaseEntity } from "../../../common/entities/base.entity";
import { GatewayEnum } from "../enums/gateway.enum";
@Entity()
export class PaymentGateway extends BaseEntity {
@Column({ type: "enum", enum: GatewayEnum, nullable: false })
name: GatewayEnum;
@Column({ type: "varchar", length: 150, nullable: false })
nameFa: string;
@Column({ type: "varchar", length: 150, nullable: false })
logoUrl: string;
@Column({ type: "boolean", default: true })
isActive: boolean;
@Column({ type: "text", nullable: true })
description: string;
}
@@ -1,16 +0,0 @@
import { Column, Entity, ManyToOne } from "typeorm";
import { BankAccount } from "./bank-account.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
@Entity()
export class PaymentMethod extends BaseEntity {
@ManyToOne(() => BankAccount, { nullable: true, onDelete: "SET NULL" })
bankAccount?: BankAccount;
@Column({ type: "varchar", length: 150, nullable: true })
gatewayName?: string;
@Column({ type: "text", nullable: true })
description?: string;
}
@@ -2,7 +2,8 @@
import Decimal from "decimal.js";
import { Column, Entity, ManyToOne } from "typeorm";
import { PaymentMethod } from "./payment-method.entity";
import { BankAccount } from "./bank-account.entity";
import { PaymentGateway } from "./payment-gateway.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
import { User } from "../../users/entities/user.entity";
@@ -29,6 +30,9 @@ export class Payment extends BaseEntity {
@ManyToOne(() => User, (user) => user.payments, { nullable: false, onDelete: "RESTRICT" })
user: User;
@ManyToOne(() => PaymentMethod, { nullable: false })
paymentMethod: PaymentMethod;
@ManyToOne(() => PaymentGateway, { nullable: true, onDelete: "SET NULL" })
paymentGateway?: PaymentGateway;
@ManyToOne(() => BankAccount, { nullable: true, onDelete: "SET NULL" })
bankAccount?: BankAccount; // for CARD_TO_CARD or SHEBA transfers
}
@@ -3,3 +3,8 @@ export enum PaymentType {
GATEWAY = "GATEWAY",
SHEBA = "SHEBA",
}
export enum TransferMethod {
CARD_TO_CARD = "CARD_TO_CARD",
SHEBA = "SHEBA",
}
@@ -52,7 +52,6 @@ export class ZarinpalGateway implements IPaymentGateway {
}),
),
);
return {
redirectUrl: `${this.gatewayApiUrl}/StartPay/${data.data.authority}`,
message: data.data.message,
+67 -2
View File
@@ -1,12 +1,18 @@
import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common";
import { Body, Controller, Get, Param, Patch, Post, Query } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { GatewayDepositDto } from "./DTO/deposit-wallet.dto";
import { CreateBankAccountDto } from "./DTO/create-bankaccount.dto";
import { GatewayDepositDto, TransferDepositDto } from "./DTO/deposit-wallet.dto";
import { SearchTransactionQueryDto } from "./DTO/search-transaction-query.dto";
import { UpdateBankAccountDto } from "./DTO/update-bankaccount.dto";
import { VerifyParamDto, VerifyQueryDto } from "./DTO/verify-payment.dto";
import { PaymentsService } from "./providers/payments.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { Pagination } from "../../common/decorators/pagination.decorator";
import { Roles } from "../../common/decorators/roles.decorator";
import { UserDec } from "../../common/decorators/user.decorator";
import { ParamDto } from "../../common/DTO/param.dto";
import { Role } from "../users/entities/role.entity";
import { RoleEnum } from "../users/enums/role.enum";
@Controller("payments")
@@ -29,8 +35,67 @@ export class PaymentsController {
return this.paymentsService.chargeWalletWithGateway(chargeDto, userId);
}
@ApiOperation({ summary: "charge wallet with transfer ==> user route" })
@AuthGuards()
@Roles(RoleEnum.USER)
@Post("deposit/transfer")
chargeWalletWithTransfer(@Body() depositDto: TransferDepositDto, @UserDec("id") userId: string) {
return this.paymentsService.chargeWalletWithTransfer(depositDto, userId);
}
@ApiOperation({ summary: "get deposit request of users" })
@AuthGuards()
@Roles(RoleEnum.ADMIN)
@Get("deposit/transfer")
getDepositRequests() {}
@ApiOperation({ summary: "get transaction for admin" })
@AuthGuards()
@Pagination()
@Roles(RoleEnum.ADMIN)
@Get("transactions")
getTransaction(@Query() queryDto: SearchTransactionQueryDto) {
return this.paymentsService.getTransactions(queryDto);
}
@Get("verify/:gateway")
verifyPayment(@Param() paramDto: VerifyParamDto, @Query() queryDto: VerifyQueryDto) {
return this.paymentsService.verifyPayment(paramDto.gateway, queryDto);
}
///------------------- bank account -----------------------------
@AuthGuards()
@Roles(RoleEnum.ADMIN)
@ApiOperation({ summary: "add bank account for transfer and deposit ==> admin route" })
@Post("bank-account")
addBankAccount(@Body() createDto: CreateBankAccountDto) {
return this.paymentsService.addBankAccount(createDto);
}
@AuthGuards()
@Roles(RoleEnum.ADMIN, RoleEnum.USER)
@ApiOperation({ summary: "get bank account by id" })
@Get("bank-account/:id")
getBankAccountById(@Param() paramDto: ParamDto) {
return this.paymentsService.getBankAccountById(paramDto.id);
}
@AuthGuards()
@Roles(RoleEnum.ADMIN)
@ApiOperation({ summary: "update bank account with its id" })
@Patch("bank-account/:id")
updateBankAccount(@Body() updateDto: UpdateBankAccountDto, @Param() paramDto: ParamDto) {
return this.paymentsService.updateBankAccount(updateDto, paramDto.id);
}
@AuthGuards()
@Roles(RoleEnum.ADMIN, RoleEnum.USER)
@ApiOperation({ summary: "get bank account" })
@Get("bank-account")
getBankAccounts(@UserDec("role") userRole: Role) {
return this.paymentsService.getBankAccounts(userRole);
}
///------------------- Deposit transfer -----------------------------
}
+7 -2
View File
@@ -4,7 +4,6 @@ import { TypeOrmModule } from "@nestjs/typeorm";
import { PAYMENT, ZARINPAL_CONFIG } from "./constants";
import { BankAccount } from "./entities/bank-account.entity";
import { PaymentMethod } from "./entities/payment-method.entity";
import { Payment } from "./entities/payment.entity";
import { PaymentGatewayFactory } from "./factories/payment.factory";
import { ZarinpalGateway } from "./gateways/zarinpal.gateway";
@@ -15,10 +14,14 @@ import { BankAccountsRepository } from "./repositories/bank-accounts.repository"
import { PaymentsRepository } from "./repositories/payments.repository";
import { zarinpalConfig } from "../../configs/zarinpal.config";
import { WalletsModule } from "../wallets/wallets.module";
import { DepositRequest } from "./entities/deposit-request.entity";
import { PaymentGateway } from "./entities/payment-gateway.entity";
import { DepositRequestsRepository } from "./repositories/deposit-requests.repository";
import { PaymentGatewaysRepository } from "./repositories/payment-gateway.repository";
@Module({
imports: [
TypeOrmModule.forFeature([Payment, PaymentMethod, BankAccount]),
TypeOrmModule.forFeature([Payment, PaymentGateway, BankAccount, DepositRequest]),
BullModule.registerQueue({ name: PAYMENT.PAYMENT_QUEUE_NAME }),
WalletsModule,
],
@@ -29,6 +32,8 @@ import { WalletsModule } from "../wallets/wallets.module";
ZarinpalGateway,
PaymentsRepository,
BankAccountsRepository,
PaymentGatewaysRepository,
DepositRequestsRepository,
PaymentProcessor,
{
provide: ZARINPAL_CONFIG,
@@ -1,22 +1,32 @@
import { randomUUID } from "node:crypto";
import { InjectQueue } from "@nestjs/bullmq";
import { BadRequestException, HttpException, Injectable, InternalServerErrorException } from "@nestjs/common";
import { Queue } from "bullmq";
// eslint-disable-next-line import/no-named-as-default
import Decimal from "decimal.js";
import { DataSource, QueryRunner } from "typeorm";
import { DataSource, Not, QueryRunner } from "typeorm";
import { PaymentMessage, WalletMessage } from "../../../common/enums/message.enum";
import { CommonMessage, PaymentMessage, UserMessage, WalletMessage } from "../../../common/enums/message.enum";
import { Role } from "../../users/entities/role.entity";
import { User } from "../../users/entities/user.entity";
import { Wallet } from "../../wallets/entities/wallet.entity";
import { RoleEnum } from "../../users/enums/role.enum";
import { WalletsService } from "../../wallets/providers/wallets.service";
import { PAYMENT } from "../constants";
import { GatewayDepositDto } from "../DTO/deposit-wallet.dto";
import { CreateBankAccountDto } from "../DTO/create-bankaccount.dto";
import { GatewayDepositDto, TransferDepositDto } from "../DTO/deposit-wallet.dto";
import { SearchTransactionQueryDto } from "../DTO/search-transaction-query.dto";
import { UpdateBankAccountDto } from "../DTO/update-bankaccount.dto";
import { VerifyQueryDto } from "../DTO/verify-payment.dto";
import { PaymentMethod } from "../entities/payment-method.entity";
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 { PaymentStatus } from "../enums/payment-status.enum";
import { PaymentType } from "../enums/payment-type.enum";
import { PaymentGatewayFactory } from "../factories/payment.factory";
import { BankAccountsRepository } from "../repositories/bank-accounts.repository";
import { PaymentGatewaysRepository } from "../repositories/payment-gateway.repository";
import { PaymentsRepository } from "../repositories/payments.repository";
import { GatewayType } from "../types/gateway.type";
@@ -25,7 +35,9 @@ export class PaymentsService {
constructor(
@InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME) private readonly paymentQueue: Queue,
private readonly gatewayFactory: PaymentGatewayFactory,
private readonly paymentGatewaysRepository: PaymentGatewaysRepository,
private readonly paymentsRepository: PaymentsRepository,
private readonly bankAccountsRepository: BankAccountsRepository,
private readonly walletsService: WalletsService,
private dataSource: DataSource,
) {}
@@ -36,19 +48,17 @@ export class PaymentsService {
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const { amount, gateway } = chargeDto;
const wallet = await queryRunner.manager.findOne(Wallet, { where: { user: { id: userId } }, relations: ["user"] });
if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND);
const { amount, gatewayId } = chargeDto;
const gatewayData = await this.processPayment(
gateway,
amount,
WalletMessage.DEPOSIT_WALLET_IPG,
wallet.user.email,
wallet.user.phone,
);
const user = await queryRunner.manager.findOneBy(User, { id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
const payment = await this.createPaymentForUser(wallet.user, amount, gatewayData.reference, gateway, queryRunner);
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 {
@@ -63,6 +73,46 @@ export class PaymentsService {
await queryRunner.release();
}
}
//*********************************** */
//TODO:complete this
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);
//
const reference = randomUUID();
const paymentType = method as unknown as PaymentType;
//
const payment = queryRunner.manager.create(Payment, { amount, reference, user, type: paymentType, bankAccount });
await queryRunner.manager.save(Payment, payment);
//
const depositRequest = queryRunner.manager.create(DepositRequest, { bankAccount, user, receiptUrl: transferReceiptUrl });
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) {
@@ -85,11 +135,7 @@ export class PaymentsService {
// lock: { mode: "pessimistic_write" },
});
if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF);
if (payment.status === PaymentStatus.COMPLETED) {
return {
message: "completed before",
};
}
if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE);
if (queryDto.Status !== "OK") {
await this.handleFailedPayment(payment.id, queryRunner);
@@ -135,20 +181,14 @@ export class PaymentsService {
//*********************************** */
//*********************************** */
async createPaymentForUser(user: User, amount: number, reference: string, gateway: GatewayType, queryRunner: QueryRunner) {
const paymentMethod = queryRunner.manager.create(PaymentMethod, {
gatewayName: gateway,
description: WalletMessage.DEPOSIT_WALLET_IPG,
});
await queryRunner.manager.save(PaymentMethod, paymentMethod);
async createGatewayPaymentForUser(user: User, amount: number, reference: string, gatewayId: string, queryRunner: QueryRunner) {
//
const payment = queryRunner.manager.create(Payment, {
type: PaymentType.GATEWAY,
amount,
reference,
user,
paymentMethod,
paymentGateway: { id: gatewayId },
});
await queryRunner.manager.save(Payment, payment);
this.paymentQueue.add(PAYMENT.PAYMENT_START, { payment }, { delay: 1000 });
@@ -158,7 +198,8 @@ export class PaymentsService {
//*********************************** */
//*********************************** */
async getAvailableGateways() {
return this.gatewayFactory.getAvailablePaymentGateways();
const paymentGateways = await this.paymentGatewaysRepository.find({ where: { isActive: true } });
return { paymentGateways };
}
//*********************************** */
//*********************************** */
@@ -181,4 +222,69 @@ export class PaymentsService {
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(role: Role) {
const userRole = role as unknown as RoleEnum;
let bankAccounts: BankAccount[];
if (userRole === RoleEnum.USER) {
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);
}
}
@@ -0,0 +1,12 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { DepositRequest } from "../entities/deposit-request.entity";
@Injectable()
export class DepositRequestsRepository extends Repository<DepositRequest> {
constructor(@InjectRepository(DepositRequest) depositRequestsRepository: Repository<DepositRequest>) {
super(depositRequestsRepository.target, depositRequestsRepository.manager, depositRequestsRepository.queryRunner);
}
}
@@ -0,0 +1,12 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { PaymentGateway } from "../entities/payment-gateway.entity";
@Injectable()
export class PaymentGatewaysRepository extends Repository<PaymentGateway> {
constructor(@InjectRepository(PaymentGateway) paymentGatewaysRepository: Repository<PaymentGateway>) {
super(paymentGatewaysRepository.target, paymentGatewaysRepository.manager, paymentGatewaysRepository.queryRunner);
}
}
@@ -1,11 +0,0 @@
import { Injectable } from "@nestjs/common";
import { Repository } from "typeorm";
import { PaymentMethod } from "../entities/payment-method.entity";
@Injectable()
export class PaymentMethodsRepository extends Repository<PaymentMethod> {
constructor(paymentMethodsRepository: Repository<PaymentMethod>) {
super(paymentMethodsRepository.target, paymentMethodsRepository.manager, paymentMethodsRepository.queryRunner);
}
}
@@ -3,15 +3,23 @@ import { BadRequestException, Injectable } from "@nestjs/common";
import Decimal from "decimal.js";
import { QueryRunner } from "typeorm";
import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { WalletMessage } from "../../../common/enums/message.enum";
import { decimal } from "../../../common/transformers/decimal.transformer";
import { SearchTransactionQueryDto } from "../../payments/DTO/search-transaction-query.dto";
import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { WalletTransaction } from "../entities/transaction.entity";
import { Wallet } from "../entities/wallet.entity";
import { TransactionType } from "../enums/transaction-type.enum";
import { WalletTransactionsRepository } from "../repositories/wallet-transactions.repository";
import { WalletsRepository } from "../repositories/wallets.repository";
@Injectable()
export class WalletsService {
constructor(private readonly walletsRepository: WalletsRepository) {}
constructor(
private readonly walletsRepository: WalletsRepository,
private readonly walletsTransactionRepo: WalletTransactionsRepository,
) {}
//*********************************** */
async getBalance(userId: string) {
@@ -37,7 +45,7 @@ export class WalletsService {
const transaction = queryRunner.manager.create(WalletTransaction, {
amount,
wallet,
wallet: { id: wallet.id },
type: TransactionType.DEPOSIT,
description: WalletMessage.DEPOSIT_WALLET_IPG,
});
@@ -50,4 +58,87 @@ export class WalletsService {
return transaction;
}
//*********************************** */
async getTransaction(userId: string, paginationDto: PaginationDto) {
const { skip, limit } = PaginationUtils(paginationDto);
const [transactions, count] = await this.walletsTransactionRepo
.createQueryBuilder("transaction")
.leftJoinAndSelect("transaction.wallet", "wallet")
.where("wallet.userId = :userId", { userId })
.select([
"transaction.id",
"transaction.type",
"transaction.amount",
"transaction.description",
"transaction.numericId",
"transaction.createdAt",
"wallet.id",
])
.orderBy("transaction.createdAt", "DESC")
.skip(skip)
.take(limit)
.getManyAndCount();
const totals = await this.walletsTransactionRepo
.createQueryBuilder("transaction")
.leftJoin("transaction.wallet", "wallet")
.where("wallet.userId = :userId", { userId })
.select([
`COALESCE(SUM(CASE WHEN transaction.type = '${TransactionType.DEPOSIT}' THEN transaction.amount ELSE 0 END), 0) AS "totalDeposits"`,
`COALESCE(SUM(CASE WHEN transaction.type = '${TransactionType.WITHDRAWAL}' THEN transaction.amount ELSE 0 END), 0) AS "totalWithdrawals"`,
])
.getRawOne();
const wallet = await this.walletsRepository.findOneBy({ user: { id: userId } });
return {
balance: wallet?.balance,
transactions,
count,
totalDeposits: decimal(totals.totalDeposits) || 0,
totalWithdrawals: decimal(totals.totalWithdrawals) || 0,
paginate: true,
};
}
//*********************************** */
async getTransactionsForAdmin(queryDto: SearchTransactionQueryDto) {
const { limit, skip } = PaginationUtils(queryDto);
const queryBuilder = this.walletsTransactionRepo.createQueryBuilder("transaction");
queryBuilder
.leftJoinAndSelect("transaction.wallet", "wallet")
// .leftJoinAndSelect("transaction.payment", "payment")
.leftJoinAndSelect("wallet.user", "user");
if (queryDto.user) {
queryBuilder.andWhere("(user.firstName LIKE :user OR user.lastName LIKE :user OR user.email LIKE :user)", {
user: `%${queryDto.user}%`,
});
}
if (queryDto.date) {
queryBuilder.andWhere("transaction.createdAt >= :date", { date: new Date(queryDto.date) });
}
// if (queryDto.status) {
// queryBuilder.andWhere("payment.status = :status", { status: queryDto.status });
// }
if (queryDto.q) {
queryBuilder.andWhere("(transaction.description LIKE :q LIKE :q OR user.name LIKE :q OR user.family LIKE :q)", {
q: `%${queryDto.q}%`,
});
}
queryBuilder.take(limit).skip(skip);
queryBuilder.orderBy("transaction.createdAt", "DESC");
const [transactions, count] = await queryBuilder.getManyAndCount();
return { transactions, count, pagination: true };
}
}
+7 -2
View File
@@ -1,10 +1,12 @@
import { Controller, Get } from "@nestjs/common";
import { Controller, Get, Query } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { WalletsService } from "./providers/wallets.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { Pagination } from "../../common/decorators/pagination.decorator";
import { Roles } from "../../common/decorators/roles.decorator";
import { UserDec } from "../../common/decorators/user.decorator";
import { PaginationDto } from "../../common/DTO/pagination.dto";
import { RoleEnum } from "../users/enums/role.enum";
@Controller("wallets")
@@ -23,6 +25,9 @@ export class WalletsController {
@ApiOperation({ summary: "get wallet transactions" })
@AuthGuards()
@Roles(RoleEnum.USER)
@Pagination()
@Get("transactions")
getTransactions() {}
getTransactions(@UserDec("id") userId: string, @Query() paginationDto: PaginationDto) {
return this.walletsService.getTransaction(userId, paginationDto);
}
}