feat: add payment module and factory
This commit is contained in:
@@ -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,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;
|
||||
// });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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) } });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
Reference in New Issue
Block a user