From 2c810afcb616ac8c4a5a303a9650a8933c6e278f Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Wed, 3 Jun 2026 12:05:19 +0330 Subject: [PATCH] YARA-948 --- .../expert-blame.v2.controller.ts | 33 +++++++++--- .../dto/claim-price-drop-v2.dto.ts | 14 ++++- src/expert-claim/expert-claim.service.ts | 54 +++++++++++++++---- .../expert-claim.v2.controller.ts | 45 +++++++++++++--- 4 files changed, 121 insertions(+), 25 deletions(-) diff --git a/src/expert-blame/expert-blame.v2.controller.ts b/src/expert-blame/expert-blame.v2.controller.ts index c1f111f..2fe856f 100644 --- a/src/expert-blame/expert-blame.v2.controller.ts +++ b/src/expert-blame/expert-blame.v2.controller.ts @@ -10,7 +10,14 @@ import { Query, UseGuards, } from "@nestjs/common"; -import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiParam, + ApiResponse, + ApiTags, +} from "@nestjs/swagger"; import { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-result.dto"; import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard"; import { RolesGuard } from "src/auth/guards/role.guard"; @@ -29,7 +36,7 @@ import { ResendRequestDto } from "./dto/resend.dto"; @UseGuards(LocalActorAuthGuard, RolesGuard) @Roles(RoleEnum.EXPERT, RoleEnum.FIELD_EXPERT) export class ExpertBlameV2Controller { - constructor(private readonly expertBlameService: ExpertBlameService) { } + constructor(private readonly expertBlameService: ExpertBlameService) {} @Get() @ApiOperation({ @@ -63,7 +70,9 @@ export class ExpertBlameV2Controller { } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( - error instanceof Error ? error.message : "Failed to load blame status report", + error instanceof Error + ? error.message + : "Failed to load blame status report", ); } } @@ -76,7 +85,9 @@ export class ExpertBlameV2Controller { } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( - error instanceof Error ? error.message : "Failed to get blame case details", + error instanceof Error + ? error.message + : "Failed to get blame case details", ); } } @@ -96,7 +107,10 @@ export class ExpertBlameV2Controller { }) async assignForReview(@Param("id") id: string, @CurrentUser() actor: any) { try { - return await this.expertBlameService.assignBlameCaseForReviewV2(id, actor); + return await this.expertBlameService.assignBlameCaseForReviewV2( + id, + actor, + ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( @@ -110,6 +124,7 @@ export class ExpertBlameV2Controller { summary: "Lock blame case for review (legacy)", description: "Same assignment logic as POST `:id/assign`, but returns the legacy `{ _id, lock }` shape and maps conflicts to 400 instead of 409.", + deprecated: true, }) @ApiParam({ name: "id", description: "Blame case request id" }) async lockRequest(@Param("id") id: string, @CurrentUser() actor: any) { @@ -156,7 +171,9 @@ export class ExpertBlameV2Controller { } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( - error instanceof Error ? error.message : "Failed to submit expert reply", + error instanceof Error + ? error.message + : "Failed to submit expert reply", ); } } @@ -174,7 +191,9 @@ export class ExpertBlameV2Controller { } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( - error instanceof Error ? error.message : "Failed to submit expert reply for in person", + error instanceof Error + ? error.message + : "Failed to submit expert reply for in person", ); } } diff --git a/src/expert-claim/dto/claim-price-drop-v2.dto.ts b/src/expert-claim/dto/claim-price-drop-v2.dto.ts index 45ccaff..9dae4c5 100644 --- a/src/expert-claim/dto/claim-price-drop-v2.dto.ts +++ b/src/expert-claim/dto/claim-price-drop-v2.dto.ts @@ -66,6 +66,16 @@ export class UpsertClaimPriceDropV2Dto { @ValidateNested({ each: true }) @Type(() => PriceDropPartSeverityV2Dto) partSeverities: PriceDropPartSeverityV2Dto[]; + + @ApiPropertyOptional({ + description: + "Override: expert manually sets the final price-drop amount (in Rials). " + + "When provided, carPrice and partSeverities are not required and calculation is skipped entirely.", + example: 150000000, + }) + @IsOptional() + @IsNumber() + manualPriceDrop?: number; } export class ClaimPriceDropContextV2Dto { @@ -99,7 +109,9 @@ export class ClaimPriceDropContextV2Dto { }) severityOptions: Array<{ value: string; label_fa: string }>; - @ApiProperty({ description: "Full coefficient table for all price-drop parts" }) + @ApiProperty({ + description: "Full coefficient table for all price-drop parts", + }) priceDropCatalog: Array>; @ApiProperty({ diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index aa14f85..22e45b9 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -4226,6 +4226,49 @@ export class ExpertClaimService { } this.assertExpertCanEditClaimDuringReviewV2(claim, actor); + const vehicleSet: Record = {}; + if (body.carName != null && String(body.carName).trim()) { + vehicleSet["vehicle.carName"] = String(body.carName).trim(); + } + if (body.carModel != null && String(body.carModel).trim()) { + vehicleSet["vehicle.carModel"] = String(body.carModel).trim(); + } + + // Manual override path — expert knows the number, skip calculation entirely + if (body.manualPriceDrop != null) { + if (!Number.isFinite(body.manualPriceDrop) || body.manualPriceDrop < 0) { + throw new BadRequestException( + "manualPriceDrop must be a non-negative finite number.", + ); + } + + const manualPayload = { + manualOverride: true, + totalPriceDrop: body.manualPriceDrop, + partLines: [], + }; + + await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { + $set: { + ...vehicleSet, + "evaluation.priceDrop": manualPayload, + }, + }); + + const updated = await this.claimCaseDbService.findById(claimRequestId); + return { + claimRequestId, + vehicle: { + carName: updated?.vehicle?.carName, + carModel: updated?.vehicle?.carModel, + carType: updated?.vehicle?.carType, + }, + priceDrop: manualPayload, + partLines: [], + }; + } + + // Calculated path — existing logic unchanged const carType = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined; const selectedNorm = normalizeDamageSelectedParts( claim.damage?.selectedParts, @@ -4235,7 +4278,7 @@ export class ExpertClaimService { if (!body.partSeverities?.length) { throw new BadRequestException( - "Provide at least one partSeverities line for a damaged part.", + "Provide at least one partSeverities line, or use manualPriceDrop for a direct override.", ); } @@ -4290,14 +4333,6 @@ export class ExpertClaimService { partLines: lines, }; - const vehicleSet: Record = {}; - if (body.carName != null && String(body.carName).trim()) { - vehicleSet["vehicle.carName"] = String(body.carName).trim(); - } - if (body.carModel != null && String(body.carModel).trim()) { - vehicleSet["vehicle.carModel"] = String(body.carModel).trim(); - } - await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { $set: { ...vehicleSet, @@ -4306,7 +4341,6 @@ export class ExpertClaimService { }); const updated = await this.claimCaseDbService.findById(claimRequestId); - return { claimRequestId, vehicle: { diff --git a/src/expert-claim/expert-claim.v2.controller.ts b/src/expert-claim/expert-claim.v2.controller.ts index 3689e87..e7ba6e5 100644 --- a/src/expert-claim/expert-claim.v2.controller.ts +++ b/src/expert-claim/expert-claim.v2.controller.ts @@ -1,4 +1,17 @@ -import { Body, Controller, Get, Header, Param, Headers, Patch, Post, Put, UseGuards, Query, Res } from "@nestjs/common"; +import { + Body, + Controller, + Get, + Header, + Param, + Headers, + Patch, + Post, + Put, + UseGuards, + Query, + Res, +} from "@nestjs/common"; import { ApiBearerAuth, ApiBody, @@ -19,7 +32,10 @@ import { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-res import { ExpertClaimService } from "./expert-claim.service"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { GetClaimListV2ResponseDto } from "./dto/claim-list-v2.dto"; -import { ClaimSubmitResendV2Dto, SubmitExpertReplyV2Dto } from "./dto/expert-claim-v2.dto"; +import { + ClaimSubmitResendV2Dto, + SubmitExpertReplyV2Dto, +} from "./dto/expert-claim-v2.dto"; import { UpdateClaimDamagedPartsV2Dto } from "./dto/update-claim-damaged-parts-v2.dto"; import { ClaimPriceDropContextV2Dto, @@ -32,7 +48,7 @@ import { OuterPartCatalogItemDto } from "src/claim-request-management/dto/select import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog"; class InPersonVisitV2Dto { - @ApiPropertyOptional({ example: 'Paint damage requires physical inspection' }) + @ApiPropertyOptional({ example: "Paint damage requires physical inspection" }) @IsOptional() @IsString() note?: string; @@ -120,7 +136,10 @@ export class ExpertClaimV2Controller { @Param("claimRequestId") claimRequestId: string, @CurrentUser() actor, ) { - return await this.expertClaimService.getClaimDetailV2(claimRequestId, actor); + return await this.expertClaimService.getClaimDetailV2( + claimRequestId, + actor, + ); } @Get("request/:claimRequestId/price-drop") @@ -193,13 +212,17 @@ export class ExpertClaimV2Controller { "1. **Damage review queue** — claim status `WAITING_FOR_DAMAGE_EXPERT`. Locking advances the claim to `status=EXPERT_REVIEWING`, `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_DAMAGE_ASSESSMENT`.\n" + "2. **Factor validation queue** — claim status `EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`) with `claimStatus=UNDER_REVIEW` and `workflow.currentStep=EXPERT_COST_EVALUATION`. Locking only sets the workflow lock fields; status/claimStatus/currentStep are left untouched so the claim stays in the factor-validation queue when the lock expires.\n\n" + "Only one expert can hold the lock at a time. Re-locking by the same expert is idempotent. Same assignment logic as POST `assign/:claimRequestId`, but returns the legacy lock shape and maps conflicts to 400 instead of 409.", + deprecated: true, }) @ApiParam({ name: "claimRequestId" }) async lockClaimRequestV2( @Param("claimRequestId") claimRequestId: string, @CurrentUser() actor, ) { - return await this.expertClaimService.lockClaimRequestV2(claimRequestId, actor); + return await this.expertClaimService.lockClaimRequestV2( + claimRequestId, + actor, + ); } @Put("reply/submit/:claimRequestId") @@ -221,7 +244,11 @@ export class ExpertClaimV2Controller { @Body() body: SubmitExpertReplyV2Dto, @CurrentUser() actor, ) { - return await this.expertClaimService.submitExpertReplyV2(claimRequestId, body, actor); + return await this.expertClaimService.submitExpertReplyV2( + claimRequestId, + body, + actor, + ); } @Put("reply/resend/:claimRequestId") @@ -239,7 +266,11 @@ export class ExpertClaimV2Controller { @Body() body: ClaimSubmitResendV2Dto, @CurrentUser() actor, ) { - return await this.expertClaimService.submitResendDocsV2(claimRequestId, body, actor); + return await this.expertClaimService.submitResendDocsV2( + claimRequestId, + body, + actor, + ); } @Patch(":claimRequestId/visit")