From f755a3343ae7f9289a8adabfd47804f9750658f1 Mon Sep 17 00:00:00 2001 From: Mahyargdz Date: Mon, 19 May 2025 14:22:41 +0330 Subject: [PATCH] chore: add company request --- src/common/enums/message.enum.ts | 7 +- src/core/interceptors/business.interceptor.ts | 6 +- src/modules/auth/auth.controller.ts | 30 +-- src/modules/auth/auth.module.ts | 3 +- src/modules/auth/providers/auth.service.ts | 34 +-- .../businesses/DTO/business-slug-param.dto.ts | 11 + .../businesses/businesses.controller.ts | 14 ++ src/modules/businesses/businesses.module.ts | 8 +- .../businesses/entities/business.entity.ts | 12 ++ .../queue/business-provisioning.processor.ts | 4 +- .../businesses/services/businesses.service.ts | 9 +- .../companies/DTO/company-list-query.dto.ts | 7 + .../companies/DTO/create-company.dto.ts | 35 ++++ src/modules/companies/companies.controller.ts | 46 +++- src/modules/companies/companies.module.ts | 8 +- .../entities/company-request.enitiy.ts | 28 +++ .../companies/entities/company.entity.ts | 37 +++- .../enums/company-request-status.enum.ts | 5 + .../company-request.repository.ts | 5 + .../repositories/company.repository.ts | 24 ++- .../companies/services/companies.service.ts | 197 ++++++++++++++---- .../industries/industries.controller.ts | 6 + .../repositories/industry.repository.ts | 7 + .../industries/services/industries.service.ts | 6 + .../users/entities/refresh-token.entity.ts | 2 +- src/modules/users/entities/user.entity.ts | 21 +- src/modules/users/services/users.service.ts | 13 +- 27 files changed, 480 insertions(+), 105 deletions(-) create mode 100644 src/modules/businesses/DTO/business-slug-param.dto.ts create mode 100644 src/modules/businesses/businesses.controller.ts create mode 100644 src/modules/companies/entities/company-request.enitiy.ts create mode 100644 src/modules/companies/enums/company-request-status.enum.ts create mode 100644 src/modules/companies/repositories/company-request.repository.ts diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 1d37fcf..37aea05 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -548,6 +548,7 @@ export const enum CompanyMessage { ADDRESS_MUST_BE_STRING = "آدرس شرکت باید یک رشته باشد", ADDRESS_REQUIRED = "آدرس شرکت مورد نیاز است", CHIEF_EXECUTIVE_MUST_BE_STRING = "نام مدیرعامل باید یک رشته باشد", + CHIEF_EXECUTIVE_LENGTH = "نام مدیرعامل باید بین ۳ تا ۲۵۵ کاراکتر باشد", CHIEF_EXECUTIVE_REQUIRED = "نام مدیرعامل مورد نیاز است", COVER_IMAGE_URL_MUST_BE_URL = "آدرس تصویر کاور شرکت باید یک URL معتبر باشد", COVER_IMAGE_URL_REQUIRED = "آدرس تصویر کاور شرکت مورد نیاز است", @@ -574,7 +575,6 @@ export const enum CompanyMessage { WEBSITE_URL_MUST_BE_URL = "آدرس وبسایت شرکت باید یک URL معتبر باشد", WEBSITE_URL_REQUIRED = "آدرس وبسایت شرکت مورد نیاز است", NAME_LENGTH = "نام شرکت باید بین ۳ تا ۱۰۰ کاراکتر باشد", - CHIEF_EXECUTIVE_LENGTH = "نام مدیرعامل باید بین ۳ تا ۱۰۰ کاراکتر باشد", COMPANY_CREATED_SUCCESSFULLY = "شرکت با موفقیت ایجاد شد", COMPANY_ALREADY_EXISTS = "این شرکت قبلا ثبت شده است", COMPANY_ALREADY_EXISTS_NAME = "این نام شرکت قبلا ثبت شده است", @@ -600,9 +600,14 @@ export const enum CompanyMessage { SERVICES_MUST_BE_ARRAY = "خدمات باید آرایه باشد", COMPANY_ID_REQUIRED = "شرکت مورد نیاز است", COMPANY_ID_SHOULD_BE_A_UUID = "شناسه شرکت باید یک UUID معتبر باشد", + COMPANY_REQUEST_STATUS_REQUIRED = "وضعیت درخواست الزامی است", + COMPANY_REQUEST_STATUS_MUST_BE_ENUM = "وضعیت درخواست باید یک عنصر از جعبه انتخابی وضعیت درخواست باشد", + COMPANY_REQUEST_ALREADY_EXISTS = "درخواست شرکت قبلا ثبت شده است", } export const enum BusinessMessage { NOT_FOUND = "کسب و کار یافت نشد", BUSINESS_ID_REQUIRED = "شناسه کسب و کار مورد نیاز است", + SLUG_REQUIRED = "اسلاگ مورد نیاز است", + SLUG_MUST_BE_STRING = "اسلاگ باید یک رشته باشد", } diff --git a/src/core/interceptors/business.interceptor.ts b/src/core/interceptors/business.interceptor.ts index 4d9604b..9e48625 100644 --- a/src/core/interceptors/business.interceptor.ts +++ b/src/core/interceptors/business.interceptor.ts @@ -12,7 +12,7 @@ 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"; +import { BusinessesService } from "../../modules/businesses/services/businesses.service"; declare module "fastify" { interface FastifyRequest { @@ -25,7 +25,7 @@ export class BusinessInterceptor implements NestInterceptor { private readonly headerName = "x-business-id"; private readonly logger = new Logger(BusinessInterceptor.name); - constructor(private readonly businessService: BusinessService) {} + constructor(private readonly businessesService: BusinessesService) {} async intercept(context: ExecutionContext, next: CallHandler): Promise> { const request = context.switchToHttp().getRequest(); @@ -34,7 +34,7 @@ export class BusinessInterceptor implements NestInterceptor { if (businessId) { try { - const business = await this.businessService.getBusinessByDanakSubscriptionId(businessId); + const business = await this.businessesService.getBusinessByDanakSubscriptionId(businessId); request.business = business; } catch (error) { diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index f63012c..3682e1d 100755 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, HttpCode, HttpStatus, Patch, Post, UseGuards } from "@nestjs/common"; +import { Body, Controller, HttpCode, HttpStatus, Patch, Post, UseGuards, UseInterceptors } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; import { Throttle, ThrottlerGuard } from "@nestjs/throttler"; @@ -11,54 +11,58 @@ import { VerifyOtpDto } from "./DTO/verify-otp.dto"; import { AuthService } from "./providers/auth.service"; import { AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL, AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL } from "../../common/constants"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { BusinessDec } from "../../common/decorators/business.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; +import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; +import { Business } from "../businesses/entities/business.entity"; @ApiTags("Auth") @Controller("auth") @Throttle({ default: { limit: AUTH_THROTTLE_LIMIT, ttl: AUTH_THROTTLE_TTL } }) @UseGuards(ThrottlerGuard) +@UseInterceptors(BusinessInterceptor) export class AuthController { constructor(private readonly authService: AuthService) {} @ApiOperation({ summary: "Initiate registration" }) @HttpCode(HttpStatus.OK) @Post("register/initiate") - register(@Body() registerDto: RequestOtpDto) { - return this.authService.initiateRegistration(registerDto); + register(@Body() registerDto: RequestOtpDto, @BusinessDec("id") businessId: string) { + return this.authService.initiateRegistration(registerDto, businessId); } @ApiOperation({ summary: "complete registration ==> step 2" }) @Post("register/complete") - completeRegistration(@Body() completeRegistrationDto: CompleteRegistrationDto) { - return this.authService.completeRegistration(completeRegistrationDto); + completeRegistration(@Body() completeRegistrationDto: CompleteRegistrationDto, @BusinessDec() business: Business) { + return this.authService.completeRegistration(completeRegistrationDto, business); } @ApiOperation({ summary: "request to login with otp" }) @HttpCode(HttpStatus.OK) @Post("otp/send") - sendOtp(@Body() requestOtpDto: RequestOtpDto) { - return this.authService.requestLoginOtp(requestOtpDto); + sendOtp(@Body() requestOtpDto: RequestOtpDto, @BusinessDec("id") businessId: string) { + return this.authService.requestLoginOtp(requestOtpDto, businessId); } @ApiOperation({ summary: "verify otp for login" }) @HttpCode(HttpStatus.OK) @Post("otp/verify") - verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) { - return this.authService.verifyLoginOtp(verifyOtpDto); + verifyOtp(@Body() verifyOtpDto: VerifyOtpDto, @BusinessDec("id") businessId: string) { + return this.authService.verifyLoginOtp(verifyOtpDto, businessId); } @ApiOperation({ summary: "check if user email exist" }) @HttpCode(HttpStatus.OK) @Post("check") - checkUserExist(@Body() checkUserExistDto: CheckUserExistDto) { - return this.authService.checkUserExist(checkUserExistDto); + checkUserExist(@Body() checkUserExistDto: CheckUserExistDto, @BusinessDec("id") businessId: string) { + return this.authService.checkUserExist(checkUserExistDto, businessId); } @ApiOperation({ summary: "login with password" }) @HttpCode(HttpStatus.OK) @Post("login/password") - loginWithPassword(@Body() loginPasswordDto: LoginPasswordDTO) { - return this.authService.loginWithPassword(loginPasswordDto); + loginWithPassword(@Body() loginPasswordDto: LoginPasswordDTO, @BusinessDec("id") businessId: string) { + return this.authService.loginWithPassword(loginPasswordDto, businessId); } @AuthGuards() diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index fc45e80..0c71665 100755 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -11,9 +11,10 @@ import { NotificationModule } from "../notifications/notifications.module"; import { UsersModule } from "../users/users.module"; import { ConsoleJwtStrategy } from "./strategies/console-jwt.strategy"; import { LocalJwtStrategy } from "./strategies/local-jwt.strategy"; +import { BusinessesModule } from "../businesses/businesses.module"; @Module({ - imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig()), NotificationModule], + imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig()), NotificationModule, BusinessesModule], controllers: [AuthController], providers: [AuthService, TokensService, LocalJwtStrategy, ConsoleJwtStrategy], exports: [AuthService], diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts index 6573c78..f886f3a 100755 --- a/src/modules/auth/providers/auth.service.ts +++ b/src/modules/auth/providers/auth.service.ts @@ -3,6 +3,7 @@ import { BadRequestException, Injectable } from "@nestjs/common"; import { TokensService } from "./tokens.service"; import { AuthMessage, UserMessage } from "../../../common/enums/message.enum"; +import { Business } from "../../businesses/entities/business.entity"; import { NotificationQueue } from "../../notifications/queue/notification.queue"; import { Role } from "../../users/entities/role.entity"; import { RoleEnum } from "../../users/enums/role.enum"; @@ -15,7 +16,6 @@ import { CompleteRegistrationDto } from "../DTO/complete-register.dto"; import { CheckUserExistDto, LoginPasswordDTO } from "../DTO/loginPassword.dto"; import { RequestOtpDto } from "../DTO/request-otp.dto"; import { VerifyOtpDto } from "../DTO/verify-otp.dto"; - @Injectable() export class AuthService { constructor( @@ -29,9 +29,9 @@ export class AuthService { ) {} //****************** */ //****************** */ - async initiateRegistration(requestOtpDto: RequestOtpDto) { + async initiateRegistration(requestOtpDto: RequestOtpDto, businessId: string) { const { phone } = requestOtpDto; - const existUser = await this.usersService.findOneWithPhone(phone); + const existUser = await this.usersService.findOneWithPhone(phone, businessId); if (existUser) throw new BadRequestException(AuthMessage.PHONE_EXISTS); const existCode = await this.otpService.checkExistOtp(phone, "REGISTER"); @@ -52,7 +52,7 @@ export class AuthService { } //****************** */ //****************** */ - async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) { + async completeRegistration(completeRegistrationDto: CompleteRegistrationDto, business: Business) { const { phone, code } = completeRegistrationDto; const entityManager = this.em.fork(); @@ -67,7 +67,7 @@ export class AuthService { await this.otpService.delOtpFormCache(phone, "REGISTER"); const hashedPassword = await this.passwordService.hashPassword(completeRegistrationDto.password); - const user = await this.usersService.createUser(completeRegistrationDto, hashedPassword, entityManager); + const user = await this.usersService.createUser(completeRegistrationDto, hashedPassword, business, entityManager); const tokens = await this.tokensService.generateTokens(user, entityManager); @@ -84,10 +84,10 @@ export class AuthService { } //****************** */ //****************** */ - async loginWithPassword(loginDto: LoginPasswordDTO) { + async loginWithPassword(loginDto: LoginPasswordDTO, businessId: string) { const { email, password } = loginDto; - const user = await this.checkUserLoginCredentialWithEmail(email, password); + const user = await this.checkUserLoginCredentialWithEmail(email, password, businessId); if (this.checkUserIsAdmin(user.role)) throw new BadRequestException(AuthMessage.ADMIN_CAN_NOT_LOGIN); @@ -104,8 +104,8 @@ export class AuthService { //****************** */ //****************** */ - async checkUserExist(checkUserExistDto: CheckUserExistDto) { - const user = await this.usersService.findOneWithEmail(checkUserExistDto.email); + async checkUserExist(checkUserExistDto: CheckUserExistDto, businessId: string) { + const user = await this.usersService.findOneWithEmail(checkUserExistDto.email, businessId); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); return { message: UserMessage.USER_EXISTS }; } @@ -113,9 +113,9 @@ export class AuthService { //****************** */ //****************** */ - async requestLoginOtp(requestOtpDto: RequestOtpDto, isAdmin: boolean = false) { + async requestLoginOtp(requestOtpDto: RequestOtpDto, businessId: string, isAdmin: boolean = false) { const { phone } = requestOtpDto; - const user = await this.usersService.findOneWithPhone(phone); + const user = await this.usersService.findOneWithPhone(phone, businessId); if (!user) throw new BadRequestException(AuthMessage.PHONE_NOT_FOUND); //check the if the method call is from admin or not @@ -144,10 +144,10 @@ export class AuthService { //****************** */ //****************** */ - async verifyLoginOtp(verifyOtpDto: VerifyOtpDto) { + async verifyLoginOtp(verifyOtpDto: VerifyOtpDto, businessId: string) { const { code, phone } = verifyOtpDto; - const user = await this.checkUserLoginCredentialWithPhone(phone, code); + const user = await this.checkUserLoginCredentialWithPhone(phone, code, businessId); if (this.checkUserIsAdmin(user.role)) throw new BadRequestException(AuthMessage.ADMIN_CAN_NOT_LOGIN); @@ -193,8 +193,8 @@ export class AuthService { //****************** */ - private async checkUserLoginCredentialWithEmail(email: string, password: string) { - const user = await this.usersService.findOneWithEmail(email); + private async checkUserLoginCredentialWithEmail(email: string, password: string, businessId: string) { + const user = await this.usersService.findOneWithEmail(email, businessId); if (!user) throw new BadRequestException(AuthMessage.INVALID_PASSWORD); const passCompare = await this.passwordService.comparePassword(password, user.password); @@ -205,13 +205,13 @@ export class AuthService { //****************** */ //****************** */ - private async checkUserLoginCredentialWithPhone(phone: string, otpCode: string) { + private async checkUserLoginCredentialWithPhone(phone: string, otpCode: string, businessId: string) { const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN"); if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); await this.otpService.delOtpFormCache(phone, "LOGIN"); - const user = await this.usersService.findOneWithPhone(phone); + const user = await this.usersService.findOneWithPhone(phone, businessId); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); return user; diff --git a/src/modules/businesses/DTO/business-slug-param.dto.ts b/src/modules/businesses/DTO/business-slug-param.dto.ts new file mode 100644 index 0000000..7ed4553 --- /dev/null +++ b/src/modules/businesses/DTO/business-slug-param.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; + +import { BusinessMessage } from "../../../common/enums/message.enum"; + +export class BusinessSlugParamDto { + @IsNotEmpty({ message: BusinessMessage.SLUG_REQUIRED }) + @IsString({ message: BusinessMessage.SLUG_MUST_BE_STRING }) + @ApiProperty({ description: "The slug of the business", example: "example-business" }) + slug: string; +} diff --git a/src/modules/businesses/businesses.controller.ts b/src/modules/businesses/businesses.controller.ts new file mode 100644 index 0000000..bd75659 --- /dev/null +++ b/src/modules/businesses/businesses.controller.ts @@ -0,0 +1,14 @@ +import { Controller, Get, Param } from "@nestjs/common"; + +import { BusinessSlugParamDto } from "./DTO/business-slug-param.dto"; +import { BusinessesService } from "./services/businesses.service"; + +@Controller("business") +export class BusinessesController { + constructor(private readonly businessesService: BusinessesService) {} + + @Get("slug/:slug") + async getBusinessBySlug(@Param() params: BusinessSlugParamDto) { + return this.businessesService.getBusinessBySlug(params.slug); + } +} diff --git a/src/modules/businesses/businesses.module.ts b/src/modules/businesses/businesses.module.ts index 759e00b..7c1932a 100644 --- a/src/modules/businesses/businesses.module.ts +++ b/src/modules/businesses/businesses.module.ts @@ -2,10 +2,11 @@ import { MikroOrmModule } from "@mikro-orm/nestjs"; import { BullModule } from "@nestjs/bullmq"; import { Module } from "@nestjs/common"; +import { BusinessesController } from "./businesses.controller"; import { SUBSCRIPTIONS } from "./constant"; import { Business } from "./entities/business.entity"; import { BusinessProvisioningProcessor } from "./queue/business-provisioning.processor"; -import { BusinessService } from "./services/businesses.service"; +import { BusinessesService } from "./services/businesses.service"; @Module({ imports: [ @@ -15,7 +16,8 @@ import { BusinessService } from "./services/businesses.service"; prefix: SUBSCRIPTIONS.PROVISIONING_QUEUE_PREFIX, }), ], - providers: [BusinessService, BusinessProvisioningProcessor], - exports: [BusinessService], + providers: [BusinessesService, BusinessProvisioningProcessor], + exports: [BusinessesService], + controllers: [BusinessesController], }) export class BusinessesModule {} diff --git a/src/modules/businesses/entities/business.entity.ts b/src/modules/businesses/entities/business.entity.ts index d777a5b..6d3c8aa 100644 --- a/src/modules/businesses/entities/business.entity.ts +++ b/src/modules/businesses/entities/business.entity.ts @@ -8,6 +8,7 @@ import { Industry } from "../../industries/entities/industry.entity"; import { Invoice } from "../../invoices/entities/invoice.entity"; import { TicketCategory } from "../../tickets/entities/ticket-category.entity"; import { Ticket } from "../../tickets/entities/ticket.entity"; +import { User } from "../../users/entities/user.entity"; import { BusinessRepository } from "../repositories/business.repository"; @Entity({ repository: () => BusinessRepository }) @@ -19,9 +20,17 @@ export class Business extends BaseEntity { @Property({ type: "varchar", length: 255, nullable: false }) name!: string; + @Property({ type: "varchar", length: 255, nullable: false }) + slug!: string; + + @Property({ type: "varchar", length: 255, nullable: true }) + domain?: string; + @Property({ type: "varchar", length: 255, nullable: true }) logoUrl?: string; + //========================= + @OneToMany(() => Company, (company) => company.business, { cascade: [Cascade.ALL] }) companies = new Collection(this); @@ -43,5 +52,8 @@ export class Business extends BaseEntity { @OneToMany(() => Criticism, (criticism) => criticism.business) criticisms = new Collection(this); + @OneToMany(() => User, (user) => user.business) + users = new Collection(this); + [EntityRepositoryType]?: BusinessRepository; } diff --git a/src/modules/businesses/queue/business-provisioning.processor.ts b/src/modules/businesses/queue/business-provisioning.processor.ts index 18e7b5b..9234d53 100644 --- a/src/modules/businesses/queue/business-provisioning.processor.ts +++ b/src/modules/businesses/queue/business-provisioning.processor.ts @@ -29,7 +29,7 @@ export class BusinessProvisioningProcessor extends WorkerProcessor { private async handleBusinessCreated(job: Job) { const em = this.em.fork(); - const { subscriptionId, serviceId, serviceName, businessName } = job.data; + const { subscriptionId, serviceId, serviceName, businessName, slug } = job.data; // Only act if the event is for this service if (serviceId !== SUBSCRIPTIONS.SERVICE_ID) { @@ -43,7 +43,7 @@ export class BusinessProvisioningProcessor extends WorkerProcessor { business = em.create(Business, { danakSubscriptionId: subscriptionId, name: businessName, - // logoUrl: job.data.logoUrl, // Uncomment if logoUrl is provided in job data + slug: slug, }); await em.persistAndFlush(business); this.logger.log(`Created business for subscription ${subscriptionId}`); diff --git a/src/modules/businesses/services/businesses.service.ts b/src/modules/businesses/services/businesses.service.ts index 2394e44..26d011b 100644 --- a/src/modules/businesses/services/businesses.service.ts +++ b/src/modules/businesses/services/businesses.service.ts @@ -4,7 +4,7 @@ import { BusinessMessage } from "../../../common/enums/message.enum"; import { BusinessRepository } from "../repositories/business.repository"; @Injectable() -export class BusinessService { +export class BusinessesService { constructor(private readonly businessRepository: BusinessRepository) {} async getBusinessByDanakSubscriptionId(danakSubscriptionId: string) { @@ -13,4 +13,11 @@ export class BusinessService { return business; } + + async getBusinessBySlug(slug: string) { + const business = await this.businessRepository.findOne({ slug, deletedAt: null }); + if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND); + + return { business }; + } } diff --git a/src/modules/companies/DTO/company-list-query.dto.ts b/src/modules/companies/DTO/company-list-query.dto.ts index d41d559..a29161c 100644 --- a/src/modules/companies/DTO/company-list-query.dto.ts +++ b/src/modules/companies/DTO/company-list-query.dto.ts @@ -38,3 +38,10 @@ export class CompanyListQueryDto extends PaginationDto { @ApiPropertyOptional({ description: "تاریخ ثبت", example: "2021-01-01" }) registrationDate?: string; } + +export class CompanyListPublicQueryDto { + @IsOptional() + @IsString({ message: CompanyMessage.INDUSTRY_ID_SHOULD_BE_UUID }) + @ApiPropertyOptional({ description: "شناسه صنعت" }) + industryId?: string; +} diff --git a/src/modules/companies/DTO/create-company.dto.ts b/src/modules/companies/DTO/create-company.dto.ts index 1e35a23..330f30c 100644 --- a/src/modules/companies/DTO/create-company.dto.ts +++ b/src/modules/companies/DTO/create-company.dto.ts @@ -6,6 +6,7 @@ import { IsBoolean, IsDateString, IsEmail, + IsEnum, IsNotEmpty, IsOptional, IsString, @@ -19,6 +20,7 @@ import { CreateCompanyProductDto } from "./create-company-product.dto"; import { CreateCompanyServiceDto } from "./create-company-service.dto"; import { CompanyMessage } from "../../../common/enums/message.enum"; import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto"; +import { CompanyRequestStatus } from "../enums/company-request-status.enum"; export class CreateCompanyDto extends PickType(CompleteRegistrationDto, ["phone", "firstName", "lastName", "password", "nationalCode"]) { @IsNotEmpty({ message: CompanyMessage.NAME_REQUIRED }) @@ -99,3 +101,36 @@ export class CreateCompanyDto extends PickType(CompleteRegistrationDto, ["phone" @ApiProperty({ description: "خدمات شرکت", type: [CreateCompanyServiceDto], required: false }) services: CreateCompanyServiceDto[]; } + +export class CreateCompanyRequestDto extends PickType(CreateCompanyDto, [ + "name", + "email", + "phone", + "identificationNumber", + "dateOfEstablishment", + "address", + "mapAddressLink", + "description", + "websiteUrl", + "profileImageUrl", + "coverImageUrl", + "industryId", + "products", + "services", +]) { + @IsNotEmpty({ message: CompanyMessage.CHIEF_EXECUTIVE_REQUIRED }) + @IsString({ message: CompanyMessage.CHIEF_EXECUTIVE_MUST_BE_STRING }) + @Length(3, 255, { message: CompanyMessage.CHIEF_EXECUTIVE_MUST_BE_STRING }) + @ApiProperty({ description: "نام مدیر عامل", example: "محمد حسین حسینی" }) + chiefExecutiveOfficer: string; +} + +export class ApproveCompanyRequestDto { + @IsNotEmpty({ message: CompanyMessage.COMPANY_REQUEST_STATUS_REQUIRED }) + @IsEnum(CompanyRequestStatus) + @ApiProperty({ enum: CompanyRequestStatus, description: "وضعیت درخواست" }) + requestStatus: CompanyRequestStatus; + + @ApiProperty({ required: false, description: "دلیل رد درخواست" }) + rejectReason?: string; +} diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts index d23ed0f..e9f3734 100644 --- a/src/modules/companies/companies.controller.ts +++ b/src/modules/companies/companies.controller.ts @@ -1,15 +1,18 @@ 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 { CompanyListPublicQueryDto, CompanyListQueryDto } from "./DTO/company-list-query.dto"; +import { ApproveCompanyRequestDto, CreateCompanyDto, CreateCompanyRequestDto } from "./DTO/create-company.dto"; import { UpdateCompanyDto } from "./DTO/update-company.dto"; import { CompaniesService } from "./services/companies.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { BusinessDec } from "../../common/decorators/business.decorator"; +import { UserDec } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; import { Business } from "../businesses/entities/business.entity"; +import { User } from "../users/entities/user.entity"; + @Controller("companies") @UseInterceptors(BusinessInterceptor) export class CompaniesController { @@ -43,6 +46,18 @@ export class CompaniesController { return this.companiesService.getCompaniesList(query, businessId); } + @Get("list/public") + @ApiOperation({ summary: "get companies list (public)" }) + getCompaniesListPublic(@Query() queryDto: CompanyListPublicQueryDto, @BusinessDec("id") businessId: string) { + return this.companiesService.getCompaniesListPublic(queryDto, businessId); + } + + @Get("public/:id") + @ApiOperation({ summary: "get company by id (public)" }) + getCompanyByIdPublic(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { + return this.companiesService.getCompanyByIdPublic(paramDto.id, businessId); + } + @Get(":id") @AuthGuards() @ApiOperation({ summary: "get company by id" }) @@ -56,4 +71,31 @@ export class CompaniesController { toggleStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { return this.companiesService.toggleStatus(paramDto.id, businessId); } + + @Post("/company-requests") + @AuthGuards() + @ApiOperation({ summary: "submit company registration request (user)" }) + createCompanyRequest(@Body() createDto: CreateCompanyRequestDto, @UserDec() user: User, @BusinessDec() business: Business) { + return this.companiesService.createCompanyRequest(createDto, user, business); + } + + @Get("/company-requests/user") + @ApiOperation({ summary: "get all company registration requests (user)" }) + getCompanyRequestsUser(@UserDec("id") userId: string, @BusinessDec("id") businessId: string) { + return this.companiesService.getCompanyRequestsUser(userId, businessId); + } + + @Get("/company-requests") + @AuthGuards() + @ApiOperation({ summary: "get all company registration requests (admin)" }) + getCompanyRequests(@BusinessDec("id") businessId: string) { + return this.companiesService.getCompanyRequests(businessId); + } + + @Patch("/company-requests/:id/approve") + @AuthGuards() + @ApiOperation({ summary: "approve/reject company registration request (admin)" }) + approveCompanyRequest(@Param() paramDto: ParamDto, @Body() approveDto: ApproveCompanyRequestDto, @BusinessDec("id") businessId: string) { + return this.companiesService.approveCompanyRequest(paramDto.id, approveDto, businessId); + } } diff --git a/src/modules/companies/companies.module.ts b/src/modules/companies/companies.module.ts index 1d015bd..3c6e783 100644 --- a/src/modules/companies/companies.module.ts +++ b/src/modules/companies/companies.module.ts @@ -3,6 +3,7 @@ import { Module } from "@nestjs/common"; import { CompaniesController } from "./companies.controller"; import { CompanyProduct } from "./entities/company-product.entity"; +import { CompanyRequest } from "./entities/company-request.enitiy"; import { CompanyService } from "./entities/company-service.entity"; import { Company } from "./entities/company.entity"; import { CompaniesService } from "./services/companies.service"; @@ -11,7 +12,12 @@ 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, BusinessesModule], + imports: [ + MikroOrmModule.forFeature([Company, Industry, CompanyProduct, CompanyService, CompanyRequest]), + UsersModule, + UtilsModule, + BusinessesModule, + ], providers: [CompaniesService], controllers: [CompaniesController], exports: [CompaniesService], diff --git a/src/modules/companies/entities/company-request.enitiy.ts b/src/modules/companies/entities/company-request.enitiy.ts new file mode 100644 index 0000000..d1eb50d --- /dev/null +++ b/src/modules/companies/entities/company-request.enitiy.ts @@ -0,0 +1,28 @@ +import { Entity, EntityRepositoryType, Enum, ManyToOne, Opt, Property } from "@mikro-orm/core"; + +import { Company } from "./company.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { Business } from "../../businesses/entities/business.entity"; +import { User } from "../../users/entities/user.entity"; +import { CompanyRequestStatus } from "../enums/company-request-status.enum"; +import { CompanyRequestRepository } from "../repositories/company-request.repository"; + +@Entity({ repository: () => CompanyRequestRepository }) +export class CompanyRequest extends BaseEntity { + @Property({ type: "text", nullable: true }) + rejectReason?: string; + + @Enum({ items: () => CompanyRequestStatus, nativeEnumName: "company_request_status", default: CompanyRequestStatus.PENDING }) + requestStatus!: CompanyRequestStatus & Opt; + + @ManyToOne(() => Company, { nullable: false }) + company!: Company; + + @ManyToOne(() => Business, { nullable: false }) + business!: Business; + + @ManyToOne(() => User, { nullable: false }) + user!: User; + + [EntityRepositoryType]?: CompanyRequestRepository; +} diff --git a/src/modules/companies/entities/company.entity.ts b/src/modules/companies/entities/company.entity.ts index c881c66..7aedccf 100644 --- a/src/modules/companies/entities/company.entity.ts +++ b/src/modules/companies/entities/company.entity.ts @@ -1,4 +1,16 @@ -import { Cascade, Collection, Entity, EntityRepositoryType, Enum, Formula, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core"; +import { + Cascade, + Collection, + Entity, + EntityRepositoryType, + Enum, + Formula, + ManyToOne, + OneToMany, + OneToOne, + Opt, + Property, +} from "@mikro-orm/core"; import { CompanyProduct } from "./company-product.entity"; import { CompanyService } from "./company-service.entity"; @@ -15,6 +27,15 @@ export class Company extends BaseEntity { @Property({ type: "varchar", length: 255, unique: true }) name!: string; + @Property({ type: "varchar", length: 255 }) + chiefExecutiveOfficer!: string; + + @Property({ type: "varchar", length: 255, unique: true }) + email!: string; + + @Property({ type: "varchar", length: 255, unique: true }) + phone!: string; + @Property({ type: "varchar", length: 255, unique: true }) identificationNumber!: string; @@ -45,6 +66,9 @@ export class Company extends BaseEntity { @Enum({ items: () => CompanyStatus, nativeEnumName: "company_status", nullable: false }) status!: CompanyStatus; + @Formula((alias) => `(SELECT COUNT(*)::int FROM invoice i WHERE i.company_id = ${alias}.id)`, { persist: false }) + invoiceCount?: number; + //----------------------------------- @OneToMany(() => CompanyProduct, (product) => product.company, { cascade: [Cascade.ALL] }) @@ -56,19 +80,18 @@ export class Company extends BaseEntity { @OneToMany(() => Invoice, (invoice) => invoice.company, { cascade: [Cascade.ALL] }) invoices = new Collection(this); - @Formula((alias) => `(SELECT COUNT(*)::int FROM invoice i WHERE i.company_id = ${alias}.id)`, { persist: false }) - invoiceCount?: number; - //----------------------------------- @ManyToOne(() => Industry, { deleteRule: "restrict" }) industry!: Industry; - @ManyToOne(() => User, { deleteRule: "restrict" }) - user!: User; - @ManyToOne(() => Business, { deleteRule: "restrict" }) business!: Business; + //----------------------------------- + + @OneToOne(() => User, (user) => user.company, { owner: true }) + user!: User; + [EntityRepositoryType]?: CompanyRepository; } diff --git a/src/modules/companies/enums/company-request-status.enum.ts b/src/modules/companies/enums/company-request-status.enum.ts new file mode 100644 index 0000000..8f09b52 --- /dev/null +++ b/src/modules/companies/enums/company-request-status.enum.ts @@ -0,0 +1,5 @@ +export enum CompanyRequestStatus { + PENDING = "PENDING", + APPROVED = "APPROVED", + REJECTED = "REJECTED", +} diff --git a/src/modules/companies/repositories/company-request.repository.ts b/src/modules/companies/repositories/company-request.repository.ts new file mode 100644 index 0000000..5be9d81 --- /dev/null +++ b/src/modules/companies/repositories/company-request.repository.ts @@ -0,0 +1,5 @@ +import { EntityRepository } from "@mikro-orm/postgresql"; + +import { CompanyRequest } from "../entities/company-request.enitiy"; + +export class CompanyRequestRepository extends EntityRepository {} diff --git a/src/modules/companies/repositories/company.repository.ts b/src/modules/companies/repositories/company.repository.ts index c8d14c9..b1912c7 100644 --- a/src/modules/companies/repositories/company.repository.ts +++ b/src/modules/companies/repositories/company.repository.ts @@ -1,9 +1,9 @@ import { EntityRepository, FilterQuery } from "@mikro-orm/postgresql"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; -import { CompanyListQueryDto } from "../DTO/company-list-query.dto"; +import { CompanyListPublicQueryDto, CompanyListQueryDto } from "../DTO/company-list-query.dto"; import { Company } from "../entities/company.entity"; - +import { CompanyStatus } from "../enums/company-status.enum"; export class CompanyRepository extends EntityRepository { // async getCompaniesListForAdmin(queryDto: CompanyListQueryDto, businessId: string) { @@ -48,6 +48,7 @@ export class CompanyRepository extends EntityRepository { "dateOfEstablishment", "status", "invoiceCount", + "chiefExecutiveOfficer", "createdAt", "industry.id", "industry.title", @@ -58,4 +59,23 @@ export class CompanyRepository extends EntityRepository { ], }); } + + async getCompaniesListForPublic(queryDto: CompanyListPublicQueryDto, businessId: string) { + const where: FilterQuery = { + deletedAt: null, + business: { id: businessId }, + isActive: true, + status: CompanyStatus.APPROVED, + }; + + if (queryDto.industryId) { + where.industry = queryDto.industryId; + } + + return this.find(where, { + populate: ["industry"], + orderBy: { createdAt: "DESC" }, + fields: ["id", "name", "description", "profileImageUrl", "coverImageUrl", "industry.id", "industry.title", "createdAt"], + }); + } } diff --git a/src/modules/companies/services/companies.service.ts b/src/modules/companies/services/companies.service.ts index 3dfbd18..4490f15 100644 --- a/src/modules/companies/services/companies.service.ts +++ b/src/modules/companies/services/companies.service.ts @@ -8,13 +8,16 @@ import { Industry } from "../../industries/entities/industry.entity"; import { User } from "../../users/entities/user.entity"; import { UsersService } from "../../users/services/users.service"; import { PasswordService } from "../../utils/providers/password.service"; -import { CompanyListQueryDto } from "../DTO/company-list-query.dto"; -import { CreateCompanyDto } from "../DTO/create-company.dto"; +import { CompanyListPublicQueryDto, CompanyListQueryDto } from "../DTO/company-list-query.dto"; +import { ApproveCompanyRequestDto, CreateCompanyDto, CreateCompanyRequestDto } from "../DTO/create-company.dto"; import { UpdateCompanyDto } from "../DTO/update-company.dto"; import { CompanyProduct } from "../entities/company-product.entity"; +import { CompanyRequest } from "../entities/company-request.enitiy"; import { CompanyService } from "../entities/company-service.entity"; import { Company } from "../entities/company.entity"; +import { CompanyRequestStatus } from "../enums/company-request-status.enum"; import { CompanyStatus } from "../enums/company-status.enum"; +import { CompanyRequestRepository } from "../repositories/company-request.repository"; import { CompanyRepository } from "../repositories/company.repository"; @Injectable() @@ -24,6 +27,7 @@ export class CompaniesService { private readonly usersService: UsersService, private readonly passwordService: PasswordService, private readonly em: EntityManager, + private readonly companyRequestRepository: CompanyRequestRepository, ) {} async create(createDto: CreateCompanyDto, business: Business) { @@ -31,45 +35,12 @@ export class CompaniesService { try { await em.begin(); - const industry = await em.findOne(Industry, { id: createDto.industryId, deletedAt: null }); - if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); - - const [existingCompany, existIdentityNumber] = await Promise.all([ - 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); - if (existIdentityNumber) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_IDENTITY_NUMBER); - const hashedPassword = await this.passwordService.hashPassword(createDto.password); - const user = await this.usersService.createUser(createDto, hashedPassword, em); + const user = await this.usersService.createUser(createDto, hashedPassword, business, em); - const products = createDto.products.map((productData) => { - const product = new CompanyProduct(); - product.title = productData.title; - product.imageUrl = productData.imageUrl; - return product; - }); + const company = await this.createCompany(createDto, user, business, em); - const services = createDto.services.map((serviceData) => { - const service = new CompanyService(); - service.title = serviceData.title; - service.imageUrl = serviceData.imageUrl; - return service; - }); - - const company = em.create(Company, { - ...createDto, - industry, - status: CompanyStatus.APPROVED, - products, - services, - user, - business, - }); - - await em.persistAndFlush(company); + await em.flush(); await em.commit(); return { @@ -192,6 +163,19 @@ export class CompaniesService { const company = await this.findCompanyById(id, businessId); return { company }; } + //----------------------------------- + + async getCompanyByIdPublic(id: string, businessId: string) { + const company = await this.companyRepository.findOne( + { id, deletedAt: null, isActive: true, status: CompanyStatus.APPROVED, business: { id: businessId } }, + { + populate: ["industry", "user", "products", "services"], + fields: ["*", "industry.id", "industry.title", "user.id"], + }, + ); + if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); + return { company }; + } //----------------------------------- @@ -203,7 +187,12 @@ export class CompaniesService { paginate: true, }; } + //----------------------------------- + async getCompaniesListPublic(queryDto: CompanyListPublicQueryDto, businessId: string) { + const companies = await this.companyRepository.getCompaniesListForPublic(queryDto, businessId); + return { companies }; + } //----------------------------------- async deleteCompanyById(id: string, businessId: string) { @@ -253,4 +242,136 @@ export class CompaniesService { if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); return company; } + + //----------------------------------- + + async createCompanyRequest(createDto: CreateCompanyRequestDto, user: User, business: Business) { + const em = this.em.fork(); + try { + await em.begin(); + + const existRequest = await em.findOne(CompanyRequest, { + user: { id: user.id }, + business: { id: business.id }, + requestStatus: CompanyRequestStatus.PENDING, + deletedAt: null, + }); + if (existRequest) throw new BadRequestException(CompanyMessage.COMPANY_REQUEST_ALREADY_EXISTS); + + const company = await this.createCompany(createDto, user, business, em); + company.status = CompanyStatus.PENDING; + + const companyRequest = em.create(CompanyRequest, { + user, + business, + company, + }); + + await em.persistAndFlush(companyRequest); + + await em.flush(); + await em.commit(); + return { message: CompanyMessage.COMPANY_CREATED_SUCCESSFULLY, companyId: company.id, requestId: companyRequest.id }; + } catch (error) { + await em.rollback(); + throw error; + } + } + + //----------------------------------- + + async getCompanyRequestsUser(userId: string, businessId: string) { + const requests = await this.companyRequestRepository.find( + { user: { id: userId }, business: { id: businessId }, deletedAt: null }, + { populate: ["company"], orderBy: { createdAt: "DESC" } }, + ); + return { requests }; + } + + //----------------------------------- + + async getCompanyRequests(businessId: string) { + const requests = await this.companyRequestRepository.find( + { business: { id: businessId }, deletedAt: null }, + { + populate: ["user", "company"], + orderBy: { createdAt: "DESC" }, + }, + ); + return { requests }; + } + + //----------------------------------- + + async approveCompanyRequest(id: string, dto: ApproveCompanyRequestDto, businessId: string) { + const em = this.em.fork(); + try { + await em.begin(); + const request = await em.findOneOrFail(CompanyRequest, { id, business: { id: businessId } }, { populate: ["company"] }); + + request.requestStatus = dto.requestStatus; + + if (dto.requestStatus === CompanyRequestStatus.REJECTED) { + request.rejectReason = dto.rejectReason || ""; + request.company.status = CompanyStatus.REJECTED; + } else { + request.rejectReason = undefined; + request.company.status = CompanyStatus.APPROVED; + } + + await em.persistAndFlush(request); + await em.flush(); + await em.commit(); + return { message: CompanyMessage.UPDATED_SUCCESSFULLY, status: request.requestStatus }; + } catch (error) { + await em.rollback(); + throw error; + } + } + + //----------------------------------- + private async createCompany(createDto: CreateCompanyDto | CreateCompanyRequestDto, user: User, business: Business, em: EntityManager) { + const industry = await em.findOne(Industry, { id: createDto.industryId, deletedAt: null }); + if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); + + const [existingCompany, existIdentityNumber] = await Promise.all([ + 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); + if (existIdentityNumber) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_IDENTITY_NUMBER); + + const products = createDto.products.map((productData) => { + const product = new CompanyProduct(); + product.title = productData.title; + product.imageUrl = productData.imageUrl; + return product; + }); + + const services = createDto.services.map((serviceData) => { + const service = new CompanyService(); + service.title = serviceData.title; + service.imageUrl = serviceData.imageUrl; + return service; + }); + + const company = em.create(Company, { + ...createDto, + industry, + status: CompanyStatus.APPROVED, + products, + services, + user, + business, + chiefExecutiveOfficer: + "chiefExecutiveOfficer" in createDto ? createDto.chiefExecutiveOfficer : `${createDto.firstName} ${createDto.lastName}`, + email: createDto.email, + phone: createDto.phone, + }); + + await em.persistAndFlush(company); + + return company; + } } diff --git a/src/modules/industries/industries.controller.ts b/src/modules/industries/industries.controller.ts index effd307..6fe0263 100644 --- a/src/modules/industries/industries.controller.ts +++ b/src/modules/industries/industries.controller.ts @@ -50,6 +50,12 @@ export class IndustriesController { return this.industriesService.getIndustriesListForAdmin(query, businessId); } + @Get("list/public") + @ApiOperation({ summary: "get industries list for public" }) + getIndustriesForPublic(@BusinessDec("id") businessId: string) { + return this.industriesService.getIndustriesListForPublic(businessId); + } + @Patch(":id/toggle-status") @AuthGuards() @ApiOperation({ summary: "toggle industry status" }) diff --git a/src/modules/industries/repositories/industry.repository.ts b/src/modules/industries/repositories/industry.repository.ts index 0ed0abe..772c58c 100644 --- a/src/modules/industries/repositories/industry.repository.ts +++ b/src/modules/industries/repositories/industry.repository.ts @@ -25,4 +25,11 @@ export class IndustryRepository extends EntityRepository { return queryBuilder.getResultAndCount(); } + //----------------------------------- + async getIndustriesListForPublic(businessId: string) { + return this.find( + { deletedAt: null, business: { id: businessId }, isActive: true }, + { orderBy: { createdAt: "DESC" }, fields: ["id", "title", "iconUrl", "createdAt"] }, + ); + } } diff --git a/src/modules/industries/services/industries.service.ts b/src/modules/industries/services/industries.service.ts index 8be130c..1c32ca4 100644 --- a/src/modules/industries/services/industries.service.ts +++ b/src/modules/industries/services/industries.service.ts @@ -78,6 +78,12 @@ export class IndustriesService { } //----------------------------------- + async getIndustriesListForPublic(businessId: string) { + const industries = await this.industryRepository.getIndustriesListForPublic(businessId); + return { industries }; + } + //----------------------------------- + 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); diff --git a/src/modules/users/entities/refresh-token.entity.ts b/src/modules/users/entities/refresh-token.entity.ts index df46a21..f00aa7b 100644 --- a/src/modules/users/entities/refresh-token.entity.ts +++ b/src/modules/users/entities/refresh-token.entity.ts @@ -8,7 +8,7 @@ export class RefreshToken extends BaseEntity { @Property({ type: "varchar", length: 255 }) token!: string; - @ManyToOne(() => User, { deleteRule: "restrict" }) + @ManyToOne(() => User, { deleteRule: "cascade" }) user!: User; @Property({ type: "timestamptz", nullable: true }) diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 2c2193b..4ebfea9 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,13 +1,15 @@ -import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core"; +import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, OneToOne, Opt, Property, Unique } from "@mikro-orm/core"; import { RefreshToken } from "./refresh-token.entity"; import { Role } from "./role.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { UserAnnouncement } from "../../announcements/entities/user-announcement.entity"; +import { Business } from "../../businesses/entities/business.entity"; import { Company } from "../../companies/entities/company.entity"; import { UserRepository } from "../repositories/user.repository"; @Entity({ repository: () => UserRepository }) +@Unique({ properties: ["phone", "business", "userName", "nationalCode"] }) export class User extends BaseEntity { @Property({ type: "varchar", length: 150 }) firstName!: string; @@ -18,10 +20,10 @@ export class User extends BaseEntity { @Property({ type: "varchar", length: 150, nullable: true }) email?: string; - @Property({ type: "varchar", length: 11, unique: true, nullable: false }) + @Property({ type: "varchar", length: 11, nullable: false }) phone!: string; - @Property({ type: "varchar", length: 50, unique: true, nullable: true }) + @Property({ type: "varchar", length: 50, nullable: true }) userName!: string; @Property({ type: "varchar", length: 150 }) @@ -30,7 +32,7 @@ export class User extends BaseEntity { @Property({ type: "varchar", length: 12, nullable: true }) birthDate?: string; - @Property({ type: "varchar", length: 100, unique: true, nullable: true }) + @Property({ type: "varchar", length: 100, nullable: true }) nationalCode?: string; @Property({ type: "varchar", length: 100, nullable: true }) @@ -49,14 +51,19 @@ export class User extends BaseEntity { @ManyToOne(() => Role, { deleteRule: "restrict" }) role!: Role; + @ManyToOne(() => Business, { deleteRule: "restrict" }) + business!: Business; + + //----------------------------------- + + @OneToOne(() => Company, (company) => company.user, { orphanRemoval: true }) + company!: Company & Opt; + //----------------------------------- @OneToMany(() => RefreshToken, (token) => token.user) refreshTokens = new Collection(this); - @OneToMany(() => Company, (company) => company.user) - companies = new Collection(this); - @OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.user) userAnnouncements = new Collection(this); diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index 6fa82ce..83f588a 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -4,12 +4,12 @@ import slugify from "slugify"; import { UserMessage } from "../../../common/enums/message.enum"; import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto"; +import { Business } from "../../businesses/entities/business.entity"; import { CreateCompanyDto } from "../../companies/DTO/create-company.dto"; import { Role } from "../entities/role.entity"; import { User } from "../entities/user.entity"; import { RoleEnum } from "../enums/role.enum"; import { UserRepository } from "../repositories/user.repository"; - @Injectable() export class UsersService { constructor( @@ -19,14 +19,14 @@ export class UsersService { /*******************************/ - async findOneWithEmail(email: string) { - const user = await this.userRepository.findOne({ email }, { populate: ["role"] }); + async findOneWithEmail(email: string, businessId: string) { + const user = await this.userRepository.findOne({ email, business: { id: businessId } }, { populate: ["role"] }); return user; } /*******************************/ - async findOneWithPhone(phone: string) { - const user = await this.userRepository.findOne({ phone }, { populate: ["role"] }); + async findOneWithPhone(phone: string, businessId: string) { + const user = await this.userRepository.findOne({ phone, business: { id: businessId } }, { populate: ["role"] }); return user; } /*******************************/ @@ -85,7 +85,7 @@ export class UsersService { } /*******************************/ - async createUser(registerDto: CompleteRegistrationDto | CreateCompanyDto, hashedPassword: string, em: EntityManager) { + async createUser(registerDto: CompleteRegistrationDto | CreateCompanyDto, hashedPassword: string, business: Business, em: EntityManager) { const [role, existPhone, existUser] = await Promise.all([ em.findOne(Role, { name: RoleEnum.USER }), em.findOne(User, { phone: registerDto.phone }), @@ -108,6 +108,7 @@ export class UsersService { userName, password: hashedPassword, role, + business, }); await em.persistAndFlush(user);