update: add company services and products

This commit is contained in:
Mahyargdz
2025-05-11 16:45:26 +03:30
parent 9a9f640622
commit 77c0f59514
18 changed files with 588 additions and 33 deletions
+2 -2
View File
@@ -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:
+58 -1
View File
@@ -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 = "خدمات باید آرایه باشد",
}
@@ -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;
}
@@ -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[];
}
@@ -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[];
}
@@ -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[];
}
@@ -0,0 +1,5 @@
import { PartialType } from "@nestjs/swagger";
import { CreateCompanyDto } from "./create-company.dto";
export class UpdateCompanyDto extends PartialType(CreateCompanyDto) {}
+46 -2
View File
@@ -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);
}
}
+6 -2
View File
@@ -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],
@@ -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;
}
@@ -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;
}
@@ -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<CompanyProduct>(this);
@OneToMany(() => CompanyService, (service) => service.company, { cascade: [Cascade.ALL] })
services = new Collection<CompanyService>(this);
//-----------------------------------
@ManyToOne(() => Industry, { deleteRule: "restrict" })
industry!: Industry;
@ManyToOne(() => User, { deleteRule: "restrict" })
user!: User;
[EntityRepositoryType]?: CompanyRepository;
}
@@ -0,0 +1,5 @@
export enum CompanyStatus {
APPROVED = "APPROVED",
PENDING = "PENDING",
REJECTED = "REJECTED",
}
@@ -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<Company> {}
export class CompanyRepository extends EntityRepository<Company> {
//
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" } },
);
}
}
@@ -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<User> = {};
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;
}
}
@@ -17,7 +17,7 @@ export class IndustryRepository extends EntityRepository<Industry> {
.offset(skip);
if (queryDto.q) {
queryBuilder.andWhere({ name: { $ilike: `%${queryDto.q}%` } });
queryBuilder.andWhere({ title: { $ilike: `%${queryDto.q}%` } });
}
if (queryDto.isActive !== undefined) {
+10 -6
View File
@@ -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<RefreshToken>(this);
@OneToMany(() => Company, (company) => company.user)
companies = new Collection<Company>(this);
[EntityRepositoryType]?: UserRepository;
}
+40 -6
View File
@@ -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<User>, 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;
}
}