From 77c0f595145178e660503b6662fa88eb107fc59e Mon Sep 17 00:00:00 2001 From: Mahyargdz Date: Sun, 11 May 2025 16:45:26 +0330 Subject: [PATCH] update: add company services and products --- .github/workflows/deploy.yaml | 4 +- src/common/enums/message.enum.ts | 59 +++++- .../companies/DTO/company-list-query.dto.ts | 40 +++++ .../DTO/create-company-product.dto.ts | 17 ++ .../DTO/create-company-service.dto.ts | 17 ++ .../companies/DTO/create-company.dto.ts | 101 +++++++++++ .../companies/DTO/update-company.dto.ts | 5 + src/modules/companies/companies.controller.ts | 48 ++++- src/modules/companies/companies.module.ts | 8 +- .../entities/company-product.entity.ts | 16 ++ .../entities/company-service.entity.ts | 16 ++ .../companies/entities/company.entity.ts | 31 ++-- .../companies/enums/company-status.enum.ts | 5 + .../repositories/company.repository.ts | 22 ++- .../companies/services/companies.service.ts | 168 +++++++++++++++++- .../repositories/industry.repository.ts | 2 +- src/modules/users/entities/user.entity.ts | 16 +- src/modules/users/services/users.service.ts | 46 ++++- 18 files changed, 588 insertions(+), 33 deletions(-) create mode 100644 src/modules/companies/DTO/company-list-query.dto.ts create mode 100644 src/modules/companies/DTO/create-company-product.dto.ts create mode 100644 src/modules/companies/DTO/create-company-service.dto.ts create mode 100644 src/modules/companies/DTO/create-company.dto.ts create mode 100644 src/modules/companies/DTO/update-company.dto.ts create mode 100644 src/modules/companies/entities/company-product.entity.ts create mode 100644 src/modules/companies/entities/company-service.entity.ts create mode 100644 src/modules/companies/enums/company-status.enum.ts diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 69942f6..ff0eee3 100755 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -10,8 +10,8 @@ jobs: env: DANAK_SERVER: "https://captain.dev.danakcorp.com" - APP_TOKEN: 1b5da4107e67bb72c17bcecdaea51e6ccd0c545a4e7ffbcf4fd7e2c83bfe24b0 - APP_NAME: danak-api + APP_TOKEN: d33544dfb7c716effb5cadb16906aaab130f2573e2c431a5b194bef770297255 + APP_NAME: danak-zone-api GITHUB_TOKEN: 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 steps: diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 120ad8d..cf67e57 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -122,6 +122,7 @@ export const enum CommonMessage { SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد", UPDATED = "با موفقیت آپدیت شد", IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت باید یکی از مقادیر ۰ و ۱ باشد", + DATE_MUST_BE_DATE = "تاریخ باید یک تاریخ معتبر به صورت رشته باشد", } export const enum CategoryMessage { @@ -476,7 +477,7 @@ export const enum InvoiceMessage { INVALID_DISCOUNT_CODE = "کد تخفیف نامعتبر است", ORIGINAL_PRICE_NOT_FOUND = "قیمت اصلی صورت حساب یافت نشد", NOT_FOUND_BY_ID_OR_NOT_BELONG_TO_USER = "صورت حساب یافت نشد یا متعلق به کاربر نمی باشد", - DISCOUNT_ALREADY_APPLIED = "ت`خفیف قبلا برای این صورت حساب اعمال شده است", + DISCOUNT_ALREADY_APPLIED = "تخفیف قبلا برای این صورت حساب اعمال شده است", DISCOUNT_EXPIRED = "تخفیف منقضی شده است", MAX_RECURRING_CYCLES_MUST_BE_POSITIVE = "تعداد تکرار باید بزرگتر از ۰ باشد", ITEMS_REQUIRED = "آیتم‌ها مورد نیاز است", @@ -538,3 +539,59 @@ export const enum IndustryMessage { UPDATED_SUCCESSFULLY = "صنعت با موفقیت به روز رسانی شد", INDUSTRY_DELETED_SUCCESSFULLY = "صنعت با موفقیت حذف شد", } + +export const enum CompanyMessage { + ADDRESS_MUST_BE_STRING = "آدرس شرکت باید یک رشته باشد", + ADDRESS_REQUIRED = "آدرس شرکت مورد نیاز است", + CHIEF_EXECUTIVE_MUST_BE_STRING = "نام مدیرعامل باید یک رشته باشد", + CHIEF_EXECUTIVE_REQUIRED = "نام مدیرعامل مورد نیاز است", + COVER_IMAGE_URL_MUST_BE_URL = "آدرس تصویر کاور شرکت باید یک URL معتبر باشد", + COVER_IMAGE_URL_REQUIRED = "آدرس تصویر کاور شرکت مورد نیاز است", + DATE_OF_ESTABLISHMENT_MUST_BE_DATE = "تاریخ تاسیس شرکت باید یک تاریخ معتبر باشد", + DATE_OF_ESTABLISHMENT_REQUIRED = "تاریخ تاسیس شرکت مورد نیاز است", + DESCRIPTION_MUST_BE_STRING = "توضیحات شرکت باید یک رشته باشد", + DESCRIPTION_REQUIRED = "توضیحات شرکت مورد نیاز است", + EMAIL_MUST_BE_VALID = "ایمیل شرکت باید معتبر باشد", + EMAIL_REQUIRED = "ایمیل شرکت مورد نیاز است", + IDENTIFICATION_NUMBER_MUST_BE_STRING = "شماره ثبت شرکت باید یک رشته باشد", + IDENTIFICATION_NUMBER_REQUIRED = "شماره ثبت شرکت مورد نیاز است", + INDUSTRY_ID_REQUIRED = "صنعت مورد نیاز است", + INDUSTRY_ID_SHOULD_BE_UUID = "شناسه صنعت باید یک UUID معتبر باشد", + IS_ACTIVE_MUST_BE_BOOLEAN = "وضعیت فعالیت شرکت باید یک مقدار بولین باشد", + IS_ACTIVE_REQUIRED = "وضعیت فعالیت شرکت مورد نیاز است", + MAP_ADDRESS_LINK_MUST_BE_URL = "لینک نقشه شرکت باید یک URL معتبر باشد", + MAP_ADDRESS_LINK_REQUIRED = "لینک نقشه شرکت مورد نیاز است", + NAME_MUST_BE_STRING = "نام شرکت باید یک رشته باشد", + NAME_REQUIRED = "نام شرکت مورد نیاز است", + PHONE_MUST_BE_STRING = "شماره تلفن شرکت باید یک رشته باشد", + PHONE_REQUIRED = "شماره تلفن شرکت مورد نیاز است", + PROFILE_IMAGE_URL_MUST_BE_URL = "آدرس تصویر پروفایل شرکت باید یک URL معتبر باشد", + PROFILE_IMAGE_URL_REQUIRED = "آدرس تصویر پروفایل شرکت مورد نیاز است", + WEBSITE_URL_MUST_BE_URL = "آدرس وبسایت شرکت باید یک URL معتبر باشد", + WEBSITE_URL_REQUIRED = "آدرس وبسایت شرکت مورد نیاز است", + NAME_LENGTH = "نام شرکت باید بین ۳ تا ۱۰۰ کاراکتر باشد", + CHIEF_EXECUTIVE_LENGTH = "نام مدیرعامل باید بین ۳ تا ۱۰۰ کاراکتر باشد", + COMPANY_CREATED_SUCCESSFULLY = "شرکت با موفقیت ایجاد شد", + COMPANY_ALREADY_EXISTS = "این شرکت قبلا ثبت شده است", + COMPANY_ALREADY_EXISTS_NAME = "این نام شرکت قبلا ثبت شده است", + COMPANY_ALREADY_EXISTS_PHONE = "این شماره تلفن قبلا ثبت شده است", + COMPANY_ALREADY_EXISTS_EMAIL = "این ایمیل قبلا ثبت شده است", + COMPANY_ALREADY_EXISTS_IDENTITY_NUMBER = "این شماره ثبت شرکت قبلا ثبت شده است", + COMPANY_NOT_FOUND = "شرکت یافت نشد", + UPDATED_SUCCESSFULLY = "با موفقیت به روز رسانی شد", + DELETED_SUCCESSFULLY = "با موفقیت حذف شد", + PRODUCT_TITLE_REQUIRED = "عنوان محصول الزامی است", + PRODUCT_TITLE_MUST_BE_STRING = "عنوان محصول باید رشته باشد", + PRODUCT_IMAGES_REQUIRED = "تصاویر محصول الزامی است", + PRODUCT_IMAGES_MUST_BE_ARRAY = "تصاویر محصول باید آرایه باشد", + PRODUCT_IMAGES_MUST_BE_URL = "تصاویر محصول باید آدرس معتبر باشد", + SERVICE_TITLE_REQUIRED = "عنوان خدمت الزامی است", + SERVICE_TITLE_MUST_BE_STRING = "عنوان خدمت باید رشته باشد", + SERVICE_IMAGES_REQUIRED = "تصاویر خدمت الزامی است", + SERVICE_IMAGES_MUST_BE_ARRAY = "تصاویر خدمت باید آرایه باشد", + SERVICE_IMAGES_MUST_BE_URL = "تصاویر خدمت باید آدرس معتبر باشد", + PRODUCTS_REQUIRED = "محصولات الزامی است", + SERVICES_REQUIRED = "خدمات الزامی است", + PRODUCTS_MUST_BE_ARRAY = "محصولات باید آرایه باشد", + SERVICES_MUST_BE_ARRAY = "خدمات باید آرایه باشد", +} diff --git a/src/modules/companies/DTO/company-list-query.dto.ts b/src/modules/companies/DTO/company-list-query.dto.ts new file mode 100644 index 0000000..d41d559 --- /dev/null +++ b/src/modules/companies/DTO/company-list-query.dto.ts @@ -0,0 +1,40 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsDateString, IsEnum, IsIn, IsOptional, IsString } from "class-validator"; + +import { PaginationDto } from "../../../common/DTO/pagination.dto"; +import { CommonMessage, CompanyMessage } from "../../../common/enums/message.enum"; +import { CompanyStatus } from "../enums/company-status.enum"; + +export class CompanyListQueryDto extends PaginationDto { + @IsOptional() + @IsString({ message: CompanyMessage.NAME_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "نام شرکت" }) + name?: string; + + @IsOptional() + @IsString({ message: CompanyMessage.INDUSTRY_ID_SHOULD_BE_UUID }) + @ApiPropertyOptional({ description: "شناسه صنعت" }) + industryId?: string; + + @IsOptional() + @Type(() => Number) + @IsIn([1, 0], { message: CommonMessage.IS_ACTIVE_SHOULD_BE_1_0 }) + @ApiPropertyOptional({ description: "ads status", example: 1 }) + isActive?: number; + + @IsOptional() + @IsEnum(CompanyStatus) + @ApiPropertyOptional({ description: "status", example: CompanyStatus.APPROVED, enum: CompanyStatus }) + status?: CompanyStatus; + + @IsOptional() + @IsString({ message: CommonMessage.SEARCH_QUERY_STRING }) + @ApiPropertyOptional({ description: "Search query", example: "search query" }) + q?: string; + + @IsOptional() + @IsDateString(undefined, { message: CommonMessage.DATE_MUST_BE_DATE }) + @ApiPropertyOptional({ description: "تاریخ ثبت", example: "2021-01-01" }) + registrationDate?: string; +} diff --git a/src/modules/companies/DTO/create-company-product.dto.ts b/src/modules/companies/DTO/create-company-product.dto.ts new file mode 100644 index 0000000..eb2ae83 --- /dev/null +++ b/src/modules/companies/DTO/create-company-product.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsArray, IsNotEmpty, IsString, IsUrl } from "class-validator"; + +import { CompanyMessage } from "../../../common/enums/message.enum"; + +export class CreateCompanyProductDto { + @IsNotEmpty({ message: CompanyMessage.PRODUCT_TITLE_REQUIRED }) + @IsString({ message: CompanyMessage.PRODUCT_TITLE_MUST_BE_STRING }) + @ApiProperty({ description: "عنوان محصول", example: "محصول نمونه" }) + title: string; + + @IsNotEmpty({ message: CompanyMessage.PRODUCT_IMAGES_REQUIRED }) + @IsArray({ message: CompanyMessage.PRODUCT_IMAGES_MUST_BE_ARRAY }) + @IsUrl({ protocols: ["http", "https"] }, { each: true, message: CompanyMessage.PRODUCT_IMAGES_MUST_BE_URL }) + @ApiProperty({ description: "تصاویر محصول", example: ["https://example.com/image1.jpg", "https://example.com/image2.jpg"] }) + imagesUrl: string[]; +} diff --git a/src/modules/companies/DTO/create-company-service.dto.ts b/src/modules/companies/DTO/create-company-service.dto.ts new file mode 100644 index 0000000..8cbdd0b --- /dev/null +++ b/src/modules/companies/DTO/create-company-service.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsArray, IsNotEmpty, IsString, IsUrl } from "class-validator"; + +import { CompanyMessage } from "../../../common/enums/message.enum"; + +export class CreateCompanyServiceDto { + @IsNotEmpty({ message: CompanyMessage.SERVICE_TITLE_REQUIRED }) + @IsString({ message: CompanyMessage.SERVICE_TITLE_MUST_BE_STRING }) + @ApiProperty({ description: "عنوان خدمت", example: "خدمت نمونه" }) + title: string; + + @IsNotEmpty({ message: CompanyMessage.SERVICE_IMAGES_REQUIRED }) + @IsArray({ message: CompanyMessage.SERVICE_IMAGES_MUST_BE_ARRAY }) + @IsUrl({ protocols: ["http", "https"] }, { each: true, message: CompanyMessage.SERVICE_IMAGES_MUST_BE_URL }) + @ApiProperty({ description: "تصاویر خدمت", example: ["https://example.com/image1.jpg", "https://example.com/image2.jpg"] }) + imagesUrl: string[]; +} diff --git a/src/modules/companies/DTO/create-company.dto.ts b/src/modules/companies/DTO/create-company.dto.ts new file mode 100644 index 0000000..1e35a23 --- /dev/null +++ b/src/modules/companies/DTO/create-company.dto.ts @@ -0,0 +1,101 @@ +import { ApiProperty, PickType } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { + ArrayMinSize, + IsArray, + IsBoolean, + IsDateString, + IsEmail, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, + IsUrl, + Length, + ValidateNested, +} from "class-validator"; + +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"; + +export class CreateCompanyDto extends PickType(CompleteRegistrationDto, ["phone", "firstName", "lastName", "password", "nationalCode"]) { + @IsNotEmpty({ message: CompanyMessage.NAME_REQUIRED }) + @Length(3, 255, { message: CompanyMessage.NAME_LENGTH }) + @IsString({ message: CompanyMessage.NAME_MUST_BE_STRING }) + @ApiProperty({ description: "نام شرکت", example: "شرکت تولیدی و صنعتی کارگر" }) + name: string; + + @IsNotEmpty({ message: CompanyMessage.EMAIL_REQUIRED }) + @IsEmail(undefined, { message: CompanyMessage.EMAIL_MUST_BE_VALID }) + @ApiProperty({ description: "ایمیل", example: "info@example.com" }) + email: string; + + @IsNotEmpty({ message: CompanyMessage.IDENTIFICATION_NUMBER_REQUIRED }) + @IsString({ message: CompanyMessage.IDENTIFICATION_NUMBER_MUST_BE_STRING }) + @ApiProperty({ description: "شماره ثبت", example: "1234567890" }) + identificationNumber: string; + + @IsNotEmpty({ message: CompanyMessage.DATE_OF_ESTABLISHMENT_REQUIRED }) + @IsDateString(undefined, { message: CompanyMessage.DATE_OF_ESTABLISHMENT_MUST_BE_DATE }) + @ApiProperty({ description: "تاریخ تاسیس", example: "2021-01-01" }) + dateOfEstablishment: string; + + @IsNotEmpty({ message: CompanyMessage.ADDRESS_REQUIRED }) + @IsString({ message: CompanyMessage.ADDRESS_MUST_BE_STRING }) + @ApiProperty({ description: "آدرس", example: "تهران، خیابان ولیعصر، پلاک 123" }) + address: string; + + @IsNotEmpty({ message: CompanyMessage.MAP_ADDRESS_LINK_REQUIRED }) + @IsUrl({ protocols: ["http", "https"] }, { message: CompanyMessage.MAP_ADDRESS_LINK_MUST_BE_URL }) + @ApiProperty({ description: "لینک آدرس", example: "https://maps.app.goo.gl/1234567890" }) + mapAddressLink: string; + + @IsNotEmpty({ message: CompanyMessage.DESCRIPTION_REQUIRED }) + @IsString({ message: CompanyMessage.DESCRIPTION_MUST_BE_STRING }) + @ApiProperty({ description: "توضیحات", example: "شرکت تولیدی و صنعتی کارگر" }) + description: string; + + @IsNotEmpty({ message: CompanyMessage.WEBSITE_URL_REQUIRED }) + @IsUrl({ protocols: ["http", "https"] }, { message: CompanyMessage.WEBSITE_URL_MUST_BE_URL }) + @ApiProperty({ description: "آدرس وبسایت", example: "https://www.example.com" }) + websiteUrl: string; + + @IsNotEmpty({ message: CompanyMessage.PROFILE_IMAGE_URL_REQUIRED }) + @IsUrl({ protocols: ["http", "https"] }, { message: CompanyMessage.PROFILE_IMAGE_URL_MUST_BE_URL }) + @ApiProperty({ description: "آدرس تصویر پروفایل", example: "https://www.example.com/profile.jpg" }) + profileImageUrl: string; + + @IsNotEmpty({ message: CompanyMessage.COVER_IMAGE_URL_REQUIRED }) + @IsUrl({ protocols: ["http", "https"] }, { message: CompanyMessage.COVER_IMAGE_URL_MUST_BE_URL }) + @ApiProperty({ description: "آدرس تصویر کاور", example: "https://www.example.com/cover.jpg" }) + coverImageUrl: string; + + @IsOptional() + @IsNotEmpty({ message: CompanyMessage.IS_ACTIVE_REQUIRED }) + @IsBoolean({ message: CompanyMessage.IS_ACTIVE_MUST_BE_BOOLEAN }) + @ApiProperty({ description: "وضعیت فعالیت", example: true }) + isActive?: boolean; + + @IsNotEmpty({ message: CompanyMessage.INDUSTRY_ID_REQUIRED }) + @IsUUID("7", { message: CompanyMessage.INDUSTRY_ID_SHOULD_BE_UUID }) + @ApiProperty({ description: "شناسه صنعت", example: "123e4567-e89b-12d3-a456-426614174000" }) + industryId: string; + + @IsNotEmpty({ message: CompanyMessage.PRODUCTS_REQUIRED }) + @IsArray({ message: CompanyMessage.PRODUCTS_MUST_BE_ARRAY }) + @ArrayMinSize(1, { message: CompanyMessage.PRODUCTS_REQUIRED }) + @ValidateNested({ each: true }) + @Type(() => CreateCompanyProductDto) + @ApiProperty({ description: "محصولات شرکت", type: [CreateCompanyProductDto], required: false }) + products: CreateCompanyProductDto[]; + + @IsNotEmpty({ message: CompanyMessage.SERVICES_REQUIRED }) + @IsArray({ message: CompanyMessage.SERVICES_MUST_BE_ARRAY }) + @ArrayMinSize(1, { message: CompanyMessage.SERVICES_REQUIRED }) + @ValidateNested({ each: true }) + @Type(() => CreateCompanyServiceDto) + @ApiProperty({ description: "خدمات شرکت", type: [CreateCompanyServiceDto], required: false }) + services: CreateCompanyServiceDto[]; +} diff --git a/src/modules/companies/DTO/update-company.dto.ts b/src/modules/companies/DTO/update-company.dto.ts new file mode 100644 index 0000000..3dd7e42 --- /dev/null +++ b/src/modules/companies/DTO/update-company.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateCompanyDto } from "./create-company.dto"; + +export class UpdateCompanyDto extends PartialType(CreateCompanyDto) {} diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts index 00b838f..2bf88d8 100644 --- a/src/modules/companies/companies.controller.ts +++ b/src/modules/companies/companies.controller.ts @@ -1,4 +1,48 @@ -import { Controller } from "@nestjs/common"; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } 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 { ParamDto } from "../../common/DTO/param.dto"; @Controller("companies") -export class CompaniesController {} +export class CompaniesController { + constructor(private readonly companiesService: CompaniesService) {} + + @Post() + @ApiOperation({ summary: "create company (admin)" }) + create(@Body() createCompanyDto: CreateCompanyDto) { + return this.companiesService.create(createCompanyDto); + } + + @Patch(":id") + @ApiOperation({ summary: "update company (admin)" }) + update(@Param() paramDto: ParamDto, @Body() updateCompanyDto: UpdateCompanyDto) { + return this.companiesService.updateCompanyById(paramDto.id, updateCompanyDto); + } + + @Delete(":id") + @ApiOperation({ summary: "delete company (admin)" }) + delete(@Param() paramDto: ParamDto) { + return this.companiesService.deleteCompanyById(paramDto.id); + } + + @Get("list") + @ApiOperation({ summary: "get companies list (admin)" }) + getCompaniesList(@Query() query: CompanyListQueryDto) { + return this.companiesService.getCompaniesList(query); + } + + @Get(":id") + @ApiOperation({ summary: "get company by id" }) + get(@Param() paramDto: ParamDto) { + return this.companiesService.getCompanyById(paramDto.id); + } + + @Patch(":id/toggle-status") + @ApiOperation({ summary: "toggle company status (admin)" }) + toggleStatus(@Param() paramDto: ParamDto) { + return this.companiesService.toggleStatus(paramDto.id); + } +} diff --git a/src/modules/companies/companies.module.ts b/src/modules/companies/companies.module.ts index f9709be..690ed55 100644 --- a/src/modules/companies/companies.module.ts +++ b/src/modules/companies/companies.module.ts @@ -2,11 +2,15 @@ import { MikroOrmModule } from "@mikro-orm/nestjs"; import { Module } from "@nestjs/common"; import { CompaniesController } from "./companies.controller"; +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 { Industry } from "../industries/entities/industry.entity"; +import { UsersModule } from "../users/users.module"; +import { UtilsModule } from "../utils/utils.module"; @Module({ - imports: [MikroOrmModule.forFeature([Company])], + imports: [MikroOrmModule.forFeature([Company, Industry, CompanyProduct, CompanyService]), UsersModule, UtilsModule], providers: [CompaniesService], controllers: [CompaniesController], exports: [CompaniesService], diff --git a/src/modules/companies/entities/company-product.entity.ts b/src/modules/companies/entities/company-product.entity.ts new file mode 100644 index 0000000..14ec9a0 --- /dev/null +++ b/src/modules/companies/entities/company-product.entity.ts @@ -0,0 +1,16 @@ +import { Entity, ManyToOne, Property } from "@mikro-orm/core"; + +import { Company } from "./company.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; + +@Entity() +export class CompanyProduct extends BaseEntity { + @Property({ type: "varchar", length: 250, nullable: false }) + title!: string; + + @Property({ type: "array", nullable: false }) + imagesUrl!: string[]; + + @ManyToOne(() => Company, { deleteRule: "cascade" }) + company!: Company; +} diff --git a/src/modules/companies/entities/company-service.entity.ts b/src/modules/companies/entities/company-service.entity.ts new file mode 100644 index 0000000..ed4c9ca --- /dev/null +++ b/src/modules/companies/entities/company-service.entity.ts @@ -0,0 +1,16 @@ +import { Entity, ManyToOne, Property } from "@mikro-orm/core"; + +import { Company } from "./company.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; + +@Entity() +export class CompanyService extends BaseEntity { + @Property({ type: "varchar", length: 250, nullable: false }) + title!: string; + + @Property({ type: "array", nullable: false }) + imagesUrl!: string[]; + + @ManyToOne(() => Company, { deleteRule: "cascade" }) + company!: Company; +} diff --git a/src/modules/companies/entities/company.entity.ts b/src/modules/companies/entities/company.entity.ts index a8ca483..99e3000 100644 --- a/src/modules/companies/entities/company.entity.ts +++ b/src/modules/companies/entities/company.entity.ts @@ -1,7 +1,11 @@ -import { Entity, EntityRepositoryType, ManyToOne, Opt, Property } from "@mikro-orm/core"; +import { Cascade, Collection, Entity, EntityRepositoryType, Enum, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core"; +import { CompanyProduct } from "./company-product.entity"; +import { CompanyService } from "./company-service.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { Industry } from "../../industries/entities/industry.entity"; +import { User } from "../../users/entities/user.entity"; +import { CompanyStatus } from "../enums/company-status.enum"; import { CompanyRepository } from "../repositories/company.repository"; @Entity({ repository: () => CompanyRepository }) @@ -9,15 +13,6 @@ export class Company extends BaseEntity { @Property({ type: "varchar", length: 255, unique: true }) name!: string; - @Property({ type: "varchar", length: 255 }) - chiefExecutive!: string; - - @Property({ type: "varchar", length: 255, unique: true }) - phone!: string; - - @Property({ type: "varchar", length: 255, unique: true }) - email!: string; - @Property({ type: "varchar", length: 255, unique: true }) identificationNumber!: string; @@ -45,8 +40,24 @@ export class Company extends BaseEntity { @Property({ type: "boolean", default: true }) isActive!: boolean & Opt; + @Enum({ items: () => CompanyStatus, nativeEnumName: "company_status", nullable: false }) + status!: CompanyStatus; + + //----------------------------------- + + @OneToMany(() => CompanyProduct, (product) => product.company, { cascade: [Cascade.ALL] }) + products = new Collection(this); + + @OneToMany(() => CompanyService, (service) => service.company, { cascade: [Cascade.ALL] }) + services = new Collection(this); + + //----------------------------------- + @ManyToOne(() => Industry, { deleteRule: "restrict" }) industry!: Industry; + @ManyToOne(() => User, { deleteRule: "restrict" }) + user!: User; + [EntityRepositoryType]?: CompanyRepository; } diff --git a/src/modules/companies/enums/company-status.enum.ts b/src/modules/companies/enums/company-status.enum.ts new file mode 100644 index 0000000..d8ec2eb --- /dev/null +++ b/src/modules/companies/enums/company-status.enum.ts @@ -0,0 +1,5 @@ +export enum CompanyStatus { + APPROVED = "APPROVED", + PENDING = "PENDING", + REJECTED = "REJECTED", +} diff --git a/src/modules/companies/repositories/company.repository.ts b/src/modules/companies/repositories/company.repository.ts index 13da204..29bf696 100644 --- a/src/modules/companies/repositories/company.repository.ts +++ b/src/modules/companies/repositories/company.repository.ts @@ -1,5 +1,25 @@ import { EntityRepository } from "@mikro-orm/core"; +import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { CompanyListQueryDto } from "../DTO/company-list-query.dto"; import { Company } from "../entities/company.entity"; -export class CompanyRepository extends EntityRepository {} +export class CompanyRepository extends EntityRepository { + // + async getCompaniesListForAdmin(queryDto: CompanyListQueryDto) { + const { limit, skip } = PaginationUtils(queryDto); + + return await this.findAndCount( + { + deletedAt: null, + ...(queryDto.status && { status: queryDto.status }), + ...(queryDto.q && { name: { $ilike: `%${queryDto.q}%` } }), + ...(queryDto.industryId && { industry: { id: queryDto.industryId } }), + ...(queryDto.isActive !== undefined && { isActive: queryDto.isActive === 1 }), + ...(queryDto.registrationDate && { createdAt: { $gte: queryDto.registrationDate, $lte: queryDto.registrationDate } }), + ...(queryDto.name && { name: { $ilike: `%${queryDto.name}%` } }), + }, + { offset: skip, limit, orderBy: { createdAt: "DESC" } }, + ); + } +} diff --git a/src/modules/companies/services/companies.service.ts b/src/modules/companies/services/companies.service.ts index b49d80e..1ad189f 100644 --- a/src/modules/companies/services/companies.service.ts +++ b/src/modules/companies/services/companies.service.ts @@ -1,8 +1,172 @@ -import { Injectable } from "@nestjs/common"; +import { Collection, EntityManager } from "@mikro-orm/postgresql"; +import { BadRequestException, Injectable } from "@nestjs/common"; +import dayjs from "dayjs"; +import { CompanyMessage, IndustryMessage } from "../../../common/enums/message.enum"; +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 { UpdateCompanyDto } from "../DTO/update-company.dto"; +import { CompanyProduct } from "../entities/company-product.entity"; +import { CompanyService } from "../entities/company-service.entity"; +import { Company } from "../entities/company.entity"; +import { CompanyStatus } from "../enums/company-status.enum"; import { CompanyRepository } from "../repositories/company.repository"; @Injectable() export class CompaniesService { - constructor(private readonly companyRepository: CompanyRepository) {} + constructor( + private readonly companyRepository: CompanyRepository, + private readonly usersService: UsersService, + private readonly passwordService: PasswordService, + private readonly em: EntityManager, + ) {} + + async create(createDto: CreateCompanyDto) { + const em = this.em.fork(); + try { + 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 }), + em.findOne(Company, { identificationNumber: createDto.identificationNumber }), + ]); + + 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 company = em.create(Company, { ...createDto, industry, status: CompanyStatus.APPROVED, user }); + + const products = createDto.products.map((productData) => em.create(CompanyProduct, { ...productData, company })); + company.products = new Collection(products); + + const services = createDto.services.map((serviceData) => em.create(CompanyService, { ...serviceData, company })); + company.services = new Collection(services); + + await em.persistAndFlush(company); + await em.commit(); + + return { + message: CompanyMessage.COMPANY_CREATED_SUCCESSFULLY, + company, + user, + }; + } catch (error) { + await em.rollback(); + throw error; + } + } + //----------------------------------- + + async updateCompanyById(id: string, updateDto: UpdateCompanyDto) { + const em = this.em.fork(); + try { + em.begin(); + + const company = await em.findOne(Company, { id, deletedAt: null }, { populate: ["user"] }); + if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); + + const updatedCompany = em.assign(company, updateDto); + + if (updateDto.industryId) { + const industry = await em.findOne(Industry, { id: updateDto.industryId, deletedAt: null }); + if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); + updatedCompany.industry = industry; + } + + if (updateDto.name) { + const existingCompany = await em.findOne(Company, { name: updateDto.name, deletedAt: null, id: { $ne: company.id } }); + if (existingCompany) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_NAME); + } + + if (updateDto.identificationNumber) { + const existingCompany = await em.findOne(Company, { + identificationNumber: updateDto.identificationNumber, + deletedAt: null, + id: { $ne: company.id }, + }); + if (existingCompany) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_IDENTITY_NUMBER); + } + + const userUpdateData: Partial = {}; + if (updateDto.phone) userUpdateData.phone = updateDto.phone; + if (updateDto.email) userUpdateData.email = updateDto.email; + if (updateDto.firstName) userUpdateData.firstName = updateDto.firstName; + if (updateDto.lastName) userUpdateData.lastName = updateDto.lastName; + if (updateDto.nationalCode) userUpdateData.nationalCode = updateDto.nationalCode; + + if (Object.keys(userUpdateData).length > 0) await this.usersService.updateUser(company.user.id, userUpdateData, em); + + await em.flush(); + await em.commit(); + + return { + message: CompanyMessage.UPDATED_SUCCESSFULLY, + updatedCompany, + }; + } catch (error) { + await em.rollback(); + throw error; + } + } + + //----------------------------------- + + async getCompanyById(id: string) { + const company = await this.findCompanyById(id); + return { company }; + } + + //----------------------------------- + + async getCompaniesList(queryDto: CompanyListQueryDto) { + const [companies, count] = await this.companyRepository.getCompaniesListForAdmin(queryDto); + return { + companies, + count, + paginate: true, + }; + } + + //----------------------------------- + + async deleteCompanyById(id: string) { + const company = await this.findCompanyById(id); + + company.deletedAt = dayjs().toDate(); + + await this.em.flush(); + return { + message: CompanyMessage.DELETED_SUCCESSFULLY, + }; + } + + //----------------------------------- + + async toggleStatus(id: string) { + const company = await this.findCompanyById(id); + + company.isActive = !company.isActive; + + await this.em.flush(); + + return { + message: CompanyMessage.UPDATED_SUCCESSFULLY, + }; + } + //----------------------------------- + private async findCompanyById(id: string) { + const company = await this.companyRepository.findOne({ id, deletedAt: null }); + if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); + return company; + } } diff --git a/src/modules/industries/repositories/industry.repository.ts b/src/modules/industries/repositories/industry.repository.ts index b2decb1..ed8c1c4 100644 --- a/src/modules/industries/repositories/industry.repository.ts +++ b/src/modules/industries/repositories/industry.repository.ts @@ -17,7 +17,7 @@ export class IndustryRepository extends EntityRepository { .offset(skip); if (queryDto.q) { - queryBuilder.andWhere({ name: { $ilike: `%${queryDto.q}%` } }); + queryBuilder.andWhere({ title: { $ilike: `%${queryDto.q}%` } }); } if (queryDto.isActive !== undefined) { diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 910d563..c10debd 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -3,10 +3,17 @@ import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, Opt, Pr import { RefreshToken } from "./refresh-token.entity"; import { Role } from "./role.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; +import { Company } from "../../companies/entities/company.entity"; import { UserRepository } from "../repositories/user.repository"; @Entity({ repository: () => UserRepository }) export class User extends BaseEntity { + @Property({ type: "varchar", length: 150 }) + firstName!: string; + + @Property({ type: "varchar", length: 200 }) + lastName!: string; + @Property({ type: "varchar", length: 150, nullable: true }) email?: string; @@ -19,12 +26,6 @@ export class User extends BaseEntity { @Property({ type: "varchar", length: 150 }) password!: string; - @Property({ type: "varchar", length: 150 }) - firstName!: string; - - @Property({ type: "varchar", length: 200 }) - lastName!: string; - @Property({ type: "varchar", length: 12, nullable: true }) birthDate?: string; @@ -47,5 +48,8 @@ export class User extends BaseEntity { @OneToMany(() => RefreshToken, (token) => token.user) refreshTokens = new Collection(this); + @OneToMany(() => Company, (company) => company.user) + companies = new Collection(this); + [EntityRepositoryType]?: UserRepository; } diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index 3e0fc15..60cbbb2 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -4,6 +4,7 @@ import slugify from "slugify"; import { UserMessage } from "../../../common/enums/message.enum"; import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto"; +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"; @@ -45,16 +46,22 @@ export class UsersService { /*******************************/ - async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string, em: EntityManager) { - const role = await em.findOne(Role, { name: RoleEnum.USER }); + async createUser(registerDto: CompleteRegistrationDto | CreateCompanyDto, hashedPassword: string, em: EntityManager) { + const [role, existPhone, existUser] = await Promise.all([ + em.findOne(Role, { name: RoleEnum.USER }), + em.findOne(User, { phone: registerDto.phone }), + em.findOne(User, { nationalCode: registerDto.nationalCode }), + ]); + if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND); - - const existPhone = await em.findOne(User, { phone: registerDto.phone }); if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); - - const existUser = await em.findOne(User, { nationalCode: registerDto.nationalCode }); if (existUser) throw new BadRequestException(UserMessage.NATIONAL_CODE_EXIST); + if ("email" in registerDto) { + const existEmail = await em.findOne(User, { email: registerDto.email }); + if (existEmail) throw new BadRequestException(UserMessage.EMAIL_EXIST); + } + const userName = slugify(`${registerDto.firstName} ${Date.now().toString().slice(-5)}`, { lower: true, trim: true }); const user = em.create(User, { @@ -76,4 +83,31 @@ export class UsersService { await this.em.flush(); return user; } + + /*******************************/ + + async updateUser(id: string, updateData: Partial, em: EntityManager) { + const user = await em.findOne(User, { id, deletedAt: null }); + if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + + if (updateData.phone) { + const existingUser = await em.findOne(User, { phone: updateData.phone, deletedAt: null, id: { $ne: id } }); + if (existingUser) throw new BadRequestException(UserMessage.PHONE_EXIST); + } + + if (updateData.email) { + const existingUser = await em.findOne(User, { email: updateData.email, deletedAt: null, id: { $ne: id } }); + if (existingUser) throw new BadRequestException(UserMessage.EMAIL_EXIST); + } + + if (updateData.nationalCode) { + const existingUser = await em.findOne(User, { nationalCode: updateData.nationalCode, deletedAt: null, id: { $ne: id } }); + if (existingUser) throw new BadRequestException(UserMessage.NATIONAL_CODE_EXIST); + } + + em.assign(user, updateData); + await em.flush(); + + return user; + } }