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

10
LICENSE
View File

@@ -1,10 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@@ -1,2 +1 @@
# api # api

View File

@@ -1 +0,0 @@
{"1000":{"info":"start","message":"start"},"1001":{"info":"Access Denied Other Actor Lock File","message":""},"1004":{"info":"g","message":""},"1005":{"info":"fSF","message":""},"1006":{"info":"request not found ","message":""}}

49
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@fraybabak/kavenegar_nest": "^1.0.5", "@fraybabak/kavenegar_nest": "^1.0.5",
"@nestjs/axios": "^3.1.3", "@nestjs/axios": "^3.1.3",
"@nestjs/common": "^10.4.15", "@nestjs/common": "^10.4.15",
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^10.4.15", "@nestjs/core": "^10.4.15",
"@nestjs/jwt": "^10.2.0", "@nestjs/jwt": "^10.2.0",
"@nestjs/mongoose": "^10.1.0", "@nestjs/mongoose": "^10.1.0",
@@ -2943,6 +2944,39 @@
} }
} }
}, },
"node_modules/@nestjs/config": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.4.tgz",
"integrity": "sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==",
"license": "MIT",
"dependencies": {
"dotenv": "17.4.1",
"dotenv-expand": "12.0.3",
"lodash": "4.18.1"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"rxjs": "^7.1.0"
}
},
"node_modules/@nestjs/config/node_modules/dotenv": {
"version": "17.4.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
"integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/config/node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/@nestjs/core": { "node_modules/@nestjs/core": {
"version": "10.4.15", "version": "10.4.15",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.15.tgz", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.15.tgz",
@@ -5564,6 +5598,21 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/dotenv-expand": {
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
"integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
"license": "BSD-2-Clause",
"dependencies": {
"dotenv": "^16.4.5"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@@ -23,6 +23,7 @@
"@fraybabak/kavenegar_nest": "^1.0.5", "@fraybabak/kavenegar_nest": "^1.0.5",
"@nestjs/axios": "^3.1.3", "@nestjs/axios": "^3.1.3",
"@nestjs/common": "^10.4.15", "@nestjs/common": "^10.4.15",
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^10.4.15", "@nestjs/core": "^10.4.15",
"@nestjs/jwt": "^10.2.0", "@nestjs/jwt": "^10.2.0",
"@nestjs/mongoose": "^10.1.0", "@nestjs/mongoose": "^10.1.0",

View File

@@ -4,15 +4,14 @@ import {
HttpException, HttpException,
HttpStatus, HttpStatus,
Injectable, Injectable,
Logger,
OnModuleInit, OnModuleInit,
} from "@nestjs/common"; } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import axios, { AxiosRequestConfig } from "axios"; import axios, { AxiosRequestConfig } from "axios";
import * as FormData from "form-data"; import * as FormData from "form-data";
@Injectable() @Injectable()
export class AiService implements OnModuleInit { export class AiService implements OnModuleInit {
private readonly logger = new Logger(AiService.name);
private apiKey: string; private apiKey: string;
private accessToken: string = null; private accessToken: string = null;
@@ -20,18 +19,20 @@ export class AiService implements OnModuleInit {
private readonly loginOptions: AxiosRequestConfig = { private readonly loginOptions: AxiosRequestConfig = {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
url: `${process.env.AI_URL_V2}/auth/login`, url: `${this.configService.get<string>("AI_URL_V2")}/auth/login`,
data: { data: {
username: process.env.AI_USERNAME, username: this.configService.get<string>("AI_USERNAME"),
password: process.env.AI_PASSWORD, 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 { private get profileOptions(): AxiosRequestConfig {
return { return {
method: "GET", method: "GET",
url: `${process.env.AI_URL_V2}/auth/profile`, url: `${this.configService.get<string>("AI_URL_V2")}/auth/profile`,
headers: { headers: {
Authorization: `Bearer ${this.accessToken}`, Authorization: `Bearer ${this.accessToken}`,
}, },
@@ -50,28 +51,16 @@ export class AiService implements OnModuleInit {
}; };
} }
constructor() {}
async onModuleInit() { async onModuleInit() {
try { try {
const res = await this.login(); const res = await this.login();
if (res?.accessToken) { if (res?.accessToken) {
this.logger.verbose("AI Service Authenticated Successfully.");
this.accessToken = res.accessToken; this.accessToken = res.accessToken;
await this.getApiKey(); 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) { } catch (error) {
// Don't prevent app startup if AI service is temporarily unavailable // Don't prevent app startup if AI service is temporarily unavailable
// The service will attempt to re-authenticate when aiRequestImage is called // 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 // Reset tokens so re-authentication will be attempted
this.accessToken = null; this.accessToken = null;
this.apiKey = null; this.apiKey = null;
@@ -79,42 +68,22 @@ export class AiService implements OnModuleInit {
} }
private async login() { private async login() {
try { const loginResponse = await axios.request(this.loginOptions);
const loginResponse = await axios.request(this.loginOptions); return loginResponse.data;
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,
);
}
} }
private async getApiKey() { private async getApiKey() {
try { const profileResponse = await axios.request(this.profileOptions);
const profileResponse = await axios.request(this.profileOptions); this.apiKey = profileResponse.data.apiKey.key;
this.apiKey = profileResponse.data.apiKey.key; return this.apiKey;
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,
);
}
} }
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 // Ensure authentication is set up
if (!this.accessToken || !this.apiKey) { if (!this.accessToken || !this.apiKey) {
this.logger.warn("AI service not authenticated, attempting to re-authenticate...");
try { try {
const res = await this.login(); const res = await this.login();
if (res?.accessToken) { if (res?.accessToken) {
@@ -127,7 +96,6 @@ export class AiService implements OnModuleInit {
); );
} }
} catch (error) { } catch (error) {
this.logger.error("Failed to re-authenticate AI service:", error.message);
throw new HttpException( throw new HttpException(
"AI Service authentication failed", "AI Service authentication failed",
HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED,
@@ -140,11 +108,8 @@ export class AiService implements OnModuleInit {
? file.path ? file.path
: join(process.cwd(), file.path.replace(/^\.\//, "")); : join(process.cwd(), file.path.replace(/^\.\//, ""));
this.logger.log(`Processing AI image request for: ${filePath}`);
// Check if file exists // Check if file exists
if (!existsSync(filePath)) { if (!existsSync(filePath)) {
this.logger.error(`File not found at path: ${filePath}`);
throw new HttpException( throw new HttpException(
`File not found: ${file.path}`, `File not found: ${file.path}`,
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
@@ -170,19 +135,8 @@ export class AiService implements OnModuleInit {
...form.getHeaders(), ...form.getHeaders(),
}; };
this.logger.log(`[STEP 1/4] Sending request to AI service: ${this.imageProcessOptions.url}`); const fs = require("fs");
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 stats = fs.statSync(filePath); 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({ const response = await axios.request({
...this.imageProcessOptions, ...this.imageProcessOptions,
@@ -192,11 +146,8 @@ export class AiService implements OnModuleInit {
maxBodyLength: Infinity, maxBodyLength: Infinity,
}); });
this.logger.log(`[STEP 2/4] Successfully received response from AI service (Status: ${response.status})`);
// Validate response structure // Validate response structure
if (!response.data) { if (!response.data) {
this.logger.error(`[ERROR] AI response is empty or missing data`);
throw new HttpException( throw new HttpException(
"AI Service returned empty response", "AI Service returned empty response",
HttpStatus.BAD_GATEWAY, 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) // Check for error in response first (AI service returns 201 with error in body)
if (response.data.error) { 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( throw new HttpException(
`AI Service error: ${response.data.error}`, `AI Service error: ${response.data.error}`,
HttpStatus.BAD_GATEWAY, HttpStatus.BAD_GATEWAY,
@@ -216,23 +164,12 @@ export class AiService implements OnModuleInit {
// Check for processed image (downloadLink) // Check for processed image (downloadLink)
if (!response.data.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( throw new HttpException(
"AI Service did not return processed image (downloadLink missing)", "AI Service did not return processed image (downloadLink missing)",
HttpStatus.BAD_GATEWAY, 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; return response.data;
} catch (er) { } catch (er) {
// Determine error source // Determine error source
@@ -242,7 +179,10 @@ export class AiService implements OnModuleInit {
if (er.response) { if (er.response) {
errorSource = "AI_SERVICE_RESPONSE"; errorSource = "AI_SERVICE_RESPONSE";
errorMessage = er.response?.data?.message || er.message || `HTTP ${er.response.status}`; errorMessage =
er.response?.data?.message ||
er.message ||
`HTTP ${er.response.status}`;
errorDetails = er.response?.data errorDetails = er.response?.data
? JSON.stringify(er.response.data, null, 2) ? JSON.stringify(er.response.data, null, 2)
: `Status: ${er.response.status}, StatusText: ${er.response.statusText}`; : `Status: ${er.response.status}, StatusText: ${er.response.statusText}`;
@@ -255,14 +195,6 @@ export class AiService implements OnModuleInit {
errorMessage = er.message || "Error setting up request"; 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 // Re-throw with detailed error information
throw new HttpException( throw new HttpException(
`[${errorSource}] ${errorMessage}`, `[${errorSource}] ${errorMessage}`,

View File

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

View File

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

View File

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