feat: add payment module and factory
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
||||||
|
import { HttpModule } from "@nestjs/axios";
|
||||||
import { CacheModule } from "@nestjs/cache-manager";
|
import { CacheModule } from "@nestjs/cache-manager";
|
||||||
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
import { ConfigModule } from "@nestjs/config";
|
||||||
@@ -6,6 +7,7 @@ import { ThrottlerModule } from "@nestjs/throttler";
|
|||||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
|
||||||
import { cacheConfig } from "./configs/cache.config";
|
import { cacheConfig } from "./configs/cache.config";
|
||||||
|
// import { httpConfig } from "./configs/http.config";
|
||||||
import { rateLimitConfig } from "./configs/rateLimit.config";
|
import { rateLimitConfig } from "./configs/rateLimit.config";
|
||||||
import { databaseConfigs } from "./configs/typeorm.config";
|
import { databaseConfigs } from "./configs/typeorm.config";
|
||||||
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
||||||
@@ -14,10 +16,12 @@ import { AuthModule } from "./modules/auth/auth.module";
|
|||||||
import { ContactUsModule } from "./modules/contact-us/contact-us.module";
|
import { ContactUsModule } from "./modules/contact-us/contact-us.module";
|
||||||
import { CriticismModule } from "./modules/criticisms/criticisms.module";
|
import { CriticismModule } from "./modules/criticisms/criticisms.module";
|
||||||
import { DanakServicesModule } from "./modules/danak-services/danak-services.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 { SettingModule } from "./modules/settings/settings.module";
|
||||||
import { TicketsModule } from "./modules/tickets/tickets.module";
|
import { TicketsModule } from "./modules/tickets/tickets.module";
|
||||||
import { UploaderModule } from "./modules/uploader/uploader.module";
|
import { UploaderModule } from "./modules/uploader/uploader.module";
|
||||||
import { UsersModule } from "./modules/users/users.module";
|
import { UsersModule } from "./modules/users/users.module";
|
||||||
|
import { WalletsModule } from "./modules/wallets/wallets.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -25,6 +29,7 @@ import { UsersModule } from "./modules/users/users.module";
|
|||||||
ConfigModule.forRoot({ cache: true, isGlobal: true }),
|
ConfigModule.forRoot({ cache: true, isGlobal: true }),
|
||||||
CacheModule.registerAsync(cacheConfig()),
|
CacheModule.registerAsync(cacheConfig()),
|
||||||
TypeOrmModule.forRootAsync(databaseConfigs()),
|
TypeOrmModule.forRootAsync(databaseConfigs()),
|
||||||
|
HttpModule.register({ global: true }),
|
||||||
FastifyMulterModule,
|
FastifyMulterModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
TicketsModule,
|
TicketsModule,
|
||||||
@@ -35,6 +40,8 @@ import { UsersModule } from "./modules/users/users.module";
|
|||||||
CriticismModule,
|
CriticismModule,
|
||||||
SettingModule,
|
SettingModule,
|
||||||
ContactUsModule,
|
ContactUsModule,
|
||||||
|
WalletsModule,
|
||||||
|
PaymentsModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
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>();
|
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 = "پیام تیکت با موفقیت ایجاد شد",
|
MESSAGE_CREATED = "پیام تیکت با موفقیت ایجاد شد",
|
||||||
TICKET_NOT_FOUND = "تیکت مورد نظر یافت نشد",
|
TICKET_NOT_FOUND = "تیکت مورد نظر یافت نشد",
|
||||||
CLOSED = "تیکت با موفقیت بسته شد",
|
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 {
|
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],
|
imports: [TypeOrmModule.forFeature([Announcement, UserAnnouncement]), DanakServicesModule, UsersModule],
|
||||||
providers: [AnnouncementService, AnnouncementRepository, UserAnnouncementRepository],
|
providers: [AnnouncementService, AnnouncementRepository, UserAnnouncementRepository],
|
||||||
controllers: [AnnouncementController],
|
controllers: [AnnouncementController],
|
||||||
exports: [TypeOrmModule],
|
exports: [AnnouncementService],
|
||||||
})
|
})
|
||||||
export class AnnouncementsModule {}
|
export class AnnouncementsModule {}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export class AuthService {
|
|||||||
private readonly tokensService: TokensService,
|
private readonly tokensService: TokensService,
|
||||||
) {}
|
) {}
|
||||||
//****************** */
|
//****************** */
|
||||||
|
//****************** */
|
||||||
async initiateRegistration(requestOtpDto: RequestOtpDto) {
|
async initiateRegistration(requestOtpDto: RequestOtpDto) {
|
||||||
const { phone } = requestOtpDto;
|
const { phone } = requestOtpDto;
|
||||||
const existUser = await this.usersService.findOneWithPhone(phone);
|
const existUser = await this.usersService.findOneWithPhone(phone);
|
||||||
@@ -44,6 +45,7 @@ export class AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
//****************** */
|
//****************** */
|
||||||
|
//****************** */
|
||||||
async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) {
|
async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) {
|
||||||
const { phone, code } = completeRegistrationDto;
|
const { phone, code } = completeRegistrationDto;
|
||||||
const isValid = await this.otpService.verifyOtp(phone, code, "REGISTER");
|
const isValid = await this.otpService.verifyOtp(phone, code, "REGISTER");
|
||||||
@@ -62,7 +64,7 @@ export class AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
//****************** */
|
//****************** */
|
||||||
|
//****************** */
|
||||||
async loginWithPassword(loginDto: LoginPasswordDTO) {
|
async loginWithPassword(loginDto: LoginPasswordDTO) {
|
||||||
const { email, password } = loginDto;
|
const { email, password } = loginDto;
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ export class AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
//****************** */
|
//****************** */
|
||||||
|
//****************** */
|
||||||
|
|
||||||
async adminLoginWithPassword(loginDto: LoginPasswordDTO) {
|
async adminLoginWithPassword(loginDto: LoginPasswordDTO) {
|
||||||
const { email, password } = loginDto;
|
const { email, password } = loginDto;
|
||||||
@@ -92,6 +95,8 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//****************** */
|
//****************** */
|
||||||
|
//****************** */
|
||||||
|
|
||||||
async checkUserExist(checkUserExistDto: CheckUserExistDto) {
|
async checkUserExist(checkUserExistDto: CheckUserExistDto) {
|
||||||
const user = await this.usersService.findOneWithEmail(checkUserExistDto.email);
|
const user = await this.usersService.findOneWithEmail(checkUserExistDto.email);
|
||||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
@@ -99,6 +104,8 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//****************** */
|
//****************** */
|
||||||
|
//****************** */
|
||||||
|
|
||||||
async requestLoginOtp(requestOtpDto: RequestOtpDto, isAdmin: boolean = false) {
|
async requestLoginOtp(requestOtpDto: RequestOtpDto, isAdmin: boolean = false) {
|
||||||
const { phone } = requestOtpDto;
|
const { phone } = requestOtpDto;
|
||||||
const user = await this.usersService.findOneWithPhone(phone);
|
const user = await this.usersService.findOneWithPhone(phone);
|
||||||
@@ -126,6 +133,8 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//****************** */
|
//****************** */
|
||||||
|
//****************** */
|
||||||
|
|
||||||
async verifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
|
async verifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
|
||||||
const { code, phone } = verifyOtpDto;
|
const { code, phone } = verifyOtpDto;
|
||||||
|
|
||||||
@@ -139,6 +148,8 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//****************** */
|
//****************** */
|
||||||
|
//****************** */
|
||||||
|
|
||||||
async adminVerifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
|
async adminVerifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
|
||||||
const { code, phone } = verifyOtpDto;
|
const { code, phone } = verifyOtpDto;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ import { UsersModule } from "../users/users.module";
|
|||||||
imports: [TypeOrmModule.forFeature([Criticism, CriticismFile]), UsersModule],
|
imports: [TypeOrmModule.forFeature([Criticism, CriticismFile]), UsersModule],
|
||||||
providers: [CriticismsService, CriticismRepository, CriticismFileRepository],
|
providers: [CriticismsService, CriticismRepository, CriticismFileRepository],
|
||||||
controllers: [CriticismController],
|
controllers: [CriticismController],
|
||||||
exports: [],
|
exports: [CriticismsService],
|
||||||
})
|
})
|
||||||
export class CriticismModule {}
|
export class CriticismModule {}
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ import { DanakServicesRepository } from "./repositories/danak-services.repositor
|
|||||||
imports: [TypeOrmModule.forFeature([DanakService, DanakServiceImage, DanakServiceCategory])],
|
imports: [TypeOrmModule.forFeature([DanakService, DanakServiceImage, DanakServiceCategory])],
|
||||||
providers: [DanakServicesService, DanakServicesRepository, DanakServicesCategoryRepository, DanakServicesImageRepository],
|
providers: [DanakServicesService, DanakServicesRepository, DanakServicesCategoryRepository, DanakServicesImageRepository],
|
||||||
controllers: [DanakServicesController],
|
controllers: [DanakServicesController],
|
||||||
exports: [DanakServicesService, TypeOrmModule],
|
exports: [DanakServicesService],
|
||||||
})
|
})
|
||||||
export class DanakServicesModule {}
|
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 { 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";
|
import { TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
@@ -11,7 +11,8 @@ export class CreateTicketMessageDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNotEmpty({ message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
@IsNotEmpty({ message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
||||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: TicketMessageEnum.ATTACHMENT_SHOULD_BE_URL })
|
@ArrayMinSize(1, { message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
||||||
@ApiProperty({ description: "attachment url", example: "https://example.com/test.png" })
|
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { each: true, message: TicketMessageEnum.ATTACHMENT_SHOULD_BE_URL })
|
||||||
attachmentUrl: string;
|
@ApiProperty({ description: "attachment url", example: ["https://example.com/test.png"] })
|
||||||
|
attachmentUrls?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
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 { TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||||
import { TicketPriority } from "../enums/ticket-priority.enum";
|
import { TicketPriority } from "../enums/ticket-priority.enum";
|
||||||
|
|
||||||
export class CreateTicketDto {
|
export class CreateTicketDto {
|
||||||
@IsNotEmpty({ message: TicketMessageEnum.TITLE_REQUIRED })
|
// @IsNotEmpty({ message: TicketMessageEnum.TITLE_REQUIRED })
|
||||||
@IsString({ message: TicketMessageEnum.TITLE_STRING })
|
// @IsString({ message: TicketMessageEnum.TITLE_STRING })
|
||||||
@Length(3, 150, { message: TicketMessageEnum.TITLE_LENGTH })
|
// @Length(3, 150, { message: TicketMessageEnum.TITLE_LENGTH })
|
||||||
@ApiProperty({ description: "The ticket title", example: "Test title" })
|
// @ApiProperty({ description: "The ticket title", example: "Test title" })
|
||||||
title: string;
|
// title: string;
|
||||||
|
|
||||||
@IsNotEmpty({ message: TicketMessageEnum.SUBJECT_REQUIRED })
|
@IsNotEmpty({ message: TicketMessageEnum.SUBJECT_REQUIRED })
|
||||||
@IsString({ message: TicketMessageEnum.SUBJECT_STRING })
|
@IsString({ message: TicketMessageEnum.SUBJECT_STRING })
|
||||||
@@ -22,11 +22,11 @@ export class CreateTicketDto {
|
|||||||
@ApiProperty({ enum: TicketPriority, example: TicketPriority.LOW, description: "LOW, MEDIUM, HIGH" })
|
@ApiProperty({ enum: TicketPriority, example: TicketPriority.LOW, description: "LOW, MEDIUM, HIGH" })
|
||||||
priority: TicketPriority;
|
priority: TicketPriority;
|
||||||
|
|
||||||
@IsOptional()
|
// @IsOptional()
|
||||||
@IsNotEmpty({ message: TicketMessageEnum.DANAK_SERVICE_ID_REQUIRED })
|
// @IsNotEmpty({ message: TicketMessageEnum.DANAK_SERVICE_ID_REQUIRED })
|
||||||
@IsUUID("4", { message: TicketMessageEnum.DANAK_SERVICE_ID_SHOULD_BE_UUID })
|
// @IsUUID("4", { message: TicketMessageEnum.DANAK_SERVICE_ID_SHOULD_BE_UUID })
|
||||||
@ApiProperty({ description: "Service ID related to the ticket", example: "550e8400-e29b-41d4-a716-446655440000" })
|
// @ApiProperty({ description: "Service ID related to the ticket", example: "550e8400-e29b-41d4-a716-446655440000" })
|
||||||
danakServiceId?: string;
|
// danakServiceId?: string;
|
||||||
|
|
||||||
@IsNotEmpty({ message: TicketMessageEnum.CATEGORY_ID_REQUIRED })
|
@IsNotEmpty({ message: TicketMessageEnum.CATEGORY_ID_REQUIRED })
|
||||||
@IsUUID("4", { message: TicketMessageEnum.CATEGORY_ID_SHOULD_BE_UUID })
|
@IsUUID("4", { message: TicketMessageEnum.CATEGORY_ID_SHOULD_BE_UUID })
|
||||||
@@ -38,4 +38,11 @@ export class CreateTicketDto {
|
|||||||
@Length(5, 500, { message: TicketMessageEnum.MESSAGE_LENGTH })
|
@Length(5, 500, { message: TicketMessageEnum.MESSAGE_LENGTH })
|
||||||
@ApiProperty({ description: "The message content of the ticket", example: "This is a detailed description of the issue." })
|
@ApiProperty({ description: "The message content of the ticket", example: "This is a detailed description of the issue." })
|
||||||
message: string;
|
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 { Ticket } from "./ticket.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
@@ -9,8 +10,8 @@ export class TicketMessage extends BaseEntity {
|
|||||||
@Column({ type: "text", nullable: false })
|
@Column({ type: "text", nullable: false })
|
||||||
content: string;
|
content: string;
|
||||||
|
|
||||||
@Column({ type: "varchar", length: 150, nullable: true, default: null })
|
@OneToMany(() => TicketMessageAttachment, (attachment) => attachment.ticketMessage, { cascade: true })
|
||||||
attachmentUrl: string;
|
attachments: TicketMessageAttachment[];
|
||||||
|
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
@ManyToOne(() => Ticket, (ticket) => ticket.messages, { onDelete: "CASCADE", nullable: false })
|
@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 })
|
@Column({ type: "int", generated: "identity", insert: false })
|
||||||
numericId: number;
|
numericId: number;
|
||||||
|
|
||||||
@Column({ type: "varchar", length: 150, nullable: false })
|
// @Column({ type: "varchar", length: 150, nullable: false })
|
||||||
title: string;
|
// title: string;
|
||||||
|
|
||||||
@Column({ type: "varchar", length: 150, nullable: false })
|
@Column({ type: "varchar", length: 150, nullable: false })
|
||||||
subject: string;
|
subject: string;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
import { BadRequestException, HttpException, Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||||
import { Not } from "typeorm";
|
import { DataSource, Not } from "typeorm";
|
||||||
|
|
||||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
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 { Role } from "../../users/entities/role.entity";
|
||||||
|
import { User } from "../../users/entities/user.entity";
|
||||||
import { RoleEnum } from "../../users/enums/role.enum";
|
import { RoleEnum } from "../../users/enums/role.enum";
|
||||||
import { UsersService } from "../../users/providers/users.service";
|
import { UsersService } from "../../users/providers/users.service";
|
||||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
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 { SearchTicketCategoryDto } from "../DTO/search-ticket-category.dto";
|
||||||
import { SearchTicketQueryDto } from "../DTO/search-ticket-query.dto";
|
import { SearchTicketQueryDto } from "../DTO/search-ticket-query.dto";
|
||||||
import { UpdateTicketCategoryDto } from "../DTO/update-ticket-category.dto";
|
import { UpdateTicketCategoryDto } from "../DTO/update-ticket-category.dto";
|
||||||
|
import { TicketMessageAttachment } from "../entities/ticket-message-attachment";
|
||||||
import { TicketMessage } from "../entities/ticket-message.entity";
|
import { TicketMessage } from "../entities/ticket-message.entity";
|
||||||
import { Ticket } from "../entities/ticket.entity";
|
import { Ticket } from "../entities/ticket.entity";
|
||||||
import { TicketStatus } from "../enums/ticket-status.enum";
|
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||||
@@ -27,6 +29,7 @@ export class TicketsService {
|
|||||||
private readonly ticketsRepository: TicketsRepository,
|
private readonly ticketsRepository: TicketsRepository,
|
||||||
private readonly ticketMessagesRepository: TicketMessagesRepository,
|
private readonly ticketMessagesRepository: TicketMessagesRepository,
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
|
private dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
//******************************** */
|
//******************************** */
|
||||||
async getTicketCategories() {
|
async getTicketCategories() {
|
||||||
@@ -109,15 +112,26 @@ export class TicketsService {
|
|||||||
const { user } = await this.usersService.findOneById(userId);
|
const { user } = await this.usersService.findOneById(userId);
|
||||||
const ticketCategory = await this.ticketsCategoryRepository.findCategoryById(createDto.categoryId);
|
const ticketCategory = await this.ticketsCategoryRepository.findCategoryById(createDto.categoryId);
|
||||||
if (!ticketCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
if (!ticketCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
||||||
|
const { attachmentUrls } = createDto;
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
const ticketMessage: Partial<TicketMessage> = { content: createDto.message, author: user };
|
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] });
|
const ticket = this.ticketsRepository.create({ ...createDto, user, category: ticketCategory, messages: [ticketMessage] });
|
||||||
|
|
||||||
await this.ticketsRepository.save(ticket);
|
await this.ticketsRepository.save(ticket);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: TicketMessageEnum.CREATED,
|
message: TicketMessageEnum.CREATED,
|
||||||
|
ticketId: ticket.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,31 +160,83 @@ export class TicketsService {
|
|||||||
|
|
||||||
return { tickets, count, paginate: true };
|
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) {
|
async createTicketMessage(ticketId: string, createDto: CreateTicketMessageDto, userId: string, role: Role) {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
const userRole = role as unknown as RoleEnum;
|
const userRole = role as unknown as RoleEnum;
|
||||||
let ticket: null | Ticket = null;
|
let ticket: null | Ticket = null;
|
||||||
|
|
||||||
if (userRole === RoleEnum.ADMIN) {
|
if (userRole === RoleEnum.ADMIN) {
|
||||||
ticket = await this.ticketsRepository.findTicketById(ticketId);
|
ticket = await queryRunner.manager.findOneBy(Ticket, { id: ticketId });
|
||||||
} else {
|
} else {
|
||||||
ticket = await this.ticketsRepository.findTicketById(ticketId, userId);
|
ticket = await queryRunner.manager.findOneBy(Ticket, { id: ticketId, user: { id: userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||||
|
|
||||||
const { user } = await this.usersService.findOneById(userId);
|
if (ticket.status === TicketStatus.CLOSED) throw new BadRequestException(TicketMessageEnum.TICKET_CLOSED);
|
||||||
|
|
||||||
const ticketMessage = this.ticketMessagesRepository.create({ ...createDto, author: user, ticket });
|
const user = await queryRunner.manager.findOneBy(User, { id: userId });
|
||||||
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
await this.ticketMessagesRepository.save(ticketMessage);
|
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 {
|
return {
|
||||||
message: TicketMessageEnum.MESSAGE_CREATED,
|
message: TicketMessageEnum.MESSAGE_CREATED,
|
||||||
// ticketMessage,
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//******************************** */
|
//******************************** */
|
||||||
|
|||||||
@@ -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({
|
return this.find({
|
||||||
relations: {
|
relations: {
|
||||||
author: true,
|
author: true,
|
||||||
|
attachments: true,
|
||||||
// ticket: true,
|
// ticket: true,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
content: true,
|
content: true,
|
||||||
|
attachments: {
|
||||||
|
id: true,
|
||||||
attachmentUrl: true,
|
attachmentUrl: true,
|
||||||
|
},
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
author: {
|
author: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -32,7 +36,7 @@ export class TicketMessagesRepository extends Repository<TicketMessage> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
where: { ticket: { id: ticketId } },
|
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> {
|
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()
|
@AuthGuards()
|
||||||
@Roles(RoleEnum.USER)
|
@Roles(RoleEnum.USER)
|
||||||
@Post()
|
@Post()
|
||||||
createTicket(@Body() createDto: CreateTicketDto, @UserDec() user: User) {
|
createTicket(@Body() createDto: CreateTicketDto, @UserDec("id") userId: string) {
|
||||||
return this.ticketsService.createTicket(createDto, user.id);
|
return this.ticketsService.createTicket(createDto, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "Get all tickets of user ==> user route" })
|
@ApiOperation({ summary: "Get all tickets of user ==> user route" })
|
||||||
@@ -80,8 +80,17 @@ export class TicketsController {
|
|||||||
@Roles(RoleEnum.USER)
|
@Roles(RoleEnum.USER)
|
||||||
@Pagination()
|
@Pagination()
|
||||||
@Get()
|
@Get()
|
||||||
getTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec() user: User) {
|
getTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") userId: string) {
|
||||||
return this.ticketsService.getTickets(queryDto, user.id);
|
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" })
|
@ApiOperation({ summary: "create ticket messages" })
|
||||||
@@ -103,7 +112,8 @@ export class TicketsController {
|
|||||||
@ApiOperation({ summary: "close ticket by user or admin" })
|
@ApiOperation({ summary: "close ticket by user or admin" })
|
||||||
@AuthGuards()
|
@AuthGuards()
|
||||||
@Roles(RoleEnum.USER, RoleEnum.ADMIN)
|
@Roles(RoleEnum.USER, RoleEnum.ADMIN)
|
||||||
@Get(":id/close")
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post(":id/close")
|
||||||
closedTicketByUser(@Param() paramDto: ParamDto, @UserDec() user: User) {
|
closedTicketByUser(@Param() paramDto: ParamDto, @UserDec() user: User) {
|
||||||
return this.ticketsService.closeTicketByUser(paramDto.id, user.id, user.role);
|
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 { TicketsRepository } from "./repositories/tickets.repository";
|
||||||
import { TicketsController } from "./tickets.controller";
|
import { TicketsController } from "./tickets.controller";
|
||||||
import { UsersModule } from "../users/users.module";
|
import { UsersModule } from "../users/users.module";
|
||||||
|
import { TicketMessageAttachment } from "./entities/ticket-message-attachment";
|
||||||
|
import { TicketMessageAttachmentsRepository } from "./repositories/ticket-message-attachment.repository";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessage]), UsersModule],
|
imports: [TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessage, TicketMessageAttachment]), UsersModule],
|
||||||
providers: [TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository],
|
providers: [TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository, TicketMessageAttachmentsRepository],
|
||||||
controllers: [TicketsController],
|
controllers: [TicketsController],
|
||||||
exports: [TypeOrmModule, TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository],
|
exports: [TicketsService],
|
||||||
})
|
})
|
||||||
export class TicketsModule {}
|
export class TicketsModule {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Exclude } from "class-transformer";
|
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 { Role } from "./role.entity";
|
||||||
import { UserGroup } from "./user-group.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 { UserAnnouncement } from "../../announcements/entities/user-announcement.entity";
|
||||||
import { Criticism } from "../../criticisms/entities/criticism.entity";
|
import { Criticism } from "../../criticisms/entities/criticism.entity";
|
||||||
import { DanakService } from "../../danak-services/entities/danak-service.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 { UserSetting } from "../../settings/entities/user-setting.entity";
|
||||||
import { TicketMessage } from "../../tickets/entities/ticket-message.entity";
|
import { TicketMessage } from "../../tickets/entities/ticket-message.entity";
|
||||||
import { Ticket } from "../../tickets/entities/ticket.entity";
|
import { Ticket } from "../../tickets/entities/ticket.entity";
|
||||||
|
import { Wallet } from "../../wallets/entities/wallet.entity";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class User extends BaseEntity {
|
export class User extends BaseEntity {
|
||||||
@@ -62,4 +64,9 @@ export class User extends BaseEntity {
|
|||||||
|
|
||||||
@OneToMany(() => UserSetting, (settings) => settings.user, { nullable: false })
|
@OneToMany(() => UserSetting, (settings) => settings.user, { nullable: false })
|
||||||
settings: UserSetting;
|
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) {
|
async updateProfile(userId: string, updateProfileDto: UpdateProfileDto) {
|
||||||
const { firstName, lastName, birthDate } = updateProfileDto;
|
|
||||||
|
|
||||||
if (updateProfileDto.userName) {
|
if (updateProfileDto.userName) {
|
||||||
updateProfileDto.userName = slugify(updateProfileDto.userName, { lower: true, trim: true });
|
updateProfileDto.userName = slugify(updateProfileDto.userName, { lower: true, trim: true });
|
||||||
const existUserName = await this.userRepository.findOneBy({ userName: updateProfileDto.userName, id: Not(userId) });
|
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 });
|
const user = await this.userRepository.findOneBy({ id: userId });
|
||||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
const { userName } = updateProfileDto;
|
// const { userName } = updateProfileDto;
|
||||||
if (userName) user.userName = userName;
|
// if (userName) user.userName = userName;
|
||||||
if (firstName) user.firstName = firstName;
|
// if (firstName) user.firstName = firstName;
|
||||||
if (lastName) user.lastName = lastName;
|
// if (lastName) user.lastName = lastName;
|
||||||
if (birthDate) user.birthDate = birthDate;
|
|
||||||
|
|
||||||
await this.userRepository.save(user);
|
await this.userRepository.save({ ...user, ...updateProfileDto });
|
||||||
return {
|
return {
|
||||||
message: CommonMessage.UPDATE_SUCCESS,
|
message: CommonMessage.UPDATE_SUCCESS,
|
||||||
};
|
};
|
||||||
@@ -95,24 +92,17 @@ export class UsersService {
|
|||||||
/************************************************************ */
|
/************************************************************ */
|
||||||
|
|
||||||
async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string): Promise<User> {
|
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();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
|
||||||
await queryRunner.connect(); // Establish a connection
|
await queryRunner.connect();
|
||||||
await queryRunner.startTransaction(); // Start the transaction
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find the USER role
|
|
||||||
const role = await queryRunner.manager.findOne(Role, {
|
const role = await queryRunner.manager.findOne(Role, {
|
||||||
where: { name: RoleEnum.USER },
|
where: { name: RoleEnum.USER },
|
||||||
});
|
});
|
||||||
if (!role) {
|
if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND);
|
||||||
throw new BadRequestException(UserMessage.ROLE_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and save the User entity
|
|
||||||
const user = queryRunner.manager.create(User, {
|
const user = queryRunner.manager.create(User, {
|
||||||
...registerDto,
|
...registerDto,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
@@ -122,10 +112,9 @@ export class UsersService {
|
|||||||
|
|
||||||
await this.userSettingsService.createUserSettings(user.id, queryRunner);
|
await this.userSettingsService.createUserSettings(user.id, queryRunner);
|
||||||
|
|
||||||
// Commit the transaction
|
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
return user; // Return the created user
|
return user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.rollbackTransaction();
|
||||||
throw error;
|
throw error;
|
||||||
@@ -151,6 +140,7 @@ export class UsersService {
|
|||||||
|
|
||||||
return { message: CommonMessage.VALID_FOR_CHOOSE };
|
return { message: CommonMessage.VALID_FOR_CHOOSE };
|
||||||
}
|
}
|
||||||
|
/************************************************************ */
|
||||||
|
|
||||||
async createUserGroup(createUserGroupDto: CreateUserGroupDto) {
|
async createUserGroup(createUserGroupDto: CreateUserGroupDto) {
|
||||||
const users = await this.userRepository.find({ where: { id: In(createUserGroupDto.userIds) } });
|
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])],
|
imports: [TypeOrmModule.forFeature([User, Role, UserGroup, UserSetting])],
|
||||||
providers: [UsersService, UserRepository, RoleRepository, UserGroupRepository, UserSettingsRepository, UserSettingsService],
|
providers: [UsersService, UserRepository, RoleRepository, UserGroupRepository, UserSettingsRepository, UserSettingsService],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
exports: [UsersService, TypeOrmModule],
|
exports: [UsersService],
|
||||||
})
|
})
|
||||||
export class UsersModule implements OnApplicationBootstrap {
|
export class UsersModule implements OnApplicationBootstrap {
|
||||||
// constructor(private userService: UsersService) {}
|
// 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