feat: add payment module and factory

This commit is contained in:
mahyargdz
2025-02-03 15:41:53 +03:30
parent 8fb587f976
commit 31368610dd
48 changed files with 931 additions and 81 deletions
+7
View File
@@ -1,4 +1,5 @@
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
import { HttpModule } from "@nestjs/axios";
import { CacheModule } from "@nestjs/cache-manager";
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
@@ -6,6 +7,7 @@ import { ThrottlerModule } from "@nestjs/throttler";
import { TypeOrmModule } from "@nestjs/typeorm";
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";
@@ -14,10 +16,12 @@ import { AuthModule } from "./modules/auth/auth.module";
import { ContactUsModule } from "./modules/contact-us/contact-us.module";
import { CriticismModule } from "./modules/criticisms/criticisms.module";
import { DanakServicesModule } from "./modules/danak-services/danak-services.module";
import { PaymentsModule } from "./modules/payments/payments.module";
import { SettingModule } from "./modules/settings/settings.module";
import { TicketsModule } from "./modules/tickets/tickets.module";
import { UploaderModule } from "./modules/uploader/uploader.module";
import { UsersModule } from "./modules/users/users.module";
import { WalletsModule } from "./modules/wallets/wallets.module";
@Module({
imports: [
@@ -25,6 +29,7 @@ import { UsersModule } from "./modules/users/users.module";
ConfigModule.forRoot({ cache: true, isGlobal: true }),
CacheModule.registerAsync(cacheConfig()),
TypeOrmModule.forRootAsync(databaseConfigs()),
HttpModule.register({ global: true }),
FastifyMulterModule,
UsersModule,
TicketsModule,
@@ -35,6 +40,8 @@ import { UsersModule } from "./modules/users/users.module";
CriticismModule,
SettingModule,
ContactUsModule,
WalletsModule,
PaymentsModule,
],
controllers: [],
providers: [],
+9 -2
View File
@@ -9,7 +9,14 @@ declare module "fastify" {
}
}
export const UserDec = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
export const UserDec = createParamDecorator((data: keyof Omit<User, "password"> | undefined, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest<FastifyRequest>();
return req.user;
const user = req.user;
return data ? user?.[data] : user;
});
// export const UserDec = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
// const req = ctx.switchToHttp().getRequest<FastifyRequest>();
// return req.user;
// });
+23
View File
@@ -179,6 +179,29 @@ export const enum TicketMessageEnum {
MESSAGE_CREATED = "پیام تیکت با موفقیت ایجاد شد",
TICKET_NOT_FOUND = "تیکت مورد نظر یافت نشد",
CLOSED = "تیکت با موفقیت بسته شد",
TICKET_CLOSED = "تیکت قبلا بسته شده است",
ERROR_IN_TICKET_MSG_CREATION = "خطا در ایجاد پیام تیکت",
}
export const enum WalletMessage {
WALLET_NOT_FOUND = "کیف پول یافت نشد",
AMOUNT_MUST_BE_INTEGER = "مبلغ شارژ باید یک عدد صحیح باشد",
AMOUNT_REQUIRED = "مبلغ شارژ مورد نیاز است",
DEPOSIT_TYPE_REQUIRED = "نوع شارژ مورد نیاز است",
AMOUNT_MINIMUM = "حداقل مبلغ شارژ ۱۰۰,۰۰۰ تومان است",
GATEWAY_REQUIRED = "درگاه پرداخت مورد نیاز است",
RECEIPT_URL_SHOULD_BE_URL = "آدرس فیش باید یک آدرس یو آر ال باشد",
RECEIPT_URL_REQUIRED = "آدرس فیش مورد نیاز است",
BANK_ACCOUNT_ID_REQUIRED = "شناسه حساب بانکی مورد نیاز است",
BANK_ACCOUNT_ID_SHOULD_BE_UUID = "شناسه حساب بانکی باید یک UUID معتبر باشد",
ERROR_IN_CHARGE_WALLET = "خطا در شارژ کیف پول",
DEPOSIT_WALLET_IPG = "شارژ کیف پول از طریق درگاه",
}
export const enum PaymentMessage {
PAYMENT_GATEWAY_NOT_FOUND = "درگاه پرداخت یافت نشد",
ERROR_IN_PROCESS_PAYMENT = "خطا در پردازش پرداخت",
ERROR_IN_VERIFY_PAYMENT = "خطا در تایید پرداخت",
}
export const enum SettingMessageEnum {
+20
View File
@@ -0,0 +1,20 @@
import { ConfigService } from "@nestjs/config";
export function zarinpalConfig() {
return {
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
merchantId: configService.getOrThrow<string>("ZARINPAL_MERCHANT_ID"),
gatewayApiUrl: configService.getOrThrow<string>("ZARINPAL_API_URL"),
callBackUrl: configService.getOrThrow<string>("CALLBACK_URL"),
ipgType: configService.getOrThrow<string>("IPG_TYPE"),
}),
};
}
export interface IZarinpalConfig {
merchantId: string;
gatewayApiUrl: string;
callBackUrl: string;
ipgType: string;
}
@@ -14,6 +14,6 @@ import { UserAnnouncementRepository } from "./repositories/user-announcement.rep
imports: [TypeOrmModule.forFeature([Announcement, UserAnnouncement]), DanakServicesModule, UsersModule],
providers: [AnnouncementService, AnnouncementRepository, UserAnnouncementRepository],
controllers: [AnnouncementController],
exports: [TypeOrmModule],
exports: [AnnouncementService],
})
export class AnnouncementsModule {}
+12 -1
View File
@@ -22,6 +22,7 @@ export class AuthService {
private readonly tokensService: TokensService,
) {}
//****************** */
//****************** */
async initiateRegistration(requestOtpDto: RequestOtpDto) {
const { phone } = requestOtpDto;
const existUser = await this.usersService.findOneWithPhone(phone);
@@ -44,6 +45,7 @@ export class AuthService {
};
}
//****************** */
//****************** */
async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) {
const { phone, code } = completeRegistrationDto;
const isValid = await this.otpService.verifyOtp(phone, code, "REGISTER");
@@ -62,7 +64,7 @@ export class AuthService {
};
}
//****************** */
//****************** */
async loginWithPassword(loginDto: LoginPasswordDTO) {
const { email, password } = loginDto;
@@ -76,6 +78,7 @@ export class AuthService {
};
}
//****************** */
//****************** */
async adminLoginWithPassword(loginDto: LoginPasswordDTO) {
const { email, password } = loginDto;
@@ -92,6 +95,8 @@ export class AuthService {
}
//****************** */
//****************** */
async checkUserExist(checkUserExistDto: CheckUserExistDto) {
const user = await this.usersService.findOneWithEmail(checkUserExistDto.email);
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
@@ -99,6 +104,8 @@ export class AuthService {
}
//****************** */
//****************** */
async requestLoginOtp(requestOtpDto: RequestOtpDto, isAdmin: boolean = false) {
const { phone } = requestOtpDto;
const user = await this.usersService.findOneWithPhone(phone);
@@ -126,6 +133,8 @@ export class AuthService {
}
//****************** */
//****************** */
async verifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
const { code, phone } = verifyOtpDto;
@@ -139,6 +148,8 @@ export class AuthService {
}
//****************** */
//****************** */
async adminVerifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
const { code, phone } = verifyOtpDto;
+1 -1
View File
@@ -13,6 +13,6 @@ import { UsersModule } from "../users/users.module";
imports: [TypeOrmModule.forFeature([Criticism, CriticismFile]), UsersModule],
providers: [CriticismsService, CriticismRepository, CriticismFileRepository],
controllers: [CriticismController],
exports: [],
exports: [CriticismsService],
})
export class CriticismModule {}
@@ -14,6 +14,6 @@ import { DanakServicesRepository } from "./repositories/danak-services.repositor
imports: [TypeOrmModule.forFeature([DanakService, DanakServiceImage, DanakServiceCategory])],
providers: [DanakServicesService, DanakServicesRepository, DanakServicesCategoryRepository, DanakServicesImageRepository],
controllers: [DanakServicesController],
exports: [DanakServicesService, TypeOrmModule],
exports: [DanakServicesService],
})
export class DanakServicesModule {}
+1
View File
@@ -0,0 +1 @@
export const ZARINPAL_CONFIG = "ZARINPAL_CONFIG";
@@ -0,0 +1,21 @@
import { Column, Entity } from "typeorm";
import { BaseEntity } from "../../../common/entities/base.entity";
@Entity()
export class BankAccount extends BaseEntity {
@Column({ type: "varchar", length: 100, unique: true, nullable: true })
cardNumber: string;
@Column({ type: "varchar", length: 100, unique: true, nullable: true })
shebaNumber: string;
@Column({ type: "varchar", length: 100, nullable: false })
bankName: string;
@Column({ type: "varchar", length: 150, nullable: false })
accountOwnerName: string;
@Column({ default: true })
isActive: boolean;
}
@@ -0,0 +1,28 @@
import { Column, Entity, ManyToOne } from "typeorm";
import { BankAccount } from "./bank-account.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
export enum PaymentMethodType {
CARD_TO_CARD = "CARD_TO_CARD",
GATEWAY = "GATEWAY",
SHEBA = "SHEBA",
}
@Entity()
export class PaymentMethod extends BaseEntity {
@Column({ type: "enum", enum: PaymentMethodType, default: PaymentMethodType.GATEWAY })
type: PaymentMethodType;
@ManyToOne(() => BankAccount, { nullable: true, onDelete: "SET NULL" })
bankAccount?: BankAccount;
@Column({ type: "varchar", length: 150, nullable: true })
gatewayName?: string;
@Column({ type: "text", nullable: true })
description?: string;
@Column({ type: "boolean", default: true })
isActive: boolean;
}
@@ -0,0 +1,27 @@
import { Column, Entity, ManyToOne } from "typeorm";
import { PaymentMethod } from "./payment-method.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
import { PaymentStatus } from "../enums/payment-status.enum";
@Entity()
export class Payment extends BaseEntity {
@Column({ type: "decimal", precision: 10, scale: 2 })
amount: number;
@Column({ type: "varchar", length: 150, nullable: false })
reference: string;
@Column({ type: "varchar", length: 150, nullable: true })
transactionId: string;
@Column({ type: "enum", enum: PaymentStatus, default: PaymentStatus.PENDING })
status: PaymentStatus;
@ManyToOne(() => User, (user) => user.payments, { nullable: false, onDelete: "RESTRICT" })
user: User;
@ManyToOne(() => PaymentMethod, { nullable: false })
paymentMethod: PaymentMethod;
}
@@ -0,0 +1,3 @@
export enum GatewayEnum {
ZARINPAL = "zarinpal",
}
@@ -0,0 +1,6 @@
export enum PaymentStatus {
PENDING = "PENDING",
COMPLETED = "COMPLETED",
FAILED = "FAILED",
CANCELLED = "CANCELLED",
}
@@ -0,0 +1,28 @@
import { BadGatewayException, Injectable } from "@nestjs/common";
import { PaymentMessage } from "../../../common/enums/message.enum";
import { GatewayEnum } from "../enums/gateway.enum";
import { ZarinpalGateway } from "../gateways/zarinpal.gateway";
import { GatewayType } from "../types/gateway.type";
@Injectable()
export class PaymentGatewayFactory {
constructor(private readonly ZarinpalGateway: ZarinpalGateway) {}
//************************ *
getPaymentGateway(provider: GatewayType) {
switch (provider) {
case "zarinpal":
return this.ZarinpalGateway;
default:
throw new BadGatewayException(PaymentMessage.PAYMENT_GATEWAY_NOT_FOUND);
}
}
//************************ */
getAvailablePaymentGateways() {
const gateways = [{ name: GatewayEnum.ZARINPAL }];
return { gateways };
}
}
@@ -0,0 +1,96 @@
import { HttpService } from "@nestjs/axios";
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
import { AxiosError } from "axios";
import { catchError, firstValueFrom } from "rxjs";
import { PaymentMessage } from "../../../common/enums/message.enum";
import { IZarinpalConfig } from "../../../configs/zarinpal.config";
import { ZARINPAL_CONFIG } from "../constants";
import { GatewayEnum } from "../enums/gateway.enum";
import {
IPaymentGateway,
IPaymentVerifyParams,
IProcessPaymentParams,
ZarinPalPGNewArgs,
ZarinPalPGNewRequestData,
ZarinPalPGVerifyData,
} from "../interfaces/IPayment";
@Injectable()
export class ZarinpalGateway implements IPaymentGateway {
private readonly logger = new Logger(ZarinpalGateway.name);
private readonly gatewayApiUrl: string;
private readonly requestHeader: Record<string, string> = { "Content-Type": "application/json", Accept: "application/json" };
constructor(
@Inject(ZARINPAL_CONFIG) private readonly config: IZarinpalConfig,
private readonly httpService: HttpService,
) {
this.gatewayApiUrl = `https://${this.config.ipgType}.${this.config.gatewayApiUrl}`;
}
//*************************************** */
async processPayment(processParams: IProcessPaymentParams) {
try {
const purchaseData: ZarinPalPGNewArgs = {
merchant_id: this.config.merchantId,
amount: processParams.amount,
callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`,
description: processParams.description,
currency: "IRT",
metadata: { email: processParams.email, mobile: processParams.mobile },
};
const { data } = await firstValueFrom(
this.httpService
.post<ZarinPalPGNewRequestData>(`${this.gatewayApiUrl}/v4/payment/request.json`, purchaseData, {
headers: this.requestHeader,
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error(err);
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
}),
),
);
return {
redirectUrl: `${this.gatewayApiUrl}/StartPay/${data.data.authority}`,
message: data.data.message,
reference: data.data.authority,
};
} catch (error) {
this.logger.error(error);
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
}
}
//********************************** */
async verifyPayment(verifyParams: IPaymentVerifyParams) {
try {
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 })
.pipe(
catchError((err: AxiosError) => {
this.logger.error(err);
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
}),
),
);
return {
code: data.data.code,
message: data.data.message,
card_hash: data.data.card_hash,
card_pan: data.data.card_pan,
ref_id: data.data.ref_id,
fee_type: data.data.fee_type,
fee: data.data.fee,
};
} catch (error) {
this.logger.error(error);
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
}
}
}
@@ -0,0 +1,84 @@
//------------------ process payment -------------------------
export interface IProcessPaymentParams {
amount: number;
description: string;
// callBackPath: string;
email?: string;
mobile?: string;
}
export interface IProcessPaymentData {
redirectUrl: string;
message: string;
reference: string;
}
//------------------ Verify payment -------------------------
//TODO: Define the IPaymentVerifyData interface
export interface IPaymentVerifyData {
[x: string]: unknown;
}
export interface IPaymentVerifyParams {
amount: number;
reference: string;
}
//--------------------- zarinpal --------------------------------
interface MetaData {
mobile?: string;
email?: string;
order_id?: string;
}
export interface ZarinPalPGNewArgs {
merchant_id: string;
amount: number;
description: string;
callback_url: string;
metadata?: MetaData;
currency?: "IRR" | "IRT";
}
interface IZarinPalPGRequestData {
code: number;
message: string;
authority: string;
fee_type: string;
fee: number;
}
export interface ZarinPalPGNewRequestData {
data: IZarinPalPGRequestData;
error: string[];
}
interface IZarinpalPaymentVerifyData {
code: number;
message: string;
card_hash: string;
card_pan: string;
ref_id: number;
fee_type: string;
fee: number;
}
export interface ZarinPalPGVerifyData {
data: IZarinpalPaymentVerifyData;
error: string[];
}
//********************************************** */
//------------------abstract class----------------
export interface IPaymentGateway {
processPayment(processPaymentParam: IProcessPaymentParams): Promise<IProcessPaymentData>;
verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise<IPaymentVerifyData>;
}
// export abstract class PaymentGateway {
// abstract processPayment(processPaymentParam: IProcessPaymentParams): Promise<IProcessPaymentData>;
// abstract verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise<IPaymentVerifyData>;
// }
@@ -0,0 +1,18 @@
import { Controller, Get } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { PaymentsService } from "./providers/payments.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
@Controller("payments")
@ApiTags("Payments")
export class PaymentsController {
constructor(private readonly paymentsService: PaymentsService) {}
@AuthGuards()
@ApiOperation({ summary: "get all available gateways" })
@Get("gateways")
getAvailableGateways() {
return this.paymentsService.getAvailableGateways();
}
}
+33
View File
@@ -0,0 +1,33 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { ZARINPAL_CONFIG } from "./constants";
import { BankAccount } from "./entities/bank-account.entity";
import { PaymentMethod } from "./entities/payment-method.entity";
import { Payment } from "./entities/payment.entity";
import { PaymentGatewayFactory } from "./factories/payment.factory";
import { ZarinpalGateway } from "./gateways/zarinpal.gateway";
import { PaymentsController } from "./payments.controller";
import { PaymentsService } from "./providers/payments.service";
import { BankAccountsRepository } from "./repositories/bank-accounts.repository";
import { PaymentsRepository } from "./repositories/payments.repository";
import { zarinpalConfig } from "../../configs/zarinpal.config";
@Module({
imports: [TypeOrmModule.forFeature([Payment, PaymentMethod, BankAccount])],
controllers: [PaymentsController],
providers: [
PaymentsService,
PaymentGatewayFactory,
ZarinpalGateway,
PaymentsRepository,
BankAccountsRepository,
{
provide: ZARINPAL_CONFIG,
useFactory: zarinpalConfig().useFactory,
inject: zarinpalConfig().inject,
},
],
exports: [PaymentsService],
})
export class PaymentsModule {}
@@ -0,0 +1,42 @@
import { Injectable } from "@nestjs/common";
import { QueryRunner } from "typeorm";
import { User } from "../../users/entities/user.entity";
// import { PaymentMethod } from "../entities/payment-method.entity";
import { Payment } from "../entities/payment.entity";
import { PaymentGatewayFactory } from "../factories/payment.factory";
// import { PaymentsRepository } from "../repositories/payments.repository";
import { GatewayType } from "../types/gateway.type";
@Injectable()
export class PaymentsService {
constructor(
private readonly gatewayFactory: PaymentGatewayFactory,
// private readonly paymentsRepository: PaymentsRepository,
) {}
//*********************************** */
//*********************************** */
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 createPaymentForUser(user: User, amount: number, reference: string, queryRunner: QueryRunner) {
// const paymentMethod = queryRunner.manager.create(PaymentMethod, {});
const payment = queryRunner.manager.create(Payment, {
amount,
reference,
user,
});
await queryRunner.manager.save(Payment, payment);
return payment;
}
//*********************************** */
//*********************************** */
async getAvailableGateways() {
return this.gatewayFactory.getAvailablePaymentGateways();
}
}
@@ -0,0 +1,12 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { BankAccount } from "../entities/bank-account.entity";
@Injectable()
export class BankAccountsRepository extends Repository<BankAccount> {
constructor(@InjectRepository(BankAccount) bankAccountsRepository: Repository<BankAccount>) {
super(bankAccountsRepository.target, bankAccountsRepository.manager, bankAccountsRepository.queryRunner);
}
}
@@ -0,0 +1,11 @@
import { Injectable } from "@nestjs/common";
import { Repository } from "typeorm";
import { PaymentMethod } from "../entities/payment-method.entity";
@Injectable()
export class PaymentMethodsRepository extends Repository<PaymentMethod> {
constructor(paymentMethodsRepository: Repository<PaymentMethod>) {
super(paymentMethodsRepository.target, paymentMethodsRepository.manager, paymentMethodsRepository.queryRunner);
}
}
@@ -0,0 +1,12 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Payment } from "../entities/payment.entity";
@Injectable()
export class PaymentsRepository extends Repository<Payment> {
constructor(@InjectRepository(Payment) paymentsRepository: Repository<Payment>) {
super(paymentsRepository.target, paymentsRepository.manager, paymentsRepository.queryRunner);
}
}
@@ -0,0 +1 @@
export type GatewayType = "zarinpal";
@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator";
import { ArrayMinSize, IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator";
import { TicketMessageEnum } from "../../../common/enums/message.enum";
@@ -11,7 +11,8 @@ export class CreateTicketMessageDto {
@IsOptional()
@IsNotEmpty({ message: TicketMessageEnum.ATTACHMENT_REQUIRED })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: TicketMessageEnum.ATTACHMENT_SHOULD_BE_URL })
@ApiProperty({ description: "attachment url", example: "https://example.com/test.png" })
attachmentUrl: string;
@ArrayMinSize(1, { message: TicketMessageEnum.ATTACHMENT_REQUIRED })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { each: true, message: TicketMessageEnum.ATTACHMENT_SHOULD_BE_URL })
@ApiProperty({ description: "attachment url", example: ["https://example.com/test.png"] })
attachmentUrls?: string[];
}
+18 -11
View File
@@ -1,15 +1,15 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID, Length } from "class-validator";
import { ArrayMinSize, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length } from "class-validator";
import { TicketMessageEnum } from "../../../common/enums/message.enum";
import { TicketPriority } from "../enums/ticket-priority.enum";
export class CreateTicketDto {
@IsNotEmpty({ message: TicketMessageEnum.TITLE_REQUIRED })
@IsString({ message: TicketMessageEnum.TITLE_STRING })
@Length(3, 150, { message: TicketMessageEnum.TITLE_LENGTH })
@ApiProperty({ description: "The ticket title", example: "Test title" })
title: string;
// @IsNotEmpty({ message: TicketMessageEnum.TITLE_REQUIRED })
// @IsString({ message: TicketMessageEnum.TITLE_STRING })
// @Length(3, 150, { message: TicketMessageEnum.TITLE_LENGTH })
// @ApiProperty({ description: "The ticket title", example: "Test title" })
// title: string;
@IsNotEmpty({ message: TicketMessageEnum.SUBJECT_REQUIRED })
@IsString({ message: TicketMessageEnum.SUBJECT_STRING })
@@ -22,11 +22,11 @@ export class CreateTicketDto {
@ApiProperty({ enum: TicketPriority, example: TicketPriority.LOW, description: "LOW, MEDIUM, HIGH" })
priority: TicketPriority;
@IsOptional()
@IsNotEmpty({ message: TicketMessageEnum.DANAK_SERVICE_ID_REQUIRED })
@IsUUID("4", { message: TicketMessageEnum.DANAK_SERVICE_ID_SHOULD_BE_UUID })
@ApiProperty({ description: "Service ID related to the ticket", example: "550e8400-e29b-41d4-a716-446655440000" })
danakServiceId?: string;
// @IsOptional()
// @IsNotEmpty({ message: TicketMessageEnum.DANAK_SERVICE_ID_REQUIRED })
// @IsUUID("4", { message: TicketMessageEnum.DANAK_SERVICE_ID_SHOULD_BE_UUID })
// @ApiProperty({ description: "Service ID related to the ticket", example: "550e8400-e29b-41d4-a716-446655440000" })
// danakServiceId?: string;
@IsNotEmpty({ message: TicketMessageEnum.CATEGORY_ID_REQUIRED })
@IsUUID("4", { message: TicketMessageEnum.CATEGORY_ID_SHOULD_BE_UUID })
@@ -38,4 +38,11 @@ export class CreateTicketDto {
@Length(5, 500, { message: TicketMessageEnum.MESSAGE_LENGTH })
@ApiProperty({ description: "The message content of the ticket", example: "This is a detailed description of the issue." })
message: string;
@IsOptional()
@IsNotEmpty({ message: TicketMessageEnum.ATTACHMENT_REQUIRED })
@ArrayMinSize(1, { message: TicketMessageEnum.ATTACHMENT_REQUIRED })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { each: true, message: TicketMessageEnum.ATTACHMENT_SHOULD_BE_URL })
@ApiProperty({ description: "attachment url", example: ["https://example.com/test.png"] })
attachmentUrls?: string[];
}
@@ -0,0 +1,13 @@
import { Column, Entity, ManyToOne } from "typeorm";
import { TicketMessage } from "./ticket-message.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
@Entity()
export class TicketMessageAttachment extends BaseEntity {
@Column({ type: "varchar", length: 150, nullable: true, default: null })
attachmentUrl: string;
@ManyToOne(() => TicketMessage, (ticketMessage) => ticketMessage.attachments, { onDelete: "CASCADE", nullable: false })
ticketMessage: TicketMessage;
}
@@ -1,5 +1,6 @@
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from "typeorm";
import { TicketMessageAttachment } from "./ticket-message-attachment";
import { Ticket } from "./ticket.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
@@ -9,8 +10,8 @@ export class TicketMessage extends BaseEntity {
@Column({ type: "text", nullable: false })
content: string;
@Column({ type: "varchar", length: 150, nullable: true, default: null })
attachmentUrl: string;
@OneToMany(() => TicketMessageAttachment, (attachment) => attachment.ticketMessage, { cascade: true })
attachments: TicketMessageAttachment[];
@JoinColumn()
@ManyToOne(() => Ticket, (ticket) => ticket.messages, { onDelete: "CASCADE", nullable: false })
@@ -13,8 +13,8 @@ export class Ticket extends BaseEntity {
@Column({ type: "int", generated: "identity", insert: false })
numericId: number;
@Column({ type: "varchar", length: 150, nullable: false })
title: string;
// @Column({ type: "varchar", length: 150, nullable: false })
// title: string;
@Column({ type: "varchar", length: 150, nullable: false })
subject: string;
@@ -1,9 +1,10 @@
import { BadRequestException, Injectable } from "@nestjs/common";
import { Not } from "typeorm";
import { BadRequestException, HttpException, Injectable, InternalServerErrorException } from "@nestjs/common";
import { DataSource, Not } from "typeorm";
import { ParamDto } from "../../../common/DTO/param.dto";
import { CommonMessage, TicketMessageEnum } from "../../../common/enums/message.enum";
import { CommonMessage, TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum";
import { Role } from "../../users/entities/role.entity";
import { User } from "../../users/entities/user.entity";
import { RoleEnum } from "../../users/enums/role.enum";
import { UsersService } from "../../users/providers/users.service";
import { PaginationUtils } from "../../utils/providers/pagination.utils";
@@ -13,6 +14,7 @@ import { CreateTicketDto } from "../DTO/create-ticket.dto";
import { SearchTicketCategoryDto } from "../DTO/search-ticket-category.dto";
import { SearchTicketQueryDto } from "../DTO/search-ticket-query.dto";
import { UpdateTicketCategoryDto } from "../DTO/update-ticket-category.dto";
import { TicketMessageAttachment } from "../entities/ticket-message-attachment";
import { TicketMessage } from "../entities/ticket-message.entity";
import { Ticket } from "../entities/ticket.entity";
import { TicketStatus } from "../enums/ticket-status.enum";
@@ -27,6 +29,7 @@ export class TicketsService {
private readonly ticketsRepository: TicketsRepository,
private readonly ticketMessagesRepository: TicketMessagesRepository,
private readonly usersService: UsersService,
private dataSource: DataSource,
) {}
//******************************** */
async getTicketCategories() {
@@ -109,15 +112,26 @@ export class TicketsService {
const { user } = await this.usersService.findOneById(userId);
const ticketCategory = await this.ticketsCategoryRepository.findCategoryById(createDto.categoryId);
if (!ticketCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
const { attachmentUrls } = createDto;
//
const ticketMessage: Partial<TicketMessage> = { content: createDto.message, author: user };
if (attachmentUrls) {
ticketMessage.attachments = attachmentUrls.map((url) => {
const attachment = new TicketMessageAttachment();
attachment.attachmentUrl = url;
return attachment;
});
}
const ticket = this.ticketsRepository.create({ ...createDto, user, category: ticketCategory, messages: [ticketMessage] });
await this.ticketsRepository.save(ticket);
return {
message: TicketMessageEnum.CREATED,
ticketId: ticket.id,
};
}
@@ -146,31 +160,83 @@ export class TicketsService {
return { tickets, count, paginate: true };
}
//******************************** */
//TODO: get for admin with group of category
async getAllTickets(queryDto: SearchTicketQueryDto) {
const { limit, skip } = PaginationUtils(queryDto);
const findOptions = {
where: {
...(queryDto.status && { status: queryDto.status }),
},
relations: {
category: true,
user: true,
},
order: {
createdAt: "DESC" as const,
},
skip,
take: limit,
};
const [tickets, count] = await this.ticketsRepository.findAndCount(findOptions);
return { tickets, count, paginate: true };
}
//******************************** */
async createTicketMessage(ticketId: string, createDto: CreateTicketMessageDto, userId: string, role: Role) {
const userRole = role as unknown as RoleEnum;
let ticket: null | Ticket = null;
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
if (userRole === RoleEnum.ADMIN) {
ticket = await this.ticketsRepository.findTicketById(ticketId);
} else {
ticket = await this.ticketsRepository.findTicketById(ticketId, userId);
try {
const userRole = role as unknown as RoleEnum;
let ticket: null | Ticket = null;
if (userRole === RoleEnum.ADMIN) {
ticket = await queryRunner.manager.findOneBy(Ticket, { id: ticketId });
} else {
ticket = await queryRunner.manager.findOneBy(Ticket, { id: ticketId, user: { id: userId } });
}
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
if (ticket.status === TicketStatus.CLOSED) throw new BadRequestException(TicketMessageEnum.TICKET_CLOSED);
const user = await queryRunner.manager.findOneBy(User, { id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
const ticketMessage = queryRunner.manager.create(TicketMessage, { ...createDto, author: user, ticket });
if (userRole === RoleEnum.ADMIN && ticket.status === TicketStatus.PENDING) {
ticket.status = TicketStatus.ANSWERED;
}
await queryRunner.manager.save(Ticket, ticket);
await queryRunner.manager.save(TicketMessage, ticketMessage);
if (createDto.attachmentUrls) {
const attachments = createDto.attachmentUrls.map((url) =>
queryRunner.manager.create(TicketMessageAttachment, { attachmentUrl: url, ticketMessage }),
);
await queryRunner.manager.save(TicketMessageAttachment, attachments);
}
await queryRunner.commitTransaction();
return {
message: TicketMessageEnum.MESSAGE_CREATED,
ticketMessage,
};
} catch (error) {
await queryRunner.rollbackTransaction();
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(TicketMessageEnum.ERROR_IN_TICKET_MSG_CREATION);
} finally {
await queryRunner.release();
}
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
const { user } = await this.usersService.findOneById(userId);
const ticketMessage = this.ticketMessagesRepository.create({ ...createDto, author: user, ticket });
await this.ticketMessagesRepository.save(ticketMessage);
return {
message: TicketMessageEnum.MESSAGE_CREATED,
// ticketMessage,
};
}
//******************************** */
@@ -0,0 +1,16 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { TicketMessageAttachment } from "../entities/ticket-message-attachment";
@Injectable()
export class TicketMessageAttachmentsRepository extends Repository<TicketMessageAttachment> {
constructor(@InjectRepository(TicketMessageAttachment) ticketMessageAttachmentsRepository: Repository<TicketMessageAttachment>) {
super(
ticketMessageAttachmentsRepository.target,
ticketMessageAttachmentsRepository.manager,
ticketMessageAttachmentsRepository.queryRunner,
);
}
}
@@ -14,12 +14,16 @@ export class TicketMessagesRepository extends Repository<TicketMessage> {
return this.find({
relations: {
author: true,
attachments: true,
// ticket: true,
},
select: {
id: true,
content: true,
attachmentUrl: true,
attachments: {
id: true,
attachmentUrl: true,
},
createdAt: true,
author: {
id: true,
@@ -32,7 +36,7 @@ export class TicketMessagesRepository extends Repository<TicketMessage> {
},
},
where: { ticket: { id: ticketId } },
order: { createdAt: "DESC" },
order: { createdAt: "ASC" },
});
}
}
@@ -11,6 +11,40 @@ export class TicketsRepository extends Repository<Ticket> {
}
async findTicketById(id: string, userId?: string): Promise<Ticket | null> {
return this.findOneBy({ id, ...(userId && { user: { id: userId } }) });
return this.findOne({
where: { id, ...(userId && { user: { id: userId } }) },
relations: {
user: true,
category: true,
assignedTo: true,
danakService: true,
},
select: {
id: true,
numericId: true,
danakService: {
id: true,
name: true,
},
subject: true,
priority: true,
status: true,
createdAt: true,
user: {
id: true,
firstName: true,
lastName: true,
},
assignedTo: {
id: true,
firstName: true,
lastName: true,
},
category: {
id: true,
title: true,
},
},
});
}
}
+15 -5
View File
@@ -71,8 +71,8 @@ export class TicketsController {
@AuthGuards()
@Roles(RoleEnum.USER)
@Post()
createTicket(@Body() createDto: CreateTicketDto, @UserDec() user: User) {
return this.ticketsService.createTicket(createDto, user.id);
createTicket(@Body() createDto: CreateTicketDto, @UserDec("id") userId: string) {
return this.ticketsService.createTicket(createDto, userId);
}
@ApiOperation({ summary: "Get all tickets of user ==> user route" })
@@ -80,8 +80,17 @@ export class TicketsController {
@Roles(RoleEnum.USER)
@Pagination()
@Get()
getTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec() user: User) {
return this.ticketsService.getTickets(queryDto, user.id);
getTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") userId: string) {
return this.ticketsService.getTickets(queryDto, userId);
}
@ApiOperation({ summary: "Get all tickets ==> admin route" })
@AuthGuards()
@Roles(RoleEnum.ADMIN)
@Pagination()
@Get("admin")
getAllTickets(@Query() queryDto: SearchTicketQueryDto) {
return this.ticketsService.getAllTickets(queryDto);
}
@ApiOperation({ summary: "create ticket messages" })
@@ -103,7 +112,8 @@ export class TicketsController {
@ApiOperation({ summary: "close ticket by user or admin" })
@AuthGuards()
@Roles(RoleEnum.USER, RoleEnum.ADMIN)
@Get(":id/close")
@HttpCode(HttpStatus.OK)
@Post(":id/close")
closedTicketByUser(@Param() paramDto: ParamDto, @UserDec() user: User) {
return this.ticketsService.closeTicketByUser(paramDto.id, user.id, user.role);
}
+5 -3
View File
@@ -10,11 +10,13 @@ import { TicketMessagesRepository } from "./repositories/tickets-message.reposit
import { TicketsRepository } from "./repositories/tickets.repository";
import { TicketsController } from "./tickets.controller";
import { UsersModule } from "../users/users.module";
import { TicketMessageAttachment } from "./entities/ticket-message-attachment";
import { TicketMessageAttachmentsRepository } from "./repositories/ticket-message-attachment.repository";
@Module({
imports: [TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessage]), UsersModule],
providers: [TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository],
imports: [TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessage, TicketMessageAttachment]), UsersModule],
providers: [TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository, TicketMessageAttachmentsRepository],
controllers: [TicketsController],
exports: [TypeOrmModule, TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository],
exports: [TicketsService],
})
export class TicketsModule {}
+8 -1
View File
@@ -1,5 +1,5 @@
import { Exclude } from "class-transformer";
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany } from "typeorm";
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, OneToOne } from "typeorm";
import { Role } from "./role.entity";
import { UserGroup } from "./user-group.entity";
@@ -7,9 +7,11 @@ import { BaseEntity } from "../../../common/entities/base.entity";
import { UserAnnouncement } from "../../announcements/entities/user-announcement.entity";
import { Criticism } from "../../criticisms/entities/criticism.entity";
import { DanakService } from "../../danak-services/entities/danak-service.entity";
import { Payment } from "../../payments/entities/payment.entity";
import { UserSetting } from "../../settings/entities/user-setting.entity";
import { TicketMessage } from "../../tickets/entities/ticket-message.entity";
import { Ticket } from "../../tickets/entities/ticket.entity";
import { Wallet } from "../../wallets/entities/wallet.entity";
@Entity()
export class User extends BaseEntity {
@@ -62,4 +64,9 @@ export class User extends BaseEntity {
@OneToMany(() => UserSetting, (settings) => settings.user, { nullable: false })
settings: UserSetting;
@OneToOne(() => Wallet, (wallet) => wallet.user)
wallet: Wallet;
@OneToMany(() => Payment, (payment) => payment.user)
payments: Payment[];
}
+10 -20
View File
@@ -58,8 +58,6 @@ export class UsersService {
/************************************************************ */
async updateProfile(userId: string, updateProfileDto: UpdateProfileDto) {
const { firstName, lastName, birthDate } = updateProfileDto;
if (updateProfileDto.userName) {
updateProfileDto.userName = slugify(updateProfileDto.userName, { lower: true, trim: true });
const existUserName = await this.userRepository.findOneBy({ userName: updateProfileDto.userName, id: Not(userId) });
@@ -68,13 +66,12 @@ export class UsersService {
const user = await this.userRepository.findOneBy({ id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
const { userName } = updateProfileDto;
if (userName) user.userName = userName;
if (firstName) user.firstName = firstName;
if (lastName) user.lastName = lastName;
if (birthDate) user.birthDate = birthDate;
// const { userName } = updateProfileDto;
// if (userName) user.userName = userName;
// if (firstName) user.firstName = firstName;
// if (lastName) user.lastName = lastName;
await this.userRepository.save(user);
await this.userRepository.save({ ...user, ...updateProfileDto });
return {
message: CommonMessage.UPDATE_SUCCESS,
};
@@ -95,24 +92,17 @@ export class UsersService {
/************************************************************ */
async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string): Promise<User> {
// const existEmail = await this.userRepository.findOneWithEmail(registerDto.email);
// if (existEmail) throw new BadRequestException(UserMessage.EMAIL_EXIST);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect(); // Establish a connection
await queryRunner.startTransaction(); // Start the transaction
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Find the USER role
const role = await queryRunner.manager.findOne(Role, {
where: { name: RoleEnum.USER },
});
if (!role) {
throw new BadRequestException(UserMessage.ROLE_NOT_FOUND);
}
if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND);
// Create and save the User entity
const user = queryRunner.manager.create(User, {
...registerDto,
password: hashedPassword,
@@ -122,10 +112,9 @@ export class UsersService {
await this.userSettingsService.createUserSettings(user.id, queryRunner);
// Commit the transaction
await queryRunner.commitTransaction();
return user; // Return the created user
return user;
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
@@ -151,6 +140,7 @@ export class UsersService {
return { message: CommonMessage.VALID_FOR_CHOOSE };
}
/************************************************************ */
async createUserGroup(createUserGroupDto: CreateUserGroupDto) {
const users = await this.userRepository.find({ where: { id: In(createUserGroupDto.userIds) } });
+1 -1
View File
@@ -17,7 +17,7 @@ import { UserSettingsRepository } from "../settings/repositories/user-settings.r
imports: [TypeOrmModule.forFeature([User, Role, UserGroup, UserSetting])],
providers: [UsersService, UserRepository, RoleRepository, UserGroupRepository, UserSettingsRepository, UserSettingsService],
controllers: [UsersController],
exports: [UsersService, TypeOrmModule],
exports: [UsersService],
})
export class UsersModule implements OnApplicationBootstrap {
// constructor(private userService: UsersService) {}
@@ -0,0 +1,36 @@
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";
export class DepositDto {
@IsNotEmpty({ message: WalletMessage.AMOUNT_REQUIRED })
@IsInt({ message: WalletMessage.AMOUNT_MUST_BE_INTEGER })
@Min(100_000, { message: WalletMessage.AMOUNT_MINIMUM })
@ApiProperty({ description: "Amount to charge", example: 100_000 })
amount: number;
}
export class GatewayDepositDto extends DepositDto {
@IsNotEmpty({ message: WalletMessage.GATEWAY_REQUIRED })
@IsEnum(GatewayEnum)
@ApiProperty({ description: "Gateway to use", example: "zarinpal" })
gateway: GatewayEnum;
}
export class CardToCardDepositDto extends DepositDto {
@IsNotEmpty({ message: WalletMessage.RECEIPT_URL_REQUIRED })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: WalletMessage.RECEIPT_URL_SHOULD_BE_URL })
@ApiProperty({ description: "the " })
transferReceiptUrl: string;
@IsNotEmpty({ message: WalletMessage.BANK_ACCOUNT_ID_REQUIRED })
@IsUUID("4", { message: WalletMessage.BANK_ACCOUNT_ID_SHOULD_BE_UUID })
bankAccountId: string;
}
// @IsNotEmpty({ message: WalletMessage.DEPOSIT_TYPE_REQUIRED })
// @IsEnum(DepositType)
// @ApiProperty({ description: "Deposit type", example: DepositType.GATEWAY })
// depositType: DepositType;
@@ -0,0 +1,20 @@
import { Column, Entity, ManyToOne } from "typeorm";
import { Wallet } from "./wallet.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { TransactionType } from "../enums/transaction-type.enum";
@Entity()
export class WalletTransaction extends BaseEntity {
@ManyToOne(() => Wallet, (wallet) => wallet.transactions, { onDelete: "CASCADE", nullable: false })
wallet: Wallet;
@Column({ type: "decimal", precision: 10, scale: 2, nullable: false })
amount: number;
@Column({ type: "enum", enum: TransactionType, nullable: false })
type: TransactionType;
@Column({ type: "text", nullable: false })
description: string;
}
@@ -0,0 +1,18 @@
import { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm";
import { WalletTransaction } from "./transaction.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
@Entity()
export class Wallet extends BaseEntity {
@OneToOne(() => User, (user) => user.wallet, { onDelete: "CASCADE" })
@JoinColumn()
user: User;
@Column({ type: "decimal", precision: 10, scale: 2, default: 0 })
balance: number;
@OneToMany(() => WalletTransaction, (transaction) => transaction.wallet)
transactions: WalletTransaction[];
}
@@ -0,0 +1,4 @@
export enum TransactionType {
DEPOSIT = "DEPOSIT",
WITHDRAWAL = "WITHDRAWAL",
}
@@ -0,0 +1,57 @@
import { BadRequestException, HttpException, Injectable, InternalServerErrorException } from "@nestjs/common";
import { DataSource } from "typeorm";
import { WalletMessage } from "../../../common/enums/message.enum";
import { PaymentsService } from "../../payments/providers/payments.service";
import { GatewayDepositDto } from "../DTO/deposit-wallet.dto";
import { Wallet } from "../entities/wallet.entity";
import { WalletsRepository } from "../repositories/wallets.repository";
@Injectable()
export class WalletsService {
constructor(
private readonly walletsRepository: WalletsRepository,
private readonly paymentsService: PaymentsService,
private readonly dataSource: DataSource,
) {}
//*********************************** */
async getBalance(userId: string) {
const wallet = await this.walletsRepository.findOneBy({ user: { id: userId } });
if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND);
return {
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,
);
const payment = await this.paymentsService.createPaymentForUser(wallet.user, amount, gatewayData.reference, queryRunner);
return {
payment,
};
} catch (error) {
await queryRunner.rollbackTransaction();
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(WalletMessage.ERROR_IN_CHARGE_WALLET);
} finally {
await queryRunner.release();
}
}
}
@@ -0,0 +1,12 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { WalletTransaction } from "../entities/transaction.entity";
@Injectable()
export class WalletTransactionsRepository extends Repository<WalletTransaction> {
constructor(@InjectRepository(WalletTransaction) walletTransactionsRepository: Repository<WalletTransaction>) {
super(walletTransactionsRepository.target, walletTransactionsRepository.manager, walletTransactionsRepository.queryRunner);
}
}
@@ -0,0 +1,12 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Wallet } from "../entities/wallet.entity";
@Injectable()
export class WalletsRepository extends Repository<Wallet> {
constructor(@InjectRepository(Wallet) walletsRepository: Repository<Wallet>) {
super(walletsRepository.target, walletsRepository.manager, walletsRepository.queryRunner);
}
}
+31
View File
@@ -0,0 +1,31 @@
import { Body, Controller, Get, Post } 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";
import { UserDec } from "../../common/decorators/user.decorator";
import { RoleEnum } from "../users/enums/role.enum";
@Controller("wallets")
@ApiTags("wallets")
export class WalletsController {
constructor(private readonly walletsService: WalletsService) {}
@ApiOperation({ summary: "Get balance ==> user route" })
@AuthGuards()
@Roles(RoleEnum.USER)
@Get("balance")
getBalance(@UserDec("id") userId: string) {
return this.walletsService.getBalance(userId);
}
@ApiOperation({ summary: "Charge wallet ==> user route" })
@AuthGuards()
@Roles(RoleEnum.USER)
@Post("deposit/gateway")
chargeWalletWithGateway(@Body() chargeDto: GatewayDepositDto, @UserDec("id") userId: string) {
return this.walletsService.chargeWalletWithGateway(chargeDto, userId);
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { WalletTransaction } from "./entities/transaction.entity";
import { Wallet } from "./entities/wallet.entity";
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],
providers: [WalletsService, WalletsRepository, WalletTransactionsRepository],
controllers: [WalletsController],
exports: [WalletsService],
})
export class WalletsModule {}