From bbd83da2d585ea2a8d80145add804c148c0b6dec Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 28 Apr 2026 14:27:12 +0330 Subject: [PATCH] YARA-858 --- src/client/client.controller.ts | 10 +- src/client/dto/create-client.dto.ts | 22 +++- .../entities/db-service/branch.db.service.ts | 2 +- src/decorators/customeHeader.decorator.ts | 6 - .../dto/create-insurer-expert.dto.ts | 83 +++++++++++++ .../expert-insurer.controller.ts | 84 +++++++++---- src/expert-insurer/expert-insurer.module.ts | 2 + src/expert-insurer/expert-insurer.service.ts | 115 ++++++++++++++++++ src/expert-insurer/panel.enum.ts | 4 - src/main.ts | 1 - .../entities/schema/damage-expert.schema.ts | 3 + .../schema/expert-file-activity.schema.ts | 2 +- src/users/entities/schema/expert.schema.ts | 4 + 13 files changed, 291 insertions(+), 47 deletions(-) delete mode 100644 src/decorators/customeHeader.decorator.ts create mode 100644 src/expert-insurer/dto/create-insurer-expert.dto.ts delete mode 100644 src/expert-insurer/panel.enum.ts diff --git a/src/client/client.controller.ts b/src/client/client.controller.ts index b32ea6b..0042443 100644 --- a/src/client/client.controller.ts +++ b/src/client/client.controller.ts @@ -7,25 +7,27 @@ import { } from "@nestjs/common"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { GlobalGuard } from "src/auth/guards/global.guard"; +import { RolesGuard } from "src/auth/guards/role.guard"; +import { Roles } from "src/decorators/roles.decorator"; import { CurrentUser } from "src/decorators/user.decorator"; +import { RoleEnum } from "src/Types&Enums/role.enum"; import { ClientService } from "./client.service"; import { ClientDto } from "./dto/create-client.dto"; @Controller("client") @ApiTags("client-management") +@ApiBearerAuth() +@UseGuards(GlobalGuard, RolesGuard) +@Roles(RoleEnum.ADMIN) export class ClientController { constructor(private readonly clientService: ClientService) {} @Post() - @UseGuards(GlobalGuard) - @ApiBearerAuth() async addClient(@Body() client: ClientDto) { return await this.clientService.addClient(client); } @Get() - @ApiBearerAuth() - @UseGuards(GlobalGuard) async getClient(@CurrentUser() user) { return await this.clientService.getClients(); } diff --git a/src/client/dto/create-client.dto.ts b/src/client/dto/create-client.dto.ts index 9644946..2101d77 100644 --- a/src/client/dto/create-client.dto.ts +++ b/src/client/dto/create-client.dto.ts @@ -1,31 +1,44 @@ -import { Injectable } from "@nestjs/common"; import { ApiProperty } from "@nestjs/swagger"; +import { IsIn, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; import { Types } from "mongoose"; class ClientName { @ApiProperty({}) + @IsString() + @IsNotEmpty() persian: string; @ApiProperty({}) + @IsString() + @IsNotEmpty() english: string; } class Property { @ApiProperty({}) + @IsString() + @IsNotEmpty() smsApiKey: string; } -@Injectable() export class ClientDto { @ApiProperty({ required: true }) + @ValidateNested() + @Type(() => ClientName) clientName: ClientName; @ApiProperty({ required: true }) + @IsNumber() clientCode: number; @ApiProperty({ required: false }) - property: Property; + @IsOptional() + @ValidateNested() + @Type(() => Property) + property?: Property; @ApiProperty({ examples: ["legal", "genuine"] }) + @IsIn(["legal", "genuine"]) useExpertMode: "legal" | "genuine"; } export class ClientDtoRs { @@ -35,6 +48,9 @@ export class ClientDtoRs { useExpertsMode: string; constructor(readonly client) { this.persian = client.clientName.persian; + this.english = client.clientName.english; + this.clientId = client._id; + this.useExpertsMode = client.useExpertMode; } } diff --git a/src/client/entities/db-service/branch.db.service.ts b/src/client/entities/db-service/branch.db.service.ts index 8b2af9f..66b79ed 100644 --- a/src/client/entities/db-service/branch.db.service.ts +++ b/src/client/entities/db-service/branch.db.service.ts @@ -25,7 +25,7 @@ export class BranchDbService { async findAll(insuranceId: string): Promise { return await this.branchModel.find({ clientKey: new Types.ObjectId(insuranceId), - }); + }, { _id: 1, name: 1, code: 1, city: 1, state: 1, address: 1, phoneNumber: 1, createdAtFa: 1, updatedAtFa: 1 }); } async findById(id: string): Promise { diff --git a/src/decorators/customeHeader.decorator.ts b/src/decorators/customeHeader.decorator.ts deleted file mode 100644 index 31d9cb7..0000000 --- a/src/decorators/customeHeader.decorator.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ExecutionContext, createParamDecorator } from "@nestjs/common"; - -export const CustomHeader = createParamDecorator( - (data, ctx: ExecutionContext) => { - }, -); diff --git a/src/expert-insurer/dto/create-insurer-expert.dto.ts b/src/expert-insurer/dto/create-insurer-expert.dto.ts new file mode 100644 index 0000000..b1e7caa --- /dev/null +++ b/src/expert-insurer/dto/create-insurer-expert.dto.ts @@ -0,0 +1,83 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsEmail, IsEnum, IsMongoId, IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { RoleEnum } from "src/Types&Enums/role.enum"; +import { UserType } from "src/Types&Enums/userType.enum"; + +export class CreateInsurerExpertDto { + @ApiProperty({ example: "Ali" }) + @IsString() + @IsNotEmpty() + firstName: string; + + @ApiProperty({ example: "Ahmadi" }) + @IsString() + @IsNotEmpty() + lastName: string; + + @ApiProperty({ example: "1234567890" }) + @IsString() + @IsNotEmpty() + nationalCode: string; + + @ApiProperty({ example: "expert@example.com" }) + @IsEmail() + email: string; + + @ApiProperty({ example: "StrongPass!123" }) + @IsString() + @IsNotEmpty() + password: string; + + @ApiProperty({ example: "09121234567" }) + @IsString() + @IsNotEmpty() + mobile: string; + + @ApiProperty({ + example: "665f0e5ab74d670939b08920", + description: "Branch id this expert belongs to", + }) + @IsMongoId() + branchId: string; + + @ApiPropertyOptional({ enum: UserType, example: UserType.LEGAL }) + @IsOptional() + @IsEnum(UserType) + userType?: UserType; + + @ApiPropertyOptional({ example: "02112345678" }) + @IsOptional() + @IsString() + phone?: string; + + @ApiPropertyOptional({ example: "Tehran" }) + @IsOptional() + @IsString() + state?: string; + + @ApiPropertyOptional({ example: "Tehran" }) + @IsOptional() + @IsString() + city?: string; + + @ApiPropertyOptional({ example: "No. 12, Sample St." }) + @IsOptional() + @IsString() + address?: string; +} + +export class CreateBlameExpertByInsurerDto extends CreateInsurerExpertDto { + @ApiPropertyOptional({ + enum: [RoleEnum.EXPERT], + default: RoleEnum.EXPERT, + }) + role?: RoleEnum.EXPERT; +} + +export class CreateClaimExpertByInsurerDto extends CreateInsurerExpertDto { + @ApiPropertyOptional({ + enum: [RoleEnum.DAMAGE_EXPERT], + default: RoleEnum.DAMAGE_EXPERT, + }) + role?: RoleEnum.DAMAGE_EXPERT; +} diff --git a/src/expert-insurer/expert-insurer.controller.ts b/src/expert-insurer/expert-insurer.controller.ts index 3fa567c..5a7b81c 100644 --- a/src/expert-insurer/expert-insurer.controller.ts +++ b/src/expert-insurer/expert-insurer.controller.ts @@ -26,6 +26,10 @@ import { FileRating } from "src/request-management/entities/schema/request-manag import { RoleEnum } from "src/Types&Enums/role.enum"; import { ExpertInsurerService } from "./expert-insurer.service"; import { CreateBranchDto } from "src/client/dto/create-branch.dto"; +import { + CreateBlameExpertByInsurerDto, + CreateClaimExpertByInsurerDto, +} from "./dto/create-insurer-expert.dto"; @Controller("expert-insurer") @ApiTags("expert-insurer-panel") @@ -35,13 +39,56 @@ import { CreateBranchDto } from "src/client/dto/create-branch.dto"; export class ExpertInsurerController { constructor(private readonly expertInsurerService: ExpertInsurerService) {} - @Get("files") - async getAllFiles(@CurrentUser() insurer) { - return await this.expertInsurerService.retrieveAllFilesOfClient( + @Get("branches") + async getInsuranceBranches(@CurrentUser() insurer) { + if (!insurer) { + throw new UnauthorizedException("Could not identify the current user."); + } + return await this.expertInsurerService.retrieveInsuranceBranches( insurer.clientKey, ); } + @Post("branches") + @ApiBody({ type: CreateBranchDto }) + async addBranch( + @CurrentUser() insurer, + @Body() createBranchDto: CreateBranchDto, + ) { + if (!insurer) { + throw new UnauthorizedException("Could not identify the current user."); + } + + return await this.expertInsurerService.addBranch( + insurer.clientKey, + createBranchDto, + ); + } + + @Post("experts/blame") + @ApiBody({ type: CreateBlameExpertByInsurerDto }) + async addBlameExpert( + @CurrentUser() insurer, + @Body() body: CreateBlameExpertByInsurerDto, + ) { + if (!insurer) { + throw new UnauthorizedException("Could not identify the current user."); + } + return this.expertInsurerService.addBlameExpert(insurer.clientKey, body); + } + + @Post("experts/claim") + @ApiBody({ type: CreateClaimExpertByInsurerDto }) + async addClaimExpert( + @CurrentUser() insurer, + @Body() body: CreateClaimExpertByInsurerDto, + ) { + if (!insurer) { + throw new UnauthorizedException("Could not identify the current user."); + } + return this.expertInsurerService.addClaimExpert(insurer.clientKey, body); + } + @ApiQuery({ name: "page", type: Number }) @ApiQuery({ name: "response_count", type: Number }) @Get("list") @@ -62,6 +109,13 @@ export class ExpertInsurerController { return await this.expertInsurerService.getTopExpertsForClient(actor); } + @Get("files") + async getAllFiles(@CurrentUser() insurer) { + return await this.expertInsurerService.retrieveAllFilesOfClient( + insurer.clientKey, + ); + } + @Get("top-files") async getTopFiles(@CurrentUser() insurer) { return await this.expertInsurerService.getTopFilesForClient( @@ -174,28 +228,4 @@ export class ExpertInsurerController { insurer.clientKey, ); } - - @Get("branches/:insuranceId") - @ApiParam({ name: "insuranceId" }) - async getInsuranceBranches(@Param("insuranceId") insuranceId: string) { - return await this.expertInsurerService.retrieveInsuranceBranches( - insuranceId, - ); - } - - @Post("branches") - @ApiBody({ type: CreateBranchDto }) - async addBranch( - @CurrentUser() insurer, - @Body() createBranchDto: CreateBranchDto, - ) { - if (!insurer) { - throw new UnauthorizedException("Could not identify the current user."); - } - - return await this.expertInsurerService.addBranch( - insurer.clientKey, - createBranchDto, - ); - } } diff --git a/src/expert-insurer/expert-insurer.module.ts b/src/expert-insurer/expert-insurer.module.ts index 0546bb4..34a1ee0 100644 --- a/src/expert-insurer/expert-insurer.module.ts +++ b/src/expert-insurer/expert-insurer.module.ts @@ -19,12 +19,14 @@ import { BranchModel, BranchSchema, } from "src/client/entities/schema/branch.schema"; +import { HashModule } from "src/utils/hash/hash.module"; @Module({ imports: [ ClaimRequestManagementModule, RequestManagementModule, AuthModule, + HashModule, UsersModule, ClientModule, MongooseModule.forFeature([ diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index 49860f9..98fc932 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -9,6 +9,11 @@ import { Types } from "mongoose"; import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service"; import { CreateBranchDto } from "src/client/dto/create-branch.dto"; import { BranchDbService } from "src/client/entities/db-service/branch.db.service"; +import { + CreateBlameExpertByInsurerDto, + CreateClaimExpertByInsurerDto, + CreateInsurerExpertDto, +} from "./dto/create-insurer-expert.dto"; import { blameCaseTouchesClient, claimCaseTouchesClient, @@ -20,6 +25,9 @@ import { DamageExpertDbService } from "src/users/entities/db-service/damage-expe import { ExpertDbService } from "src/users/entities/db-service/expert.db.service"; import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service"; import { ExpertFileActivityType } from "src/users/entities/schema/expert-file-activity.schema"; +import { HashService } from "src/utils/hash/hash.service"; +import { RoleEnum } from "src/Types&Enums/role.enum"; +import { UserType } from "src/Types&Enums/userType.enum"; @Injectable() export class ExpertInsurerService { @@ -30,6 +38,7 @@ export class ExpertInsurerService { private readonly blameRequestDbService: BlameRequestDbService, private readonly claimCaseDbService: ClaimCaseDbService, private readonly branchDbService: BranchDbService, + private readonly hashService: HashService, ) {} private getClientId(actorOrId: any): Types.ObjectId { @@ -420,6 +429,112 @@ export class ExpertInsurerService { return newBranch; } + private async assertBranchBelongsToClient( + branchId: string, + clientId: Types.ObjectId, + ) { + const branch = await this.branchDbService.findById(branchId); + if (!branch) { + throw new NotFoundException("Branch not found"); + } + if (String(branch.clientKey) !== String(clientId)) { + throw new ForbiddenException( + "Selected branch does not belong to your insurance company.", + ); + } + return branch; + } + + private sanitizeExpertResponse(doc: any) { + const obj = typeof doc?.toObject === "function" ? doc.toObject() : doc; + if (!obj) return obj; + const { password, otp, ...rest } = obj; + return rest; + } + + private async createExpertForInsurer( + insurerClientKey: string, + payload: CreateInsurerExpertDto, + role: RoleEnum.EXPERT | RoleEnum.DAMAGE_EXPERT, + ) { + const clientObjectId = this.getClientId(insurerClientKey); + const branch = await this.assertBranchBelongsToClient( + payload.branchId, + clientObjectId, + ); + + const email = payload.email.trim().toLowerCase(); + const existingByEmail = await this.expertDbService.findOne({ email }); + const existingDamageByEmail = await this.damageExpertDbService.findOne({ email }); + if (existingByEmail || existingDamageByEmail) { + throw new ConflictException("An expert with this email already exists."); + } + + const nationalCode = payload.nationalCode.trim(); + const existingByNational = await this.expertDbService.findOne({ nationalCode }); + const existingDamageByNational = await this.damageExpertDbService.findOne({ + nationalCode, + }); + if (existingByNational || existingDamageByNational) { + throw new ConflictException( + "An expert with this national code already exists.", + ); + } + + const hashedPassword = await this.hashService.hash(payload.password); + const basePayload: any = { + ...payload, + email, + nationalCode, + password: hashedPassword, + role, + userType: payload.userType ?? UserType.LEGAL, + clientKey: + role === RoleEnum.EXPERT ? String(clientObjectId) : clientObjectId, + branchId: new Types.ObjectId(payload.branchId), + insuActivityCo: String(clientObjectId), + }; + + const created = + role === RoleEnum.EXPERT + ? await this.expertDbService.create(basePayload) + : await this.damageExpertDbService.create(basePayload); + + return { + expert: this.sanitizeExpertResponse(created), + branch: { + _id: (branch as any)._id, + name: branch.name, + code: branch.code, + city: branch.city, + state: branch.state, + address: branch.address, + }, + }; + } + + async addBlameExpert( + insurerClientKey: string, + payload: CreateBlameExpertByInsurerDto, + ) { + return this.createExpertForInsurer( + insurerClientKey, + payload, + RoleEnum.EXPERT, + ); + } + + async addClaimExpert( + insurerClientKey: string, + payload: CreateClaimExpertByInsurerDto, + ) { + return this.createExpertForInsurer( + insurerClientKey, + payload, + RoleEnum.DAMAGE_EXPERT, + ); + } + /** * Get comprehensive statistics for all experts of a client * Returns: diff --git a/src/expert-insurer/panel.enum.ts b/src/expert-insurer/panel.enum.ts deleted file mode 100644 index c3119eb..0000000 --- a/src/expert-insurer/panel.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum PanelModels { - BLAME = "blame", - CLAIM = "claim", -} diff --git a/src/main.ts b/src/main.ts index 09e0018..6e47176 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,7 +32,6 @@ async function bootstrap() { .setVersion("1.0.0") .addServer(process.env.URL) .addServer(process.env.BASE_URL + "/api") - .addServer("http://192.168.20.249:9001") .addServer("http://192.168.20.170:9001") .addServer("http://localhost:9001") .addBearerAuth() diff --git a/src/users/entities/schema/damage-expert.schema.ts b/src/users/entities/schema/damage-expert.schema.ts index 58fa125..84f6029 100644 --- a/src/users/entities/schema/damage-expert.schema.ts +++ b/src/users/entities/schema/damage-expert.schema.ts @@ -123,6 +123,9 @@ export class DamageExpertModel { @Prop({ type: Types.ObjectId }) clientKey?: Types.ObjectId; + @Prop({ type: Types.ObjectId, index: true }) + branchId?: Types.ObjectId; + @Prop({ type: PersonalInfoSchema }) personalInfo?: PersonalInfo; diff --git a/src/users/entities/schema/expert-file-activity.schema.ts b/src/users/entities/schema/expert-file-activity.schema.ts index 2554dec..21b7d57 100644 --- a/src/users/entities/schema/expert-file-activity.schema.ts +++ b/src/users/entities/schema/expert-file-activity.schema.ts @@ -39,7 +39,7 @@ export class ExpertFileActivity { @Prop({ type: Date, default: Date.now, index: true }) occurredAt: Date; - @Prop({ type: String, required: false, index: true }) + @Prop({ type: String, required: false }) idempotencyKey?: string; } diff --git a/src/users/entities/schema/expert.schema.ts b/src/users/entities/schema/expert.schema.ts index 71163ec..d328f9d 100644 --- a/src/users/entities/schema/expert.schema.ts +++ b/src/users/entities/schema/expert.schema.ts @@ -1,4 +1,5 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; +import { Types } from "mongoose"; import { Degrees } from "src/Types&Enums/degrees.enum"; import { RoleEnum } from "src/Types&Enums/role.enum"; import { UserType } from "src/Types&Enums/userType.enum"; @@ -65,6 +66,9 @@ export class ExpertModel { @Prop({}) clientKey?: string; + @Prop({ type: Types.ObjectId, index: true }) + branchId?: Types.ObjectId; + @Prop({ type: "string" }) otp: string;