forked from Yara724/api
Compare commits
9 Commits
456135ad08
...
2b1edd64c1
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b1edd64c1 | |||
|
|
d92231e517 | ||
| dc14698823 | |||
|
|
8236f0440d | ||
| ffcedcd5f1 | |||
|
|
bd5a33e2ba | ||
|
|
0b47e8789b | ||
|
|
2c810afcb6 | ||
|
|
077bae429e |
@@ -22,7 +22,7 @@ export class UserAuthController {
|
|||||||
@Post("/send-otp")
|
@Post("/send-otp")
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
type: UserLoginDto,
|
type: UserLoginDto,
|
||||||
description: "user login api -- call this api and send otp",
|
description: "Users can ask for OTP via this API and receive it",
|
||||||
})
|
})
|
||||||
@ApiAcceptedResponse()
|
@ApiAcceptedResponse()
|
||||||
async sendOtpRq(@Body() body: UserLoginDto) {
|
async sendOtpRq(@Body() body: UserLoginDto) {
|
||||||
@@ -40,7 +40,8 @@ export class UserAuthController {
|
|||||||
@UseGuards(LocalUserAuthGuard)
|
@UseGuards(LocalUserAuthGuard)
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
type: UserVerifyOtp,
|
type: UserVerifyOtp,
|
||||||
description: "user verify otp -- call this api and get a tokens",
|
description:
|
||||||
|
"Users can send their credentials and get their access token to server",
|
||||||
})
|
})
|
||||||
@ApiAcceptedResponse()
|
@ApiAcceptedResponse()
|
||||||
async login(@Body() body, @Req() req, @CurrentUser() user) {
|
async login(@Body() body, @Req() req, @CurrentUser() user) {
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ import {
|
|||||||
} from "src/helpers/claim-capture-phase";
|
} from "src/helpers/claim-capture-phase";
|
||||||
import { HttpService } from "@nestjs/axios";
|
import { HttpService } from "@nestjs/axios";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ClaimRequestManagementService {
|
export class ClaimRequestManagementService {
|
||||||
@@ -3983,16 +3984,28 @@ export class ClaimRequestManagementService {
|
|||||||
owner: {
|
owner: {
|
||||||
userId: new Types.ObjectId(currentUserId),
|
userId: new Types.ObjectId(currentUserId),
|
||||||
userRole: currentUserParty.role as any,
|
userRole: currentUserParty.role as any,
|
||||||
...(currentUserParty.person?.clientId
|
fullName: currentUserParty.person?.fullName,
|
||||||
? {
|
userClientKey: currentUserParty.person?.clientId
|
||||||
clientId: new Types.ObjectId(
|
? String(currentUserParty.person.clientId)
|
||||||
String(blameRequest?.expert?.decision?.guiltyPartyId) ===
|
: undefined,
|
||||||
String(blameRequest?.parties[0]?.person?.userId)
|
// clientId must be the GUILTY party's clientId — their insurer pays the claim
|
||||||
? blameRequest?.parties[0]?.person?.clientId
|
...(() => {
|
||||||
: blameRequest?.parties[1]?.person?.clientId,
|
if (isCarBody) {
|
||||||
),
|
// CAR_BODY has no guilty party — use the first party's own clientId
|
||||||
|
const firstParty = parties.find((p) => p.role === "FIRST");
|
||||||
|
const cid = firstParty?.person?.clientId;
|
||||||
|
return cid ? { clientId: new Types.ObjectId(String(cid)) } : {};
|
||||||
}
|
}
|
||||||
: {}),
|
|
||||||
|
const guiltyPartyId = String(
|
||||||
|
blameRequest?.expert?.decision?.guiltyPartyId ?? "",
|
||||||
|
);
|
||||||
|
const guiltyParty = parties.find(
|
||||||
|
(p) => String(p.person?.userId) === guiltyPartyId,
|
||||||
|
);
|
||||||
|
const cid = guiltyParty?.person?.clientId;
|
||||||
|
return cid ? { clientId: new Types.ObjectId(String(cid)) } : {};
|
||||||
|
})(),
|
||||||
},
|
},
|
||||||
history: [
|
history: [
|
||||||
{
|
{
|
||||||
@@ -4660,7 +4673,7 @@ export class ClaimRequestManagementService {
|
|||||||
"money.sheba": shebaNumber,
|
"money.sheba": shebaNumber,
|
||||||
"money.nationalCodeOfInsurer": nationalCode,
|
"money.nationalCodeOfInsurer": nationalCode,
|
||||||
|
|
||||||
status: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
status: ClaimCaseStatus.CAPTURING_PART_DAMAGES,
|
||||||
"workflow.currentStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
"workflow.currentStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
"workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
"workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
|
||||||
@@ -6074,6 +6087,9 @@ export class ClaimRequestManagementService {
|
|||||||
"workflow.nextStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION,
|
"workflow.nextStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION,
|
||||||
"evaluation.objection": objectionPayload,
|
"evaluation.objection": objectionPayload,
|
||||||
"damage.selectedParts": mergedNorm,
|
"damage.selectedParts": mergedNorm,
|
||||||
|
...(pendingResendGate && {
|
||||||
|
"evaluation.damageExpertResend.fulfilledAt": new Date(),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
$unset: {
|
$unset: {
|
||||||
"evaluation.ownerPricedPartsApproval": "",
|
"evaluation.ownerPricedPartsApproval": "",
|
||||||
@@ -6657,24 +6673,15 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const damagedParts = displayParts.map((sp, index) => {
|
const damagedParts = buildEnrichedDamagedParts({
|
||||||
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp);
|
selectedParts: displayParts,
|
||||||
const cap = getDamagedPartCaptureBlob(
|
damagedPartsData: damagedPartsData,
|
||||||
damagedPartsData,
|
evaluationBlock: (claim as any).evaluation,
|
||||||
ck,
|
expertAddedParts: (claim.damage as any)?.expertAddedParts ?? [],
|
||||||
selectedNormDetails,
|
getDamagedPartCaptureBlob,
|
||||||
) as { url?: string; path?: string; fileName?: string } | undefined;
|
hasDamagedPartCapture,
|
||||||
return {
|
catalogLikeKeyFromPart,
|
||||||
index,
|
buildFileLink,
|
||||||
id: sp.id,
|
|
||||||
name: sp.name,
|
|
||||||
side: sp.side,
|
|
||||||
label_fa: sp.label_fa,
|
|
||||||
captured: !!cap,
|
|
||||||
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),
|
|
||||||
...(cap?.path ? { path: cap.path } : {}),
|
|
||||||
...(cap?.fileName ? { fileName: cap.fileName } : {}),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const er = claim.evaluation?.damageExpertResend;
|
const er = claim.evaluation?.damageExpertResend;
|
||||||
|
|||||||
@@ -99,19 +99,34 @@ export class ClientService {
|
|||||||
try {
|
try {
|
||||||
const newClient = await this.clientDbService.create({
|
const newClient = await this.clientDbService.create({
|
||||||
clientCode: client.clientCode,
|
clientCode: client.clientCode,
|
||||||
|
|
||||||
clientName: {
|
clientName: {
|
||||||
persian: client.clientName.persian,
|
persian:
|
||||||
english: client.clientName.english || null,
|
typeof client.clientName === "string"
|
||||||
|
? client.clientName
|
||||||
|
: client.clientName?.persian,
|
||||||
|
|
||||||
|
english:
|
||||||
|
typeof client.clientName === "string"
|
||||||
|
? null
|
||||||
|
: (client.clientName?.english ?? null),
|
||||||
},
|
},
|
||||||
|
|
||||||
property: {
|
property: {
|
||||||
smsApiKey: client.property.smsApiKey || null,
|
smsApiKey: client.property?.smsApiKey ?? null,
|
||||||
},
|
},
|
||||||
useExpertMode: client.useExpertMode || null,
|
|
||||||
|
useExpertMode: client.useExpertMode ?? null,
|
||||||
});
|
});
|
||||||
if (newClient) return new ClientDtoRs(newClient);
|
|
||||||
else throw new GoneException("database not connected");
|
if (!newClient) {
|
||||||
|
throw new GoneException("database not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClientDtoRs(newClient);
|
||||||
} catch (er) {
|
} catch (er) {
|
||||||
throw new BadGatewayException(er.errors);
|
console.error("ADD CLIENT ERROR:", er);
|
||||||
|
throw er;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { IsIn, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
import {
|
||||||
|
IsIn,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
@@ -15,9 +22,8 @@ class ClientName {
|
|||||||
english: string;
|
english: string;
|
||||||
}
|
}
|
||||||
class Property {
|
class Property {
|
||||||
@ApiProperty({})
|
@ApiPropertyOptional({})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
|
||||||
smsApiKey: string;
|
smsApiKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,14 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiResponse,
|
||||||
|
ApiTags,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
import { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-result.dto";
|
import { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-result.dto";
|
||||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
@@ -29,7 +36,7 @@ import { ResendRequestDto } from "./dto/resend.dto";
|
|||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@Roles(RoleEnum.EXPERT, RoleEnum.FIELD_EXPERT)
|
@Roles(RoleEnum.EXPERT, RoleEnum.FIELD_EXPERT)
|
||||||
export class ExpertBlameV2Controller {
|
export class ExpertBlameV2Controller {
|
||||||
constructor(private readonly expertBlameService: ExpertBlameService) { }
|
constructor(private readonly expertBlameService: ExpertBlameService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@@ -63,7 +70,9 @@ export class ExpertBlameV2Controller {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error ? error.message : "Failed to load blame status report",
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to load blame status report",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +85,9 @@ export class ExpertBlameV2Controller {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error ? error.message : "Failed to get blame case details",
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to get blame case details",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +107,10 @@ export class ExpertBlameV2Controller {
|
|||||||
})
|
})
|
||||||
async assignForReview(@Param("id") id: string, @CurrentUser() actor: any) {
|
async assignForReview(@Param("id") id: string, @CurrentUser() actor: any) {
|
||||||
try {
|
try {
|
||||||
return await this.expertBlameService.assignBlameCaseForReviewV2(id, actor);
|
return await this.expertBlameService.assignBlameCaseForReviewV2(
|
||||||
|
id,
|
||||||
|
actor,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
@@ -110,6 +124,7 @@ export class ExpertBlameV2Controller {
|
|||||||
summary: "Lock blame case for review (legacy)",
|
summary: "Lock blame case for review (legacy)",
|
||||||
description:
|
description:
|
||||||
"Same assignment logic as POST `:id/assign`, but returns the legacy `{ _id, lock }` shape and maps conflicts to 400 instead of 409.",
|
"Same assignment logic as POST `:id/assign`, but returns the legacy `{ _id, lock }` shape and maps conflicts to 400 instead of 409.",
|
||||||
|
deprecated: true,
|
||||||
})
|
})
|
||||||
@ApiParam({ name: "id", description: "Blame case request id" })
|
@ApiParam({ name: "id", description: "Blame case request id" })
|
||||||
async lockRequest(@Param("id") id: string, @CurrentUser() actor: any) {
|
async lockRequest(@Param("id") id: string, @CurrentUser() actor: any) {
|
||||||
@@ -156,7 +171,9 @@ export class ExpertBlameV2Controller {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error ? error.message : "Failed to submit expert reply",
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to submit expert reply",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +191,9 @@ export class ExpertBlameV2Controller {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error ? error.message : "Failed to submit expert reply for in person",
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to submit expert reply for in person",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,16 @@ export class UpsertClaimPriceDropV2Dto {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => PriceDropPartSeverityV2Dto)
|
@Type(() => PriceDropPartSeverityV2Dto)
|
||||||
partSeverities: PriceDropPartSeverityV2Dto[];
|
partSeverities: PriceDropPartSeverityV2Dto[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Override: expert manually sets the final price-drop amount (in Rials). " +
|
||||||
|
"When provided, carPrice and partSeverities are not required and calculation is skipped entirely.",
|
||||||
|
example: 150000000,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
manualPriceDrop?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ClaimPriceDropContextV2Dto {
|
export class ClaimPriceDropContextV2Dto {
|
||||||
@@ -99,7 +109,9 @@ export class ClaimPriceDropContextV2Dto {
|
|||||||
})
|
})
|
||||||
severityOptions: Array<{ value: string; label_fa: string }>;
|
severityOptions: Array<{ value: string; label_fa: string }>;
|
||||||
|
|
||||||
@ApiProperty({ description: "Full coefficient table for all price-drop parts" })
|
@ApiProperty({
|
||||||
|
description: "Full coefficient table for all price-drop parts",
|
||||||
|
})
|
||||||
priceDropCatalog: Array<Record<string, unknown>>;
|
priceDropCatalog: Array<Record<string, unknown>>;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
|
|||||||
@@ -4226,6 +4226,49 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
this.assertExpertCanEditClaimDuringReviewV2(claim, actor);
|
this.assertExpertCanEditClaimDuringReviewV2(claim, actor);
|
||||||
|
|
||||||
|
const vehicleSet: Record<string, string> = {};
|
||||||
|
if (body.carName != null && String(body.carName).trim()) {
|
||||||
|
vehicleSet["vehicle.carName"] = String(body.carName).trim();
|
||||||
|
}
|
||||||
|
if (body.carModel != null && String(body.carModel).trim()) {
|
||||||
|
vehicleSet["vehicle.carModel"] = String(body.carModel).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual override path — expert knows the number, skip calculation entirely
|
||||||
|
if (body.manualPriceDrop != null) {
|
||||||
|
if (!Number.isFinite(body.manualPriceDrop) || body.manualPriceDrop < 0) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"manualPriceDrop must be a non-negative finite number.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const manualPayload = {
|
||||||
|
manualOverride: true,
|
||||||
|
totalPriceDrop: body.manualPriceDrop,
|
||||||
|
partLines: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
|
$set: {
|
||||||
|
...vehicleSet,
|
||||||
|
"evaluation.priceDrop": manualPayload,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
return {
|
||||||
|
claimRequestId,
|
||||||
|
vehicle: {
|
||||||
|
carName: updated?.vehicle?.carName,
|
||||||
|
carModel: updated?.vehicle?.carModel,
|
||||||
|
carType: updated?.vehicle?.carType,
|
||||||
|
},
|
||||||
|
priceDrop: manualPayload,
|
||||||
|
partLines: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculated path — existing logic unchanged
|
||||||
const carType = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
const carType = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||||
const selectedNorm = normalizeDamageSelectedParts(
|
const selectedNorm = normalizeDamageSelectedParts(
|
||||||
claim.damage?.selectedParts,
|
claim.damage?.selectedParts,
|
||||||
@@ -4235,7 +4278,7 @@ export class ExpertClaimService {
|
|||||||
|
|
||||||
if (!body.partSeverities?.length) {
|
if (!body.partSeverities?.length) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Provide at least one partSeverities line for a damaged part.",
|
"Provide at least one partSeverities line, or use manualPriceDrop for a direct override.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4290,14 +4333,6 @@ export class ExpertClaimService {
|
|||||||
partLines: lines,
|
partLines: lines,
|
||||||
};
|
};
|
||||||
|
|
||||||
const vehicleSet: Record<string, string> = {};
|
|
||||||
if (body.carName != null && String(body.carName).trim()) {
|
|
||||||
vehicleSet["vehicle.carName"] = String(body.carName).trim();
|
|
||||||
}
|
|
||||||
if (body.carModel != null && String(body.carModel).trim()) {
|
|
||||||
vehicleSet["vehicle.carModel"] = String(body.carModel).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
$set: {
|
$set: {
|
||||||
...vehicleSet,
|
...vehicleSet,
|
||||||
@@ -4306,7 +4341,6 @@ export class ExpertClaimService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updated = await this.claimCaseDbService.findById(claimRequestId);
|
const updated = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
vehicle: {
|
vehicle: {
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
import { Body, Controller, Get, Header, Param, Headers, Patch, Post, Put, UseGuards, Query, Res } from "@nestjs/common";
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Header,
|
||||||
|
Param,
|
||||||
|
Headers,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
UseGuards,
|
||||||
|
Query,
|
||||||
|
Res,
|
||||||
|
} from "@nestjs/common";
|
||||||
import {
|
import {
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiBody,
|
ApiBody,
|
||||||
@@ -19,7 +32,10 @@ import { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-res
|
|||||||
import { ExpertClaimService } from "./expert-claim.service";
|
import { ExpertClaimService } from "./expert-claim.service";
|
||||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
import { GetClaimListV2ResponseDto } from "./dto/claim-list-v2.dto";
|
import { GetClaimListV2ResponseDto } from "./dto/claim-list-v2.dto";
|
||||||
import { ClaimSubmitResendV2Dto, SubmitExpertReplyV2Dto } from "./dto/expert-claim-v2.dto";
|
import {
|
||||||
|
ClaimSubmitResendV2Dto,
|
||||||
|
SubmitExpertReplyV2Dto,
|
||||||
|
} from "./dto/expert-claim-v2.dto";
|
||||||
import { UpdateClaimDamagedPartsV2Dto } from "./dto/update-claim-damaged-parts-v2.dto";
|
import { UpdateClaimDamagedPartsV2Dto } from "./dto/update-claim-damaged-parts-v2.dto";
|
||||||
import {
|
import {
|
||||||
ClaimPriceDropContextV2Dto,
|
ClaimPriceDropContextV2Dto,
|
||||||
@@ -32,7 +48,7 @@ import { OuterPartCatalogItemDto } from "src/claim-request-management/dto/select
|
|||||||
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||||
|
|
||||||
class InPersonVisitV2Dto {
|
class InPersonVisitV2Dto {
|
||||||
@ApiPropertyOptional({ example: 'Paint damage requires physical inspection' })
|
@ApiPropertyOptional({ example: "Paint damage requires physical inspection" })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
note?: string;
|
note?: string;
|
||||||
@@ -120,7 +136,10 @@ export class ExpertClaimV2Controller {
|
|||||||
@Param("claimRequestId") claimRequestId: string,
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
@CurrentUser() actor,
|
@CurrentUser() actor,
|
||||||
) {
|
) {
|
||||||
return await this.expertClaimService.getClaimDetailV2(claimRequestId, actor);
|
return await this.expertClaimService.getClaimDetailV2(
|
||||||
|
claimRequestId,
|
||||||
|
actor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("request/:claimRequestId/price-drop")
|
@Get("request/:claimRequestId/price-drop")
|
||||||
@@ -193,13 +212,17 @@ export class ExpertClaimV2Controller {
|
|||||||
"1. **Damage review queue** — claim status `WAITING_FOR_DAMAGE_EXPERT`. Locking advances the claim to `status=EXPERT_REVIEWING`, `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_DAMAGE_ASSESSMENT`.\n" +
|
"1. **Damage review queue** — claim status `WAITING_FOR_DAMAGE_EXPERT`. Locking advances the claim to `status=EXPERT_REVIEWING`, `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_DAMAGE_ASSESSMENT`.\n" +
|
||||||
"2. **Factor validation queue** — claim status `EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`) with `claimStatus=UNDER_REVIEW` and `workflow.currentStep=EXPERT_COST_EVALUATION`. Locking only sets the workflow lock fields; status/claimStatus/currentStep are left untouched so the claim stays in the factor-validation queue when the lock expires.\n\n" +
|
"2. **Factor validation queue** — claim status `EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`) with `claimStatus=UNDER_REVIEW` and `workflow.currentStep=EXPERT_COST_EVALUATION`. Locking only sets the workflow lock fields; status/claimStatus/currentStep are left untouched so the claim stays in the factor-validation queue when the lock expires.\n\n" +
|
||||||
"Only one expert can hold the lock at a time. Re-locking by the same expert is idempotent. Same assignment logic as POST `assign/:claimRequestId`, but returns the legacy lock shape and maps conflicts to 400 instead of 409.",
|
"Only one expert can hold the lock at a time. Re-locking by the same expert is idempotent. Same assignment logic as POST `assign/:claimRequestId`, but returns the legacy lock shape and maps conflicts to 400 instead of 409.",
|
||||||
|
deprecated: true,
|
||||||
})
|
})
|
||||||
@ApiParam({ name: "claimRequestId" })
|
@ApiParam({ name: "claimRequestId" })
|
||||||
async lockClaimRequestV2(
|
async lockClaimRequestV2(
|
||||||
@Param("claimRequestId") claimRequestId: string,
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
@CurrentUser() actor,
|
@CurrentUser() actor,
|
||||||
) {
|
) {
|
||||||
return await this.expertClaimService.lockClaimRequestV2(claimRequestId, actor);
|
return await this.expertClaimService.lockClaimRequestV2(
|
||||||
|
claimRequestId,
|
||||||
|
actor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("reply/submit/:claimRequestId")
|
@Put("reply/submit/:claimRequestId")
|
||||||
@@ -221,7 +244,11 @@ export class ExpertClaimV2Controller {
|
|||||||
@Body() body: SubmitExpertReplyV2Dto,
|
@Body() body: SubmitExpertReplyV2Dto,
|
||||||
@CurrentUser() actor,
|
@CurrentUser() actor,
|
||||||
) {
|
) {
|
||||||
return await this.expertClaimService.submitExpertReplyV2(claimRequestId, body, actor);
|
return await this.expertClaimService.submitExpertReplyV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
actor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("reply/resend/:claimRequestId")
|
@Put("reply/resend/:claimRequestId")
|
||||||
@@ -239,7 +266,11 @@ export class ExpertClaimV2Controller {
|
|||||||
@Body() body: ClaimSubmitResendV2Dto,
|
@Body() body: ClaimSubmitResendV2Dto,
|
||||||
@CurrentUser() actor,
|
@CurrentUser() actor,
|
||||||
) {
|
) {
|
||||||
return await this.expertClaimService.submitResendDocsV2(claimRequestId, body, actor);
|
return await this.expertClaimService.submitResendDocsV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
actor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(":claimRequestId/visit")
|
@Patch(":claimRequestId/visit")
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ import {
|
|||||||
} from "src/helpers/outer-damage-parts";
|
} from "src/helpers/outer-damage-parts";
|
||||||
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||||
import { ClaimSignDbService } from "src/claim-request-management/entites/db-service/claim-sign.db.service";
|
import { ClaimSignDbService } from "src/claim-request-management/entites/db-service/claim-sign.db.service";
|
||||||
|
import {
|
||||||
|
extractExpertNamesFromBlame,
|
||||||
|
extractExpertNamesFromClaim,
|
||||||
|
} from "./helper/insurer.helper";
|
||||||
|
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpertInsurerService {
|
export class ExpertInsurerService {
|
||||||
@@ -70,7 +75,8 @@ export class ExpertInsurerService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
private getClientId(actorOrId: any): Types.ObjectId {
|
private getClientId(actorOrId: any): Types.ObjectId {
|
||||||
const raw = typeof actorOrId === "string" ? actorOrId : actorOrId?.clientKey;
|
const raw =
|
||||||
|
typeof actorOrId === "string" ? actorOrId : actorOrId?.clientKey;
|
||||||
if (!raw || !Types.ObjectId.isValid(raw)) {
|
if (!raw || !Types.ObjectId.isValid(raw)) {
|
||||||
throw new BadRequestException("Client key is required");
|
throw new BadRequestException("Client key is required");
|
||||||
}
|
}
|
||||||
@@ -91,15 +97,22 @@ export class ExpertInsurerService {
|
|||||||
tenantId: Types.ObjectId,
|
tenantId: Types.ObjectId,
|
||||||
expertIds: string[],
|
expertIds: string[],
|
||||||
): Promise<Record<string, { totalHandled: number; totalChecked: number }>> {
|
): Promise<Record<string, { totalHandled: number; totalChecked: number }>> {
|
||||||
const events = await this.expertFileActivityDbService.findByTenantAndExperts(
|
const events =
|
||||||
|
await this.expertFileActivityDbService.findByTenantAndExperts(
|
||||||
tenantId,
|
tenantId,
|
||||||
expertIds,
|
expertIds,
|
||||||
);
|
);
|
||||||
const stateByExpertFile = new Map<string, { checked: boolean; handled: boolean }>();
|
const stateByExpertFile = new Map<
|
||||||
|
string,
|
||||||
|
{ checked: boolean; handled: boolean }
|
||||||
|
>();
|
||||||
|
|
||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
const key = `${String(e.expertId)}:${String(e.fileId)}`;
|
const key = `${String(e.expertId)}:${String(e.fileId)}`;
|
||||||
const prev = stateByExpertFile.get(key) ?? { checked: false, handled: false };
|
const prev = stateByExpertFile.get(key) ?? {
|
||||||
|
checked: false,
|
||||||
|
handled: false,
|
||||||
|
};
|
||||||
if (e.eventType === ExpertFileActivityType.CHECKED) {
|
if (e.eventType === ExpertFileActivityType.CHECKED) {
|
||||||
if (!prev.handled) prev.checked = true;
|
if (!prev.handled) prev.checked = true;
|
||||||
} else if (e.eventType === ExpertFileActivityType.UNCHECKED) {
|
} else if (e.eventType === ExpertFileActivityType.UNCHECKED) {
|
||||||
@@ -111,13 +124,17 @@ export class ExpertInsurerService {
|
|||||||
stateByExpertFile.set(key, prev);
|
stateByExpertFile.set(key, prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: Record<string, { totalHandled: number; totalChecked: number }> = {};
|
const result: Record<
|
||||||
|
string,
|
||||||
|
{ totalHandled: number; totalChecked: number }
|
||||||
|
> = {};
|
||||||
for (const expertId of expertIds) {
|
for (const expertId of expertIds) {
|
||||||
result[expertId] = { totalHandled: 0, totalChecked: 0 };
|
result[expertId] = { totalHandled: 0, totalChecked: 0 };
|
||||||
}
|
}
|
||||||
for (const [key, st] of stateByExpertFile.entries()) {
|
for (const [key, st] of stateByExpertFile.entries()) {
|
||||||
const [expertId] = key.split(":");
|
const [expertId] = key.split(":");
|
||||||
if (!result[expertId]) result[expertId] = { totalHandled: 0, totalChecked: 0 };
|
if (!result[expertId])
|
||||||
|
result[expertId] = { totalHandled: 0, totalChecked: 0 };
|
||||||
if (st.handled) result[expertId].totalHandled += 1;
|
if (st.handled) result[expertId].totalHandled += 1;
|
||||||
else if (st.checked) result[expertId].totalChecked += 1;
|
else if (st.checked) result[expertId].totalChecked += 1;
|
||||||
}
|
}
|
||||||
@@ -265,18 +282,18 @@ export class ExpertInsurerService {
|
|||||||
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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +317,8 @@ export class ExpertInsurerService {
|
|||||||
if (!Array.isArray(parties) || parties.length === 0) return out;
|
if (!Array.isArray(parties) || parties.length === 0) return out;
|
||||||
|
|
||||||
const first =
|
const first =
|
||||||
(parties as any[]).find((p: any) => p?.role === PartyRole.FIRST) ?? (parties as any[])[0];
|
(parties as any[]).find((p: any) => p?.role === PartyRole.FIRST) ??
|
||||||
|
(parties as any[])[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") {
|
||||||
@@ -340,7 +358,8 @@ export class ExpertInsurerService {
|
|||||||
) {
|
) {
|
||||||
if (!person || typeof person !== "object") return person;
|
if (!person || typeof person !== "object") return person;
|
||||||
const cid =
|
const cid =
|
||||||
person.clientId?.toString?.() ?? (person.clientId != null ? String(person.clientId) : undefined);
|
person.clientId?.toString?.() ??
|
||||||
|
(person.clientId != null ? String(person.clientId) : undefined);
|
||||||
return {
|
return {
|
||||||
...person,
|
...person,
|
||||||
userId: person.userId?.toString?.() ?? undefined,
|
userId: person.userId?.toString?.() ?? undefined,
|
||||||
@@ -412,7 +431,11 @@ export class ExpertInsurerService {
|
|||||||
side: sp.side,
|
side: sp.side,
|
||||||
label_fa: sp.label_fa,
|
label_fa: sp.label_fa,
|
||||||
catalogKey: sp.catalogKey,
|
catalogKey: sp.catalogKey,
|
||||||
captured: hasDamagedPartCapture(damagedPartsDataExpert, ck, selectedNormExpert),
|
captured: hasDamagedPartCapture(
|
||||||
|
damagedPartsDataExpert,
|
||||||
|
ck,
|
||||||
|
selectedNormExpert,
|
||||||
|
),
|
||||||
path: cap?.path,
|
path: cap?.path,
|
||||||
fileName: cap?.fileName,
|
fileName: cap?.fileName,
|
||||||
url,
|
url,
|
||||||
@@ -439,7 +462,9 @@ export class ExpertInsurerService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async claimSignLinkFromId(signDetailId: unknown): Promise<string | undefined> {
|
private async claimSignLinkFromId(
|
||||||
|
signDetailId: unknown,
|
||||||
|
): Promise<string | undefined> {
|
||||||
if (signDetailId == null || signDetailId === "") return undefined;
|
if (signDetailId == null || signDetailId === "") return undefined;
|
||||||
const id = String(signDetailId);
|
const id = String(signDetailId);
|
||||||
if (!Types.ObjectId.isValid(id)) return undefined;
|
if (!Types.ObjectId.isValid(id)) return undefined;
|
||||||
@@ -454,7 +479,10 @@ export class ExpertInsurerService {
|
|||||||
evaluation: Record<string, unknown> | undefined,
|
evaluation: Record<string, unknown> | undefined,
|
||||||
): Promise<Record<string, unknown> | undefined> {
|
): Promise<Record<string, unknown> | undefined> {
|
||||||
if (!evaluation || typeof evaluation !== "object") return evaluation;
|
if (!evaluation || typeof evaluation !== "object") return evaluation;
|
||||||
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 (reply: any) => {
|
const enrichReply = async (reply: any) => {
|
||||||
if (!reply || typeof reply !== "object") return;
|
if (!reply || typeof reply !== "object") return;
|
||||||
@@ -487,14 +515,18 @@ export class ExpertInsurerService {
|
|||||||
const evidence = party.evidence as Record<string, unknown>;
|
const evidence = party.evidence as Record<string, unknown>;
|
||||||
|
|
||||||
if (evidence.videoId) {
|
if (evidence.videoId) {
|
||||||
const videoDoc = await this.blameVideoDbService.findById(String(evidence.videoId));
|
const videoDoc = await this.blameVideoDbService.findById(
|
||||||
|
String(evidence.videoId),
|
||||||
|
);
|
||||||
if (videoDoc?.path) evidence.videoUrl = buildFileLink(videoDoc.path);
|
if (videoDoc?.path) evidence.videoUrl = buildFileLink(videoDoc.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evidence.voices && Array.isArray(evidence.voices)) {
|
if (evidence.voices && Array.isArray(evidence.voices)) {
|
||||||
const voiceUrls: string[] = [];
|
const voiceUrls: string[] = [];
|
||||||
for (const voiceId of evidence.voices) {
|
for (const voiceId of evidence.voices) {
|
||||||
const voiceDoc = await this.blameVoiceDbService.findById(String(voiceId));
|
const voiceDoc = await this.blameVoiceDbService.findById(
|
||||||
|
String(voiceId),
|
||||||
|
);
|
||||||
if (voiceDoc?.path) voiceUrls.push(buildFileLink(voiceDoc.path));
|
if (voiceDoc?.path) voiceUrls.push(buildFileLink(voiceDoc.path));
|
||||||
}
|
}
|
||||||
evidence.voiceUrls = voiceUrls;
|
evidence.voiceUrls = voiceUrls;
|
||||||
@@ -511,14 +543,20 @@ export class ExpertInsurerService {
|
|||||||
doc.parties = enrichBlamePartiesForAgreementView(doc);
|
doc.parties = enrichBlamePartiesForAgreementView(doc);
|
||||||
|
|
||||||
const mergedParties = doc.parties as any[];
|
const mergedParties = doc.parties as any[];
|
||||||
const clientIds = mergedParties.map((p) => p?.person?.clientId?.toString?.()).filter(Boolean);
|
const clientIds = mergedParties
|
||||||
|
.map((p) => p?.person?.clientId?.toString?.())
|
||||||
|
.filter(Boolean);
|
||||||
const clientNames = await this.clientNamesByClientIds(clientIds);
|
const clientNames = await this.clientNamesByClientIds(clientIds);
|
||||||
doc.parties = mergedParties.map((p) => this.mapPartyForInsurerFull(p, clientNames));
|
doc.parties = mergedParties.map((p) =>
|
||||||
|
this.mapPartyForInsurerFull(p, clientNames),
|
||||||
|
);
|
||||||
this.enrichPartiesConfirmationSignatures(doc.parties as any[]);
|
this.enrichPartiesConfirmationSignatures(doc.parties as any[]);
|
||||||
|
|
||||||
if (doc.expert && typeof doc.expert === "object") {
|
if (doc.expert && typeof doc.expert === "object") {
|
||||||
const ex = { ...(doc.expert as Record<string, unknown>) };
|
const ex = { ...(doc.expert as Record<string, unknown>) };
|
||||||
const serialized = this.serializeBlameExpertDecisionForInsurer(ex.decision);
|
const serialized = this.serializeBlameExpertDecisionForInsurer(
|
||||||
|
ex.decision,
|
||||||
|
);
|
||||||
if (serialized !== undefined) ex.decision = serialized;
|
if (serialized !== undefined) ex.decision = serialized;
|
||||||
doc.expert = ex;
|
doc.expert = ex;
|
||||||
}
|
}
|
||||||
@@ -543,6 +581,28 @@ export class ExpertInsurerService {
|
|||||||
claim: Record<string, unknown>,
|
claim: Record<string, unknown>,
|
||||||
blameForContext: Record<string, unknown> | null,
|
blameForContext: Record<string, unknown> | null,
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
|
// Add at the top of buildInsurerClaimDetail, before the return:
|
||||||
|
const carType = (claim as any).vehicle?.carType as
|
||||||
|
| ClaimVehicleTypeV2
|
||||||
|
| undefined;
|
||||||
|
const selectedNorm = normalizeDamageSelectedParts(
|
||||||
|
(claim as any).damage?.selectedParts,
|
||||||
|
carType,
|
||||||
|
(claim as any).damage?.selectedOuterParts,
|
||||||
|
);
|
||||||
|
|
||||||
|
const damagedParts = buildEnrichedDamagedParts({
|
||||||
|
selectedParts: selectedNorm,
|
||||||
|
damagedPartsData: (claim as any).media?.damagedParts,
|
||||||
|
evaluationBlock: (claim as any).evaluation,
|
||||||
|
expertAddedParts: (claim as any).damage?.expertAddedParts ?? [],
|
||||||
|
getDamagedPartCaptureBlob,
|
||||||
|
hasDamagedPartCapture,
|
||||||
|
catalogLikeKeyFromPart,
|
||||||
|
buildFileLink,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then add damagedParts to the return object
|
||||||
const requiredDocs = claim.requiredDocuments as any;
|
const requiredDocs = claim.requiredDocuments as any;
|
||||||
const requiredDocumentsStatus: Record<string, any> = {};
|
const requiredDocumentsStatus: Record<string, any> = {};
|
||||||
if (requiredDocs) {
|
if (requiredDocs) {
|
||||||
@@ -551,7 +611,8 @@ export class ExpertInsurerService {
|
|||||||
? 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 row = requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
|
const row =
|
||||||
|
requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
|
||||||
requiredDocumentsStatus[k] = {
|
requiredDocumentsStatus[k] = {
|
||||||
uploaded: !!row?.uploaded,
|
uploaded: !!row?.uploaded,
|
||||||
fileId: row?.fileId?.toString?.(),
|
fileId: row?.fileId?.toString?.(),
|
||||||
@@ -576,30 +637,6 @@ export class ExpertInsurerService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const carTypeExpert = (claim.vehicle as any)?.carType as ClaimVehicleTypeV2 | undefined;
|
|
||||||
const selectedNormExpert = normalizeDamageSelectedParts(
|
|
||||||
(claim.damage as any)?.selectedParts,
|
|
||||||
carTypeExpert,
|
|
||||||
(claim.damage as any)?.selectedOuterParts,
|
|
||||||
);
|
|
||||||
const damagedPartsDataExpert = (claim.media as any)?.damagedParts;
|
|
||||||
const damagedParts = selectedNormExpert.map((sp, index) =>
|
|
||||||
this.insurerDamagedPartRowFromCapture(
|
|
||||||
sp,
|
|
||||||
index,
|
|
||||||
damagedPartsDataExpert,
|
|
||||||
selectedNormExpert,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const selectedPartsNormalized = selectedNormExpert.map((sp, index) =>
|
|
||||||
this.insurerDamagedPartRowFromCapture(
|
|
||||||
sp,
|
|
||||||
index,
|
|
||||||
damagedPartsDataExpert,
|
|
||||||
selectedNormExpert,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const videoCaptureIdRaw = (claim.media as any)?.videoCaptureId;
|
const videoCaptureIdRaw = (claim.media as any)?.videoCaptureId;
|
||||||
let videoCapture:
|
let videoCapture:
|
||||||
| { id: string; fileName?: string; path?: string; url?: string }
|
| { id: string; fileName?: string; path?: string; url?: string }
|
||||||
@@ -646,13 +683,17 @@ export class ExpertInsurerService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluationEnriched = await this.enrichClaimEvaluationForInsurer(evaluation);
|
const evaluationEnriched =
|
||||||
|
await this.enrichClaimEvaluationForInsurer(evaluation);
|
||||||
const vehicleSanitized = claim.vehicle
|
const vehicleSanitized = claim.vehicle
|
||||||
? this.sanitizeVehicleInquiry(claim.vehicle as any)
|
? this.sanitizeVehicleInquiry(claim.vehicle as any)
|
||||||
: undefined;
|
: undefined;
|
||||||
const vehicleOut = vehicleSanitized
|
const vehicleOut = vehicleSanitized
|
||||||
? (() => {
|
? (() => {
|
||||||
const { carType: _omit, ...rest } = vehicleSanitized as Record<string, unknown>;
|
const { carType: _omit, ...rest } = vehicleSanitized as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
return rest;
|
return rest;
|
||||||
})()
|
})()
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -680,7 +721,6 @@ export class ExpertInsurerService {
|
|||||||
: undefined,
|
: undefined,
|
||||||
carAngles,
|
carAngles,
|
||||||
damagedParts,
|
damagedParts,
|
||||||
selectedPartsNormalized,
|
|
||||||
videoCapture,
|
videoCapture,
|
||||||
evaluation: evaluationEnriched,
|
evaluation: evaluationEnriched,
|
||||||
userRating: claim.userRating,
|
userRating: claim.userRating,
|
||||||
@@ -705,9 +745,11 @@ export class ExpertInsurerService {
|
|||||||
).filter((v): v is number => typeof v === "number" && !isNaN(v))
|
).filter((v): v is number => typeof v === "number" && !isNaN(v))
|
||||||
: [];
|
: [];
|
||||||
const userValues = userRating
|
const userValues = userRating
|
||||||
? [userRating.progressSpeed, userRating.registrationEase, userRating.overallEvaluation].filter(
|
? [
|
||||||
(v) => typeof v === "number" && !isNaN(v),
|
userRating.progressSpeed,
|
||||||
)
|
userRating.registrationEase,
|
||||||
|
userRating.overallEvaluation,
|
||||||
|
].filter((v) => typeof v === "number" && !isNaN(v))
|
||||||
: [];
|
: [];
|
||||||
const insurerAvg = insurerValues.length
|
const insurerAvg = insurerValues.length
|
||||||
? insurerValues.reduce((a, b) => a + b, 0) / insurerValues.length
|
? insurerValues.reduce((a, b) => a + b, 0) / insurerValues.length
|
||||||
@@ -715,30 +757,50 @@ export class ExpertInsurerService {
|
|||||||
const userAvg = userValues.length
|
const userAvg = userValues.length
|
||||||
? userValues.reduce((a, b) => a + b, 0) / userValues.length
|
? userValues.reduce((a, b) => a + b, 0) / userValues.length
|
||||||
: null;
|
: null;
|
||||||
const scores = [insurerAvg, userAvg].filter((v): v is number => typeof v === "number");
|
const scores = [insurerAvg, userAvg].filter(
|
||||||
|
(v): v is number => typeof v === "number",
|
||||||
|
);
|
||||||
if (!scores.length) return null;
|
if (!scores.length) return null;
|
||||||
return parseFloat((scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(2));
|
return parseFloat(
|
||||||
|
(scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(2),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getClientBlameFiles(clientObjectId: Types.ObjectId): Promise<any[]> {
|
private async getClientBlameFiles(
|
||||||
const all = (await this.blameRequestDbService.find({}, { lean: true })) as any[];
|
clientObjectId: Types.ObjectId,
|
||||||
|
): Promise<any[]> {
|
||||||
|
const all = (await this.blameRequestDbService.find(
|
||||||
|
{},
|
||||||
|
{ lean: true },
|
||||||
|
)) as any[];
|
||||||
const idStr = String(clientObjectId);
|
const idStr = String(clientObjectId);
|
||||||
return all
|
return all
|
||||||
.filter((f) =>
|
.filter((f) =>
|
||||||
(f?.parties || []).some((p) => String(p?.person?.clientId || "") === idStr),
|
(f?.parties || []).some(
|
||||||
|
(p) => String(p?.person?.clientId || "") === idStr,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.map((f) => this.normalizeBlameCase(f));
|
.map((f) => this.normalizeBlameCase(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getClientClaimFiles(clientObjectId: Types.ObjectId): Promise<any[]> {
|
private async getClientClaimFiles(
|
||||||
const all = (await this.claimCaseDbService.find({}, { lean: true })) as any[];
|
clientObjectId: Types.ObjectId,
|
||||||
|
): Promise<any[]> {
|
||||||
|
const all = (await this.claimCaseDbService.find(
|
||||||
|
{},
|
||||||
|
{ lean: true },
|
||||||
|
)) as any[];
|
||||||
const idStr = String(clientObjectId);
|
const idStr = String(clientObjectId);
|
||||||
return all
|
return all
|
||||||
.filter((f) => claimCaseTouchesClient(f, idStr))
|
.filter((f) => claimCaseTouchesClient(f, idStr))
|
||||||
.map((f) => this.normalizeClaimCase(f));
|
.map((f) => this.normalizeClaimCase(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieveAllExpertsOfClient(actor, currentPage: number, countPerPage: number) {
|
async retrieveAllExpertsOfClient(
|
||||||
|
actor,
|
||||||
|
currentPage: number,
|
||||||
|
countPerPage: number,
|
||||||
|
) {
|
||||||
const clientObjectId = this.getClientId(actor);
|
const clientObjectId = this.getClientId(actor);
|
||||||
const ckFilter = this.clientKeyScopeFilter(clientObjectId);
|
const ckFilter = this.clientKeyScopeFilter(clientObjectId);
|
||||||
const [experts, damageExperts, blameFiles, claimFiles] = await Promise.all([
|
const [experts, damageExperts, blameFiles, claimFiles] = await Promise.all([
|
||||||
@@ -755,18 +817,23 @@ export class ExpertInsurerService {
|
|||||||
expertIds,
|
expertIds,
|
||||||
);
|
);
|
||||||
const expertTotalRatingsMap: Record<string, number[]> = {};
|
const expertTotalRatingsMap: Record<string, number[]> = {};
|
||||||
const expertRatingsByCategoryMap: Record<string, Record<string, number[]>> = {};
|
const expertRatingsByCategoryMap: Record<
|
||||||
|
string,
|
||||||
|
Record<string, number[]>
|
||||||
|
> = {};
|
||||||
|
|
||||||
const processRatings = (expertId: string | undefined, file: any) => {
|
const processRatings = (expertId: string | undefined, file: any) => {
|
||||||
if (!expertId) return;
|
if (!expertId) return;
|
||||||
const rating = file?.rating;
|
const rating = file?.rating;
|
||||||
const combinedScore = this.getCombinedFileScore(file);
|
const combinedScore = this.getCombinedFileScore(file);
|
||||||
if (combinedScore !== null) {
|
if (combinedScore !== null) {
|
||||||
if (!expertTotalRatingsMap[expertId]) expertTotalRatingsMap[expertId] = [];
|
if (!expertTotalRatingsMap[expertId])
|
||||||
|
expertTotalRatingsMap[expertId] = [];
|
||||||
expertTotalRatingsMap[expertId].push(combinedScore);
|
expertTotalRatingsMap[expertId].push(combinedScore);
|
||||||
}
|
}
|
||||||
if (!rating || typeof rating !== "object") return;
|
if (!rating || typeof rating !== "object") return;
|
||||||
if (!expertRatingsByCategoryMap[expertId]) expertRatingsByCategoryMap[expertId] = {};
|
if (!expertRatingsByCategoryMap[expertId])
|
||||||
|
expertRatingsByCategoryMap[expertId] = {};
|
||||||
for (const [category, value] of Object.entries(rating)) {
|
for (const [category, value] of Object.entries(rating)) {
|
||||||
if (category === "botRating") continue;
|
if (category === "botRating") continue;
|
||||||
if (typeof value === "number" && !isNaN(value)) {
|
if (typeof value === "number" && !isNaN(value)) {
|
||||||
@@ -790,7 +857,9 @@ export class ExpertInsurerService {
|
|||||||
const totalRatings = expertTotalRatingsMap[expertIdStr] || [];
|
const totalRatings = expertTotalRatingsMap[expertIdStr] || [];
|
||||||
const overallAverageRating = totalRatings.length
|
const overallAverageRating = totalRatings.length
|
||||||
? parseFloat(
|
? parseFloat(
|
||||||
(totalRatings.reduce((a, b) => a + b, 0) / totalRatings.length).toFixed(2),
|
(
|
||||||
|
totalRatings.reduce((a, b) => a + b, 0) / totalRatings.length
|
||||||
|
).toFixed(2),
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
const averageRatingsByCategory: Record<string, number> = {};
|
const averageRatingsByCategory: Record<string, number> = {};
|
||||||
@@ -869,7 +938,9 @@ export class ExpertInsurerService {
|
|||||||
* combined insurer + user ratings.
|
* combined insurer + user ratings.
|
||||||
*/
|
*/
|
||||||
async getTopFilesForClient(insurerId: string): Promise<any[]> {
|
async getTopFilesForClient(insurerId: string): Promise<any[]> {
|
||||||
const claimFiles = await this.getClientClaimFiles(this.getClientId(insurerId));
|
const claimFiles = await this.getClientClaimFiles(
|
||||||
|
this.getClientId(insurerId),
|
||||||
|
);
|
||||||
const scored = claimFiles
|
const scored = claimFiles
|
||||||
.map((file) => {
|
.map((file) => {
|
||||||
const combinedScore = this.getCombinedFileScore(file);
|
const combinedScore = this.getCombinedFileScore(file);
|
||||||
@@ -877,10 +948,15 @@ export class ExpertInsurerService {
|
|||||||
return { ...file, combinedScore };
|
return { ...file, combinedScore };
|
||||||
})
|
})
|
||||||
.filter((f) => f !== null);
|
.filter((f) => f !== null);
|
||||||
return scored.sort((a, b) => b.combinedScore - a.combinedScore).slice(0, 10);
|
return scored
|
||||||
|
.sort((a, b) => b.combinedScore - a.combinedScore)
|
||||||
|
.slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllFilesForInsurerExpert(expertId: string, insurerClientKey: string) {
|
async getAllFilesForInsurerExpert(
|
||||||
|
expertId: string,
|
||||||
|
insurerClientKey: string,
|
||||||
|
) {
|
||||||
const expertObjectId = this.parseObjectId(expertId, "expert id");
|
const expertObjectId = this.parseObjectId(expertId, "expert id");
|
||||||
const clientOid = this.getClientId(insurerClientKey);
|
const clientOid = this.getClientId(insurerClientKey);
|
||||||
const ckFilter = this.clientKeyScopeFilter(clientOid);
|
const ckFilter = this.clientKeyScopeFilter(clientOid);
|
||||||
@@ -899,7 +975,10 @@ export class ExpertInsurerService {
|
|||||||
|
|
||||||
const clientKeyStr = String(clientOid);
|
const clientKeyStr = String(clientOid);
|
||||||
if (onBlameRoster) {
|
if (onBlameRoster) {
|
||||||
const blames = (await this.blameRequestDbService.find({}, { lean: true })) as any[];
|
const blames = (await this.blameRequestDbService.find(
|
||||||
|
{},
|
||||||
|
{ lean: true },
|
||||||
|
)) as any[];
|
||||||
return blames
|
return blames
|
||||||
.filter(
|
.filter(
|
||||||
(b) =>
|
(b) =>
|
||||||
@@ -909,7 +988,10 @@ export class ExpertInsurerService {
|
|||||||
.map((b) => this.mapBlameFileSummaryForInsurerExpert(b));
|
.map((b) => this.mapBlameFileSummaryForInsurerExpert(b));
|
||||||
}
|
}
|
||||||
if (onClaimRoster) {
|
if (onClaimRoster) {
|
||||||
const claims = (await this.claimCaseDbService.find({}, { lean: true })) as any[];
|
const claims = (await this.claimCaseDbService.find(
|
||||||
|
{},
|
||||||
|
{ lean: true },
|
||||||
|
)) as any[];
|
||||||
return claims
|
return claims
|
||||||
.filter(
|
.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
@@ -932,7 +1014,9 @@ export class ExpertInsurerService {
|
|||||||
for (const key of required) {
|
for (const key of required) {
|
||||||
const value = rating?.[key];
|
const value = rating?.[key];
|
||||||
if (typeof value !== "number" || value < 0 || value > 5) {
|
if (typeof value !== "number" || value < 0 || value > 5) {
|
||||||
throw new BadRequestException(`${key} must be a number between 0 and 5`);
|
throw new BadRequestException(
|
||||||
|
`${key} must be a number between 0 and 5`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -957,8 +1041,12 @@ export class ExpertInsurerService {
|
|||||||
this.getClientClaimFiles(id),
|
this.getClientClaimFiles(id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const blame = blameFiles.find((b) => String((b as any).publicId) === publicId);
|
const blame = blameFiles.find(
|
||||||
const claim = claimFiles.find((c) => String((c as any).publicId) === publicId);
|
(b) => String((b as any).publicId) === publicId,
|
||||||
|
);
|
||||||
|
const claim = claimFiles.find(
|
||||||
|
(c) => String((c as any).publicId) === publicId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!blame && !claim) {
|
if (!blame && !claim) {
|
||||||
throw new NotFoundException("File not found for this publicId");
|
throw new NotFoundException("File not found for this publicId");
|
||||||
@@ -971,7 +1059,10 @@ export class ExpertInsurerService {
|
|||||||
} = { publicId };
|
} = { publicId };
|
||||||
|
|
||||||
if (claim) {
|
if (claim) {
|
||||||
const claimId = this.parseObjectId(String((claim as any)._id), "claim id");
|
const claimId = this.parseObjectId(
|
||||||
|
String((claim as any)._id),
|
||||||
|
"claim id",
|
||||||
|
);
|
||||||
const updated = await this.claimCaseDbService.findByIdAndUpdate(claimId, {
|
const updated = await this.claimCaseDbService.findByIdAndUpdate(claimId, {
|
||||||
$set: { "evaluation.rating": rating },
|
$set: { "evaluation.rating": rating },
|
||||||
});
|
});
|
||||||
@@ -983,10 +1074,16 @@ export class ExpertInsurerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (blame) {
|
if (blame) {
|
||||||
const blameId = this.parseObjectId(String((blame as any)._id), "blame id");
|
const blameId = this.parseObjectId(
|
||||||
const updated = await this.blameRequestDbService.findByIdAndUpdate(blameId, {
|
String((blame as any)._id),
|
||||||
|
"blame id",
|
||||||
|
);
|
||||||
|
const updated = await this.blameRequestDbService.findByIdAndUpdate(
|
||||||
|
blameId,
|
||||||
|
{
|
||||||
$set: { "expert.rating": rating },
|
$set: { "expert.rating": rating },
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (!updated) throw new NotFoundException("Blame file not found");
|
if (!updated) throw new NotFoundException("Blame file not found");
|
||||||
out.blame = {
|
out.blame = {
|
||||||
requestId: String((updated as any)._id),
|
requestId: String((updated as any)._id),
|
||||||
@@ -1034,6 +1131,7 @@ export class ExpertInsurerService {
|
|||||||
plate?: string;
|
plate?: string;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
expertNames?: string[];
|
||||||
createdAt?: Date | string;
|
createdAt?: Date | string;
|
||||||
updatedAt?: Date | string;
|
updatedAt?: Date | string;
|
||||||
};
|
};
|
||||||
@@ -1043,6 +1141,7 @@ export class ExpertInsurerService {
|
|||||||
status?: string;
|
status?: string;
|
||||||
claimStatus?: string;
|
claimStatus?: string;
|
||||||
currentStep?: string;
|
currentStep?: string;
|
||||||
|
expertNames?: string[];
|
||||||
createdAt?: Date | string;
|
createdAt?: Date | string;
|
||||||
updatedAt?: Date | string;
|
updatedAt?: Date | string;
|
||||||
};
|
};
|
||||||
@@ -1058,10 +1157,13 @@ export class ExpertInsurerService {
|
|||||||
const partiesPreview = this.buildPartiesPreview((b as any).parties);
|
const partiesPreview = this.buildPartiesPreview((b as any).parties);
|
||||||
prev.fileType = prev.fileType ?? (b as any).type;
|
prev.fileType = prev.fileType ?? (b as any).type;
|
||||||
prev.blame = {
|
prev.blame = {
|
||||||
requestId: (b as any)?._id?.toString?.() || String((b as any)?._id || ""),
|
requestId:
|
||||||
requestNo: (b as any).requestNo || String((b as any).requestNumber || ""),
|
(b as any)?._id?.toString?.() || String((b as any)?._id || ""),
|
||||||
|
requestNo:
|
||||||
|
(b as any).requestNo || String((b as any).requestNumber || ""),
|
||||||
status: (b as any).status,
|
status: (b as any).status,
|
||||||
parties: partiesPreview,
|
parties: partiesPreview,
|
||||||
|
expertNames: extractExpertNamesFromBlame(b), // ← add
|
||||||
createdAt: (b as any).createdAt,
|
createdAt: (b as any).createdAt,
|
||||||
updatedAt: (b as any).updatedAt,
|
updatedAt: (b as any).updatedAt,
|
||||||
};
|
};
|
||||||
@@ -1075,14 +1177,19 @@ export class ExpertInsurerService {
|
|||||||
const key = String((c as any).publicId || (c as any)._id || "");
|
const key = String((c as any).publicId || (c as any)._id || "");
|
||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
const prev = byPublicId.get(key) ?? { publicId: key };
|
const prev = byPublicId.get(key) ?? { publicId: key };
|
||||||
const claimPartiesPreview = this.buildPartiesPreview((c as any)?.snapshot?.parties);
|
const claimPartiesPreview = this.buildPartiesPreview(
|
||||||
|
(c as any)?.snapshot?.parties,
|
||||||
|
);
|
||||||
prev.fileType = prev.fileType ?? (c as any)?.snapshot?.accident?.type;
|
prev.fileType = prev.fileType ?? (c as any)?.snapshot?.accident?.type;
|
||||||
prev.claim = {
|
prev.claim = {
|
||||||
requestId: (c as any)?._id?.toString?.() || String((c as any)?._id || ""),
|
requestId:
|
||||||
requestNo: (c as any).requestNo || String((c as any).requestNumber || ""),
|
(c as any)?._id?.toString?.() || String((c as any)?._id || ""),
|
||||||
|
requestNo:
|
||||||
|
(c as any).requestNo || String((c as any).requestNumber || ""),
|
||||||
status: (c as any).status,
|
status: (c as any).status,
|
||||||
claimStatus: (c as any).claimStatus,
|
claimStatus: (c as any).claimStatus,
|
||||||
currentStep: (c as any).currentStep,
|
currentStep: (c as any).currentStep,
|
||||||
|
expertNames: extractExpertNamesFromClaim(c), // ← add
|
||||||
createdAt: (c as any).createdAt,
|
createdAt: (c as any).createdAt,
|
||||||
updatedAt: (c as any).updatedAt,
|
updatedAt: (c as any).updatedAt,
|
||||||
};
|
};
|
||||||
@@ -1127,8 +1234,12 @@ export class ExpertInsurerService {
|
|||||||
this.getClientClaimFiles(id),
|
this.getClientClaimFiles(id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const blame = blameFiles.find((b) => String((b as any).publicId) === publicId);
|
const blame = blameFiles.find(
|
||||||
const claim = claimFiles.find((c) => String((c as any).publicId) === publicId);
|
(b) => String((b as any).publicId) === publicId,
|
||||||
|
);
|
||||||
|
const claim = claimFiles.find(
|
||||||
|
(c) => String((c as any).publicId) === publicId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!blame && !claim) {
|
if (!blame && !claim) {
|
||||||
throw new NotFoundException("File not found for this publicId");
|
throw new NotFoundException("File not found for this publicId");
|
||||||
@@ -1303,13 +1414,17 @@ export class ExpertInsurerService {
|
|||||||
|
|
||||||
const email = payload.email.trim().toLowerCase();
|
const email = payload.email.trim().toLowerCase();
|
||||||
const existingByEmail = await this.expertDbService.findOne({ email });
|
const existingByEmail = await this.expertDbService.findOne({ email });
|
||||||
const existingDamageByEmail = await this.damageExpertDbService.findOne({ email });
|
const existingDamageByEmail = await this.damageExpertDbService.findOne({
|
||||||
|
email,
|
||||||
|
});
|
||||||
if (existingByEmail || existingDamageByEmail) {
|
if (existingByEmail || existingDamageByEmail) {
|
||||||
throw new ConflictException("An expert with this email already exists.");
|
throw new ConflictException("An expert with this email already exists.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const nationalCode = payload.nationalCode.trim();
|
const nationalCode = payload.nationalCode.trim();
|
||||||
const existingByNational = await this.expertDbService.findOne({ nationalCode });
|
const existingByNational = await this.expertDbService.findOne({
|
||||||
|
nationalCode,
|
||||||
|
});
|
||||||
const existingDamageByNational = await this.damageExpertDbService.findOne({
|
const existingDamageByNational = await this.damageExpertDbService.findOne({
|
||||||
nationalCode,
|
nationalCode,
|
||||||
});
|
});
|
||||||
@@ -1388,7 +1503,14 @@ export class ExpertInsurerService {
|
|||||||
// Calculate current month date range
|
// Calculate current month date range
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
|
const monthEnd = new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth() + 1,
|
||||||
|
0,
|
||||||
|
23,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
);
|
||||||
|
|
||||||
// Filter files created this month
|
// Filter files created this month
|
||||||
const filesThisMonth = claimFiles.filter((file) => {
|
const filesThisMonth = claimFiles.filter((file) => {
|
||||||
@@ -1416,7 +1538,9 @@ export class ExpertInsurerService {
|
|||||||
insurerRating.evaluationTimeliness,
|
insurerRating.evaluationTimeliness,
|
||||||
insurerRating.accidentCauseAccuracy,
|
insurerRating.accidentCauseAccuracy,
|
||||||
insurerRating.guiltyVehicleIdentification,
|
insurerRating.guiltyVehicleIdentification,
|
||||||
].filter((val): val is number => typeof val === "number" && !isNaN(val));
|
].filter(
|
||||||
|
(val): val is number => typeof val === "number" && !isNaN(val),
|
||||||
|
);
|
||||||
|
|
||||||
if (insurerValues.length > 0) {
|
if (insurerValues.length > 0) {
|
||||||
filesWithInsurerRating++;
|
filesWithInsurerRating++;
|
||||||
@@ -1428,7 +1552,11 @@ export class ExpertInsurerService {
|
|||||||
|
|
||||||
// Check for bot rating (if botRating field exists in rating object)
|
// Check for bot rating (if botRating field exists in rating object)
|
||||||
const botRating = (insurerRating as any)?.botRating;
|
const botRating = (insurerRating as any)?.botRating;
|
||||||
if (botRating !== undefined && !isNaN(botRating) && typeof botRating === "number") {
|
if (
|
||||||
|
botRating !== undefined &&
|
||||||
|
!isNaN(botRating) &&
|
||||||
|
typeof botRating === "number"
|
||||||
|
) {
|
||||||
filesWithBotRating++;
|
filesWithBotRating++;
|
||||||
totalBotRatings += botRating;
|
totalBotRatings += botRating;
|
||||||
}
|
}
|
||||||
@@ -1511,12 +1639,15 @@ export class ExpertInsurerService {
|
|||||||
const clientObjectId = this.getClientId(insuranceId);
|
const clientObjectId = this.getClientId(insuranceId);
|
||||||
const { fromDate, toDate } = this.parseDateRange(opts?.from, opts?.to);
|
const { fromDate, toDate } = this.parseDateRange(opts?.from, opts?.to);
|
||||||
const isActiveFilter = this.parseOptionalBoolean(opts?.isActive);
|
const isActiveFilter = this.parseOptionalBoolean(opts?.isActive);
|
||||||
const branches = await this.branchDbService.findAllWithFilters(String(clientObjectId), {
|
const branches = await this.branchDbService.findAllWithFilters(
|
||||||
|
String(clientObjectId),
|
||||||
|
{
|
||||||
search: opts?.search,
|
search: opts?.search,
|
||||||
from: fromDate,
|
from: fromDate,
|
||||||
to: toDate,
|
to: toDate,
|
||||||
isActive: isActiveFilter,
|
isActive: isActiveFilter,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const branchIdSet = new Set(branches.map((b: any) => String(b._id)));
|
const branchIdSet = new Set(branches.map((b: any) => String(b._id)));
|
||||||
const ckFilter = this.clientKeyScopeFilter(clientObjectId);
|
const ckFilter = this.clientKeyScopeFilter(clientObjectId);
|
||||||
@@ -1536,7 +1667,8 @@ export class ExpertInsurerService {
|
|||||||
const eid = String(e?._id || "");
|
const eid = String(e?._id || "");
|
||||||
if (!eid) continue;
|
if (!eid) continue;
|
||||||
expertBranchById.set(eid, bid);
|
expertBranchById.set(eid, bid);
|
||||||
if (!activeExpertsByBranch.has(bid)) activeExpertsByBranch.set(bid, new Set());
|
if (!activeExpertsByBranch.has(bid))
|
||||||
|
activeExpertsByBranch.set(bid, new Set());
|
||||||
activeExpertsByBranch.get(bid)!.add(eid);
|
activeExpertsByBranch.get(bid)!.add(eid);
|
||||||
}
|
}
|
||||||
for (const e of damageExperts as any[]) {
|
for (const e of damageExperts as any[]) {
|
||||||
@@ -1545,14 +1677,20 @@ export class ExpertInsurerService {
|
|||||||
const eid = String(e?._id || "");
|
const eid = String(e?._id || "");
|
||||||
if (!eid) continue;
|
if (!eid) continue;
|
||||||
claimExpertBranchById.set(eid, bid);
|
claimExpertBranchById.set(eid, bid);
|
||||||
if (!activeExpertsByBranch.has(bid)) activeExpertsByBranch.set(bid, new Set());
|
if (!activeExpertsByBranch.has(bid))
|
||||||
|
activeExpertsByBranch.set(bid, new Set());
|
||||||
activeExpertsByBranch.get(bid)!.add(eid);
|
activeExpertsByBranch.get(bid)!.add(eid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const completedByBranch = new Map<string, Set<string>>();
|
const completedByBranch = new Map<string, Set<string>>();
|
||||||
const handlingByBranch = new Map<string, Set<string>>();
|
const handlingByBranch = new Map<string, Set<string>>();
|
||||||
const { blameInHandling, claimInHandling } = this.buildHandlingBranchStatusSets();
|
const { blameInHandling, claimInHandling } =
|
||||||
const addPublicId = (map: Map<string, Set<string>>, branchId: string, fileKey: string) => {
|
this.buildHandlingBranchStatusSets();
|
||||||
|
const addPublicId = (
|
||||||
|
map: Map<string, Set<string>>,
|
||||||
|
branchId: string,
|
||||||
|
fileKey: string,
|
||||||
|
) => {
|
||||||
if (!map.has(branchId)) map.set(branchId, new Set());
|
if (!map.has(branchId)) map.set(branchId, new Set());
|
||||||
map.get(branchId)!.add(fileKey);
|
map.get(branchId)!.add(fileKey);
|
||||||
};
|
};
|
||||||
@@ -1565,8 +1703,10 @@ export class ExpertInsurerService {
|
|||||||
const fileKey = String(b?.publicId || b?._id || "");
|
const fileKey = String(b?.publicId || b?._id || "");
|
||||||
if (!fileKey) continue;
|
if (!fileKey) continue;
|
||||||
const status = String(b?.status || "");
|
const status = String(b?.status || "");
|
||||||
if (status === CaseStatus.COMPLETED) addPublicId(completedByBranch, bid, fileKey);
|
if (status === CaseStatus.COMPLETED)
|
||||||
if (blameInHandling.has(status)) addPublicId(handlingByBranch, bid, fileKey);
|
addPublicId(completedByBranch, bid, fileKey);
|
||||||
|
if (blameInHandling.has(status))
|
||||||
|
addPublicId(handlingByBranch, bid, fileKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const c of claimFiles as any[]) {
|
for (const c of claimFiles as any[]) {
|
||||||
@@ -1577,8 +1717,10 @@ export class ExpertInsurerService {
|
|||||||
const fileKey = String(c?.publicId || c?._id || "");
|
const fileKey = String(c?.publicId || c?._id || "");
|
||||||
if (!fileKey) continue;
|
if (!fileKey) continue;
|
||||||
const status = String(c?.status || "");
|
const status = String(c?.status || "");
|
||||||
if (status === ClaimCaseStatus.COMPLETED) addPublicId(completedByBranch, bid, fileKey);
|
if (status === ClaimCaseStatus.COMPLETED)
|
||||||
if (claimInHandling.has(status)) addPublicId(handlingByBranch, bid, fileKey);
|
addPublicId(completedByBranch, bid, fileKey);
|
||||||
|
if (claimInHandling.has(status))
|
||||||
|
addPublicId(handlingByBranch, bid, fileKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = branches.map((branch: any) => {
|
const list = branches.map((branch: any) => {
|
||||||
@@ -1654,10 +1796,7 @@ export class ExpertInsurerService {
|
|||||||
this.isInDateRange((f as any)?.createdAt, fromDate, toDate),
|
this.isInDateRange((f as any)?.createdAt, fromDate, toDate),
|
||||||
);
|
);
|
||||||
|
|
||||||
const fileMap = new Map<
|
const fileMap = new Map<string, { blame?: any; claim?: any }>();
|
||||||
string,
|
|
||||||
{ blame?: any; claim?: any }
|
|
||||||
>();
|
|
||||||
|
|
||||||
for (const b of blameFiles) {
|
for (const b of blameFiles) {
|
||||||
const key = String((b as any).publicId || (b as any)._id || "");
|
const key = String((b as any).publicId || (b as any)._id || "");
|
||||||
@@ -1686,8 +1825,7 @@ export class ExpertInsurerService {
|
|||||||
completed += 1;
|
completed += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blameUnderReview =
|
const blameUnderReview = blameStatus === CaseStatus.WAITING_FOR_EXPERT;
|
||||||
blameStatus === CaseStatus.WAITING_FOR_EXPERT;
|
|
||||||
const claimUnderReview =
|
const claimUnderReview =
|
||||||
claimStatus === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT ||
|
claimStatus === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT ||
|
||||||
claimStatus === ClaimCaseStatus.EXPERT_REVIEWING;
|
claimStatus === ClaimCaseStatus.EXPERT_REVIEWING;
|
||||||
|
|||||||
42
src/expert-insurer/helper/insurer.helper.ts
Normal file
42
src/expert-insurer/helper/insurer.helper.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export function extractExpertNamesFromBlame(b: any): string[] {
|
||||||
|
const names = new Set<string>();
|
||||||
|
|
||||||
|
const add = (v: unknown) => {
|
||||||
|
if (typeof v === "string" && v.trim()) names.add(v.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
add(b?.workflow?.assignedForReviewBy?.actorName);
|
||||||
|
add(b?.workflow?.lockedBy?.actorName);
|
||||||
|
|
||||||
|
// History entries from expert actions
|
||||||
|
for (const h of b?.history ?? []) {
|
||||||
|
if (
|
||||||
|
h?.actor?.actorType === "field_expert" ||
|
||||||
|
h?.actor?.actorType === "damage_expert"
|
||||||
|
) {
|
||||||
|
add(h?.actor?.actorName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...names];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractExpertNamesFromClaim(c: any): string[] {
|
||||||
|
const names = new Set<string>();
|
||||||
|
|
||||||
|
const add = (v: unknown) => {
|
||||||
|
if (typeof v === "string" && v.trim()) names.add(v.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
add(c?.workflow?.assignedForReviewBy?.actorName);
|
||||||
|
add(c?.workflow?.lockedBy?.actorName);
|
||||||
|
|
||||||
|
// Evaluation snapshots
|
||||||
|
add(c?.evaluation?.damageExpertResend?.expertProfileSnapshot?.fullName);
|
||||||
|
add(c?.evaluation?.damageExpertReply?.expertProfileSnapshot?.fullName);
|
||||||
|
add(c?.evaluation?.damageExpertReplyFinal?.expertProfileSnapshot?.fullName);
|
||||||
|
add(c?.evaluation?.factorValidationExpertProfileSnapshot?.fullName);
|
||||||
|
add(c?.evaluation?.inPersonVisitExpertProfileSnapshot?.fullName);
|
||||||
|
|
||||||
|
return [...names];
|
||||||
|
}
|
||||||
@@ -32,13 +32,17 @@ export function resendRequestHasPayload(r: {
|
|||||||
}): boolean {
|
}): boolean {
|
||||||
if (!r) return false;
|
if (!r) return false;
|
||||||
if (String(r.resendDescription || "").trim() !== "") return true;
|
if (String(r.resendDescription || "").trim() !== "") return true;
|
||||||
if (Array.isArray(r.resendDocuments) && r.resendDocuments.length > 0) return true;
|
if (Array.isArray(r.resendDocuments) && r.resendDocuments.length > 0)
|
||||||
if (Array.isArray(r.resendCarParts) && r.resendCarParts.length > 0) return true;
|
return true;
|
||||||
|
if (Array.isArray(r.resendCarParts) && r.resendCarParts.length > 0)
|
||||||
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Normalize expert-requested document keys (strings or `{ key }`) to canonical enum strings. */
|
/** Normalize expert-requested document keys (strings or `{ key }`) to canonical enum strings. */
|
||||||
export function normalizeResendDocumentKeys(docs: unknown[] | undefined): string[] {
|
export function normalizeResendDocumentKeys(
|
||||||
|
docs: unknown[] | undefined,
|
||||||
|
): string[] {
|
||||||
if (!Array.isArray(docs)) return [];
|
if (!Array.isArray(docs)) return [];
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
for (const d of docs) {
|
for (const d of docs) {
|
||||||
@@ -61,10 +65,7 @@ export function normalizeResendPartKeys(
|
|||||||
selectedParts?: unknown,
|
selectedParts?: unknown,
|
||||||
): string[] {
|
): string[] {
|
||||||
if (!Array.isArray(parts)) return [];
|
if (!Array.isArray(parts)) return [];
|
||||||
const selectedNorm = normalizeDamageSelectedParts(
|
const selectedNorm = normalizeDamageSelectedParts(selectedParts, carType);
|
||||||
selectedParts,
|
|
||||||
carType,
|
|
||||||
);
|
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
for (const p of parts) {
|
for (const p of parts) {
|
||||||
const enriched =
|
const enriched =
|
||||||
@@ -72,8 +73,7 @@ export function normalizeResendPartKeys(
|
|||||||
matchResendPartFromSelected(p, selectedNorm);
|
matchResendPartFromSelected(p, selectedNorm);
|
||||||
if (enriched) {
|
if (enriched) {
|
||||||
const ck =
|
const ck =
|
||||||
enriched.catalogKey?.trim() ||
|
enriched.catalogKey?.trim() || catalogLikeKeyFromPart(enriched);
|
||||||
catalogLikeKeyFromPart(enriched);
|
|
||||||
if (ck) keys.push(ck);
|
if (ck) keys.push(ck);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,10 @@ function resolveResendPartRow(
|
|||||||
const fromSelected = matchResendPartFromSelected(raw, selectedNorm ?? []);
|
const fromSelected = matchResendPartFromSelected(raw, selectedNorm ?? []);
|
||||||
if (fromSelected) return fromSelected;
|
if (fromSelected) return fromSelected;
|
||||||
if (fromRaw) return fromRaw;
|
if (fromRaw) return fromRaw;
|
||||||
const o = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
|
const o = (raw && typeof raw === "object" ? raw : {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
const fallbackKey =
|
const fallbackKey =
|
||||||
typeof o.key === "string" && o.key.trim()
|
typeof o.key === "string" && o.key.trim()
|
||||||
? o.key.trim()
|
? o.key.trim()
|
||||||
@@ -123,8 +126,7 @@ function resolveResendPartRow(
|
|||||||
typeof o.label_fa === "string" && o.label_fa.trim()
|
typeof o.label_fa === "string" && o.label_fa.trim()
|
||||||
? o.label_fa.trim()
|
? o.label_fa.trim()
|
||||||
: fallbackKey,
|
: fallbackKey,
|
||||||
catalogKey:
|
catalogKey: typeof o.catalogKey === "string" ? o.catalogKey : undefined,
|
||||||
typeof o.catalogKey === "string" ? o.catalogKey : undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,8 +151,7 @@ export function mapExpertResendCarPartsForClient(
|
|||||||
const stored =
|
const stored =
|
||||||
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
||||||
const sp = resolveResendPartRow(raw, options.carType, selectedNorm);
|
const sp = resolveResendPartRow(raw, options.carType, selectedNorm);
|
||||||
const catalogKey =
|
const catalogKey = sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
|
||||||
sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
|
|
||||||
const captureKey = catalogKey || partLookupKey(sp);
|
const captureKey = catalogKey || partLookupKey(sp);
|
||||||
const cap = getDamagedPartCaptureBlob(
|
const cap = getDamagedPartCaptureBlob(
|
||||||
options.damagedPartsData,
|
options.damagedPartsData,
|
||||||
@@ -193,8 +194,7 @@ export function normalizeResendCarPartsForStorage(
|
|||||||
const stored =
|
const stored =
|
||||||
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
||||||
const sp = resolveResendPartRow(raw, carType, selectedNorm);
|
const sp = resolveResendPartRow(raw, carType, selectedNorm);
|
||||||
const catalogKey =
|
const catalogKey = sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
|
||||||
sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
|
|
||||||
const row: Record<string, unknown> = {
|
const row: Record<string, unknown> = {
|
||||||
id: sp.id,
|
id: sp.id,
|
||||||
name: sp.name,
|
name: sp.name,
|
||||||
@@ -217,8 +217,13 @@ function getRequiredDocEntry(claim: any, documentKey: string): unknown {
|
|||||||
return rd instanceof Map ? rd.get(documentKey) : rd[documentKey];
|
return rd instanceof Map ? rd.get(documentKey) : rd[documentKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRequiredDocumentUploaded(claim: any, documentKey: string): boolean {
|
export function isRequiredDocumentUploaded(
|
||||||
const doc = getRequiredDocEntry(claim, documentKey) as { uploaded?: boolean } | undefined;
|
claim: any,
|
||||||
|
documentKey: string,
|
||||||
|
): boolean {
|
||||||
|
const doc = getRequiredDocEntry(claim, documentKey) as
|
||||||
|
| { uploaded?: boolean }
|
||||||
|
| undefined;
|
||||||
return !!doc?.uploaded;
|
return !!doc?.uploaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +246,8 @@ export function hasDamagedPartCapture(claim: any, partKey: string): boolean {
|
|||||||
export function canFinalizeExpertResend(claim: any): boolean {
|
export function canFinalizeExpertResend(claim: any): boolean {
|
||||||
if (!claim) return false;
|
if (!claim) return false;
|
||||||
if (claim.status !== ClaimCaseStatus.WAITING_FOR_USER_RESEND) return false;
|
if (claim.status !== ClaimCaseStatus.WAITING_FOR_USER_RESEND) return false;
|
||||||
if (claim.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND) return false;
|
if (claim.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND)
|
||||||
|
return false;
|
||||||
|
|
||||||
const r = claim.evaluation?.damageExpertResend;
|
const r = claim.evaluation?.damageExpertResend;
|
||||||
if (!r || r.fulfilledAt) return false;
|
if (!r || r.fulfilledAt) return false;
|
||||||
@@ -279,16 +285,39 @@ export function documentKeyAllowedForExpertResend(
|
|||||||
return allowed.has(normalized);
|
return allowed.has(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function partKeyAllowedForExpertResend(claim: any, partKey: string): boolean {
|
export function partKeyAllowedForExpertResend(
|
||||||
|
claim: any,
|
||||||
|
partKey: string,
|
||||||
|
): boolean {
|
||||||
const r = claim?.evaluation?.damageExpertResend;
|
const r = claim?.evaluation?.damageExpertResend;
|
||||||
if (!r || r.fulfilledAt) return false;
|
if (!r || r.fulfilledAt) return false;
|
||||||
|
|
||||||
const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||||
const allowed = new Set(
|
const selectedParts = claim?.damage?.selectedParts;
|
||||||
normalizeResendPartKeys(
|
|
||||||
r.resendCarParts,
|
// Normalize the stored resend parts to their canonical catalog keys
|
||||||
carType,
|
const allowedKeys = new Set(
|
||||||
claim?.damage?.selectedParts,
|
normalizeResendPartKeys(r.resendCarParts, carType, selectedParts),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return allowed.has(partKey);
|
|
||||||
|
// Normalize the incoming partKey the same way so the comparison is apples-to-apples
|
||||||
|
const resolvedKeys = normalizeResendPartKeys(
|
||||||
|
[partKey],
|
||||||
|
carType,
|
||||||
|
selectedParts,
|
||||||
|
);
|
||||||
|
const resolvedKey = resolvedKeys[0];
|
||||||
|
|
||||||
|
// Also try direct catalogKey/name match against the stored objects as fallback
|
||||||
|
const directMatch = ((r.resendCarParts as any[]) ?? []).some((p: any) => {
|
||||||
|
if (!p || typeof p !== "object") return String(p) === partKey;
|
||||||
|
return (
|
||||||
|
p.catalogKey === partKey ||
|
||||||
|
p.key === partKey ||
|
||||||
|
p.name === partKey ||
|
||||||
|
(p.id != null && String(p.id) === partKey)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return directMatch || (resolvedKey != null && allowedKeys.has(resolvedKey));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,27 @@ const REQUIRED_VALUES = new Set<string>(
|
|||||||
* Non-canonical document keys that may still appear on stored
|
* Non-canonical document keys that may still appear on stored
|
||||||
* `evaluation.damageExpertResend.resendDocuments` → {@link ClaimRequiredDocumentType}.
|
* `evaluation.damageExpertResend.resendDocuments` → {@link ClaimRequiredDocumentType}.
|
||||||
*/
|
*/
|
||||||
const RESEND_DOCUMENT_KEY_ALIASES: Readonly<
|
const RESEND_DOCUMENT_KEY_ALIASES = {
|
||||||
Record<string, ClaimRequiredDocumentType>
|
// existing entries
|
||||||
> = {
|
|
||||||
nationalCertificate: ClaimRequiredDocumentType.NATIONAL_CARD,
|
nationalCertificate: ClaimRequiredDocumentType.NATIONAL_CARD,
|
||||||
NationalCertificate: ClaimRequiredDocumentType.NATIONAL_CARD,
|
NationalCertificate: ClaimRequiredDocumentType.NATIONAL_CARD,
|
||||||
|
national_card: ClaimRequiredDocumentType.NATIONAL_CARD, // ← add
|
||||||
drivingLicense: ClaimRequiredDocumentType.DAMAGED_DRIVING_LICENSE_FRONT,
|
drivingLicense: ClaimRequiredDocumentType.DAMAGED_DRIVING_LICENSE_FRONT,
|
||||||
DrivingLicense: ClaimRequiredDocumentType.DAMAGED_DRIVING_LICENSE_FRONT,
|
DrivingLicense: ClaimRequiredDocumentType.DAMAGED_DRIVING_LICENSE_FRONT,
|
||||||
|
driving_license: ClaimRequiredDocumentType.DAMAGED_DRIVING_LICENSE_FRONT, // ← add
|
||||||
carGreenCard: ClaimRequiredDocumentType.CAR_GREEN_CARD,
|
carGreenCard: ClaimRequiredDocumentType.CAR_GREEN_CARD,
|
||||||
CarGreenCard: ClaimRequiredDocumentType.CAR_GREEN_CARD,
|
CarGreenCard: ClaimRequiredDocumentType.CAR_GREEN_CARD,
|
||||||
|
car_green_card: ClaimRequiredDocumentType.CAR_GREEN_CARD, // ← add (redundant but safe)
|
||||||
carCertificate: ClaimRequiredDocumentType.DAMAGED_CAR_CARD_FRONT,
|
carCertificate: ClaimRequiredDocumentType.DAMAGED_CAR_CARD_FRONT,
|
||||||
CarCertificate: ClaimRequiredDocumentType.DAMAGED_CAR_CARD_FRONT,
|
CarCertificate: ClaimRequiredDocumentType.DAMAGED_CAR_CARD_FRONT,
|
||||||
|
car_certificate: ClaimRequiredDocumentType.CAR_CERTIFICATE, // ← add
|
||||||
plate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE,
|
plate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE,
|
||||||
carPlate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE,
|
carPlate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE,
|
||||||
CarPlate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE,
|
CarPlate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE,
|
||||||
|
// capture-phase docs — already in REQUIRED_VALUES so these are just safety nets
|
||||||
|
damaged_chassis_number: ClaimRequiredDocumentType.DAMAGED_CHASSIS_NUMBER,
|
||||||
|
damaged_engine_photo: ClaimRequiredDocumentType.DAMAGED_ENGINE_PHOTO,
|
||||||
|
damaged_metal_plate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Normalize a single key to a {@link ClaimRequiredDocumentType} value, or null if unknown. */
|
/** Normalize a single key to a {@link ClaimRequiredDocumentType} value, or null if unknown. */
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ export function findCatalogItemByPersianLabel(
|
|||||||
|
|
||||||
/** Non-catalog lines (expert-added interior / free-text); never use catalog numeric ids. */
|
/** Non-catalog lines (expert-added interior / free-text); never use catalog numeric ids. */
|
||||||
export function isInternalDamageSide(side: string): boolean {
|
export function isInternalDamageSide(side: string): boolean {
|
||||||
const s = String(side ?? "").trim().toLowerCase();
|
const s = String(side ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
if (!s || s === "internal" || s === "other") return true;
|
if (!s || s === "internal" || s === "other") return true;
|
||||||
return !SIDE_SET.has(s);
|
return !SIDE_SET.has(s);
|
||||||
}
|
}
|
||||||
@@ -120,7 +122,9 @@ const SIDE_LABEL_FA: Record<string, string> = {
|
|||||||
[OuterPartSideV2.TOP]: "بالا",
|
[OuterPartSideV2.TOP]: "بالا",
|
||||||
};
|
};
|
||||||
|
|
||||||
function outerCatalogListForItem(item: OuterPartCatalogItem): OuterPartCatalogItem[] {
|
function outerCatalogListForItem(
|
||||||
|
item: OuterPartCatalogItem,
|
||||||
|
): OuterPartCatalogItem[] {
|
||||||
for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) {
|
for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) {
|
||||||
if (list.some((x) => x.key === item.key || x.id === item.id)) {
|
if (list.some((x) => x.key === item.key || x.id === item.id)) {
|
||||||
return list;
|
return list;
|
||||||
@@ -165,9 +169,10 @@ function findCatalogItemByKey(key: string): OuterPartCatalogItem | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Strip leading `left_|right_|…` when present so `name` is side-agnostic for clients. */
|
/** Strip leading `left_|right_|…` when present so `name` is side-agnostic for clients. */
|
||||||
export function splitCatalogKeyToNameAndSide(
|
export function splitCatalogKeyToNameAndSide(fullKey: string): {
|
||||||
fullKey: string,
|
name: string;
|
||||||
): { name: string; side: string } {
|
side: string;
|
||||||
|
} {
|
||||||
const t = String(fullKey ?? "").trim();
|
const t = String(fullKey ?? "").trim();
|
||||||
const i = t.indexOf("_");
|
const i = t.indexOf("_");
|
||||||
if (i <= 0) return { name: t, side: "" };
|
if (i <= 0) return { name: t, side: "" };
|
||||||
@@ -329,7 +334,8 @@ export function normalizeDamageSelectedParts(
|
|||||||
let catItem: OuterPartCatalogItem | undefined;
|
let catItem: OuterPartCatalogItem | undefined;
|
||||||
if (catalog && byCatalogKey) {
|
if (catalog && byCatalogKey) {
|
||||||
if (ck) catItem = byCatalogKey.get(ck);
|
if (ck) catItem = byCatalogKey.get(ck);
|
||||||
if (!catItem && id != null) catItem = catalog.find((c) => c.id === id);
|
if (!catItem && id != null)
|
||||||
|
catItem = catalog.find((c) => c.id === id);
|
||||||
if (!catItem && side && name) {
|
if (!catItem && side && name) {
|
||||||
catItem = byCatalogKey.get(catalogLikeKeyFromPart({ side, name }));
|
catItem = byCatalogKey.get(catalogLikeKeyFromPart({ side, name }));
|
||||||
}
|
}
|
||||||
@@ -377,7 +383,9 @@ export function normalizeDamageSelectedParts(
|
|||||||
const cat =
|
const cat =
|
||||||
byCatalogKey?.get(k) ??
|
byCatalogKey?.get(k) ??
|
||||||
findCatalogItemByKey(k) ??
|
findCatalogItemByKey(k) ??
|
||||||
(toNum(o.id) != null ? CATALOG_ITEM_BY_ID.get(toNum(o.id)!) : undefined);
|
(toNum(o.id) != null
|
||||||
|
? CATALOG_ITEM_BY_ID.get(toNum(o.id)!)
|
||||||
|
: undefined);
|
||||||
if (cat) {
|
if (cat) {
|
||||||
const list = catalog ?? outerCatalogListForItem(cat);
|
const list = catalog ?? outerCatalogListForItem(cat);
|
||||||
out.push(catalogItemToSelectedPart(cat, list));
|
out.push(catalogItemToSelectedPart(cat, list));
|
||||||
@@ -581,7 +589,9 @@ export function catalogPartIdFromCarPartDamage(
|
|||||||
export const partIdentityKeyFromCarPartDamage = catalogPartIdFromCarPartDamage;
|
export const partIdentityKeyFromCarPartDamage = catalogPartIdFromCarPartDamage;
|
||||||
|
|
||||||
function normPartSegment(s: string): string {
|
function normPartSegment(s: string): string {
|
||||||
return String(s ?? "").replace(/[^a-z0-9]/gi, "").toLowerCase();
|
return String(s ?? "")
|
||||||
|
.replace(/[^a-z0-9]/gi, "")
|
||||||
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function looseCatalogNameMatch(
|
function looseCatalogNameMatch(
|
||||||
@@ -601,9 +611,13 @@ export function findCatalogItemFromLegacyExpertPart(
|
|||||||
side: string,
|
side: string,
|
||||||
carType?: ClaimVehicleTypeV2,
|
carType?: ClaimVehicleTypeV2,
|
||||||
): OuterPartCatalogItem | undefined {
|
): OuterPartCatalogItem | undefined {
|
||||||
const s = String(side ?? "").toLowerCase().trim();
|
const s = String(side ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.trim();
|
||||||
const lists = carType
|
const lists = carType
|
||||||
? [OUTER_PARTS_BY_CAR_TYPE[carType]].filter(Boolean) as OuterPartCatalogItem[][]
|
? ([OUTER_PARTS_BY_CAR_TYPE[carType]].filter(
|
||||||
|
Boolean,
|
||||||
|
) as OuterPartCatalogItem[][])
|
||||||
: (Object.values(OUTER_PARTS_BY_CAR_TYPE) as OuterPartCatalogItem[][]);
|
: (Object.values(OUTER_PARTS_BY_CAR_TYPE) as OuterPartCatalogItem[][]);
|
||||||
for (const catalog of lists) {
|
for (const catalog of lists) {
|
||||||
if (!catalog?.length) continue;
|
if (!catalog?.length) continue;
|
||||||
@@ -615,7 +629,9 @@ export function findCatalogItemFromLegacyExpertPart(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectedPartToStoredRecord(p: DamageSelectedPartV2): Record<string, unknown> {
|
function selectedPartToStoredRecord(
|
||||||
|
p: DamageSelectedPartV2,
|
||||||
|
): Record<string, unknown> {
|
||||||
const r: Record<string, unknown> = {
|
const r: Record<string, unknown> = {
|
||||||
name: p.name,
|
name: p.name,
|
||||||
side: String(p.side || "").toLowerCase(),
|
side: String(p.side || "").toLowerCase(),
|
||||||
@@ -780,9 +796,7 @@ export function isDamagedPartsMediaArray(data: unknown): boolean {
|
|||||||
return Array.isArray(data);
|
return Array.isArray(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLegacyMapRecord(
|
function getLegacyMapRecord(data: unknown): Record<string, unknown> | null {
|
||||||
data: unknown,
|
|
||||||
): Record<string, unknown> | null {
|
|
||||||
if (data == null) return null;
|
if (data == null) return null;
|
||||||
if (data instanceof Map) {
|
if (data instanceof Map) {
|
||||||
return Object.fromEntries((data as Map<string, unknown>).entries());
|
return Object.fromEntries((data as Map<string, unknown>).entries());
|
||||||
@@ -819,11 +833,9 @@ export function migrateLegacyDamagedPartsMapToArray(
|
|||||||
selected: DamageSelectedPartV2[],
|
selected: DamageSelectedPartV2[],
|
||||||
): Record<string, unknown>[] {
|
): Record<string, unknown>[] {
|
||||||
return selected.map((sp) => {
|
return selected.map((sp) => {
|
||||||
const keysTry = [
|
const keysTry = [sp.catalogKey, catalogLikeKeyFromPart(sp), sp.name].filter(
|
||||||
sp.catalogKey,
|
Boolean,
|
||||||
catalogLikeKeyFromPart(sp),
|
) as string[];
|
||||||
sp.name,
|
|
||||||
].filter(Boolean) as string[];
|
|
||||||
let blob: unknown;
|
let blob: unknown;
|
||||||
for (const k of keysTry) {
|
for (const k of keysTry) {
|
||||||
blob = getCaptureBlobLoose(legacyMap, k);
|
blob = getCaptureBlobLoose(legacyMap, k);
|
||||||
@@ -887,12 +899,14 @@ export function resolvePartCaptureIndex(
|
|||||||
if (!raw) return -1;
|
if (!raw) return -1;
|
||||||
|
|
||||||
if (selected.length) {
|
if (selected.length) {
|
||||||
|
// Pass 1: exact matches on catalogKey and catalogLikeKey
|
||||||
for (let i = 0; i < selected.length; i++) {
|
for (let i = 0; i < selected.length; i++) {
|
||||||
const sp = selected[i];
|
const sp = selected[i];
|
||||||
if (sp.catalogKey === raw) return i;
|
if (sp.catalogKey === raw) return i;
|
||||||
if (catalogLikeKeyFromPart(sp) === raw) return i;
|
if (catalogLikeKeyFromPart(sp) === raw) return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass 2: numeric id or array index
|
||||||
if (/^\d+$/.test(raw)) {
|
if (/^\d+$/.test(raw)) {
|
||||||
const n = Number(raw);
|
const n = Number(raw);
|
||||||
const byId = selected.findIndex((p) => p.id === n);
|
const byId = selected.findIndex((p) => p.id === n);
|
||||||
@@ -901,31 +915,35 @@ export function resolvePartCaptureIndex(
|
|||||||
if (!hasIdCollision && n >= 0 && n < selected.length) return n;
|
if (!hasIdCollision && n >= 0 && n < selected.length) return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass 3: loose normalized match (handles spaces, underscores, case)
|
||||||
const loose = raw.toLowerCase().replace(/[\s_-]/g, "");
|
const loose = raw.toLowerCase().replace(/[\s_-]/g, "");
|
||||||
for (let i = 0; i < selected.length; i++) {
|
for (let i = 0; i < selected.length; i++) {
|
||||||
const sp = selected[i];
|
const sp = selected[i];
|
||||||
const cand = [
|
const candidates = [
|
||||||
sp.catalogKey,
|
sp.catalogKey,
|
||||||
catalogLikeKeyFromPart(sp),
|
catalogLikeKeyFromPart(sp),
|
||||||
sp.name,
|
sp.name,
|
||||||
].filter(Boolean) as string[];
|
].filter(Boolean) as string[];
|
||||||
for (const c of cand) {
|
for (const c of candidates) {
|
||||||
if (c.toLowerCase().replace(/[\s_-]/g, "") === loose) return i;
|
if (c.toLowerCase().replace(/[\s_-]/g, "") === loose) return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (damagedPartsArray && damagedPartsArray.length) {
|
// Pass 4: suffix match — handles car-type-prefixed catalog keys
|
||||||
for (let i = 0; i < damagedPartsArray.length; i++) {
|
// e.g. "sedan_front_hood" should match sp.catalogKey="front_hood"
|
||||||
const row = damagedPartsArray[i];
|
// or normalizeResendPartKeys output "sedan_front_hood" vs stored "front_hood"
|
||||||
if (!row || typeof row !== "object") continue;
|
for (let i = 0; i < selected.length; i++) {
|
||||||
const id = toNum((row as { id?: unknown }).id);
|
const sp = selected[i];
|
||||||
if (id != null && String(id) === raw) return i;
|
const candidates = [
|
||||||
const ck = String((row as { catalogKey?: unknown }).catalogKey || "");
|
sp.catalogKey,
|
||||||
if (ck && ck === raw) return i;
|
catalogLikeKeyFromPart(sp),
|
||||||
const name = String((row as { name?: unknown }).name || "");
|
sp.name,
|
||||||
const side = String((row as { side?: unknown }).side || "");
|
].filter(Boolean) as string[];
|
||||||
if (side && name && `${side}_${name}` === raw) return i;
|
for (const c of candidates) {
|
||||||
|
const cLoose = c.toLowerCase().replace(/[\s_-]/g, "");
|
||||||
|
// raw ends with the candidate (prefix stripped)
|
||||||
|
if (loose.endsWith(cLoose) || cLoose.endsWith(loose)) return i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -939,7 +957,11 @@ export function mediaRowOrLegacyHasCapture(
|
|||||||
): boolean {
|
): boolean {
|
||||||
if (damagedPartsData == null) return false;
|
if (damagedPartsData == null) return false;
|
||||||
if (Array.isArray(damagedPartsData)) {
|
if (Array.isArray(damagedPartsData)) {
|
||||||
const idx = resolvePartCaptureIndex(partKey, selected, damagedPartsData as any[]);
|
const idx = resolvePartCaptureIndex(
|
||||||
|
partKey,
|
||||||
|
selected,
|
||||||
|
damagedPartsData as any[],
|
||||||
|
);
|
||||||
if (idx < 0) return false;
|
if (idx < 0) return false;
|
||||||
return mediaRowHasCapture((damagedPartsData as unknown[])[idx]);
|
return mediaRowHasCapture((damagedPartsData as unknown[])[idx]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -945,6 +945,7 @@ export class RequestManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialFormV2(requestId: string, body: AddPlateDto, user: any) {
|
async initialFormV2(requestId: string, body: AddPlateDto, user: any) {
|
||||||
|
try {
|
||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) {
|
if (!req) {
|
||||||
throw new NotFoundException("Request not found");
|
throw new NotFoundException("Request not found");
|
||||||
@@ -997,7 +998,8 @@ export class RequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (body.driverIsInsurer === true) {
|
} else if (body.driverIsInsurer === true) {
|
||||||
const sameNat = body.nationalCodeOfInsurer === body.nationalCodeOfDriver;
|
const sameNat =
|
||||||
|
body.nationalCodeOfInsurer === body.nationalCodeOfDriver;
|
||||||
const sameLic = body.insurerLicense === body.driverLicense;
|
const sameLic = body.insurerLicense === body.driverLicense;
|
||||||
const sameBirthday =
|
const sameBirthday =
|
||||||
body.driverBirthday == null ||
|
body.driverBirthday == null ||
|
||||||
@@ -1152,14 +1154,22 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
// Find client by company code
|
// Find client by company code
|
||||||
const clientName = inquiryMapped?.CompanyName;
|
const clientName = inquiryMapped?.CompanyName;
|
||||||
|
if (!clientName) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`CompanyName missing from inquiry response`,
|
||||||
|
);
|
||||||
|
}
|
||||||
const companyCode = inquiryMapped?.CompanyCode;
|
const companyCode = inquiryMapped?.CompanyCode;
|
||||||
const client =
|
let client =
|
||||||
await this.clientService.findClientWithCompanyCode(+companyCode);
|
await this.clientService.findClientWithCompanyCode(+companyCode);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
await this.clientService.addClient({
|
await this.clientService.addClient({
|
||||||
clientName,
|
clientName: {
|
||||||
clientCode: companyCode,
|
persian: clientName,
|
||||||
|
english: null,
|
||||||
|
},
|
||||||
|
clientCode: Number(companyCode),
|
||||||
useExpertMode: "legal",
|
useExpertMode: "legal",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1170,7 +1180,8 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
// Persist inquiry/body data into new model
|
// Persist inquiry/body data into new model
|
||||||
if (!party.person) party.person = {} as any;
|
if (!party.person) party.person = {} as any;
|
||||||
const resolvedClientId = (client as any)?._id ?? (client as any)?._doc?._id;
|
const resolvedClientId =
|
||||||
|
(client as any)?._id ?? (client as any)?._doc?._id;
|
||||||
party.person.clientId = resolvedClientId;
|
party.person.clientId = resolvedClientId;
|
||||||
party.person.nationalCodeOfInsurer = body.nationalCodeOfInsurer;
|
party.person.nationalCodeOfInsurer = body.nationalCodeOfInsurer;
|
||||||
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
||||||
@@ -1222,7 +1233,14 @@ export class RequestManagementService {
|
|||||||
this.logger.error(
|
this.logger.error(
|
||||||
`[TEJARAT] car body inquiry failed for request=${req._id}: ${err?.message || err}`,
|
`[TEJARAT] car body inquiry failed for request=${req._id}: ${err?.message || err}`,
|
||||||
);
|
);
|
||||||
this.recordPartyCaseInquiryStatus(req, "carBody", role, false, {}, err);
|
this.recordPartyCaseInquiryStatus(
|
||||||
|
req,
|
||||||
|
"carBody",
|
||||||
|
role,
|
||||||
|
false,
|
||||||
|
{},
|
||||||
|
err,
|
||||||
|
);
|
||||||
await (req as any).save();
|
await (req as any).save();
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
"Car body inquiry failed",
|
"Car body inquiry failed",
|
||||||
@@ -1293,6 +1311,9 @@ export class RequestManagementService {
|
|||||||
blameStatus: req.blameStatus,
|
blameStatus: req.blameStatus,
|
||||||
status: req.status,
|
status: req.status,
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDetailLocationV2(requestId: string, body: LocationDto, user: any) {
|
async addDetailLocationV2(requestId: string, body: LocationDto, user: any) {
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ export class SmsOrchestrationService implements OnModuleInit {
|
|||||||
return `${process.env.URL}/${frontendRoute}?token=${requestId}`;
|
return `${process.env.URL}/${frontendRoute}?token=${requestId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildBlamePartyLink(requestId: string, partyRole: "FIRST" | "SECOND"): string {
|
buildBlamePartyLink(
|
||||||
|
requestId: string,
|
||||||
|
partyRole: "FIRST" | "SECOND",
|
||||||
|
): string {
|
||||||
const route = partyRole === "SECOND" ? "user2" : "user";
|
const route = partyRole === "SECOND" ? "user2" : "user";
|
||||||
return `${process.env.URL}/${route}?token=${requestId}`;
|
return `${process.env.URL}/${route}?token=${requestId}`;
|
||||||
}
|
}
|
||||||
@@ -50,7 +53,11 @@ export class SmsOrchestrationService implements OnModuleInit {
|
|||||||
return `${process.env.URL}/caseClaim?token=${claimRequestId}`;
|
return `${process.env.URL}/caseClaim?token=${claimRequestId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendInviteLink(phoneNumber: string, publicId: string, link: string): Promise<boolean> {
|
async sendInviteLink(
|
||||||
|
phoneNumber: string,
|
||||||
|
publicId: string,
|
||||||
|
link: string,
|
||||||
|
): Promise<boolean> {
|
||||||
return this.sendTemplate({
|
return this.sendTemplate({
|
||||||
template: "yara724-invite-link",
|
template: "yara724-invite-link",
|
||||||
receptor: phoneNumber,
|
receptor: phoneNumber,
|
||||||
@@ -132,14 +139,12 @@ export class SmsOrchestrationService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendThirdPartyExpertStartedReviewNotice(
|
async sendThirdPartyExpertStartedReviewNotice(params: {
|
||||||
params: {
|
|
||||||
receptor: string;
|
receptor: string;
|
||||||
fileKind: "blame" | "claim";
|
fileKind: "blame" | "claim";
|
||||||
publicId: string;
|
publicId: string;
|
||||||
expertLastName: string;
|
expertLastName: string;
|
||||||
},
|
}): Promise<boolean> {
|
||||||
): Promise<boolean> {
|
|
||||||
return this.sendTemplate({
|
return this.sendTemplate({
|
||||||
template: "yara-expert-lock",
|
template: "yara-expert-lock",
|
||||||
receptor: params.receptor,
|
receptor: params.receptor,
|
||||||
|
|||||||
Reference in New Issue
Block a user