forked from Yara724/api
Fixed enrichedEvaluation
This commit is contained in:
@@ -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}`,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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`)));
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user