Fixed enrichedEvaluation

This commit is contained in:
SepehrYahyaee
2026-05-26 16:35:13 +03:30
parent a994331439
commit 9003a7abb6
12 changed files with 364 additions and 366 deletions

View File

@@ -4,15 +4,14 @@ import {
HttpException,
HttpStatus,
Injectable,
Logger,
OnModuleInit,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import axios, { AxiosRequestConfig } from "axios";
import * as FormData from "form-data";
@Injectable()
export class AiService implements OnModuleInit {
private readonly logger = new Logger(AiService.name);
private apiKey: string;
private accessToken: string = null;
@@ -20,18 +19,20 @@ export class AiService implements OnModuleInit {
private readonly loginOptions: AxiosRequestConfig = {
method: "POST",
headers: { "Content-Type": "application/json" },
url: `${process.env.AI_URL_V2}/auth/login`,
url: `${this.configService.get<string>("AI_URL_V2")}/auth/login`,
data: {
username: process.env.AI_USERNAME,
password: process.env.AI_PASSWORD,
username: this.configService.get<string>("AI_USERNAME"),
password: this.configService.get<string>("AI_PASSWORD"),
},
timeout: 1000, // 30 second timeout
timeout: 1000, // TODO: Make this ENV
};
constructor(private readonly configService: ConfigService) {}
private get profileOptions(): AxiosRequestConfig {
return {
method: "GET",
url: `${process.env.AI_URL_V2}/auth/profile`,
url: `${this.configService.get<string>("AI_URL_V2")}/auth/profile`,
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
@@ -50,28 +51,16 @@ export class AiService implements OnModuleInit {
};
}
constructor() {}
async onModuleInit() {
try {
const res = await this.login();
if (res?.accessToken) {
this.logger.verbose("AI Service Authenticated Successfully.");
this.accessToken = res.accessToken;
await this.getApiKey();
this.logger.log("AI Service initialized and ready.");
} else {
this.logger.warn(
"AI Service Unavailable: Login did not return an access token. Will retry on first request.",
);
}
} catch (error) {
// Don't prevent app startup if AI service is temporarily unavailable
// The service will attempt to re-authenticate when aiRequestImage is called
this.logger.warn(
"AI Service Unavailable: Failed during initial login. Will retry on first request.",
);
this.logger.warn(`Error: ${error.message}`);
// Reset tokens so re-authentication will be attempted
this.accessToken = null;
this.apiKey = null;
@@ -79,42 +68,22 @@ export class AiService implements OnModuleInit {
}
private async login() {
try {
const loginResponse = await axios.request(this.loginOptions);
return loginResponse.data;
} catch (err) {
const errorMessage = err.response?.data?.message || err.message || "Unknown error";
const statusCode = err.response?.status || 500;
this.logger.error(`AI login failed: ${errorMessage} (Status: ${statusCode})`);
if (err.response?.data) {
this.logger.error(`AI login error details: ${JSON.stringify(err.response.data, null, 2)}`);
}
throw new HttpException(
`Could not authenticate with AI service: ${errorMessage}`,
statusCode >= 400 && statusCode < 500 ? statusCode : HttpStatus.UNAUTHORIZED,
);
}
const loginResponse = await axios.request(this.loginOptions);
return loginResponse.data;
}
private async getApiKey() {
try {
const profileResponse = await axios.request(this.profileOptions);
this.apiKey = profileResponse.data.apiKey.key;
this.logger.log("Successfully retrieved AI gateway API key.");
return this.apiKey;
} catch (err) {
this.logger.error("Failed to retrieve AI API key:", err.message);
throw new HttpException(
"Could not get API key from AI service",
HttpStatus.FAILED_DEPENDENCY,
);
}
const profileResponse = await axios.request(this.profileOptions);
this.apiKey = profileResponse.data.apiKey.key;
return this.apiKey;
}
public async aiRequestImage(file: { path: string; fileName?: string }): Promise<any> {
public async aiRequestImage(file: {
path: string;
fileName?: string;
}): Promise<any> {
// Ensure authentication is set up
if (!this.accessToken || !this.apiKey) {
this.logger.warn("AI service not authenticated, attempting to re-authenticate...");
try {
const res = await this.login();
if (res?.accessToken) {
@@ -127,7 +96,6 @@ export class AiService implements OnModuleInit {
);
}
} catch (error) {
this.logger.error("Failed to re-authenticate AI service:", error.message);
throw new HttpException(
"AI Service authentication failed",
HttpStatus.UNAUTHORIZED,
@@ -136,15 +104,12 @@ export class AiService implements OnModuleInit {
}
// Resolve relative paths to absolute paths
const filePath = file.path.startsWith("/")
? file.path
const filePath = file.path.startsWith("/")
? file.path
: join(process.cwd(), file.path.replace(/^\.\//, ""));
this.logger.log(`Processing AI image request for: ${filePath}`);
// Check if file exists
if (!existsSync(filePath)) {
this.logger.error(`File not found at path: ${filePath}`);
throw new HttpException(
`File not found: ${file.path}`,
HttpStatus.NOT_FOUND,
@@ -153,7 +118,7 @@ export class AiService implements OnModuleInit {
const form = new FormData();
const fileStream = createReadStream(filePath);
// Append file with filename if available
if (file.fileName) {
form.append("images", fileStream, file.fileName);
@@ -169,21 +134,10 @@ export class AiService implements OnModuleInit {
...this.imageProcessOptions.headers,
...form.getHeaders(),
};
this.logger.log(`[STEP 1/4] Sending request to AI service: ${this.imageProcessOptions.url}`);
this.logger.log(`[STEP 1/4] File: ${filePath}, Filename: ${file.fileName || 'extracted from path'}`);
this.logger.log(`[STEP 1/4] FormData Content-Type: ${form.getHeaders()['content-type']}`);
this.logger.log(`[STEP 1/4] Authorization header present: ${!!requestHeaders.Authorization}`);
this.logger.log(`[STEP 1/4] Gateway API key present: ${!!this.apiKey}`);
this.logger.log(`[STEP 1/4] Request method: POST`);
this.logger.log(`[STEP 1/4] FormData field name: "images"`);
// Get file stats for debugging
const fs = require('fs');
const fs = require("fs");
const stats = fs.statSync(filePath);
this.logger.log(`[STEP 1/4] File size: ${stats.size} bytes`);
this.logger.log(`[STEP 1/4] File exists: ${existsSync(filePath)}`);
const response = await axios.request({
...this.imageProcessOptions,
headers: requestHeaders,
@@ -192,11 +146,8 @@ export class AiService implements OnModuleInit {
maxBodyLength: Infinity,
});
this.logger.log(`[STEP 2/4] Successfully received response from AI service (Status: ${response.status})`);
// Validate response structure
if (!response.data) {
this.logger.error(`[ERROR] AI response is empty or missing data`);
throw new HttpException(
"AI Service returned empty response",
HttpStatus.BAD_GATEWAY,
@@ -205,9 +156,6 @@ export class AiService implements OnModuleInit {
// Check for error in response first (AI service returns 201 with error in body)
if (response.data.error) {
this.logger.error(`[ERROR] AI service returned an error in response body`);
this.logger.error(`[ERROR] Error message: ${response.data.error}`);
this.logger.error(`[ERROR] Full response: ${JSON.stringify(response.data, null, 2)}`);
throw new HttpException(
`AI Service error: ${response.data.error}`,
HttpStatus.BAD_GATEWAY,
@@ -216,34 +164,26 @@ export class AiService implements OnModuleInit {
// Check for processed image (downloadLink)
if (!response.data.downloadLink) {
this.logger.error(`[ERROR] AI response missing processed image (downloadLink)`);
this.logger.error(`[ERROR] Response structure: ${JSON.stringify(Object.keys(response.data))}`);
this.logger.error(`[ERROR] Full response: ${JSON.stringify(response.data, null, 2)}`);
throw new HttpException(
"AI Service did not return processed image (downloadLink missing)",
HttpStatus.BAD_GATEWAY,
);
}
// Check for reports
if (!response.data.reports) {
this.logger.warn(`[WARNING] AI response missing reports object, but downloadLink exists`);
this.logger.warn(`[WARNING] Response keys: ${JSON.stringify(Object.keys(response.data))}`);
}
this.logger.log(`[STEP 3/4] Validated AI response - downloadLink: ${response.data.downloadLink ? 'present' : 'missing'}, reports: ${response.data.reports ? 'present' : 'missing'}`);
return response.data;
} catch (er) {
// Determine error source
let errorSource = "UNKNOWN";
let errorMessage = er.message;
let errorDetails = "No error details available";
if (er.response) {
errorSource = "AI_SERVICE_RESPONSE";
errorMessage = er.response?.data?.message || er.message || `HTTP ${er.response.status}`;
errorDetails = er.response?.data
errorMessage =
er.response?.data?.message ||
er.message ||
`HTTP ${er.response.status}`;
errorDetails = er.response?.data
? JSON.stringify(er.response.data, null, 2)
: `Status: ${er.response.status}, StatusText: ${er.response.statusText}`;
} else if (er.request) {
@@ -254,15 +194,7 @@ export class AiService implements OnModuleInit {
errorSource = "REQUEST_SETUP_ERROR";
errorMessage = er.message || "Error setting up request";
}
this.logger.error(`[ERROR] AI request failed - Source: ${errorSource}`);
this.logger.error(`[ERROR] File path: ${filePath}`);
this.logger.error(`[ERROR] Error message: ${errorMessage}`);
this.logger.error(`[ERROR] Error details: ${errorDetails}`);
if (er.stack) {
this.logger.error(`[ERROR] Stack trace: ${er.stack}`);
}
// Re-throw with detailed error information
throw new HttpException(
`[${errorSource}] ${errorMessage}`,

View File

@@ -1,6 +1,7 @@
import { join } from "node:path";
import { Module, ValidationPipe } from "@nestjs/common";
import { APP_INTERCEPTOR, APP_PIPE } from "@nestjs/core";
import { APP_INTERCEPTOR } from "@nestjs/core";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { UnicodeDigitsNormalizeInterceptor } from "./common/interceptors/unicode-digits-normalize.interceptor";
import { MongooseModule } from "@nestjs/mongoose";
import { ScheduleModule } from "@nestjs/schedule";
@@ -30,6 +31,7 @@ dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ScheduleModule.forRoot(),
CronModule,
ServeStaticModule.forRoot({

View File

@@ -26,7 +26,6 @@ import { RolesGuard } from "src/auth/guards/role.guard";
import { ClientKey } from "src/decorators/clientKey.decorator";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
import { LoggingInterceptor } from "src/interceptor/logging.interceptors";
import { RoleEnum } from "src/Types&Enums/role.enum";
import {
ResendFirstPartyDto,
@@ -135,7 +134,6 @@ export class ExpertBlameController {
return this.expertBlameService.streamVideo(requestId);
}
@UseInterceptors(LoggingInterceptor)
@ApiOperation({ deprecated: true })
@Get("voice/:requestId/:voiceId")
@Roles(RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT)

View File

@@ -1,6 +1,6 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { BlameRequestType } from 'src/Types&Enums/blame-request-management/blameRequestType.enum';
import { DamageSelectedPartV2BodyDto } from 'src/claim-request-management/dto/damage-selected-part-v2.dto';
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { DamageSelectedPartV2BodyDto } from "src/claim-request-management/dto/damage-selected-part-v2.dto";
export class ClaimDetailV2ResponseDto {
@ApiProperty()
@@ -21,7 +21,7 @@ export class ClaimDetailV2ResponseDto {
@ApiPropertyOptional()
nextStep?: string;
@ApiProperty({ description: 'Whether claim is locked' })
@ApiProperty({ description: "Whether claim is locked" })
locked: boolean;
@ApiPropertyOptional()
@@ -47,64 +47,70 @@ export class ClaimDetailV2ResponseDto {
};
@ApiPropertyOptional({
description: 'Blame file type from linked blame case',
description: "Blame file type from linked blame case",
enum: BlameRequestType,
})
blameRequestType?: BlameRequestType;
@ApiPropertyOptional({
description:
'CAR_BODY only: first-step flags — another car (`car`) and/or object (`object`)',
"CAR_BODY only: first-step flags — another car (`car`) and/or object (`object`)",
example: { car: true, object: false },
})
carBodyFirstForm?: { car?: boolean; object?: boolean };
@ApiPropertyOptional({ description: 'Blame request ID', example: '507f...' })
@ApiPropertyOptional({ description: "Blame request ID", example: "507f..." })
blameRequestId?: string;
@ApiPropertyOptional({ description: 'Blame request number', example: 'BL12345' })
@ApiPropertyOptional({
description: "Blame request number",
example: "BL12345",
})
blameRequestNo?: string;
@ApiPropertyOptional({
description: 'Selected outer damaged parts (id, name, side, label_fa)',
description: "Selected outer damaged parts (id, name, side, label_fa)",
type: [DamageSelectedPartV2BodyDto],
})
selectedParts?: DamageSelectedPartV2BodyDto[];
@ApiPropertyOptional({ description: 'Selected other damaged parts' })
@ApiPropertyOptional({ description: "Selected other damaged parts" })
otherParts?: string[];
@ApiPropertyOptional({ description: 'Required documents status' })
requiredDocuments?: Record<string, {
uploaded: boolean;
fileId?: string;
fileName?: string;
filePath?: string;
}>;
@ApiPropertyOptional({ description: "Required documents status" })
requiredDocuments?: Record<
string,
{
uploaded: boolean;
fileId?: string;
fileName?: string;
filePath?: string;
}
>;
@ApiPropertyOptional({ description: 'Car angles captured' })
@ApiPropertyOptional({ description: "Car angles captured" })
carAngles?: Record<string, { captured: boolean; url?: string }>;
@ApiPropertyOptional({
description:
'Per-part captures (array index matches selectedParts; includes id, name, side, label_fa, captured, url)',
type: 'array',
"Per-part captures (array index matches selectedParts; includes id, name, side, label_fa, captured, url)",
type: "array",
items: {
type: 'object',
type: "object",
properties: {
index: { type: 'number' },
index: { type: "number" },
partId: {
type: 'number',
type: "number",
nullable: true,
description:
'Numeric catalog part id for outer parts (`null` for non-catalog lines); use in reply/submit and price-drop',
"Numeric catalog part id for outer parts (`null` for non-catalog lines); use in reply/submit and price-drop",
},
id: { type: 'number', nullable: true },
name: { type: 'string' },
side: { type: 'string' },
label_fa: { type: 'string' },
captured: { type: 'boolean' },
url: { type: 'string' },
id: { type: "number", nullable: true },
name: { type: "string" },
side: { type: "string" },
label_fa: { type: "string" },
captured: { type: "boolean" },
url: { type: "string" },
},
},
})
@@ -121,24 +127,25 @@ export class ClaimDetailV2ResponseDto {
@ApiPropertyOptional({
description:
'True when user uploaded all required factors and the case awaits expert approve/reject.',
"True when user uploaded all required factors and the case awaits expert approve/reject.",
})
awaitingFactorValidation?: boolean;
@ApiPropertyOptional({
description:
'Slice of `claim.evaluation` exposed to the damage expert. ' +
'`damageExpertReply` / `damageExpertReplyFinal` are returned only while ' +
'awaiting factor validation. `ownerInsurerApproval` and ' +
'`ownerPricedPartsApproval` are returned whenever they exist on the ' +
'claim — each carries `signLink` (resolved from `signDetailId`) so the ' +
'front-end can render the user signature directly. Likewise, ' +
'`damageExpertReply.userComment` / `damageExpertReplyFinal.userComment` ' +
'expose `signLink` when a user comment signature is present.',
"Slice of `claim.evaluation` exposed to the damage expert. " +
"`damageExpertReply` / `damageExpertReplyFinal` are returned only while " +
"awaiting factor validation. `ownerInsurerApproval` and " +
"`ownerPricedPartsApproval` are returned whenever they exist on the " +
"claim — each carries `signLink` (resolved from `signDetailId`) so the " +
"front-end can render the user signature directly. Likewise, " +
"`damageExpertReply.userComment` / `damageExpertReplyFinal.userComment` " +
"expose `signLink` when a user comment signature is present.",
})
evaluation?: {
damageExpertReply?: unknown;
damageExpertReplyFinal?: unknown;
damageExpertResend: unknown;
ownerInsurerApproval?: {
agree: boolean;
branchId?: string;
@@ -166,7 +173,7 @@ export class ClaimDetailV2ResponseDto {
@ApiPropertyOptional({
description:
'Owner objection to expert-priced parts (`evaluation.objection`): disputed lines + optional new parts + submission time.',
"Owner objection to expert-priced parts (`evaluation.objection`): disputed lines + optional new parts + submission time.",
})
objection?: {
objectionParts?: Array<Record<string, unknown>>;
@@ -176,7 +183,7 @@ export class ClaimDetailV2ResponseDto {
@ApiPropertyOptional({
description:
'Car walk-around video from `claim-video-capture` (resolved from `media.videoCaptureId`).',
"Car walk-around video from `claim-video-capture` (resolved from `media.videoCaptureId`).",
})
videoCapture?: {
id: string;
@@ -187,18 +194,19 @@ export class ClaimDetailV2ResponseDto {
@ApiPropertyOptional({
description:
'Linked blame case (`blameCases`), same shape as expert-blame detail: parties with video/voice URLs, workflow, expert, formatted dates.',
"Linked blame case (`blameCases`), same shape as expert-blame detail: parties with video/voice URLs, workflow, expert, formatted dates.",
})
blameCase?: Record<string, unknown>;
@ApiPropertyOptional({
description:
'Same as `blameCase.expert.decision` when present: guilty party id, description, accident fields, decidedAt.',
"Same as `blameCase.expert.decision` when present: guilty party id, description, accident fields, decidedAt.",
})
blameExpertDecision?: Record<string, unknown>;
@ApiPropertyOptional({
description: 'Claim payout / banking metadata from the claim file (e.g. Sheba).',
description:
"Claim payout / banking metadata from the claim file (e.g. Sheba).",
})
money?: {
sheba?: string;

View File

@@ -73,7 +73,10 @@ import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/clai
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import { GetClaimListV2ResponseDto, ClaimListItemV2Dto } from "./dto/claim-list-v2.dto";
import {
GetClaimListV2ResponseDto,
ClaimListItemV2Dto,
} from "./dto/claim-list-v2.dto";
import { ClaimDetailV2ResponseDto } from "./dto/claim-detail-v2.dto";
import { ClaimSubmitReplyDto, ClaimSubmitResendDto } from "./dto/reply.dto";
import { UpdateClaimDamagedPartsV2Dto } from "./dto/update-claim-damaged-parts-v2.dto";
@@ -257,7 +260,7 @@ export class ExpertClaimService {
private readonly userDbService: UserDbService,
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
private readonly claimSignDbService: ClaimSignDbService,
) { }
) {}
/**
* Resolve a `claim-sign` document id to a downloadable file URL.
@@ -294,7 +297,10 @@ export class ExpertClaimService {
evaluation: Record<string, unknown> | undefined | null,
): Promise<Record<string, unknown> | undefined> {
if (!evaluation || typeof evaluation !== "object") return undefined;
const ev = JSON.parse(JSON.stringify(evaluation)) as Record<string, unknown>;
const ev = JSON.parse(JSON.stringify(evaluation)) as Record<
string,
unknown
>;
const enrichReply = async (key: string) => {
const reply = ev[key] as Record<string, unknown> | undefined;
@@ -389,7 +395,9 @@ export class ExpertClaimService {
}
/** Owner mobile: linked blame party phone when available, else `users.mobile`. */
private async resolveClaimOwnerPhone(claim: any): Promise<string | undefined> {
private async resolveClaimOwnerPhone(
claim: any,
): Promise<string | undefined> {
if (!claim?.owner?.userId) return undefined;
const ownerUserId = String(claim.owner.userId);
if (claim.blameRequestId) {
@@ -412,12 +420,14 @@ export class ExpertClaimService {
}
/** Map blame party `Vehicle` (name/model/type) to expert panel shape. */
private expertVehicleFromPartyVehicle(v: any): {
carName?: string;
carModel?: string;
carType?: string;
plate?: { plateId?: string };
} | undefined {
private expertVehicleFromPartyVehicle(v: any):
| {
carName?: string;
carModel?: string;
carType?: string;
plate?: { plateId?: string };
}
| undefined {
if (!v || typeof v !== "object") return undefined;
const carName = v.name ?? v.carName;
const carModel = v.model ?? v.carModel;
@@ -449,16 +459,14 @@ export class ExpertClaimService {
const ownerId = claim?.owner?.userId ? String(claim.owner.userId) : "";
let party = ownerId
? parties.find(
(p: any) =>
p?.person?.userId && String(p.person.userId) === ownerId,
(p: any) => p?.person?.userId && String(p.person.userId) === ownerId,
)
: undefined;
if (!party && blame?.expert?.decision?.guiltyPartyId) {
const guilty = String(blame.expert.decision.guiltyPartyId);
party = parties.find(
(p: any) =>
p?.person?.userId && String(p.person.userId) !== guilty,
(p: any) => p?.person?.userId && String(p.person.userId) !== guilty,
);
}
@@ -471,13 +479,15 @@ export class ExpertClaimService {
claim: any,
blameById: Map<string, any>,
):
| { carName?: string; carModel?: string; carType?: string; plate?: { plateId?: string } }
| {
carName?: string;
carModel?: string;
carType?: string;
plate?: { plateId?: string };
}
| undefined {
const cv = claim?.vehicle;
if (
cv &&
(cv.carName || cv.carModel || cv.carType || (cv as any).plate)
) {
if (cv && (cv.carName || cv.carModel || cv.carType || (cv as any).plate)) {
return {
carName: cv.carName,
carModel: cv.carModel,
@@ -498,7 +508,7 @@ export class ExpertClaimService {
private blameFileContextForExpert(blame: any): {
blameRequestType?: BlameRequestType;
carBodyFirstForm?: { car?: boolean; object?: boolean };
blameStatus?: string
blameStatus?: string;
} {
if (!blame?.type) return {};
const blameRequestType = blame.type as BlameRequestType;
@@ -516,7 +526,7 @@ export class ExpertClaimService {
const first =
parties.find((p: any) => p?.role === PartyRole.FIRST) ?? parties[0];
const cbf = first?.carBodyFirstForm;
delete out.blameStatus
delete out.blameStatus;
if (cbf && typeof cbf === "object") {
out.carBodyFirstForm = {
...(typeof cbf.car === "boolean" ? { car: cbf.car } : {}),
@@ -568,7 +578,7 @@ export class ExpertClaimService {
// ReqClaimStatus.PendingFactorValidation,
// ],
// },
// "damageExpertReply.actorDetail.actorId":actor.sub
// "damageExpertReply.actorDetail.actorId":actor.sub
// },
// );
@@ -592,7 +602,7 @@ export class ExpertClaimService {
"damageExpertReply.actorDetail.actorId": actor.sub,
},
],
}
},
);
const filteredRequests = [];
@@ -681,14 +691,17 @@ export class ExpertClaimService {
const lockedByCurrent =
String(request?.actorLocked?.actorId || "") === String(actorDetail.sub);
const isLockExpired =
!!request.unlockTime && Date.now() >= new Date(request.unlockTime).getTime();
!!request.unlockTime &&
Date.now() >= new Date(request.unlockTime).getTime();
// Idempotent behavior: same expert can re-enter their locked file.
if (isLocked && lockedByCurrent && !isLockExpired) {
return { _id: requestId, lock: true, message: "Already locked by you" };
}
if (isLocked && !lockedByCurrent && !isLockExpired) {
throw new BadRequestException("Claim request is locked by another expert");
throw new BadRequestException(
"Claim request is locked by another expert",
);
}
if (request.claimStatus === ReqClaimStatus.UserPending) {
@@ -747,8 +760,8 @@ export class ExpertClaimService {
}
private normalizeText(str: string) {
if (!str || typeof str !== 'string') {
return '';
if (!str || typeof str !== "string") {
return "";
}
return str
.replace(/[٠-٩۰-۹]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 1728))
@@ -779,10 +792,8 @@ export class ExpertClaimService {
const tp =
decision.totalPayment != null &&
String(decision.totalPayment).trim() !== "";
const pr =
decision.price != null && String(decision.price).trim() !== "";
const sa =
decision.salary != null && String(decision.salary).trim() !== "";
const pr = decision.price != null && String(decision.price).trim() !== "";
const sa = decision.salary != null && String(decision.salary).trim() !== "";
return tp || (pr && sa);
}
@@ -925,7 +936,7 @@ export class ExpertClaimService {
}
async calculatePriceDrop(request: any, carPartsAi: any, query?: any) {
const requestId = request?._id?.toString() || 'unknown';
const requestId = request?._id?.toString() || "unknown";
try {
// Step 1: Check SandHub data
@@ -946,7 +957,10 @@ export class ExpertClaimService {
return;
}
if (sandHubData.MapTypNam === undefined || sandHubData.MapTypNam === null) {
if (
sandHubData.MapTypNam === undefined ||
sandHubData.MapTypNam === null
) {
this.logger.warn(
`[PriceDrop] Request ${requestId}: SandHub data missing ModelCii. Cannot calculate price drop.`,
);
@@ -957,7 +971,10 @@ export class ExpertClaimService {
const manualOverride = query || {};
// Step 2: Check car parts and get severity values
if (!carPartsAi || (Array.isArray(carPartsAi) && carPartsAi.length === 0)) {
if (
!carPartsAi ||
(Array.isArray(carPartsAi) && carPartsAi.length === 0)
) {
this.logger.warn(
`[PriceDrop] Request ${requestId}: No car parts data provided. Cannot calculate severity values.`,
);
@@ -1035,19 +1052,23 @@ export class ExpertClaimService {
}
// Step 5: Log calculation result
if (!finalPriceDropData.carPrice || !finalPriceDropData.carValue || finalPriceDropData.carValue.length === 0) {
if (
!finalPriceDropData.carPrice ||
!finalPriceDropData.carValue ||
finalPriceDropData.carValue.length === 0
) {
this.logger.warn(
`[PriceDrop] Request ${requestId}: Price drop calculated but missing critical data. ` +
`carPrice: ${finalPriceDropData.carPrice}, ` +
`carValue length: ${finalPriceDropData.carValue?.length || 0}, ` +
`total: ${finalPriceDropData.total}`,
`carPrice: ${finalPriceDropData.carPrice}, ` +
`carValue length: ${finalPriceDropData.carValue?.length || 0}, ` +
`total: ${finalPriceDropData.total}`,
);
} else {
this.logger.debug(
`[PriceDrop] Request ${requestId}: Successfully calculated price drop. ` +
`carPrice: ${finalPriceDropData.carPrice}, ` +
`sumOfSeverity: ${finalPriceDropData.sumOfSeverity}, ` +
`total: ${finalPriceDropData.total}`,
`carPrice: ${finalPriceDropData.carPrice}, ` +
`sumOfSeverity: ${finalPriceDropData.sumOfSeverity}, ` +
`total: ${finalPriceDropData.total}`,
);
}
@@ -1123,7 +1144,7 @@ export class ExpertClaimService {
const severityValue =
this.priceDropPart[originalPriceDropKey]?.[
damagedPartInfo.severity
damagedPartInfo.severity
];
if (severityValue !== undefined) {
@@ -1146,9 +1167,7 @@ export class ExpertClaimService {
for (const item of endpoints) {
try {
const url = `${process.env.CW_URL}price?${item}`;
const response = await lastValueFrom(
this.httpService.get(url),
);
const response = await lastValueFrom(this.httpService.get(url));
if (Array.isArray(response.data)) {
let validEntries = 0;
for (const r of response.data) {
@@ -1178,7 +1197,7 @@ export class ExpertClaimService {
if (carPrices.length === 0) {
this.logger.error(
`[PriceDrop] No car prices fetched from any endpoint. Endpoints tried: ${endpoints.join(', ')}`,
`[PriceDrop] No car prices fetched from any endpoint. Endpoints tried: ${endpoints.join(", ")}`,
);
} else {
this.logger.debug(
@@ -1234,7 +1253,10 @@ export class ExpertClaimService {
await this.populateFactorLinks(requestUpdated.damageExpertReplyFinal);
// 2. Populate required documents with file links
if (requestUpdated.requiredDocuments && Object.keys(requestUpdated.requiredDocuments).length > 0) {
if (
requestUpdated.requiredDocuments &&
Object.keys(requestUpdated.requiredDocuments).length > 0
) {
for (const documentType in requestUpdated.requiredDocuments) {
const documentId = requestUpdated.requiredDocuments[documentType];
if (documentId) {
@@ -1244,7 +1266,9 @@ export class ExpertClaimService {
);
if (doc && doc.path) {
// Replace ID with file URL
requestUpdated.requiredDocuments[documentType] = buildFileLink(doc.path) as any;
requestUpdated.requiredDocuments[documentType] = buildFileLink(
doc.path,
) as any;
}
} catch (error) {
this.logger.warn(
@@ -1384,9 +1408,8 @@ export class ExpertClaimService {
}
// Fetch all required documents for this claim
const documents = await this.claimRequiredDocumentDbService.findByClaimId(
requestId,
);
const documents =
await this.claimRequiredDocumentDbService.findByClaimId(requestId);
// Populate with file URLs
const populatedDocuments = documents.map((doc) => ({
@@ -1443,7 +1466,10 @@ export class ExpertClaimService {
}
private isRequestLocked(request: any, currentUser?: any): boolean {
if (!request.lockFile || request.claimStatus !== ReqClaimStatus.ReviewRequest) {
if (
!request.lockFile ||
request.claimStatus !== ReqClaimStatus.ReviewRequest
) {
return false;
}
@@ -2370,7 +2396,8 @@ export class ExpertClaimService {
assertClaimCaseForTenant(claim, actor);
const isFactorValidationLock = claimIsAwaitingExpertFactorValidationV2(claim);
const isFactorValidationLock =
claimIsAwaitingExpertFactorValidationV2(claim);
const isDamageReviewLock =
claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT;
@@ -2657,7 +2684,7 @@ export class ExpertClaimService {
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException('Claim request not found');
throw new NotFoundException("Claim request not found");
}
assertClaimCaseForTenant(claim, actor);
@@ -2670,12 +2697,12 @@ export class ExpertClaimService {
if (!claim.workflow?.locked) {
throw new ForbiddenException(
'You must lock the claim before submitting a resend request',
"You must lock the claim before submitting a resend request",
);
}
if (claim.workflow.lockedBy?.actorId?.toString() !== actor.sub) {
throw new ForbiddenException('This claim is locked by another expert');
throw new ForbiddenException("This claim is locked by another expert");
}
// Block duplicate resend while a prior request is still unfinished (stale EXPERT_REVIEWING + pending resend).
@@ -2815,14 +2842,14 @@ export class ExpertClaimService {
*/
async submitExpertReplyV2(
claimRequestId: string,
reply: import('./dto/expert-claim-v2.dto').SubmitExpertReplyV2Dto,
reply: import("./dto/expert-claim-v2.dto").SubmitExpertReplyV2Dto,
actor: any,
) {
requireActorClientKey(actor);
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException('Claim request not found');
throw new NotFoundException("Claim request not found");
}
assertClaimCaseForTenant(claim, actor);
@@ -2835,31 +2862,33 @@ export class ExpertClaimService {
if (!claim.workflow?.locked) {
throw new ForbiddenException(
'You must lock the claim before submitting a reply',
"You must lock the claim before submitting a reply",
);
}
if (claim.workflow.lockedBy?.actorId?.toString() !== actor.sub) {
throw new ForbiddenException('This claim is locked by another expert');
throw new ForbiddenException("This claim is locked by another expert");
}
// Price cap validation
const PRICE_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP;
let totalPrice = 0;
for (const part of reply.parts || []) {
const parsed = this.parsePersianNumber(String(part.totalPayment ?? '0'));
const parsed = this.parsePersianNumber(String(part.totalPayment ?? "0"));
if (!isNaN(parsed)) totalPrice += parsed;
}
if (totalPrice > PRICE_CAP) {
throw new BadRequestException({
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${PRICE_CAP.toLocaleString()}).`,
error: 'PRICE_CAP_ERROR',
error: "PRICE_CAP_ERROR",
totalPrice,
priceCap: PRICE_CAP,
});
}
const carTypeSubmit = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
const carTypeSubmit = claim.vehicle?.carType as
| ClaimVehicleTypeV2
| undefined;
const daghiNormalized =
reply.parts?.length > 0
? this.validateAndNormalizeDaghiForExpertReplyV2(reply.parts)
@@ -2933,9 +2962,8 @@ export class ExpertClaimService {
};
});
const { mixedFactorAndPrice, allFactorLines } = classifyV2ExpertPricingParts(
processedParts,
);
const { mixedFactorAndPrice, allFactorLines } =
classifyV2ExpertPricingParts(processedParts);
const needsFactorUpload =
reply.parts?.some((p) => p.factorNeeded === true) ?? false;
@@ -2944,14 +2972,14 @@ export class ExpertClaimService {
if (objectionSubmitted && hasFinalReply) {
throw new ConflictException(
'A final expert reply after objection already exists for this claim.',
"A final expert reply after objection already exists for this claim.",
);
}
const isFinalReplyAfterObjection = objectionSubmitted && !hasFinalReply;
const replyField = isFinalReplyAfterObjection
? 'damageExpertReplyFinal'
: 'damageExpertReply';
? "damageExpertReplyFinal"
: "damageExpertReply";
const completedStep = isFinalReplyAfterObjection
? ClaimWorkflowStep.EXPERT_FINAL_REPLY
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
@@ -3006,28 +3034,30 @@ export class ExpertClaimService {
...(expertAddedParts.length > 0
? { "damage.selectedParts": mergedSelectedParts }
: {}),
'workflow.locked': false,
"workflow.locked": false,
$unset: {
'workflow.lockedAt': '',
'workflow.expiredAt': '',
'workflow.lockedBy': '',
'workflow.preLockQueueSnapshot': '',
'evaluation.ownerInsurerApproval': "",
'evaluation.ownerPricedPartsApproval': "",
"workflow.lockedAt": "",
"workflow.expiredAt": "",
"workflow.lockedBy": "",
"workflow.preLockQueueSnapshot": "",
"evaluation.ownerInsurerApproval": "",
"evaluation.ownerPricedPartsApproval": "",
},
'workflow.currentStep': currentStep,
'workflow.nextStep': needsFactorUpload ? nextWorkflowStep : ClaimWorkflowStep.CLAIM_COMPLETED,
"workflow.currentStep": currentStep,
"workflow.nextStep": needsFactorUpload
? nextWorkflowStep
: ClaimWorkflowStep.CLAIM_COMPLETED,
[`evaluation.${replyField}`]: replyPayload,
$push: {
'workflow.completedSteps': completedStep,
"workflow.completedSteps": completedStep,
history: {
type: isFinalReplyAfterObjection
? 'EXPERT_FINAL_REPLY_SUBMITTED'
: 'EXPERT_REPLY_SUBMITTED',
? "EXPERT_FINAL_REPLY_SUBMITTED"
: "EXPERT_REPLY_SUBMITTED",
actor: {
actorId: new Types.ObjectId(actor.sub),
actorName: actor.fullName,
actorType: 'damage_expert',
actorType: "damage_expert",
},
timestamp: new Date(),
metadata: {
@@ -3042,7 +3072,10 @@ export class ExpertClaimService {
},
};
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updatePayload);
await this.claimCaseDbService.findByIdAndUpdate(
claimRequestId,
updatePayload,
);
await this.recordClaimExpertActivity({
expertId: String(actor.sub),
@@ -3070,7 +3103,9 @@ export class ExpertClaimService {
status: nextCaseStatus,
claimStatus: nextClaimStatus,
currentStep,
workflowNextStep: needsFactorUpload ? nextWorkflowStep : ClaimWorkflowStep.CLAIM_COMPLETED,
workflowNextStep: needsFactorUpload
? nextWorkflowStep
: ClaimWorkflowStep.CLAIM_COMPLETED,
factorNeeded: needsFactorUpload,
mixedPricingAndFactors: mixedFactorAndPrice,
allPartsFactorNeeded: !!needsFactorUpload && !!allFactorLines,
@@ -3095,12 +3130,16 @@ export class ExpertClaimService {
* - Records history event IN_PERSON_VISIT_REQUESTED
* - Unlocks the workflow so user can act
*/
async requestInPersonVisitV2(claimRequestId: string, actor: any, note?: string) {
async requestInPersonVisitV2(
claimRequestId: string,
actor: any,
note?: string,
) {
requireActorClientKey(actor);
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException('Claim request not found');
throw new NotFoundException("Claim request not found");
}
assertClaimCaseForTenant(claim, actor);
@@ -3113,28 +3152,28 @@ export class ExpertClaimService {
if (!claim.workflow?.locked) {
throw new ForbiddenException(
'You must lock the claim before requesting an in-person visit',
"You must lock the claim before requesting an in-person visit",
);
}
if (claim.workflow.lockedBy?.actorId?.toString() !== actor.sub) {
throw new ForbiddenException('This claim is locked by another expert');
throw new ForbiddenException("This claim is locked by another expert");
}
const visitSnapshot = await this.snapshotDamageExpert(actor.sub);
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
claimStatus: ClaimStatus.NEEDS_REVISION,
'workflow.locked': false,
"workflow.locked": false,
$unset: {
'workflow.lockedAt': '',
'workflow.expiredAt': '',
'workflow.lockedBy': '',
'workflow.preLockQueueSnapshot': '',
"workflow.lockedAt": "",
"workflow.expiredAt": "",
"workflow.lockedBy": "",
"workflow.preLockQueueSnapshot": "",
},
...(note ? { 'evaluation.visitLocation': note } : {}),
...(note ? { "evaluation.visitLocation": note } : {}),
...(visitSnapshot && {
'evaluation.inPersonVisitExpertProfileSnapshot': visitSnapshot,
"evaluation.inPersonVisitExpertProfileSnapshot": visitSnapshot,
}),
$push: {
history: {
@@ -3199,7 +3238,10 @@ export class ExpertClaimService {
const rows =
orClauses.length === 0
? []
: await this.claimCaseDbService.find({ $or: orClauses }, { lean: true });
: await this.claimCaseDbService.find(
{ $or: orClauses },
{ lean: true },
);
const buckets = initialClaimExpertReportBuckets();
const seen = new Set<string>();
@@ -3207,12 +3249,7 @@ export class ExpertClaimService {
const id = String(doc._id ?? "");
if (seen.has(id)) continue;
if (
!claimDocInExpertPortfolio(
doc,
expertId,
activityFileIds,
clientKey,
)
!claimDocInExpertPortfolio(doc, expertId, activityFileIds, clientKey)
) {
continue;
}
@@ -3221,10 +3258,17 @@ export class ExpertClaimService {
buckets.all++;
buckets[key] = (buckets[key] ?? 0) + 1;
}
// Add new calculated fields for front-end
buckets["PENDING_ON_USER"] = buckets[ClaimCaseStatus.WAITING_FOR_USER_RESEND] + buckets[ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL] + buckets[ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN] + buckets[ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING] + buckets[ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING];
buckets["CHECK_NEEDED"] = buckets["PENDING_ON_USER"] + buckets[ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT]
buckets["PENDING_ON_USER"] =
buckets[ClaimCaseStatus.WAITING_FOR_USER_RESEND] +
buckets[ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL] +
buckets[ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN] +
buckets[ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING] +
buckets[ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING];
buckets["CHECK_NEEDED"] =
buckets["PENDING_ON_USER"] +
buckets[ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT];
return buckets;
}
@@ -3252,17 +3296,17 @@ export class ExpertClaimService {
// Available claims: waiting for expert, not locked
{
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
'workflow.locked': { $ne: true },
"workflow.locked": { $ne: true },
},
// Expert reviewing but lock cleared (e.g. TTL expiry) — still list until status is reconciled
{
status: ClaimCaseStatus.EXPERT_REVIEWING,
'workflow.locked': { $ne: true },
"workflow.locked": { $ne: true },
},
// This expert's own locked/in-progress claims
{
'workflow.locked': true,
'workflow.lockedBy.actorId': new Types.ObjectId(actorId),
"workflow.locked": true,
"workflow.lockedBy.actorId": new Types.ObjectId(actorId),
},
// User uploaded all factors; expert must approve/reject (unlocked queue)
{
@@ -3270,12 +3314,12 @@ export class ExpertClaimService {
{
status: ClaimCaseStatus.EXPERT_VALIDATING_REPAIR_FACTORS,
claimStatus: ClaimStatus.UNDER_REVIEW,
'workflow.currentStep': ClaimWorkflowStep.EXPERT_COST_EVALUATION,
"workflow.currentStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION,
},
{
status: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
claimStatus: ClaimStatus.UNDER_REVIEW,
'workflow.currentStep': ClaimWorkflowStep.EXPERT_COST_EVALUATION,
"workflow.currentStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION,
},
],
},
@@ -3288,8 +3332,7 @@ export class ExpertClaimService {
const staleLockToReconcile = filtered.filter(
(c) =>
c.workflow?.locked &&
!this.isClaimV2WorkflowLockCurrentlyEnforced(c),
c.workflow?.locked && !this.isClaimV2WorkflowLockCurrentlyEnforced(c),
);
const touchedIds = (
await Promise.all(
@@ -3306,9 +3349,7 @@ export class ExpertClaimService {
const refreshed = await this.claimCaseDbService.find({
_id: { $in: touchedIds.map((id) => new Types.ObjectId(id)) },
});
const byId = new Map(
refreshed.map((r) => [String(r._id), r] as const),
);
const byId = new Map(refreshed.map((r) => [String(r._id), r] as const));
for (const c of filtered) {
const r = byId.get(String(c._id));
if (r) Object.assign(c, r);
@@ -3334,14 +3375,16 @@ export class ExpertClaimService {
);
const list = filtered.map((c) => {
const awaitingFactorValidation = claimIsAwaitingExpertFactorValidationV2(c);
const awaitingFactorValidation =
claimIsAwaitingExpertFactorValidationV2(c);
const v = this.vehicleForExpertFromClaimAndBlameMap(c, blameById);
const blame = c.blameRequestId
? blameById.get(c.blameRequestId.toString())
: undefined;
const fileCtx = blame ? this.blameFileContextForExpert(blame) : {};
const lockActive =
!!(c.workflow?.locked && this.isClaimV2WorkflowLockCurrentlyEnforced(c));
const lockActive = !!(
c.workflow?.locked && this.isClaimV2WorkflowLockCurrentlyEnforced(c)
);
const statusForList =
c.status === ClaimCaseStatus.EXPERT_REVIEWING
? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT
@@ -3374,20 +3417,24 @@ export class ExpertClaimService {
};
}) as ClaimListItemV2Dto[];
const paged = applyListQueryV2(list, {
publicId: (r) => r.publicId,
createdAt: (r) => r.createdAt,
requestNo: (r) => r.publicId,
status: (r) => r.status,
searchExtras: (r) =>
[
r.claimRequestId,
r.currentStep,
r.vehicle?.carName,
r.vehicle?.carModel,
r.blameRequestType,
].filter(Boolean) as string[],
}, query);
const paged = applyListQueryV2(
list,
{
publicId: (r) => r.publicId,
createdAt: (r) => r.createdAt,
requestNo: (r) => r.publicId,
status: (r) => r.status,
searchExtras: (r) =>
[
r.claimRequestId,
r.currentStep,
r.vehicle?.carName,
r.vehicle?.carModel,
r.blameRequestType,
].filter(Boolean) as string[],
},
query,
);
return {
list: paged.list,
@@ -3405,17 +3452,17 @@ export class ExpertClaimService {
private async buildBlameCaseSnapshotForClaimDetail(
blameRequestId: string,
): Promise<Record<string, unknown> | null> {
const doc = await this.blameRequestDbService.findByIdWithoutHistory(
blameRequestId,
);
const doc =
await this.blameRequestDbService.findByIdWithoutHistory(blameRequestId);
if (!doc) {
return null;
}
const docRec = doc as Record<string, unknown>;
const built = buildMutualAgreementExpertDecision(docRec);
const existingDecision = (docRec.expert as Record<string, unknown> | undefined)
?.decision as Record<string, unknown> | undefined;
const existingDecision = (
docRec.expert as Record<string, unknown> | undefined
)?.decision as Record<string, unknown> | undefined;
if (built && !existingDecision?.guiltyPartyId) {
const prevExpert =
typeof docRec.expert === "object" && docRec.expert !== null
@@ -3524,18 +3571,18 @@ export class ExpertClaimService {
return {
...d,
guiltyPartyId:
guilty != null && typeof (guilty as { toString?: () => string }).toString === "function"
guilty != null &&
typeof (guilty as { toString?: () => string }).toString === "function"
? String(guilty)
: guilty,
decidedByExpertId:
decidedBy != null &&
typeof (decidedBy as { toString?: () => string }).toString === "function"
typeof (decidedBy as { toString?: () => string }).toString ===
"function"
? String(decidedBy)
: decidedBy,
decidedAt:
decidedAt instanceof Date
? decidedAt.toISOString()
: decidedAt,
decidedAt instanceof Date ? decidedAt.toISOString() : decidedAt,
};
}
@@ -3563,10 +3610,7 @@ export class ExpertClaimService {
}
const la = claim.workflow?.lockedAt as Date | string | undefined;
if (!la) return true;
return (
Date.now() <
new Date(la).getTime() + this.claimV2WorkflowLockTtlMs
);
return Date.now() < new Date(la).getTime() + this.claimV2WorkflowLockTtlMs;
}
/**
@@ -3591,10 +3635,7 @@ export class ExpertClaimService {
$and: [
{ $eq: ["$workflow.locked", true] },
{
$lte: [
{ $add: ["$workflow.lockedAt", lockTtlMs] },
now,
],
$lte: [{ $add: ["$workflow.lockedAt", lockTtlMs] }, now],
},
],
},
@@ -3608,9 +3649,7 @@ export class ExpertClaimService {
claimCaseTouchesClient(c, clientKey),
);
await Promise.all(
relevant.map((c) =>
this.expireClaimWorkflowLockV2IfStale(String(c._id)),
),
relevant.map((c) => this.expireClaimWorkflowLockV2IfStale(String(c._id))),
);
}
@@ -3650,7 +3689,9 @@ export class ExpertClaimService {
} else if (lockedAt) {
filter["workflow.lockedAt"] = lockedAt;
} else if (lockedById) {
filter["workflow.lockedBy.actorId"] = new Types.ObjectId(String(lockedById));
filter["workflow.lockedBy.actorId"] = new Types.ObjectId(
String(lockedById),
);
}
const needsQueueRestore = claim.status === ClaimCaseStatus.EXPERT_REVIEWING;
@@ -3743,7 +3784,7 @@ export class ExpertClaimService {
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException('Claim request not found');
throw new NotFoundException("Claim request not found");
}
assertClaimCaseForTenant(claim, actor);
@@ -3782,7 +3823,8 @@ export class ExpertClaimService {
? Array.from(requiredDocs.keys())
: Object.keys(requiredDocs);
for (const k of keys as string[]) {
const doc = requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
const doc =
requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
requiredDocumentsStatus[k] = {
uploaded: !!doc?.uploaded,
fileId: doc?.fileId?.toString(),
@@ -3796,7 +3838,7 @@ export class ExpertClaimService {
const carAnglesData = claim.media?.carAngles as any;
const damagedPartsDataForAngles = claim.media?.damagedParts as any;
const carAngles: Record<string, { captured: boolean; url?: string }> = {};
for (const k of ['front', 'back', 'left', 'right']) {
for (const k of ["front", "back", "left", "right"]) {
const cap = getClaimCarAngleCaptureBlob(
carAnglesData,
damagedPartsDataForAngles,
@@ -3809,7 +3851,9 @@ export class ExpertClaimService {
}
// Build damaged parts list (aligned with normalized selected parts)
const carTypeExpert = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
const carTypeExpert = claim.vehicle?.carType as
| ClaimVehicleTypeV2
| undefined;
const selectedNormExpert = normalizeDamageSelectedParts(
claim.damage?.selectedParts,
carTypeExpert,
@@ -3856,11 +3900,7 @@ export class ExpertClaimService {
const videoCaptureIdRaw = (claim.media as any)?.videoCaptureId;
const blameRequestIdStr = claim.blameRequestId?.toString();
const [
videoCaptureRow,
blameCase,
blameLeanRows
] = await Promise.all([
const [videoCaptureRow, blameCase, blameLeanRows] = await Promise.all([
videoCaptureIdRaw
? this.claimVideoCaptureDbService.findById(String(videoCaptureIdRaw))
: Promise.resolve(null),
@@ -3891,7 +3931,7 @@ export class ExpertClaimService {
: {};
// 4. videoCapture logic from "stashed"
let videoCapture: ClaimDetailV2ResponseDto['videoCapture'] = undefined;
let videoCapture: ClaimDetailV2ResponseDto["videoCapture"] = undefined;
if (videoCaptureRow) {
const vc = videoCaptureRow as any;
videoCapture = {
@@ -3908,8 +3948,9 @@ export class ExpertClaimService {
)
: undefined;
const money = (claim as { money?: { sheba?: string; nationalCodeOfInsurer?: string } })
.money;
const money = (
claim as { money?: { sheba?: string; nationalCodeOfInsurer?: string } }
).money;
const moneyPayload =
money && (money.sheba != null || money.nationalCodeOfInsurer != null)
? {
@@ -3948,9 +3989,8 @@ export class ExpertClaimService {
const claimEvaluationRaw = (claim as any).evaluation as
| Record<string, unknown>
| undefined;
const enrichedEvaluation = await this.enrichClaimEvaluationForExpert(
claimEvaluationRaw,
);
const enrichedEvaluation =
await this.enrichClaimEvaluationForExpert(claimEvaluationRaw);
let evaluationForApi: Record<string, unknown> | undefined;
if (enrichedEvaluation) {
@@ -3973,10 +4013,7 @@ export class ExpertClaimService {
evaluationForApi.ownerPricedPartsApproval =
enrichedEvaluation.ownerPricedPartsApproval;
}
if (
isDamageExpertPhase &&
enrichedEvaluation.priceDrop !== undefined
) {
if (isDamageExpertPhase && enrichedEvaluation.priceDrop !== undefined) {
evaluationForApi.priceDrop = enrichedEvaluation.priceDrop;
}
if (Object.keys(evaluationForApi).length === 0) {
@@ -3988,23 +4025,23 @@ export class ExpertClaimService {
claimRequestId: claim._id.toString(),
publicId: claim.publicId,
status: statusForExpertApi,
claimStatus: claim.claimStatus || 'PENDING',
currentStep: claim.workflow?.currentStep || '',
claimStatus: claim.claimStatus || "PENDING",
currentStep: claim.workflow?.currentStep || "",
nextStep: claim.workflow?.nextStep,
locked: claim.workflow?.locked || false,
lockedBy: claim.workflow?.lockedBy
? {
actorId: claim.workflow.lockedBy.actorId?.toString(),
actorName: claim.workflow.lockedBy.actorName,
lockedAt: (claim.workflow as any).lockedAt?.toISOString(),
expiredAt: (claim.workflow as any).expiredAt?.toISOString(),
}
actorId: claim.workflow.lockedBy.actorId?.toString(),
actorName: claim.workflow.lockedBy.actorName,
lockedAt: (claim.workflow as any).lockedAt?.toISOString(),
expiredAt: (claim.workflow as any).expiredAt?.toISOString(),
}
: undefined,
owner: claim.owner
? {
userId: claim.owner.userId?.toString(),
fullName: claim.owner.fullName,
}
userId: claim.owner.userId?.toString(),
fullName: claim.owner.fullName,
}
: undefined,
vehicle: vehiclePayload
? this.sanitizeVehicleInquiryForApi(vehiclePayload)
@@ -4022,7 +4059,7 @@ export class ExpertClaimService {
carAngles,
damagedParts,
awaitingFactorValidation: isFactorValidationPending,
evaluation: evaluationForApi as
evaluation: enrichedEvaluation as
| ClaimDetailV2ResponseDto["evaluation"]
| undefined,
objection,
@@ -4142,7 +4179,9 @@ export class ExpertClaimService {
carModelYear =
resolveVehicleModelYearFromBlame(
claim,
blameRows[0] as Parameters<typeof resolveVehicleModelYearFromBlame>[1],
blameRows[0] as Parameters<
typeof resolveVehicleModelYearFromBlame
>[1],
) ?? undefined;
}
if (carModelYear == null || !Number.isFinite(carModelYear)) {
@@ -4273,7 +4312,9 @@ export class ExpertClaimService {
if (byId >= 0) return byId;
}
if (next.catalogKey) {
const byCk = previous.findIndex((x) => x.catalogKey === next.catalogKey);
const byCk = previous.findIndex(
(x) => x.catalogKey === next.catalogKey,
);
if (byCk >= 0) return byCk;
}
return previous.findIndex(

View File

@@ -1,20 +0,0 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log("Before...");
const now = Date.now();
return next
.handle()
.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
}
}

View File

@@ -28,7 +28,6 @@ async function bootstrap() {
const config = new DocumentBuilder()
.setTitle("yara724-backend")
.setVersion("1.0.0")
.addServer(process.env.URL)
.addServer(process.env.BASE_URL + "/api")
.addServer("http://192.168.20.170:9001")
.addServer("http://localhost:9001")