chore: complete the payment service
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# Stage 1: Build Stage
|
# Stage 1: Build Stage
|
||||||
FROM node:22-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
|
RUN npm install -g corepack@latest
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
# Install tzdata to support timezone settings
|
# Install tzdata to support timezone settings
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"cache-manager": "^6.3.2",
|
"cache-manager": "^6.3.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"decimal.js": "^10.5.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"fastify": "^5.2.1",
|
"fastify": "^5.2.1",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
|
|||||||
Generated
+8
@@ -86,6 +86,9 @@ importers:
|
|||||||
class-validator:
|
class-validator:
|
||||||
specifier: ^0.14.1
|
specifier: ^0.14.1
|
||||||
version: 0.14.1
|
version: 0.14.1
|
||||||
|
decimal.js:
|
||||||
|
specifier: ^10.5.0
|
||||||
|
version: 10.5.0
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.7
|
specifier: ^16.4.7
|
||||||
version: 16.4.7
|
version: 16.4.7
|
||||||
@@ -2277,6 +2280,9 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decimal.js@10.5.0:
|
||||||
|
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
|
||||||
|
|
||||||
dedent@1.5.3:
|
dedent@1.5.3:
|
||||||
resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==}
|
resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -8316,6 +8322,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decimal.js@10.5.0: {}
|
||||||
|
|
||||||
dedent@1.5.3: {}
|
dedent@1.5.3: {}
|
||||||
|
|
||||||
deep-extend@0.6.0:
|
deep-extend@0.6.0:
|
||||||
|
|||||||
+3
-1
@@ -1,13 +1,14 @@
|
|||||||
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
||||||
import { HttpModule } from "@nestjs/axios";
|
import { HttpModule } from "@nestjs/axios";
|
||||||
|
import { BullModule } from "@nestjs/bullmq";
|
||||||
import { CacheModule } from "@nestjs/cache-manager";
|
import { CacheModule } from "@nestjs/cache-manager";
|
||||||
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
import { ConfigModule } from "@nestjs/config";
|
||||||
import { ThrottlerModule } from "@nestjs/throttler";
|
import { ThrottlerModule } from "@nestjs/throttler";
|
||||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
|
||||||
|
import { bullMqConfig } from "./configs/bullmq.config";
|
||||||
import { cacheConfig } from "./configs/cache.config";
|
import { cacheConfig } from "./configs/cache.config";
|
||||||
// import { httpConfig } from "./configs/http.config";
|
|
||||||
import { rateLimitConfig } from "./configs/rateLimit.config";
|
import { rateLimitConfig } from "./configs/rateLimit.config";
|
||||||
import { databaseConfigs } from "./configs/typeorm.config";
|
import { databaseConfigs } from "./configs/typeorm.config";
|
||||||
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
||||||
@@ -25,6 +26,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module";
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
BullModule.forRootAsync(bullMqConfig()),
|
||||||
ThrottlerModule.forRootAsync(rateLimitConfig()),
|
ThrottlerModule.forRootAsync(rateLimitConfig()),
|
||||||
ConfigModule.forRoot({ cache: true, isGlobal: true }),
|
ConfigModule.forRoot({ cache: true, isGlobal: true }),
|
||||||
CacheModule.registerAsync(cacheConfig()),
|
CacheModule.registerAsync(cacheConfig()),
|
||||||
|
|||||||
@@ -2,7 +2,3 @@
|
|||||||
export const AUTH_THROTTLE_TTL = 5 * 60 * 1000;
|
export const AUTH_THROTTLE_TTL = 5 * 60 * 1000;
|
||||||
export const AUTH_THROTTLE_LIMIT = 5;
|
export const AUTH_THROTTLE_LIMIT = 5;
|
||||||
export const JWT_STRATEGY_NAME = "jwt_Strategy";
|
export const JWT_STRATEGY_NAME = "jwt_Strategy";
|
||||||
|
|
||||||
export const PAYMENT = Object.freeze({
|
|
||||||
PAYMENT_QUEUE: "payment",
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { InjectQueue } from "@nestjs/bullmq";
|
||||||
|
|
||||||
|
import { PAYMENT } from "../../modules/payments/constants";
|
||||||
|
|
||||||
|
export const InjectPaymentQueue = () => InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME);
|
||||||
@@ -202,6 +202,7 @@ export const enum PaymentMessage {
|
|||||||
PAYMENT_GATEWAY_NOT_FOUND = "درگاه پرداخت یافت نشد",
|
PAYMENT_GATEWAY_NOT_FOUND = "درگاه پرداخت یافت نشد",
|
||||||
ERROR_IN_PROCESS_PAYMENT = "خطا در پردازش پرداخت",
|
ERROR_IN_PROCESS_PAYMENT = "خطا در پردازش پرداخت",
|
||||||
ERROR_IN_VERIFY_PAYMENT = "خطا در تایید پرداخت",
|
ERROR_IN_VERIFY_PAYMENT = "خطا در تایید پرداخت",
|
||||||
|
PAYMENT_NOT_FOUND_WITH_REF = "پرداختی با این شناسه یافت نشد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum SettingMessageEnum {
|
export const enum SettingMessageEnum {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
|
|||||||
import { IsEnum, IsInt, IsNotEmpty, IsUUID, IsUrl, Min } from "class-validator";
|
import { IsEnum, IsInt, IsNotEmpty, IsUUID, IsUrl, Min } from "class-validator";
|
||||||
|
|
||||||
import { WalletMessage } from "../../../common/enums/message.enum";
|
import { WalletMessage } from "../../../common/enums/message.enum";
|
||||||
import { GatewayEnum } from "../../payments/enums/gateway.enum";
|
import { GatewayEnum } from "../enums/gateway.enum";
|
||||||
|
|
||||||
export class DepositDto {
|
export class DepositDto {
|
||||||
@IsNotEmpty({ message: WalletMessage.AMOUNT_REQUIRED })
|
@IsNotEmpty({ message: WalletMessage.AMOUNT_REQUIRED })
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1 +1,6 @@
|
|||||||
export const ZARINPAL_CONFIG = "ZARINPAL_CONFIG";
|
export const ZARINPAL_CONFIG = "ZARINPAL_CONFIG";
|
||||||
|
|
||||||
|
export const PAYMENT = Object.freeze({
|
||||||
|
PAYMENT_QUEUE_NAME: "payment",
|
||||||
|
PAYMENT_START: "payment_start",
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 { Column, Entity, ManyToOne } from "typeorm";
|
||||||
|
|
||||||
import { PaymentMethod } from "./payment-method.entity";
|
import { PaymentMethod } from "./payment-method.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
import { PaymentStatus } from "../enums/payment-status.enum";
|
import { PaymentStatus } from "../enums/payment-status.enum";
|
||||||
import { PaymentType } from "../enums/payment-type.enum";
|
import { PaymentType } from "../enums/payment-type.enum";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Payment extends BaseEntity {
|
export class Payment extends BaseEntity {
|
||||||
@Column({ type: "decimal", precision: 10, scale: 2 })
|
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
|
||||||
amount: number;
|
amount: Decimal;
|
||||||
|
|
||||||
@Column({ type: "varchar", length: 150, nullable: false })
|
@Column({ type: "varchar", length: 150, nullable: false })
|
||||||
reference: string;
|
reference: string;
|
||||||
|
|||||||
@@ -66,11 +66,14 @@ export class ZarinpalGateway implements IPaymentGateway {
|
|||||||
//********************************** */
|
//********************************** */
|
||||||
async verifyPayment(verifyParams: IPaymentVerifyParams) {
|
async verifyPayment(verifyParams: IPaymentVerifyParams) {
|
||||||
try {
|
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(
|
const { data } = await firstValueFrom(
|
||||||
this.httpService
|
this.httpService
|
||||||
.post<ZarinPalPGVerifyData>(`${this.config.gatewayApiUrl}/v4/payment/verify.json`, verifyData, { headers: this.requestHeader })
|
.post<ZarinPalPGVerifyData>(`${this.gatewayApiUrl}/v4/payment/verify.json`, verifyData, { headers: this.requestHeader })
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((err: AxiosError) => {
|
catchError((err: AxiosError) => {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
@@ -89,6 +92,7 @@ export class ZarinpalGateway implements IPaymentGateway {
|
|||||||
fee: data.data.fee,
|
fee: data.data.fee,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error({ error });
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
|
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
//------------------ process payment -------------------------
|
//------------------ process payment -------------------------
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-named-as-default
|
||||||
|
import Decimal from "decimal.js";
|
||||||
|
|
||||||
export interface IProcessPaymentParams {
|
export interface IProcessPaymentParams {
|
||||||
amount: number;
|
amount: number;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -22,8 +25,9 @@ export interface IPaymentVerifyData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IPaymentVerifyParams {
|
export interface IPaymentVerifyParams {
|
||||||
amount: number;
|
amount: Decimal;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
// status: GatewayPaymentStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------- zarinpal --------------------------------
|
//--------------------- zarinpal --------------------------------
|
||||||
|
|||||||
@@ -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 { 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 { PaymentsService } from "./providers/payments.service";
|
||||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
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")
|
@Controller("payments")
|
||||||
@ApiTags("Payments")
|
@ApiTags("Payments")
|
||||||
@@ -16,6 +21,16 @@ export class PaymentsController {
|
|||||||
return this.paymentsService.getAvailableGateways();
|
return this.paymentsService.getAvailableGateways();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("verify")
|
@ApiOperation({ summary: "Charge wallet ==> user route" })
|
||||||
verifyPayment() {}
|
@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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { BullModule } from "@nestjs/bullmq";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
|
||||||
import { ZARINPAL_CONFIG } from "./constants";
|
import { PAYMENT, ZARINPAL_CONFIG } from "./constants";
|
||||||
import { BankAccount } from "./entities/bank-account.entity";
|
import { BankAccount } from "./entities/bank-account.entity";
|
||||||
import { PaymentMethod } from "./entities/payment-method.entity";
|
import { PaymentMethod } from "./entities/payment-method.entity";
|
||||||
import { Payment } from "./entities/payment.entity";
|
import { Payment } from "./entities/payment.entity";
|
||||||
@@ -9,12 +10,18 @@ import { PaymentGatewayFactory } from "./factories/payment.factory";
|
|||||||
import { ZarinpalGateway } from "./gateways/zarinpal.gateway";
|
import { ZarinpalGateway } from "./gateways/zarinpal.gateway";
|
||||||
import { PaymentsController } from "./payments.controller";
|
import { PaymentsController } from "./payments.controller";
|
||||||
import { PaymentsService } from "./providers/payments.service";
|
import { PaymentsService } from "./providers/payments.service";
|
||||||
|
import { PaymentProcessor } from "./queue/payment.processor";
|
||||||
import { BankAccountsRepository } from "./repositories/bank-accounts.repository";
|
import { BankAccountsRepository } from "./repositories/bank-accounts.repository";
|
||||||
import { PaymentsRepository } from "./repositories/payments.repository";
|
import { PaymentsRepository } from "./repositories/payments.repository";
|
||||||
import { zarinpalConfig } from "../../configs/zarinpal.config";
|
import { zarinpalConfig } from "../../configs/zarinpal.config";
|
||||||
|
import { WalletsModule } from "../wallets/wallets.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Payment, PaymentMethod, BankAccount])],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Payment, PaymentMethod, BankAccount]),
|
||||||
|
BullModule.registerQueue({ name: PAYMENT.PAYMENT_QUEUE_NAME }),
|
||||||
|
WalletsModule,
|
||||||
|
],
|
||||||
controllers: [PaymentsController],
|
controllers: [PaymentsController],
|
||||||
providers: [
|
providers: [
|
||||||
PaymentsService,
|
PaymentsService,
|
||||||
@@ -22,6 +29,7 @@ import { zarinpalConfig } from "../../configs/zarinpal.config";
|
|||||||
ZarinpalGateway,
|
ZarinpalGateway,
|
||||||
PaymentsRepository,
|
PaymentsRepository,
|
||||||
BankAccountsRepository,
|
BankAccountsRepository,
|
||||||
|
PaymentProcessor,
|
||||||
{
|
{
|
||||||
provide: ZARINPAL_CONFIG,
|
provide: ZARINPAL_CONFIG,
|
||||||
useFactory: zarinpalConfig().useFactory,
|
useFactory: zarinpalConfig().useFactory,
|
||||||
|
|||||||
@@ -1,27 +1,138 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { InjectQueue } from "@nestjs/bullmq";
|
||||||
import { QueryRunner } from "typeorm";
|
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 { 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 { PaymentMethod } from "../entities/payment-method.entity";
|
||||||
import { Payment } from "../entities/payment.entity";
|
import { Payment } from "../entities/payment.entity";
|
||||||
|
import { PaymentStatus } from "../enums/payment-status.enum";
|
||||||
import { PaymentType } from "../enums/payment-type.enum";
|
import { PaymentType } from "../enums/payment-type.enum";
|
||||||
import { PaymentGatewayFactory } from "../factories/payment.factory";
|
import { PaymentGatewayFactory } from "../factories/payment.factory";
|
||||||
// import { PaymentsRepository } from "../repositories/payments.repository";
|
import { PaymentsRepository } from "../repositories/payments.repository";
|
||||||
import { GatewayType } from "../types/gateway.type";
|
import { GatewayType } from "../types/gateway.type";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaymentsService {
|
export class PaymentsService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME) private readonly paymentQueue: Queue,
|
||||||
private readonly gatewayFactory: PaymentGatewayFactory,
|
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) {
|
async processPayment(provider: GatewayType, amount: number, description: string, email?: string, mobile?: string) {
|
||||||
const paymentGateway = this.gatewayFactory.getPaymentGateway(provider);
|
const paymentGateway = this.gatewayFactory.getPaymentGateway(provider);
|
||||||
return paymentGateway.processPayment({ amount, description, email, mobile });
|
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) {
|
async createPaymentForUser(user: User, amount: number, reference: string, gateway: GatewayType, queryRunner: QueryRunner) {
|
||||||
@@ -39,8 +150,9 @@ export class PaymentsService {
|
|||||||
user,
|
user,
|
||||||
paymentMethod,
|
paymentMethod,
|
||||||
});
|
});
|
||||||
|
|
||||||
await queryRunner.manager.save(Payment, payment);
|
await queryRunner.manager.save(Payment, payment);
|
||||||
|
this.paymentQueue.add(PAYMENT.PAYMENT_START, { payment }, { delay: 1000 });
|
||||||
|
|
||||||
return payment;
|
return payment;
|
||||||
}
|
}
|
||||||
//*********************************** */
|
//*********************************** */
|
||||||
@@ -48,4 +160,25 @@ export class PaymentsService {
|
|||||||
async getAvailableGateways() {
|
async getAvailableGateways() {
|
||||||
return this.gatewayFactory.getAvailablePaymentGateways();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -2,10 +2,10 @@ import { Processor } from "@nestjs/bullmq";
|
|||||||
import { Logger } from "@nestjs/common";
|
import { Logger } from "@nestjs/common";
|
||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
|
|
||||||
import { PAYMENT } from "../../../common/constants";
|
|
||||||
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
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 {
|
export class PaymentProcessor extends WorkerProcessor {
|
||||||
protected readonly logger = new Logger(PaymentProcessor.name);
|
protected readonly logger = new Logger(PaymentProcessor.name);
|
||||||
|
|
||||||
@@ -9,4 +9,8 @@ export class PaymentsRepository extends Repository<Payment> {
|
|||||||
constructor(@InjectRepository(Payment) paymentsRepository: Repository<Payment>) {
|
constructor(@InjectRepository(Payment) paymentsRepository: Repository<Payment>) {
|
||||||
super(paymentsRepository.target, paymentsRepository.manager, paymentsRepository.queryRunner);
|
super(paymentsRepository.target, paymentsRepository.manager, paymentsRepository.queryRunner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOneWithReference(reference: string): Promise<Payment | null> {
|
||||||
|
return this.findOneBy({ reference });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export type GatewayPaymentStatus = "OK" | "NOK";
|
||||||
@@ -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 { Column, Entity, ManyToOne } from "typeorm";
|
||||||
|
|
||||||
import { Wallet } from "./wallet.entity";
|
import { Wallet } from "./wallet.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
||||||
import { TransactionType } from "../enums/transaction-type.enum";
|
import { TransactionType } from "../enums/transaction-type.enum";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class WalletTransaction extends BaseEntity {
|
export class WalletTransaction extends BaseEntity {
|
||||||
|
@Column({ type: "int", generated: "identity", insert: false })
|
||||||
|
numericId: number;
|
||||||
|
|
||||||
@ManyToOne(() => Wallet, (wallet) => wallet.transactions, { onDelete: "CASCADE", nullable: false })
|
@ManyToOne(() => Wallet, (wallet) => wallet.transactions, { onDelete: "CASCADE", nullable: false })
|
||||||
wallet: Wallet;
|
wallet: Wallet;
|
||||||
|
|
||||||
@Column({ type: "decimal", precision: 10, scale: 2, nullable: false })
|
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
|
||||||
amount: number;
|
amount: Decimal;
|
||||||
|
|
||||||
@Column({ type: "enum", enum: TransactionType, nullable: false })
|
@Column({ type: "enum", enum: TransactionType, nullable: false })
|
||||||
type: TransactionType;
|
type: TransactionType;
|
||||||
|
|||||||
@@ -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 { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm";
|
||||||
|
|
||||||
import { WalletTransaction } from "./transaction.entity";
|
import { WalletTransaction } from "./transaction.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@@ -10,8 +13,8 @@ export class Wallet extends BaseEntity {
|
|||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
user: User;
|
user: User;
|
||||||
|
|
||||||
@Column({ type: "decimal", precision: 10, scale: 2, default: 0 })
|
@Column({ type: "decimal", precision: 16, scale: 2, default: 0, transformer: new DecimalTransformer() })
|
||||||
balance: number;
|
balance: Decimal;
|
||||||
|
|
||||||
@OneToMany(() => WalletTransaction, (transaction) => transaction.wallet)
|
@OneToMany(() => WalletTransaction, (transaction) => transaction.wallet)
|
||||||
transactions: WalletTransaction[];
|
transactions: WalletTransaction[];
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { BadRequestException, HttpException, Injectable, InternalServerErrorException } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import { DataSource, QueryRunner } from "typeorm";
|
// 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 { WalletMessage } from "../../../common/enums/message.enum";
|
||||||
import { PaymentsService } from "../../payments/providers/payments.service";
|
import { WalletTransaction } from "../entities/transaction.entity";
|
||||||
import { GatewayDepositDto } from "../DTO/deposit-wallet.dto";
|
|
||||||
import { Wallet } from "../entities/wallet.entity";
|
import { Wallet } from "../entities/wallet.entity";
|
||||||
|
import { TransactionType } from "../enums/transaction-type.enum";
|
||||||
import { WalletsRepository } from "../repositories/wallets.repository";
|
import { WalletsRepository } from "../repositories/wallets.repository";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WalletsService {
|
export class WalletsService {
|
||||||
constructor(
|
constructor(private readonly walletsRepository: WalletsRepository) {}
|
||||||
private readonly walletsRepository: WalletsRepository,
|
|
||||||
private readonly paymentsService: PaymentsService,
|
|
||||||
private readonly dataSource: DataSource,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
//*********************************** */
|
//*********************************** */
|
||||||
async getBalance(userId: string) {
|
async getBalance(userId: string) {
|
||||||
@@ -24,41 +22,6 @@ export class WalletsService {
|
|||||||
balance: wallet.balance,
|
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) {
|
async createUserWallet(userId: string, queryRunner: QueryRunner) {
|
||||||
@@ -67,4 +30,24 @@ export class WalletsService {
|
|||||||
//
|
//
|
||||||
return wallet;
|
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;
|
||||||
|
}
|
||||||
|
//*********************************** */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { GatewayDepositDto } from "./DTO/deposit-wallet.dto";
|
|
||||||
import { WalletsService } from "./providers/wallets.service";
|
import { WalletsService } from "./providers/wallets.service";
|
||||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
import { Roles } from "../../common/decorators/roles.decorator";
|
import { Roles } from "../../common/decorators/roles.decorator";
|
||||||
@@ -21,11 +20,9 @@ export class WalletsController {
|
|||||||
return this.walletsService.getBalance(userId);
|
return this.walletsService.getBalance(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "Charge wallet ==> user route" })
|
@ApiOperation({ summary: "get wallet transactions" })
|
||||||
@AuthGuards()
|
@AuthGuards()
|
||||||
@Roles(RoleEnum.USER)
|
@Roles(RoleEnum.USER)
|
||||||
@Post("deposit/gateway")
|
@Get("transactions")
|
||||||
chargeWalletWithGateway(@Body() chargeDto: GatewayDepositDto, @UserDec("id") userId: string) {
|
getTransactions() {}
|
||||||
return this.walletsService.chargeWalletWithGateway(chargeDto, userId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import { WalletsService } from "./providers/wallets.service";
|
|||||||
import { WalletTransactionsRepository } from "./repositories/wallet-transactions.repository";
|
import { WalletTransactionsRepository } from "./repositories/wallet-transactions.repository";
|
||||||
import { WalletsRepository } from "./repositories/wallets.repository";
|
import { WalletsRepository } from "./repositories/wallets.repository";
|
||||||
import { WalletsController } from "./wallets.controller";
|
import { WalletsController } from "./wallets.controller";
|
||||||
import { PaymentsModule } from "../payments/payments.module";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Wallet, WalletTransaction]), PaymentsModule],
|
imports: [TypeOrmModule.forFeature([Wallet, WalletTransaction])],
|
||||||
providers: [WalletsService, WalletsRepository, WalletTransactionsRepository],
|
providers: [WalletsService, WalletsRepository, WalletTransactionsRepository],
|
||||||
controllers: [WalletsController],
|
controllers: [WalletsController],
|
||||||
exports: [WalletsService],
|
exports: [WalletsService],
|
||||||
|
|||||||
Reference in New Issue
Block a user