chore: normalize the payment and payment transaction
This commit is contained in:
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 -----------------------------
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user