From 626206c3892c887471f0753fcec7dd97532f39ed Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Tue, 4 Feb 2025 16:12:33 +0330 Subject: [PATCH] chore: complete the payment service --- Dockerfile | 1 + package.json | 1 + pnpm-lock.yaml | 8 + src/app.module.ts | 4 +- src/common/constants/index.ts | 4 - .../decorators/inject-queue.decorator.ts | 5 + src/common/enums/message.enum.ts | 1 + .../transformers/decimal.transformer.ts | 16 ++ .../DTO/deposit-wallet.dto.ts | 2 +- .../payments/DTO/verify-payment.dto.ts | 19 +++ src/modules/payments/constants/index.ts | 5 + .../payments/entities/payment.entity.ts | 7 +- .../payments/gateways/zarinpal.gateway.ts | 10 +- src/modules/payments/interfaces/IPayment.ts | 6 +- src/modules/payments/payments.controller.ts | 21 ++- src/modules/payments/payments.module.ts | 12 +- .../payments/providers/payments.service.ts | 145 +++++++++++++++++- ...{payment.queue.ts => payment.processor.ts} | 4 +- .../repositories/payments.repository.ts | 4 + .../payments/types/payment-status.type.ts | 1 + .../wallets/entities/transaction.entity.ts | 10 +- src/modules/wallets/entities/wallet.entity.ts | 7 +- .../wallets/providers/wallets.service.ts | 71 ++++----- src/modules/wallets/wallets.controller.ts | 11 +- src/modules/wallets/wallets.module.ts | 3 +- 25 files changed, 296 insertions(+), 82 deletions(-) create mode 100644 src/common/decorators/inject-queue.decorator.ts create mode 100644 src/common/transformers/decimal.transformer.ts rename src/modules/{wallets => payments}/DTO/deposit-wallet.dto.ts (95%) create mode 100644 src/modules/payments/DTO/verify-payment.dto.ts rename src/modules/payments/queue/{payment.queue.ts => payment.processor.ts} (83%) create mode 100644 src/modules/payments/types/payment-status.type.ts diff --git a/Dockerfile b/Dockerfile index 0a8d236..bdce6d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ # Stage 1: Build Stage FROM node:22-alpine AS base +RUN npm install -g corepack@latest RUN corepack enable && corepack prepare pnpm@latest --activate # Install tzdata to support timezone settings diff --git a/package.json b/package.json index 37922c1..92fe201 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "cache-manager": "^6.3.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "decimal.js": "^10.5.0", "dotenv": "^16.4.7", "fastify": "^5.2.1", "handlebars": "^4.7.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eeb59aa..43ae138 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: class-validator: specifier: ^0.14.1 version: 0.14.1 + decimal.js: + specifier: ^10.5.0 + version: 10.5.0 dotenv: specifier: ^16.4.7 version: 16.4.7 @@ -2277,6 +2280,9 @@ packages: supports-color: optional: true + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -8316,6 +8322,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.5.0: {} + dedent@1.5.3: {} deep-extend@0.6.0: diff --git a/src/app.module.ts b/src/app.module.ts index f3f1456..8110764 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,13 +1,14 @@ import { FastifyMulterModule } from "@nest-lab/fastify-multer"; import { HttpModule } from "@nestjs/axios"; +import { BullModule } from "@nestjs/bullmq"; import { CacheModule } from "@nestjs/cache-manager"; import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { ThrottlerModule } from "@nestjs/throttler"; import { TypeOrmModule } from "@nestjs/typeorm"; +import { bullMqConfig } from "./configs/bullmq.config"; import { cacheConfig } from "./configs/cache.config"; -// import { httpConfig } from "./configs/http.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; import { databaseConfigs } from "./configs/typeorm.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; @@ -25,6 +26,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module"; @Module({ imports: [ + BullModule.forRootAsync(bullMqConfig()), ThrottlerModule.forRootAsync(rateLimitConfig()), ConfigModule.forRoot({ cache: true, isGlobal: true }), CacheModule.registerAsync(cacheConfig()), diff --git a/src/common/constants/index.ts b/src/common/constants/index.ts index 38a1149..8fb1921 100644 --- a/src/common/constants/index.ts +++ b/src/common/constants/index.ts @@ -2,7 +2,3 @@ export const AUTH_THROTTLE_TTL = 5 * 60 * 1000; export const AUTH_THROTTLE_LIMIT = 5; export const JWT_STRATEGY_NAME = "jwt_Strategy"; - -export const PAYMENT = Object.freeze({ - PAYMENT_QUEUE: "payment", -}); diff --git a/src/common/decorators/inject-queue.decorator.ts b/src/common/decorators/inject-queue.decorator.ts new file mode 100644 index 0000000..104c193 --- /dev/null +++ b/src/common/decorators/inject-queue.decorator.ts @@ -0,0 +1,5 @@ +import { InjectQueue } from "@nestjs/bullmq"; + +import { PAYMENT } from "../../modules/payments/constants"; + +export const InjectPaymentQueue = () => InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME); diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 310a48f..8efe13d 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -202,6 +202,7 @@ export const enum PaymentMessage { PAYMENT_GATEWAY_NOT_FOUND = "درگاه پرداخت یافت نشد", ERROR_IN_PROCESS_PAYMENT = "خطا در پردازش پرداخت", ERROR_IN_VERIFY_PAYMENT = "خطا در تایید پرداخت", + PAYMENT_NOT_FOUND_WITH_REF = "پرداختی با این شناسه یافت نشد", } export const enum SettingMessageEnum { diff --git a/src/common/transformers/decimal.transformer.ts b/src/common/transformers/decimal.transformer.ts new file mode 100644 index 0000000..7995d1a --- /dev/null +++ b/src/common/transformers/decimal.transformer.ts @@ -0,0 +1,16 @@ +// eslint-disable-next-line import/no-named-as-default +import Decimal from "decimal.js"; + +export class DecimalTransformer { + to(value: Decimal | string | number): string { + return new Decimal(value).toString(); + } + + from(value: string): number { + return new Decimal(value).toNumber(); + } +} + +export function decimal(value: Decimal | number | string): number { + return new Decimal(value).toNumber(); +} diff --git a/src/modules/wallets/DTO/deposit-wallet.dto.ts b/src/modules/payments/DTO/deposit-wallet.dto.ts similarity index 95% rename from src/modules/wallets/DTO/deposit-wallet.dto.ts rename to src/modules/payments/DTO/deposit-wallet.dto.ts index 515e30f..070a37b 100644 --- a/src/modules/wallets/DTO/deposit-wallet.dto.ts +++ b/src/modules/payments/DTO/deposit-wallet.dto.ts @@ -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 "../../payments/enums/gateway.enum"; +import { GatewayEnum } from "../enums/gateway.enum"; export class DepositDto { @IsNotEmpty({ message: WalletMessage.AMOUNT_REQUIRED }) diff --git a/src/modules/payments/DTO/verify-payment.dto.ts b/src/modules/payments/DTO/verify-payment.dto.ts new file mode 100644 index 0000000..e730b40 --- /dev/null +++ b/src/modules/payments/DTO/verify-payment.dto.ts @@ -0,0 +1,19 @@ +import { IsEnum, IsNotEmpty } from "class-validator"; + +import { GatewayEnum } from "../enums/gateway.enum"; +import { GatewayType } from "../types/gateway.type"; +import { GatewayPaymentStatus } from "../types/payment-status.type"; + +export class VerifyParamDto { + @IsNotEmpty() + @IsEnum(GatewayEnum) + gateway: GatewayType; +} + +export class VerifyQueryDto { + @IsNotEmpty() + Authority: string; + + @IsNotEmpty() + Status: GatewayPaymentStatus; +} diff --git a/src/modules/payments/constants/index.ts b/src/modules/payments/constants/index.ts index d79f558..c5b69f1 100644 --- a/src/modules/payments/constants/index.ts +++ b/src/modules/payments/constants/index.ts @@ -1 +1,6 @@ export const ZARINPAL_CONFIG = "ZARINPAL_CONFIG"; + +export const PAYMENT = Object.freeze({ + PAYMENT_QUEUE_NAME: "payment", + PAYMENT_START: "payment_start", +}); diff --git a/src/modules/payments/entities/payment.entity.ts b/src/modules/payments/entities/payment.entity.ts index 6b1be48..4bd7da0 100644 --- a/src/modules/payments/entities/payment.entity.ts +++ b/src/modules/payments/entities/payment.entity.ts @@ -1,15 +1,18 @@ +// eslint-disable-next-line import/no-named-as-default +import Decimal from "decimal.js"; import { Column, Entity, ManyToOne } from "typeorm"; import { PaymentMethod } from "./payment-method.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; +import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; import { User } from "../../users/entities/user.entity"; import { PaymentStatus } from "../enums/payment-status.enum"; import { PaymentType } from "../enums/payment-type.enum"; @Entity() export class Payment extends BaseEntity { - @Column({ type: "decimal", precision: 10, scale: 2 }) - amount: number; + @Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() }) + amount: Decimal; @Column({ type: "varchar", length: 150, nullable: false }) reference: string; diff --git a/src/modules/payments/gateways/zarinpal.gateway.ts b/src/modules/payments/gateways/zarinpal.gateway.ts index 03086cb..1db1743 100644 --- a/src/modules/payments/gateways/zarinpal.gateway.ts +++ b/src/modules/payments/gateways/zarinpal.gateway.ts @@ -66,11 +66,14 @@ export class ZarinpalGateway implements IPaymentGateway { //********************************** */ async verifyPayment(verifyParams: IPaymentVerifyParams) { try { - const verifyData = { authority: verifyParams.reference, amount: verifyParams.amount, merchant_id: this.config.merchantId }; - + const verifyData = { + authority: verifyParams.reference, + amount: verifyParams.amount, + merchant_id: this.config.merchantId, + }; const { data } = await firstValueFrom( this.httpService - .post(`${this.config.gatewayApiUrl}/v4/payment/verify.json`, verifyData, { headers: this.requestHeader }) + .post(`${this.gatewayApiUrl}/v4/payment/verify.json`, verifyData, { headers: this.requestHeader }) .pipe( catchError((err: AxiosError) => { this.logger.error(err); @@ -89,6 +92,7 @@ export class ZarinpalGateway implements IPaymentGateway { fee: data.data.fee, }; } catch (error) { + console.error({ error }); this.logger.error(error); throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT); } diff --git a/src/modules/payments/interfaces/IPayment.ts b/src/modules/payments/interfaces/IPayment.ts index d9e1276..c2e82c2 100644 --- a/src/modules/payments/interfaces/IPayment.ts +++ b/src/modules/payments/interfaces/IPayment.ts @@ -1,5 +1,8 @@ //------------------ process payment ------------------------- +// eslint-disable-next-line import/no-named-as-default +import Decimal from "decimal.js"; + export interface IProcessPaymentParams { amount: number; description: string; @@ -22,8 +25,9 @@ export interface IPaymentVerifyData { } export interface IPaymentVerifyParams { - amount: number; + amount: Decimal; reference: string; + // status: GatewayPaymentStatus; } //--------------------- zarinpal -------------------------------- diff --git a/src/modules/payments/payments.controller.ts b/src/modules/payments/payments.controller.ts index 799b11d..f54b318 100644 --- a/src/modules/payments/payments.controller.ts +++ b/src/modules/payments/payments.controller.ts @@ -1,8 +1,13 @@ -import { Controller, Get } from "@nestjs/common"; +import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { GatewayDepositDto } from "./DTO/deposit-wallet.dto"; +import { VerifyParamDto, VerifyQueryDto } from "./DTO/verify-payment.dto"; import { PaymentsService } from "./providers/payments.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { Roles } from "../../common/decorators/roles.decorator"; +import { UserDec } from "../../common/decorators/user.decorator"; +import { RoleEnum } from "../users/enums/role.enum"; @Controller("payments") @ApiTags("Payments") @@ -16,6 +21,16 @@ export class PaymentsController { return this.paymentsService.getAvailableGateways(); } - @Get("verify") - verifyPayment() {} + @ApiOperation({ summary: "Charge wallet ==> user route" }) + @AuthGuards() + @Roles(RoleEnum.USER) + @Post("deposit/gateway") + chargeWalletWithGateway(@Body() chargeDto: GatewayDepositDto, @UserDec("id") userId: string) { + return this.paymentsService.chargeWalletWithGateway(chargeDto, userId); + } + + @Get("verify/:gateway") + verifyPayment(@Param() paramDto: VerifyParamDto, @Query() queryDto: VerifyQueryDto) { + return this.paymentsService.verifyPayment(paramDto.gateway, queryDto); + } } diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts index 2d5fe2a..6df0aa0 100644 --- a/src/modules/payments/payments.module.ts +++ b/src/modules/payments/payments.module.ts @@ -1,7 +1,8 @@ +import { BullModule } from "@nestjs/bullmq"; import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; -import { ZARINPAL_CONFIG } from "./constants"; +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"; @@ -9,12 +10,18 @@ import { PaymentGatewayFactory } from "./factories/payment.factory"; import { ZarinpalGateway } from "./gateways/zarinpal.gateway"; import { PaymentsController } from "./payments.controller"; import { PaymentsService } from "./providers/payments.service"; +import { PaymentProcessor } from "./queue/payment.processor"; 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"; @Module({ - imports: [TypeOrmModule.forFeature([Payment, PaymentMethod, BankAccount])], + imports: [ + TypeOrmModule.forFeature([Payment, PaymentMethod, BankAccount]), + BullModule.registerQueue({ name: PAYMENT.PAYMENT_QUEUE_NAME }), + WalletsModule, + ], controllers: [PaymentsController], providers: [ PaymentsService, @@ -22,6 +29,7 @@ import { zarinpalConfig } from "../../configs/zarinpal.config"; ZarinpalGateway, PaymentsRepository, BankAccountsRepository, + PaymentProcessor, { provide: ZARINPAL_CONFIG, useFactory: zarinpalConfig().useFactory, diff --git a/src/modules/payments/providers/payments.service.ts b/src/modules/payments/providers/payments.service.ts index 6304626..d0ae9cd 100644 --- a/src/modules/payments/providers/payments.service.ts +++ b/src/modules/payments/providers/payments.service.ts @@ -1,27 +1,138 @@ -import { Injectable } from "@nestjs/common"; -import { QueryRunner } from "typeorm"; +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 { WalletMessage } from "../../../common/enums/message.enum"; +import { PaymentMessage, WalletMessage } from "../../../common/enums/message.enum"; import { User } from "../../users/entities/user.entity"; +import { Wallet } from "../../wallets/entities/wallet.entity"; +import { WalletsService } from "../../wallets/providers/wallets.service"; +import { PAYMENT } from "../constants"; +import { GatewayDepositDto } from "../DTO/deposit-wallet.dto"; +import { VerifyQueryDto } from "../DTO/verify-payment.dto"; import { PaymentMethod } from "../entities/payment-method.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 { PaymentsRepository } from "../repositories/payments.repository"; +import { PaymentsRepository } from "../repositories/payments.repository"; import { GatewayType } from "../types/gateway.type"; @Injectable() export class PaymentsService { constructor( + @InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME) private readonly paymentQueue: Queue, private readonly gatewayFactory: PaymentGatewayFactory, - // private readonly paymentsRepository: PaymentsRepository, + private readonly paymentsRepository: PaymentsRepository, + private readonly walletsService: WalletsService, + private dataSource: DataSource, ) {} + + //*********************************** */ + async chargeWalletWithGateway(chargeDto: GatewayDepositDto, userId: string) { + const queryRunner = this.dataSource.createQueryRunner(); + 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 gatewayData = await this.processPayment( + gateway, + amount, + WalletMessage.DEPOSIT_WALLET_IPG, + wallet.user.email, + wallet.user.phone, + ); + + const payment = await this.createPaymentForUser(wallet.user, amount, gatewayData.reference, gateway, queryRunner); + + await queryRunner.commitTransaction(); + return { + payment, + ...gatewayData, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + if (error instanceof HttpException) throw error; + throw new InternalServerErrorException(WalletMessage.ERROR_IN_CHARGE_WALLET); + } finally { + await queryRunner.release(); + } + } //*********************************** */ //*********************************** */ async 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) { + 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"], + // 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 (queryDto.Status !== "OK") { + await this.handleFailedPayment(payment.id, queryRunner); + await queryRunner.commitTransaction(); + return { message: "nok" }; + } + const verifyData = await paymentGateway.verifyPayment({ reference: queryDto.Authority, amount: payment.amount }); + // + if (verifyData.code === 100) { + const transaction = await this.handleSuccessfulPayment( + payment.id, + payment.user.id, + payment.amount, + `${verifyData.ref_id}`, + queryRunner, + ); + await queryRunner.commitTransaction(); + return { + message: "success", + transaction, + }; + } + + if (verifyData.code === 101) { + await queryRunner.commitTransaction(); + + return { + message: "not valid payment", + }; + } + + await this.handleFailedPayment(payment.id, queryRunner); + await queryRunner.commitTransaction(); + + return { message: "nok" }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + //*********************************** */ //*********************************** */ async createPaymentForUser(user: User, amount: number, reference: string, gateway: GatewayType, queryRunner: QueryRunner) { @@ -39,8 +150,9 @@ export class PaymentsService { user, paymentMethod, }); - await queryRunner.manager.save(Payment, payment); + this.paymentQueue.add(PAYMENT.PAYMENT_START, { payment }, { delay: 1000 }); + return payment; } //*********************************** */ @@ -48,4 +160,25 @@ export class PaymentsService { async getAvailableGateways() { return this.gatewayFactory.getAvailablePaymentGateways(); } + //*********************************** */ + //*********************************** */ + async getPaymentWithReference(reference: string) { + const payment = await this.paymentsRepository.findOneWithReference(reference); + if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF); + + return { payment }; + } + + //*********************************** */ + //*********************************** */ + async handleFailedPayment(paymentId: string, queryRunner: QueryRunner) { + await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.CANCELLED }); + } + + //*********************************** */ + async handleSuccessfulPayment(paymentId: string, userId: string, amount: Decimal, transactionId: string, queryRunner: QueryRunner) { + await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.COMPLETED, transactionId }); + const transaction = await this.walletsService.createPaymentTransaction(userId, amount, queryRunner); + return transaction; + } } diff --git a/src/modules/payments/queue/payment.queue.ts b/src/modules/payments/queue/payment.processor.ts similarity index 83% rename from src/modules/payments/queue/payment.queue.ts rename to src/modules/payments/queue/payment.processor.ts index 6d1056b..27fc2dc 100644 --- a/src/modules/payments/queue/payment.queue.ts +++ b/src/modules/payments/queue/payment.processor.ts @@ -2,10 +2,10 @@ import { Processor } from "@nestjs/bullmq"; import { Logger } from "@nestjs/common"; import { Job } from "bullmq"; -import { PAYMENT } from "../../../common/constants"; import { WorkerProcessor } from "../../../common/queues/worker.processor"; +import { PAYMENT } from "../constants"; -@Processor(PAYMENT.PAYMENT_QUEUE) +@Processor(PAYMENT.PAYMENT_QUEUE_NAME) export class PaymentProcessor extends WorkerProcessor { protected readonly logger = new Logger(PaymentProcessor.name); diff --git a/src/modules/payments/repositories/payments.repository.ts b/src/modules/payments/repositories/payments.repository.ts index 23d2954..2e2fcc6 100644 --- a/src/modules/payments/repositories/payments.repository.ts +++ b/src/modules/payments/repositories/payments.repository.ts @@ -9,4 +9,8 @@ export class PaymentsRepository extends Repository { constructor(@InjectRepository(Payment) paymentsRepository: Repository) { super(paymentsRepository.target, paymentsRepository.manager, paymentsRepository.queryRunner); } + + async findOneWithReference(reference: string): Promise { + return this.findOneBy({ reference }); + } } diff --git a/src/modules/payments/types/payment-status.type.ts b/src/modules/payments/types/payment-status.type.ts new file mode 100644 index 0000000..b9d2848 --- /dev/null +++ b/src/modules/payments/types/payment-status.type.ts @@ -0,0 +1 @@ +export type GatewayPaymentStatus = "OK" | "NOK"; diff --git a/src/modules/wallets/entities/transaction.entity.ts b/src/modules/wallets/entities/transaction.entity.ts index 532c260..fe42b8f 100644 --- a/src/modules/wallets/entities/transaction.entity.ts +++ b/src/modules/wallets/entities/transaction.entity.ts @@ -1,16 +1,22 @@ +// eslint-disable-next-line import/no-named-as-default +import Decimal from "decimal.js"; import { Column, Entity, ManyToOne } from "typeorm"; import { Wallet } from "./wallet.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; +import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; import { TransactionType } from "../enums/transaction-type.enum"; @Entity() export class WalletTransaction extends BaseEntity { + @Column({ type: "int", generated: "identity", insert: false }) + numericId: number; + @ManyToOne(() => Wallet, (wallet) => wallet.transactions, { onDelete: "CASCADE", nullable: false }) wallet: Wallet; - @Column({ type: "decimal", precision: 10, scale: 2, nullable: false }) - amount: number; + @Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() }) + amount: Decimal; @Column({ type: "enum", enum: TransactionType, nullable: false }) type: TransactionType; diff --git a/src/modules/wallets/entities/wallet.entity.ts b/src/modules/wallets/entities/wallet.entity.ts index f8d783b..ce840d2 100644 --- a/src/modules/wallets/entities/wallet.entity.ts +++ b/src/modules/wallets/entities/wallet.entity.ts @@ -1,7 +1,10 @@ +// eslint-disable-next-line import/no-named-as-default +import Decimal from "decimal.js"; import { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm"; import { WalletTransaction } from "./transaction.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; +import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; import { User } from "../../users/entities/user.entity"; @Entity() @@ -10,8 +13,8 @@ export class Wallet extends BaseEntity { @JoinColumn() user: User; - @Column({ type: "decimal", precision: 10, scale: 2, default: 0 }) - balance: number; + @Column({ type: "decimal", precision: 16, scale: 2, default: 0, transformer: new DecimalTransformer() }) + balance: Decimal; @OneToMany(() => WalletTransaction, (transaction) => transaction.wallet) transactions: WalletTransaction[]; diff --git a/src/modules/wallets/providers/wallets.service.ts b/src/modules/wallets/providers/wallets.service.ts index 60b078b..e51f4d8 100644 --- a/src/modules/wallets/providers/wallets.service.ts +++ b/src/modules/wallets/providers/wallets.service.ts @@ -1,19 +1,17 @@ -import { BadRequestException, HttpException, Injectable, InternalServerErrorException } from "@nestjs/common"; -import { DataSource, QueryRunner } from "typeorm"; +import { BadRequestException, Injectable } from "@nestjs/common"; +// eslint-disable-next-line import/no-named-as-default +import Decimal from "decimal.js"; +import { QueryRunner } from "typeorm"; import { WalletMessage } from "../../../common/enums/message.enum"; -import { PaymentsService } from "../../payments/providers/payments.service"; -import { GatewayDepositDto } from "../DTO/deposit-wallet.dto"; +import { WalletTransaction } from "../entities/transaction.entity"; import { Wallet } from "../entities/wallet.entity"; +import { TransactionType } from "../enums/transaction-type.enum"; import { WalletsRepository } from "../repositories/wallets.repository"; @Injectable() export class WalletsService { - constructor( - private readonly walletsRepository: WalletsRepository, - private readonly paymentsService: PaymentsService, - private readonly dataSource: DataSource, - ) {} + constructor(private readonly walletsRepository: WalletsRepository) {} //*********************************** */ async getBalance(userId: string) { @@ -24,41 +22,6 @@ export class WalletsService { balance: wallet.balance, }; } - //*********************************** */ - async chargeWalletWithGateway(chargeDto: GatewayDepositDto, userId: string) { - const queryRunner = this.dataSource.createQueryRunner(); - 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 gatewayData = await this.paymentsService.processPayment( - gateway, - amount, - WalletMessage.DEPOSIT_WALLET_IPG, - wallet.user.email, - wallet.user.phone, - ); - - console.log(gatewayData); - - const payment = await this.paymentsService.createPaymentForUser(wallet.user, amount, gatewayData.reference, gateway, queryRunner); - - await queryRunner.commitTransaction(); - return { - payment, - ...gatewayData, - }; - } catch (error) { - await queryRunner.rollbackTransaction(); - if (error instanceof HttpException) throw error; - throw new InternalServerErrorException(WalletMessage.ERROR_IN_CHARGE_WALLET); - } finally { - await queryRunner.release(); - } - } //*********************************** */ async createUserWallet(userId: string, queryRunner: QueryRunner) { @@ -67,4 +30,24 @@ export class WalletsService { // return wallet; } + //*********************************** */ + async createPaymentTransaction(userId: string, amount: Decimal, queryRunner: QueryRunner) { + const wallet = await queryRunner.manager.findOne(Wallet, { where: { user: { id: userId } }, lock: { mode: "pessimistic_write" } }); + if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND); + + const transaction = queryRunner.manager.create(WalletTransaction, { + amount, + wallet, + type: TransactionType.DEPOSIT, + description: WalletMessage.DEPOSIT_WALLET_IPG, + }); + + wallet.balance = new Decimal(wallet.balance).add(transaction.amount); + + await queryRunner.manager.save(Wallet, wallet); + await queryRunner.manager.save(WalletTransaction, transaction); + + return transaction; + } + //*********************************** */ } diff --git a/src/modules/wallets/wallets.controller.ts b/src/modules/wallets/wallets.controller.ts index b42901e..14cda82 100644 --- a/src/modules/wallets/wallets.controller.ts +++ b/src/modules/wallets/wallets.controller.ts @@ -1,7 +1,6 @@ -import { Body, Controller, Get, Post } from "@nestjs/common"; +import { Controller, Get } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; -import { GatewayDepositDto } from "./DTO/deposit-wallet.dto"; import { WalletsService } from "./providers/wallets.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { Roles } from "../../common/decorators/roles.decorator"; @@ -21,11 +20,9 @@ export class WalletsController { return this.walletsService.getBalance(userId); } - @ApiOperation({ summary: "Charge wallet ==> user route" }) + @ApiOperation({ summary: "get wallet transactions" }) @AuthGuards() @Roles(RoleEnum.USER) - @Post("deposit/gateway") - chargeWalletWithGateway(@Body() chargeDto: GatewayDepositDto, @UserDec("id") userId: string) { - return this.walletsService.chargeWalletWithGateway(chargeDto, userId); - } + @Get("transactions") + getTransactions() {} } diff --git a/src/modules/wallets/wallets.module.ts b/src/modules/wallets/wallets.module.ts index 98466b0..ea7892c 100644 --- a/src/modules/wallets/wallets.module.ts +++ b/src/modules/wallets/wallets.module.ts @@ -7,10 +7,9 @@ import { WalletsService } from "./providers/wallets.service"; import { WalletTransactionsRepository } from "./repositories/wallet-transactions.repository"; import { WalletsRepository } from "./repositories/wallets.repository"; import { WalletsController } from "./wallets.controller"; -import { PaymentsModule } from "../payments/payments.module"; @Module({ - imports: [TypeOrmModule.forFeature([Wallet, WalletTransaction]), PaymentsModule], + imports: [TypeOrmModule.forFeature([Wallet, WalletTransaction])], providers: [WalletsService, WalletsRepository, WalletTransactionsRepository], controllers: [WalletsController], exports: [WalletsService],