From 16c598118d644c43fd3490b1f35663e7b4adcfba Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Wed, 3 Jun 2026 16:51:56 +0330 Subject: [PATCH 1/4] New module for expert field --- src/expert-initiated/dtos/login.dto.ts | 32 +++++++++++++++++++ .../expert-initiated.controller.ts | 16 ++++++++++ .../expert-initiated.module.ts | 25 +++++++++++++++ .../expert-initiated.service.ts | 32 +++++++++++++++++++ .../repositories/field-expert.repository.ts | 22 +++++++++++++ .../schemas/field-expert.schema.ts | 27 ++++++++++++++++ 6 files changed, 154 insertions(+) create mode 100644 src/expert-initiated/dtos/login.dto.ts create mode 100644 src/expert-initiated/expert-initiated.controller.ts create mode 100644 src/expert-initiated/expert-initiated.module.ts create mode 100644 src/expert-initiated/expert-initiated.service.ts create mode 100644 src/expert-initiated/repositories/field-expert.repository.ts create mode 100644 src/expert-initiated/schemas/field-expert.schema.ts diff --git a/src/expert-initiated/dtos/login.dto.ts b/src/expert-initiated/dtos/login.dto.ts new file mode 100644 index 0000000..68b650b --- /dev/null +++ b/src/expert-initiated/dtos/login.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEmail, IsNotEmpty, IsString } from "class-validator"; + +export class FieldExpertLoginDto { + @ApiProperty() + @IsEmail({ allow_underscores: true }) + @IsString({ + message: "email is not string", + context: { + errorCode: 4000, + note: "The validated email type must be string", + }, + }) + readonly email: string; + + @ApiProperty() + @IsNotEmpty({ + message: "password is empty", + context: { + errorCode: 4001, + note: "The validated password must not be empty", + }, + }) + @IsString({ + message: "password is not string", + context: { + errorCode: 4002, + note: "The validated password type must be string", + }, + }) + readonly password: string; +} diff --git a/src/expert-initiated/expert-initiated.controller.ts b/src/expert-initiated/expert-initiated.controller.ts new file mode 100644 index 0000000..8f2a1e8 --- /dev/null +++ b/src/expert-initiated/expert-initiated.controller.ts @@ -0,0 +1,16 @@ +import { Body, Controller, Post } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { FieldExpertLoginDto } from "./dtos/login.dto"; +import { FieldExpertService } from "./expert-initiated.service"; + +@Controller("expert-initiated") +@ApiTags("Expert Initiated Flow (Field Expert)") +@ApiBearerAuth() +export class FieldExpertController { + constructor(private readonly fieldExpertService: FieldExpertService) {} + + @Post("login") + async login(@Body() fieldExpertLoginDto: FieldExpertLoginDto) { + return await this.fieldExpertService.login(fieldExpertLoginDto); + } +} diff --git a/src/expert-initiated/expert-initiated.module.ts b/src/expert-initiated/expert-initiated.module.ts new file mode 100644 index 0000000..2050ef1 --- /dev/null +++ b/src/expert-initiated/expert-initiated.module.ts @@ -0,0 +1,25 @@ +import { Module } from "@nestjs/common"; +import { MongooseModule } from "@nestjs/mongoose"; +import { FieldExpert, FieldExpertSchema } from "./schemas/field-expert.schema"; +import { FieldExpertController } from "./expert-initiated.controller"; +import { FieldExpertService } from "./expert-initiated.service"; +import { FieldExpertRepository } from "./repositories/field-expert.repository"; +import { AuthModule } from "src/auth/auth.module"; +import { JwtModule } from "@nestjs/jwt"; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { + name: FieldExpert.name, + schema: FieldExpertSchema, + }, + ]), + AuthModule, + JwtModule, + ], + controllers: [FieldExpertController], + providers: [FieldExpertRepository, FieldExpertService], + exports: [], +}) +export class ExpertInitiatedModule {} diff --git a/src/expert-initiated/expert-initiated.service.ts b/src/expert-initiated/expert-initiated.service.ts new file mode 100644 index 0000000..2a88ce7 --- /dev/null +++ b/src/expert-initiated/expert-initiated.service.ts @@ -0,0 +1,32 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import { FieldExpertRepository } from "./repositories/field-expert.repository"; +import { FieldExpertLoginDto } from "./dtos/login.dto"; +import { UserAuthService } from "src/auth/auth-services/user.auth.service"; + +@Injectable() +export class FieldExpertService { + constructor( + private readonly fieldExpertRepository: FieldExpertRepository, + private readonly authService: UserAuthService, + ) {} + + async login(fieldExpertLoginDto: FieldExpertLoginDto) { + const user = await this.fieldExpertRepository.retrieveByEmail( + fieldExpertLoginDto.email, + ); + + if (!user) throw new NotFoundException("User not found"); + + const validate = await this.authService.validateUser( + user.email, + user.password, + ); + + if (!validate) + throw new BadRequestException("Username/Password does not match"); + } +} diff --git a/src/expert-initiated/repositories/field-expert.repository.ts b/src/expert-initiated/repositories/field-expert.repository.ts new file mode 100644 index 0000000..2e4784d --- /dev/null +++ b/src/expert-initiated/repositories/field-expert.repository.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@nestjs/common"; +import { InjectModel } from "@nestjs/mongoose"; +import { Model } from "mongoose"; +import { FieldExpert } from "../schemas/field-expert.schema"; + +@Injectable() +export class FieldExpertRepository { + constructor( + @InjectModel(FieldExpert.name) + private readonly fieldExpertModel: Model, + ) {} + + async retrieveById(id: string): Promise { + return await this.fieldExpertModel.findById(id).exec(); + } + + async retrieveByEmail(email: string): Promise { + return await this.fieldExpertModel.findOne({ + email, + }); + } +} diff --git a/src/expert-initiated/schemas/field-expert.schema.ts b/src/expert-initiated/schemas/field-expert.schema.ts new file mode 100644 index 0000000..cde1c7d --- /dev/null +++ b/src/expert-initiated/schemas/field-expert.schema.ts @@ -0,0 +1,27 @@ +import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; +import { HydratedDocument, Types } from "mongoose"; + +export type FieldExpertDocument = HydratedDocument; + +@Schema({ + id: true, + timestamps: true, +}) +export class FieldExpert { + @Prop({ unique: true }) + email: string; + + @Prop({ required: true }) + password: string; + + @Prop({ required: true }) + firstName: string; + + @Prop({ required: true }) + lastName: string; + + @Prop({ required: true }) + cellphone: string; +} + +export const FieldExpertSchema = SchemaFactory.createForClass(FieldExpert); From 9ee933cb76ca4c99cf4eaceaa90684abf4c250df Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Wed, 3 Jun 2026 17:04:16 +0330 Subject: [PATCH 2/4] Fixed price-drop --- src/expert-claim/expert-claim.service.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 22e45b9..4bf2b95 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -4242,9 +4242,10 @@ export class ExpertClaimService { ); } + // Manual path — change totalPriceDrop → total to match calculated path const manualPayload = { manualOverride: true, - totalPriceDrop: body.manualPriceDrop, + total: body.manualPriceDrop, // ← was totalPriceDrop partLines: [], }; @@ -4269,6 +4270,18 @@ export class ExpertClaimService { } // Calculated path — existing logic unchanged + // At the top of the calculated path, after the manualPriceDrop early return: + const existingPriceDrop = (claim as any).evaluation?.priceDrop; + if ( + existingPriceDrop?.manualOverride === true && + !body.partSeverities?.length + ) { + throw new BadRequestException( + "This claim has a manual price-drop correction. " + + "To recalculate, provide partSeverities explicitly — this will replace the manual correction.", + ); + } + const carType = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined; const selectedNorm = normalizeDamageSelectedParts( claim.damage?.selectedParts, From f023d0f3e7bf46f4833e285a84e840f59fd96035 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sat, 6 Jun 2026 10:45:53 +0330 Subject: [PATCH 3/4] Simplified captcha --- src/captcha/captcha.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/captcha/captcha.service.ts b/src/captcha/captcha.service.ts index c7a8149..5b00d88 100644 --- a/src/captcha/captcha.service.ts +++ b/src/captcha/captcha.service.ts @@ -13,9 +13,9 @@ export class CaptchaService { generate(): GeneratedCaptcha { const captcha = svgCaptcha.create({ size: 5, - ignoreChars: "0oO1ilI", - noise: 3, - color: true, + ignoreChars: "abcdefghijklmnopqrstuvwxyzACDEFGHIJKLMNOPQRSTUVWXYZ", + noise: 1, + color: false, background: "#f8fafc", width: 160, height: 56, From 34142942e5a1c283624c9fa362c15294c2b520a9 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sun, 7 Jun 2026 10:34:10 +0330 Subject: [PATCH 4/4] Simplified captcha, Fixed unified damaged parts --- src/captcha/captcha.service.ts | 2 +- .../dto/claim-damaged-part.enricher.ts | 69 +++++++++++++++---- src/expert-claim/expert-claim.service.ts | 4 +- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/captcha/captcha.service.ts b/src/captcha/captcha.service.ts index 5b00d88..c7ed27f 100644 --- a/src/captcha/captcha.service.ts +++ b/src/captcha/captcha.service.ts @@ -13,7 +13,7 @@ export class CaptchaService { generate(): GeneratedCaptcha { const captcha = svgCaptcha.create({ size: 5, - ignoreChars: "abcdefghijklmnopqrstuvwxyzACDEFGHIJKLMNOPQRSTUVWXYZ", + ignoreChars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", noise: 1, color: false, background: "#f8fafc", diff --git a/src/expert-claim/dto/claim-damaged-part.enricher.ts b/src/expert-claim/dto/claim-damaged-part.enricher.ts index 99d8741..6e47ce2 100644 --- a/src/expert-claim/dto/claim-damaged-part.enricher.ts +++ b/src/expert-claim/dto/claim-damaged-part.enricher.ts @@ -27,26 +27,48 @@ export function buildEnrichedDamagedParts(params: { } = params; const priceDropLines: any[] = evaluationBlock?.priceDrop?.partLines ?? []; + + // objectionParts stores { partId, carPartDamage, side } — not { id, name } const objectionParts: any[] = evaluationBlock?.objection?.objectionParts ?? []; + + // newParts stores { partId, partName, side } — not { id, name } const newObjectionParts: any[] = evaluationBlock?.objection?.newParts ?? []; - // Resend block — lives at evaluation.damageExpertResend const damageExpertResend = evaluationBlock?.damageExpertResend; const resendCarParts: any[] = damageExpertResend?.resendCarParts ?? []; const resendFulfilledAt: Date | undefined = damageExpertResend?.fulfilledAt; + // Helpers that match against the ACTUAL stored field names + function partMatchesObjection(sp: any, objPart: any): boolean { + if (sp.id != null && objPart.partId != null && sp.id === objPart.partId) + return true; + const objName = objPart.carPartDamage ?? objPart.name ?? objPart.partName; + if (objName && (sp.name === objName || sp.label_fa === objName)) + return true; + return false; + } + + function partMatchesNewObjection(sp: any, newPart: any): boolean { + if (sp.id != null && newPart.partId != null && sp.id === newPart.partId) + return true; + const newName = newPart.partName ?? newPart.name; + if (newName && (sp.name === newName || sp.label_fa === newName)) + return true; + return false; + } + function isPartInResend(sp: any): boolean { return resendCarParts.some( (r: any) => - r.id === sp.id || r.name === sp.name || r.catalogKey === sp.catalogKey, + (sp.id != null && r.id === sp.id) || + r.catalogKey === sp.catalogKey || + r.name === sp.name, ); } function resolveOrigin(sp: any): EnrichedDamagedPart["origin"] { - if ( - newObjectionParts.some((p: any) => p.id === sp.id || p.name === sp.name) - ) { + if (newObjectionParts.some((p) => partMatchesNewObjection(sp, p))) { return "added_by_objection"; } if ( @@ -65,7 +87,6 @@ export function buildEnrichedDamagedParts(params: { if (!captured) { return inResend ? "resend_requested" : "pending"; } - // captured=true + was in a resend request that is now fulfilled → "resent" return inResend && resendFulfilledAt ? "resent" : "uploaded"; } @@ -77,8 +98,30 @@ export function buildEnrichedDamagedParts(params: { ck, selectedParts, ) as { url?: string; path?: string } | undefined; - const captured = hasDamagedPartCapture(damagedPartsData, ck, selectedParts); - const url = cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined); + + // Fallback: if id is null but media.damagedParts has a match by name + const captured = + hasDamagedPartCapture(damagedPartsData, ck, selectedParts) || + (sp.id == null && Array.isArray(damagedPartsData) + ? (damagedPartsData as any[]).some( + (d) => d.name === sp.name && d.side === sp.side && d.path, + ) + : false); + + const capUrl = + cap?.url || + (cap?.path ? buildFileLink(cap.path) : undefined) || + (sp.id == null && Array.isArray(damagedPartsData) + ? (() => { + const match = (damagedPartsData as any[]).find( + (d) => d.name === sp.name && d.side === sp.side && d.path, + ); + return ( + match?.url || + (match?.path ? buildFileLink(match.path) : undefined) + ); + })() + : undefined); const matchingPriceDrop = priceDropLines.find( (line: any) => @@ -86,11 +129,9 @@ export function buildEnrichedDamagedParts(params: { line.priceDropPartKey?.toLowerCase() === sp.name?.toLowerCase(), ); - const isObjected = objectionParts.some( - (p: any) => p.id === sp.id || p.name === sp.name, - ); - const isNewlyAdded = newObjectionParts.some( - (p: any) => p.id === sp.id || p.name === sp.name, + const isObjected = objectionParts.some((p) => partMatchesObjection(sp, p)); + const isNewlyAdded = newObjectionParts.some((p) => + partMatchesNewObjection(sp, p), ); const isNewByExpert = expertAddedParts.some( (p: any) => p.id === sp.id || p.name === sp.name, @@ -109,7 +150,7 @@ export function buildEnrichedDamagedParts(params: { origin: resolveOrigin(sp), captureStatus: resolveCaptureStatus(sp, captured), captured, - url, + url: capUrl, objection: { isObjected, diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 4bf2b95..a69f8cf 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -4122,7 +4122,7 @@ export class ExpertClaimService { blameRequestId: claim.blameRequestId?.toString(), blameRequestNo: claim.blameRequestNo, money: moneyPayload, - selectedParts: selectedNormExpert, + // selectedParts: selectedNormExpert, otherParts: claim.damage?.otherParts, requiredDocuments: Object.keys(requiredDocumentsStatus).length > 0 @@ -4134,7 +4134,7 @@ export class ExpertClaimService { evaluation: enrichedEvaluation as | ClaimDetailV2ResponseDto["evaluation"] | undefined, - objection, + // objection, videoCapture, blameCase: blameCaseForApi, blameExpertDecision,