From 9eeab529a9ffd191a15779e5f8dd227c0e8c1079 Mon Sep 17 00:00:00 2001 From: Mahyargdz Date: Sun, 18 May 2025 10:19:00 +0330 Subject: [PATCH] chore: add business logic --- package.json | 2 - pnpm-lock.yaml | 25 +++++---- src/app.module.ts | 9 ++++ .../business-id-header.decorator.ts | 17 ++++++ src/common/decorators/business.decorator.ts | 11 ++++ src/common/enums/message.enum.ts | 5 ++ src/configs/bullmq.config.ts | 2 +- src/core/interceptors/business.interceptor.ts | 50 +++++++++++++++++ src/modules/businesses/businesses.module.ts | 21 ++++++++ src/modules/businesses/constant/index.ts | 9 ++++ .../businesses/entities/business.entity.ts | 27 ++++++++++ .../businesses/interfaces/IBusiness.ts | 8 +++ .../queue/business-provisioning.processor.ts | 54 +++++++++++++++++++ .../repositories/business.repository.ts | 5 ++ .../businesses/services/businesses.service.ts | 16 ++++++ src/modules/companies/companies.controller.ts | 30 ++++++----- src/modules/companies/companies.module.ts | 3 +- .../companies/entities/company.entity.ts | 4 ++ .../repositories/company.repository.ts | 3 +- .../companies/services/companies.service.ts | 46 ++++++++++------ .../industries/entities/industry.entity.ts | 6 ++- .../industries/industries.controller.ts | 30 ++++++----- src/modules/industries/industries.module.ts | 3 +- .../repositories/industry.repository.ts | 8 ++- .../industries/services/industries.service.ts | 30 ++++++----- 25 files changed, 350 insertions(+), 74 deletions(-) create mode 100644 src/common/decorators/business-id-header.decorator.ts create mode 100644 src/common/decorators/business.decorator.ts create mode 100644 src/core/interceptors/business.interceptor.ts create mode 100644 src/modules/businesses/businesses.module.ts create mode 100644 src/modules/businesses/constant/index.ts create mode 100644 src/modules/businesses/entities/business.entity.ts create mode 100644 src/modules/businesses/interfaces/IBusiness.ts create mode 100644 src/modules/businesses/queue/business-provisioning.processor.ts create mode 100644 src/modules/businesses/repositories/business.repository.ts create mode 100644 src/modules/businesses/services/businesses.service.ts diff --git a/package.json b/package.json index 5128775..b00c04d 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "@nestjs/core": "^11.0.1", "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", - "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-fastify": "^11.1.0", "@nestjs/swagger": "^11.2.0", "@nestjs/throttler": "^6.4.0", @@ -80,7 +79,6 @@ "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", "@types/bcrypt": "^5.0.2", - "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", "@types/nodemailer": "^6.4.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6801ae..c642534 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,9 +65,6 @@ importers: '@nestjs/passport': specifier: ^11.0.5 version: 11.0.5(@nestjs/common@11.1.0(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) - '@nestjs/platform-express': - specifier: ^11.0.1 - version: 11.1.0(@nestjs/common@11.1.0(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.0) '@nestjs/platform-fastify': specifier: ^11.1.0 version: 11.1.0(@fastify/static@8.1.1)(@nestjs/common@11.1.0(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.0) @@ -153,9 +150,6 @@ importers: '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 - '@types/express': - specifier: ^5.0.0 - version: 5.0.1 '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -8048,6 +8042,7 @@ snapshots: tslib: 2.8.1 transitivePeerDependencies: - supports-color + optional: true '@nestjs/platform-fastify@11.1.0(@fastify/static@8.1.1)(@nestjs/common@11.1.0(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.0)': dependencies: @@ -9423,6 +9418,7 @@ snapshots: busboy@1.6.0: dependencies: streamsearch: 1.1.0 + optional: true bytes@3.1.2: {} @@ -9642,6 +9638,7 @@ snapshots: inherits: 2.0.4 readable-stream: 2.3.8 typedarray: 0.0.6 + optional: true concat-stream@2.0.0: dependencies: @@ -11165,7 +11162,8 @@ snapshots: is-docker: 2.2.1 optional: true - isarray@1.0.0: {} + isarray@1.0.0: + optional: true isarray@2.0.5: {} @@ -12302,6 +12300,7 @@ snapshots: mkdirp@0.5.6: dependencies: minimist: 1.2.8 + optional: true mkdirp@1.0.4: {} @@ -12334,6 +12333,7 @@ snapshots: object-assign: 4.1.1 type-is: 1.6.18 xtend: 4.0.2 + optional: true mute-stream@2.0.0: {} @@ -12776,7 +12776,8 @@ snapshots: uuid: 9.0.1 optional: true - process-nextick-args@2.0.1: {} + process-nextick-args@2.0.1: + optional: true process-warning@4.0.1: {} @@ -12930,6 +12931,7 @@ snapshots: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 + optional: true readable-stream@3.6.2: dependencies: @@ -13066,7 +13068,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 - safe-buffer@5.1.2: {} + safe-buffer@5.1.2: + optional: true safe-buffer@5.2.1: {} @@ -13290,7 +13293,8 @@ snapshots: statuses@2.0.1: {} - streamsearch@1.1.0: {} + streamsearch@1.1.0: + optional: true streamx@2.22.0: dependencies: @@ -13350,6 +13354,7 @@ snapshots: string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 + optional: true string_decoder@1.3.0: dependencies: diff --git a/src/app.module.ts b/src/app.module.ts index 1d29b0d..0a21650 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -15,6 +15,7 @@ import { databaseConfig } from "./configs/mikro-orm.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; import { AuthModule } from "./modules/auth/auth.module"; +import { BusinessesModule } from "./modules/businesses/businesses.module"; import { CompaniesModule } from "./modules/companies/companies.module"; import { IndustriesModule } from "./modules/industries/industries.module"; import { InvoicesModule } from "./modules/invoices/invoices.module"; @@ -23,6 +24,7 @@ import { TicketsModule } from "./modules/tickets/tickets.module"; import { UploaderModule } from "./modules/uploader/uploader.module"; import { UsersModule } from "./modules/users/users.module"; import { UtilsModule } from "./modules/utils/utils.module"; + @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, cache: true }), @@ -50,7 +52,14 @@ import { UtilsModule } from "./modules/utils/utils.module"; UploaderModule, TicketsModule, InvoicesModule, + BusinessesModule, ], + // providers: [ + // { + // provide: APP_INTERCEPTOR, + // useClass: BusinessInterceptor, + // }, + // ], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/src/common/decorators/business-id-header.decorator.ts b/src/common/decorators/business-id-header.decorator.ts new file mode 100644 index 0000000..04dc280 --- /dev/null +++ b/src/common/decorators/business-id-header.decorator.ts @@ -0,0 +1,17 @@ +import { ExecutionContext, createParamDecorator } from "@nestjs/common"; +import { FastifyRequest } from "fastify"; + +/** + * Extracts the business ID from the request header + * @example + * ```typescript + * @Get() + * findAll(@BusinessIdHeader() businessId: string) { + * // Use businessId + * } + * ``` + */ +export const BusinessIdHeader = createParamDecorator((headerName: string = "X-Business-Id", ctx: ExecutionContext) => { + const req = ctx.switchToHttp().getRequest(); + return req.headers[headerName.toLocaleLowerCase()]; +}); diff --git a/src/common/decorators/business.decorator.ts b/src/common/decorators/business.decorator.ts new file mode 100644 index 0000000..9ab45d8 --- /dev/null +++ b/src/common/decorators/business.decorator.ts @@ -0,0 +1,11 @@ +import { ExecutionContext, createParamDecorator } from "@nestjs/common"; +import { FastifyRequest } from "fastify"; + +import { Business } from "../../modules/businesses/entities/business.entity"; + +export const BusinessDec = createParamDecorator((data: keyof Business | undefined, ctx: ExecutionContext) => { + const req = ctx.switchToHttp().getRequest(); + const business = req.business; + + return data ? business?.[data] : business; +}); diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 1e86b25..5e50805 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -597,3 +597,8 @@ export const enum CompanyMessage { COMPANY_ID_REQUIRED = "شرکت مورد نیاز است", COMPANY_ID_SHOULD_BE_A_UUID = "شناسه شرکت باید یک UUID معتبر باشد", } + +export const enum BusinessMessage { + NOT_FOUND = "کسب و کار یافت نشد", + BUSINESS_ID_REQUIRED = "شناسه کسب و کار مورد نیاز است", +} diff --git a/src/configs/bullmq.config.ts b/src/configs/bullmq.config.ts index cb5889c..d14807a 100755 --- a/src/configs/bullmq.config.ts +++ b/src/configs/bullmq.config.ts @@ -10,7 +10,7 @@ export function bullMqConfig(): SharedBullAsyncConfiguration { }, prefix: "dzone", defaultJobOptions: { - removeOnComplete: false, + removeOnComplete: true, removeOnFail: false, attempts: 5, }, diff --git a/src/core/interceptors/business.interceptor.ts b/src/core/interceptors/business.interceptor.ts new file mode 100644 index 0000000..4d9604b --- /dev/null +++ b/src/core/interceptors/business.interceptor.ts @@ -0,0 +1,50 @@ +import { + BadRequestException, + CallHandler, + ExecutionContext, + ForbiddenException, + Injectable, + Logger, + NestInterceptor, +} from "@nestjs/common"; +import { FastifyRequest } from "fastify"; +import { Observable } from "rxjs"; + +import { BusinessMessage } from "../../common/enums/message.enum"; +import { Business } from "../../modules/businesses/entities/business.entity"; +import { BusinessService } from "../../modules/businesses/services/businesses.service"; + +declare module "fastify" { + interface FastifyRequest { + business?: Business; + } +} + +@Injectable() +export class BusinessInterceptor implements NestInterceptor { + private readonly headerName = "x-business-id"; + private readonly logger = new Logger(BusinessInterceptor.name); + + constructor(private readonly businessService: BusinessService) {} + + async intercept(context: ExecutionContext, next: CallHandler): Promise> { + const request = context.switchToHttp().getRequest(); + + const businessId = request.headers[this.headerName.toLocaleLowerCase()] as string; + + if (businessId) { + try { + const business = await this.businessService.getBusinessByDanakSubscriptionId(businessId); + + request.business = business; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(BusinessMessage.NOT_FOUND); + } + } else { + throw new ForbiddenException(BusinessMessage.BUSINESS_ID_REQUIRED); + } + + return next.handle(); + } +} diff --git a/src/modules/businesses/businesses.module.ts b/src/modules/businesses/businesses.module.ts new file mode 100644 index 0000000..759e00b --- /dev/null +++ b/src/modules/businesses/businesses.module.ts @@ -0,0 +1,21 @@ +import { MikroOrmModule } from "@mikro-orm/nestjs"; +import { BullModule } from "@nestjs/bullmq"; +import { Module } from "@nestjs/common"; + +import { SUBSCRIPTIONS } from "./constant"; +import { Business } from "./entities/business.entity"; +import { BusinessProvisioningProcessor } from "./queue/business-provisioning.processor"; +import { BusinessService } from "./services/businesses.service"; + +@Module({ + imports: [ + MikroOrmModule.forFeature([Business]), + BullModule.registerQueue({ + name: SUBSCRIPTIONS.PROVISIONING_QUEUE_NAME, + prefix: SUBSCRIPTIONS.PROVISIONING_QUEUE_PREFIX, + }), + ], + providers: [BusinessService, BusinessProvisioningProcessor], + exports: [BusinessService], +}) +export class BusinessesModule {} diff --git a/src/modules/businesses/constant/index.ts b/src/modules/businesses/constant/index.ts new file mode 100644 index 0000000..5a15abd --- /dev/null +++ b/src/modules/businesses/constant/index.ts @@ -0,0 +1,9 @@ +export const SUBSCRIPTIONS = Object.freeze({ + PROVISIONING_QUEUE_PREFIX: "provisioning", + PROVISIONING_QUEUE_NAME: "provisioning", + PROVISIONING_JOB_NAME: "business.created", + + // + SERVICE_SLUG: "Dzonea", + SERVICE_ID: "7e3c2f08-b7e7-4402-ae5f-fea99cff56bd", +}); diff --git a/src/modules/businesses/entities/business.entity.ts b/src/modules/businesses/entities/business.entity.ts new file mode 100644 index 0000000..66e30cc --- /dev/null +++ b/src/modules/businesses/entities/business.entity.ts @@ -0,0 +1,27 @@ +import { Cascade, Collection, Entity, EntityRepositoryType, OneToMany, Property, Unique } from "@mikro-orm/core"; + +import { BaseEntity } from "../../../common/entities/base.entity"; +import { Company } from "../../companies/entities/company.entity"; +import { Industry } from "../../industries/entities/industry.entity"; +import { BusinessRepository } from "../repositories/business.repository"; + +@Entity({ repository: () => BusinessRepository }) +export class Business extends BaseEntity { + @Unique() + @Property({ type: "uuid", nullable: false }) + danakSubscriptionId!: string; + + @Property({ type: "varchar", length: 255, nullable: false }) + name!: string; + + @Property({ type: "varchar", length: 255, nullable: true }) + logoUrl?: string; + + @OneToMany(() => Company, (company) => company.business, { cascade: [Cascade.ALL] }) + companies = new Collection(this); + + @OneToMany(() => Industry, (industry) => industry.business) + industries = new Collection(this); + + [EntityRepositoryType]?: BusinessRepository; +} diff --git a/src/modules/businesses/interfaces/IBusiness.ts b/src/modules/businesses/interfaces/IBusiness.ts new file mode 100644 index 0000000..578c70d --- /dev/null +++ b/src/modules/businesses/interfaces/IBusiness.ts @@ -0,0 +1,8 @@ +export interface IBusinessProvisioningJob { + subscriptionId: string; + serviceId: string; + serviceName: string; + businessName: string; + userId: string; + slug: string; +} diff --git a/src/modules/businesses/queue/business-provisioning.processor.ts b/src/modules/businesses/queue/business-provisioning.processor.ts new file mode 100644 index 0000000..18e7b5b --- /dev/null +++ b/src/modules/businesses/queue/business-provisioning.processor.ts @@ -0,0 +1,54 @@ +// src/business/queue/business-provisioning.processor.ts +import { EntityManager } from "@mikro-orm/postgresql"; +import { Processor } from "@nestjs/bullmq"; +import { Logger } from "@nestjs/common"; +import { Job } from "bullmq"; + +import { WorkerProcessor } from "../../../common/queues/worker.processor"; +import { SUBSCRIPTIONS } from "../constant"; +import { Business } from "../entities/business.entity"; +import { IBusinessProvisioningJob } from "../interfaces/IBusiness"; + +@Processor(SUBSCRIPTIONS.PROVISIONING_QUEUE_NAME) +export class BusinessProvisioningProcessor extends WorkerProcessor { + protected readonly logger = new Logger(BusinessProvisioningProcessor.name); + + constructor(private readonly em: EntityManager) { + super(); + } + + async process(job: Job) { + switch (job.name) { + case SUBSCRIPTIONS.PROVISIONING_JOB_NAME: + await this.handleBusinessCreated(job); + break; + default: + this.logger.warn(`Unknown job name: ${job.name}`); + } + } + + private async handleBusinessCreated(job: Job) { + const em = this.em.fork(); + const { subscriptionId, serviceId, serviceName, businessName } = job.data; + + // Only act if the event is for this service + if (serviceId !== SUBSCRIPTIONS.SERVICE_ID) { + this.logger.debug(`Skipping job for service: ${serviceName}`); + return; + } + + // Check if already exists + let business = await em.findOne(Business, { danakSubscriptionId: subscriptionId }); + if (!business) { + business = em.create(Business, { + danakSubscriptionId: subscriptionId, + name: businessName, + // logoUrl: job.data.logoUrl, // Uncomment if logoUrl is provided in job data + }); + await em.persistAndFlush(business); + this.logger.log(`Created business for subscription ${subscriptionId}`); + } else { + this.logger.debug(`Business already exists for subscription ${subscriptionId}`); + } + } +} diff --git a/src/modules/businesses/repositories/business.repository.ts b/src/modules/businesses/repositories/business.repository.ts new file mode 100644 index 0000000..8afcdc0 --- /dev/null +++ b/src/modules/businesses/repositories/business.repository.ts @@ -0,0 +1,5 @@ +import { EntityRepository } from "@mikro-orm/postgresql"; + +import { Business } from "../entities/business.entity"; + +export class BusinessRepository extends EntityRepository {} diff --git a/src/modules/businesses/services/businesses.service.ts b/src/modules/businesses/services/businesses.service.ts new file mode 100644 index 0000000..2394e44 --- /dev/null +++ b/src/modules/businesses/services/businesses.service.ts @@ -0,0 +1,16 @@ +import { BadRequestException, Injectable } from "@nestjs/common"; + +import { BusinessMessage } from "../../../common/enums/message.enum"; +import { BusinessRepository } from "../repositories/business.repository"; + +@Injectable() +export class BusinessService { + constructor(private readonly businessRepository: BusinessRepository) {} + + async getBusinessByDanakSubscriptionId(danakSubscriptionId: string) { + const business = await this.businessRepository.findOne({ danakSubscriptionId, deletedAt: null }); + if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND); + + return business; + } +} diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts index 2bf88d8..b2159c2 100644 --- a/src/modules/companies/companies.controller.ts +++ b/src/modules/companies/companies.controller.ts @@ -1,48 +1,52 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common"; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common"; import { ApiOperation } from "@nestjs/swagger"; import { CompanyListQueryDto } from "./DTO/company-list-query.dto"; import { CreateCompanyDto } from "./DTO/create-company.dto"; import { UpdateCompanyDto } from "./DTO/update-company.dto"; import { CompaniesService } from "./services/companies.service"; +import { BusinessDec } from "../../common/decorators/business.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; +import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; +import { Business } from "../businesses/entities/business.entity"; @Controller("companies") +@UseInterceptors(BusinessInterceptor) export class CompaniesController { constructor(private readonly companiesService: CompaniesService) {} @Post() @ApiOperation({ summary: "create company (admin)" }) - create(@Body() createCompanyDto: CreateCompanyDto) { - return this.companiesService.create(createCompanyDto); + create(@Body() createCompanyDto: CreateCompanyDto, @BusinessDec() business: Business) { + return this.companiesService.create(createCompanyDto, business); } @Patch(":id") @ApiOperation({ summary: "update company (admin)" }) - update(@Param() paramDto: ParamDto, @Body() updateCompanyDto: UpdateCompanyDto) { - return this.companiesService.updateCompanyById(paramDto.id, updateCompanyDto); + update(@Param() paramDto: ParamDto, @Body() updateCompanyDto: UpdateCompanyDto, @BusinessDec("id") businessId: string) { + return this.companiesService.updateCompanyById(paramDto.id, updateCompanyDto, businessId); } @Delete(":id") @ApiOperation({ summary: "delete company (admin)" }) - delete(@Param() paramDto: ParamDto) { - return this.companiesService.deleteCompanyById(paramDto.id); + delete(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { + return this.companiesService.deleteCompanyById(paramDto.id, businessId); } @Get("list") @ApiOperation({ summary: "get companies list (admin)" }) - getCompaniesList(@Query() query: CompanyListQueryDto) { - return this.companiesService.getCompaniesList(query); + getCompaniesList(@Query() query: CompanyListQueryDto, @BusinessDec("id") businessId: string) { + return this.companiesService.getCompaniesList(query, businessId); } @Get(":id") @ApiOperation({ summary: "get company by id" }) - get(@Param() paramDto: ParamDto) { - return this.companiesService.getCompanyById(paramDto.id); + get(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { + return this.companiesService.getCompanyById(paramDto.id, businessId); } @Patch(":id/toggle-status") @ApiOperation({ summary: "toggle company status (admin)" }) - toggleStatus(@Param() paramDto: ParamDto) { - return this.companiesService.toggleStatus(paramDto.id); + toggleStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { + return this.companiesService.toggleStatus(paramDto.id, businessId); } } diff --git a/src/modules/companies/companies.module.ts b/src/modules/companies/companies.module.ts index 690ed55..1d015bd 100644 --- a/src/modules/companies/companies.module.ts +++ b/src/modules/companies/companies.module.ts @@ -6,11 +6,12 @@ import { CompanyProduct } from "./entities/company-product.entity"; import { CompanyService } from "./entities/company-service.entity"; import { Company } from "./entities/company.entity"; import { CompaniesService } from "./services/companies.service"; +import { BusinessesModule } from "../businesses/businesses.module"; import { Industry } from "../industries/entities/industry.entity"; import { UsersModule } from "../users/users.module"; import { UtilsModule } from "../utils/utils.module"; @Module({ - imports: [MikroOrmModule.forFeature([Company, Industry, CompanyProduct, CompanyService]), UsersModule, UtilsModule], + imports: [MikroOrmModule.forFeature([Company, Industry, CompanyProduct, CompanyService]), UsersModule, UtilsModule, BusinessesModule], providers: [CompaniesService], controllers: [CompaniesController], exports: [CompaniesService], diff --git a/src/modules/companies/entities/company.entity.ts b/src/modules/companies/entities/company.entity.ts index be07aa8..c881c66 100644 --- a/src/modules/companies/entities/company.entity.ts +++ b/src/modules/companies/entities/company.entity.ts @@ -3,6 +3,7 @@ import { Cascade, Collection, Entity, EntityRepositoryType, Enum, Formula, ManyT import { CompanyProduct } from "./company-product.entity"; import { CompanyService } from "./company-service.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; +import { Business } from "../../businesses/entities/business.entity"; import { Industry } from "../../industries/entities/industry.entity"; import { Invoice } from "../../invoices/entities/invoice.entity"; import { User } from "../../users/entities/user.entity"; @@ -66,5 +67,8 @@ export class Company extends BaseEntity { @ManyToOne(() => User, { deleteRule: "restrict" }) user!: User; + @ManyToOne(() => Business, { deleteRule: "restrict" }) + business!: Business; + [EntityRepositoryType]?: CompanyRepository; } diff --git a/src/modules/companies/repositories/company.repository.ts b/src/modules/companies/repositories/company.repository.ts index 3cb9697..c8d14c9 100644 --- a/src/modules/companies/repositories/company.repository.ts +++ b/src/modules/companies/repositories/company.repository.ts @@ -6,11 +6,12 @@ import { Company } from "../entities/company.entity"; export class CompanyRepository extends EntityRepository { // - async getCompaniesListForAdmin(queryDto: CompanyListQueryDto) { + async getCompaniesListForAdmin(queryDto: CompanyListQueryDto, businessId: string) { const { limit, skip } = PaginationUtils(queryDto); const where: FilterQuery = { deletedAt: null, + business: { id: businessId }, }; if (queryDto.status) { diff --git a/src/modules/companies/services/companies.service.ts b/src/modules/companies/services/companies.service.ts index aee12ff..3dfbd18 100644 --- a/src/modules/companies/services/companies.service.ts +++ b/src/modules/companies/services/companies.service.ts @@ -3,6 +3,7 @@ import { BadRequestException, Injectable } from "@nestjs/common"; import dayjs from "dayjs"; import { CompanyMessage, IndustryMessage } from "../../../common/enums/message.enum"; +import { Business } from "../../businesses/entities/business.entity"; import { Industry } from "../../industries/entities/industry.entity"; import { User } from "../../users/entities/user.entity"; import { UsersService } from "../../users/services/users.service"; @@ -25,7 +26,7 @@ export class CompaniesService { private readonly em: EntityManager, ) {} - async create(createDto: CreateCompanyDto) { + async create(createDto: CreateCompanyDto, business: Business) { const em = this.em.fork(); try { await em.begin(); @@ -34,8 +35,8 @@ export class CompaniesService { if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); const [existingCompany, existIdentityNumber] = await Promise.all([ - em.findOne(Company, { name: createDto.name }), - em.findOne(Company, { identificationNumber: createDto.identificationNumber }), + em.findOne(Company, { name: createDto.name, business: { id: business.id } }), + em.findOne(Company, { identificationNumber: createDto.identificationNumber, business: { id: business.id } }), ]); if (existingCompany) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_NAME); @@ -65,6 +66,7 @@ export class CompaniesService { products, services, user, + business, }); await em.persistAndFlush(company); @@ -82,14 +84,18 @@ export class CompaniesService { //----------------------------------- - async updateCompanyById(id: string, updateDto: UpdateCompanyDto) { + async updateCompanyById(id: string, updateDto: UpdateCompanyDto, businessId: string) { const em = this.em.fork(); try { await em.begin(); const { services, products, ...rest } = updateDto; - const company = await em.findOne(Company, { id, deletedAt: null }, { populate: ["user", "services", "products"] }); + const company = await em.findOne( + Company, + { id, deletedAt: null, business: { id: businessId } }, + { populate: ["user", "services", "products"] }, + ); if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); const updatedCompany = em.assign(company, rest); @@ -101,8 +107,14 @@ export class CompaniesService { } if (updateDto.name) { - const existingCompany = await em.findOne(Company, { name: updateDto.name, deletedAt: null, id: { $ne: company.id } }); + const existingCompany = await em.findOne(Company, { + name: updateDto.name, + deletedAt: null, + id: { $ne: company.id }, + business: { id: businessId }, + }); if (existingCompany) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_NAME); + updatedCompany.name = updateDto.name; } if (updateDto.identificationNumber) { @@ -110,8 +122,10 @@ export class CompaniesService { identificationNumber: updateDto.identificationNumber, deletedAt: null, id: { $ne: company.id }, + business: { id: businessId }, }); if (existingCompany) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_IDENTITY_NUMBER); + updatedCompany.identificationNumber = updateDto.identificationNumber; } const userUpdateData: Partial = {}; @@ -174,15 +188,15 @@ export class CompaniesService { //----------------------------------- - async getCompanyById(id: string) { - const company = await this.findCompanyById(id); + async getCompanyById(id: string, businessId: string) { + const company = await this.findCompanyById(id, businessId); return { company }; } //----------------------------------- - async getCompaniesList(queryDto: CompanyListQueryDto) { - const [companies, count] = await this.companyRepository.getCompaniesListForAdmin(queryDto); + async getCompaniesList(queryDto: CompanyListQueryDto, businessId: string) { + const [companies, count] = await this.companyRepository.getCompaniesListForAdmin(queryDto, businessId); return { companies, count, @@ -192,8 +206,8 @@ export class CompaniesService { //----------------------------------- - async deleteCompanyById(id: string) { - const company = await this.findCompanyById(id); + async deleteCompanyById(id: string, businessId: string) { + const company = await this.findCompanyById(id, businessId); company.deletedAt = dayjs().toDate(); @@ -205,8 +219,8 @@ export class CompaniesService { //----------------------------------- - async toggleStatus(id: string) { - const company = await this.findCompanyById(id); + async toggleStatus(id: string, businessId: string) { + const company = await this.findCompanyById(id, businessId); company.isActive = !company.isActive; @@ -231,9 +245,9 @@ export class CompaniesService { return company; } //----------------------------------- - private async findCompanyById(id: string) { + private async findCompanyById(id: string, businessId: string) { const company = await this.companyRepository.findOne( - { id, deletedAt: null }, + { id, deletedAt: null, business: { id: businessId } }, { populate: ["user", "industry", "services", "products"] }, ); if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); diff --git a/src/modules/industries/entities/industry.entity.ts b/src/modules/industries/entities/industry.entity.ts index 6c494ba..f19174c 100644 --- a/src/modules/industries/entities/industry.entity.ts +++ b/src/modules/industries/entities/industry.entity.ts @@ -1,6 +1,7 @@ -import { Collection, Entity, EntityRepositoryType, Formula, OneToMany, Opt, Property } from "@mikro-orm/core"; +import { Collection, Entity, EntityRepositoryType, Formula, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core"; import { BaseEntity } from "../../../common/entities/base.entity"; +import { Business } from "../../businesses/entities/business.entity"; import { Company } from "../../companies/entities/company.entity"; import { IndustryRepository } from "../repositories/industry.repository"; @@ -21,5 +22,8 @@ export class Industry extends BaseEntity { @OneToMany(() => Company, (company) => company.industry) companies = new Collection(this); + @ManyToOne(() => Business, { deleteRule: "restrict" }) + business!: Business; + [EntityRepositoryType]?: IndustryRepository; } diff --git a/src/modules/industries/industries.controller.ts b/src/modules/industries/industries.controller.ts index a17db6f..1d931dd 100644 --- a/src/modules/industries/industries.controller.ts +++ b/src/modules/industries/industries.controller.ts @@ -1,49 +1,53 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common"; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common"; import { ApiOperation } from "@nestjs/swagger"; import { CreateIndustryDto } from "./DTO/create-industry.dto"; import { IndustryListQueryDto } from "./DTO/industry-list-query.dto"; import { UpdateIndustryDto } from "./DTO/update-industry.dto"; import { IndustriesService } from "./services/industries.service"; +import { BusinessDec } from "../../common/decorators/business.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; +import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; +import { Business } from "../businesses/entities/business.entity"; @Controller("industries") +@UseInterceptors(BusinessInterceptor) export class IndustriesController { constructor(private readonly industriesService: IndustriesService) {} @Post() @ApiOperation({ summary: "create industry" }) - create(@Body() createIndustryDto: CreateIndustryDto) { - return this.industriesService.create(createIndustryDto); + create(@Body() createIndustryDto: CreateIndustryDto, @BusinessDec() business: Business) { + return this.industriesService.create(createIndustryDto, business); } @Get(":id") @ApiOperation({ summary: "get industry by id" }) - getIndustryById(@Param() paramDto: ParamDto) { - return this.industriesService.getIndustriesById(paramDto.id); + getIndustryById(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { + return this.industriesService.getIndustriesById(paramDto.id, businessId); } @Patch(":id") @ApiOperation({ summary: "update industry" }) - updateIndustry(@Param() paramDto: ParamDto, @Body() updateIndustryDto: UpdateIndustryDto) { - return this.industriesService.updateIndustry(paramDto.id, updateIndustryDto); + updateIndustry(@Param() paramDto: ParamDto, @Body() updateIndustryDto: UpdateIndustryDto, @BusinessDec("id") businessId: string) { + return this.industriesService.updateIndustry(paramDto.id, updateIndustryDto, businessId); } @Delete(":id") @ApiOperation({ summary: "delete industry" }) - deleteIndustry(@Param() paramDto: ParamDto) { - return this.industriesService.deleteIndustry(paramDto.id); + deleteIndustry(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { + return this.industriesService.deleteIndustry(paramDto.id, businessId); } @Get("list") @ApiOperation({ summary: "get industries list for admin" }) - getIndustriesForAdmin(@Query() query: IndustryListQueryDto) { - return this.industriesService.getIndustriesListForAdmin(query); + getIndustriesForAdmin(@Query() query: IndustryListQueryDto, @BusinessDec("id") businessId: string) { + return this.industriesService.getIndustriesListForAdmin(query, businessId); } @Patch(":id/toggle-status") @ApiOperation({ summary: "toggle industry status" }) - toggleStatus(@Param() paramDto: ParamDto) { - return this.industriesService.toggleStatus(paramDto.id); + toggleStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { + return this.industriesService.toggleStatus(paramDto.id, businessId); } } diff --git a/src/modules/industries/industries.module.ts b/src/modules/industries/industries.module.ts index d2c1ed6..a156909 100644 --- a/src/modules/industries/industries.module.ts +++ b/src/modules/industries/industries.module.ts @@ -1,12 +1,13 @@ import { MikroOrmModule } from "@mikro-orm/nestjs"; import { Module } from "@nestjs/common"; +import { BusinessesModule } from "../businesses/businesses.module"; import { Industry } from "./entities/industry.entity"; import { IndustriesController } from "./industries.controller"; import { IndustriesService } from "./services/industries.service"; @Module({ - imports: [MikroOrmModule.forFeature([Industry])], + imports: [MikroOrmModule.forFeature([Industry]), BusinessesModule], controllers: [IndustriesController], providers: [IndustriesService], exports: [IndustriesService], diff --git a/src/modules/industries/repositories/industry.repository.ts b/src/modules/industries/repositories/industry.repository.ts index 693a4e8..0ed0abe 100644 --- a/src/modules/industries/repositories/industry.repository.ts +++ b/src/modules/industries/repositories/industry.repository.ts @@ -6,10 +6,14 @@ import { Industry } from "../entities/industry.entity"; export class IndustryRepository extends EntityRepository { // - async getIndustriesListForAdmin(queryDto: IndustryListQueryDto) { + async getIndustriesListForAdmin(queryDto: IndustryListQueryDto, businessId: string) { const { limit, skip } = PaginationUtils(queryDto); - const queryBuilder = this.createQueryBuilder("i").where({ deletedAt: null }).orderBy({ createdAt: "DESC" }).limit(limit).offset(skip); + const queryBuilder = this.createQueryBuilder("i") + .where({ deletedAt: null, business: { id: businessId } }) + .orderBy({ createdAt: "DESC" }) + .limit(limit) + .offset(skip); if (queryDto.q) { queryBuilder.andWhere({ title: { $ilike: `%${queryDto.q}%` } }); diff --git a/src/modules/industries/services/industries.service.ts b/src/modules/industries/services/industries.service.ts index 1f487f5..8be130c 100644 --- a/src/modules/industries/services/industries.service.ts +++ b/src/modules/industries/services/industries.service.ts @@ -3,6 +3,7 @@ import { BadRequestException, Injectable } from "@nestjs/common"; import dayjs from "dayjs"; import { IndustryMessage } from "../../../common/enums/message.enum"; +import { Business } from "../../businesses/entities/business.entity"; import { CreateIndustryDto } from "../DTO/create-industry.dto"; import { IndustryListQueryDto } from "../DTO/industry-list-query.dto"; import { UpdateIndustryDto } from "../DTO/update-industry.dto"; @@ -17,11 +18,14 @@ export class IndustriesService { //----------------------------------- - async create(createIndustryDto: CreateIndustryDto) { - const existingIndustry = await this.industryRepository.findOne({ title: createIndustryDto.title }); + async create(createIndustryDto: CreateIndustryDto, business: Business) { + const existingIndustry = await this.industryRepository.findOne({ + title: createIndustryDto.title, + business: { id: business.id }, + }); if (existingIndustry) throw new BadRequestException(IndustryMessage.INDUSTRY_ALREADY_EXISTS); - const industry = this.industryRepository.create(createIndustryDto); + const industry = this.industryRepository.create({ ...createIndustryDto, business }); this.em.persistAndFlush(industry); return { @@ -31,16 +35,16 @@ export class IndustriesService { } //----------------------------------- - async getIndustriesById(id: string) { - const industry = await this.industryRepository.findOne({ id, deletedAt: null }); + async getIndustriesById(id: string, businessId: string) { + const industry = await this.industryRepository.findOne({ id, deletedAt: null, business: { id: businessId } }); if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); return { industry }; } //----------------------------------- - async updateIndustry(id: string, updateIndustryDto: UpdateIndustryDto) { - const industry = await this.industryRepository.findOne({ id, deletedAt: null }); + async updateIndustry(id: string, updateIndustryDto: UpdateIndustryDto, businessId: string) { + const industry = await this.industryRepository.findOne({ id, deletedAt: null, business: { id: businessId } }); if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); this.industryRepository.assign(industry, updateIndustryDto); @@ -54,8 +58,8 @@ export class IndustriesService { //----------------------------------- - async deleteIndustry(id: string) { - const industry = await this.industryRepository.findOne({ id, deletedAt: null }); + async deleteIndustry(id: string, businessId: string) { + const industry = await this.industryRepository.findOne({ id, deletedAt: null, business: { id: businessId } }); if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); industry.deletedAt = dayjs().toDate(); @@ -68,14 +72,14 @@ export class IndustriesService { } //----------------------------------- - async getIndustriesListForAdmin(queryDto: IndustryListQueryDto) { - const [industries, count] = await this.industryRepository.getIndustriesListForAdmin(queryDto); + async getIndustriesListForAdmin(queryDto: IndustryListQueryDto, businessId: string) { + const [industries, count] = await this.industryRepository.getIndustriesListForAdmin(queryDto, businessId); return { industries, count, paginate: true }; } //----------------------------------- - async toggleStatus(id: string) { - const industry = await this.industryRepository.findOne({ id, deletedAt: null }); + async toggleStatus(id: string, businessId: string) { + const industry = await this.industryRepository.findOne({ id, deletedAt: null, business: { id: businessId } }); if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); industry.isActive = !industry.isActive;