chore: complete the payment service
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+8
@@ -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:
|
||||
|
||||
+3
-1
@@ -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()),
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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 = "درگاه پرداخت یافت نشد",
|
||||
ERROR_IN_PROCESS_PAYMENT = "خطا در پردازش پرداخت",
|
||||
ERROR_IN_VERIFY_PAYMENT = "خطا در تایید پرداخت",
|
||||
PAYMENT_NOT_FOUND_WITH_REF = "پرداختی با این شناسه یافت نشد",
|
||||
}
|
||||
|
||||
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 { 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 })
|
||||
@@ -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 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 { 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;
|
||||
|
||||
@@ -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<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(
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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 --------------------------------
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
@@ -9,4 +9,8 @@ export class PaymentsRepository extends Repository<Payment> {
|
||||
constructor(@InjectRepository(Payment) paymentsRepository: Repository<Payment>) {
|
||||
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 { 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;
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
//*********************************** */
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user