1
0
forked from Yara724/api

Fixed enrichedEvaluation

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

View File

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