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