forked from Yara724/api
fix: harden claim review and inquiry workflows
Preserve damage history and current vehicle price, restore depreciation mapping, normalize inquiry/report output, and support resumable expert review with paginated case retrieval.
This commit is contained in:
@@ -143,6 +143,10 @@ import {
|
||||
} from "src/helpers/outer-damage-parts";
|
||||
import { normalizeResendCarPartsForStorage } from "src/helpers/claim-expert-resend";
|
||||
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||
import {
|
||||
buildDamagedPartSelectionRevision,
|
||||
serializeDamagedPartSelectionHistory,
|
||||
} from "src/helpers/claim-damaged-part-audit";
|
||||
import { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot";
|
||||
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
|
||||
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
||||
@@ -172,6 +176,10 @@ import {
|
||||
import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher";
|
||||
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
|
||||
import { getExpertReplyPricingValidationError } from "src/helpers/expert-reply-pricing";
|
||||
import {
|
||||
normalizeMoneyAmountString,
|
||||
parseMoneyAmountToman,
|
||||
} from "src/utils/unicode-digits";
|
||||
|
||||
@Injectable()
|
||||
export class ExpertClaimService {
|
||||
@@ -2719,9 +2727,14 @@ export class ExpertClaimService {
|
||||
|
||||
if (blameStatus === "WAITING_FOR_FILE_REVIEWER") {
|
||||
if (assignedReviewerId && assignedReviewerId === actor.sub) {
|
||||
// Reviewer already assigned (e.g. after a FileMaker rejection that reset
|
||||
// blame back to WAITING_FOR_FILE_REVIEWER) — skip Phase 1 and fall
|
||||
// through to the damage-expert workflow lock below.
|
||||
// Phase 1 is intentionally idempotent. The linked claim can still be
|
||||
// in a data-capture status here, so falling through to the damage
|
||||
// assessment status gate would incorrectly reject the same reviewer.
|
||||
return {
|
||||
success: true,
|
||||
status: "already_assigned_to_you",
|
||||
message: "You have already taken this file.",
|
||||
};
|
||||
} else {
|
||||
// Phase 1: first-time blame assignment
|
||||
return this.assignFileReviewerToV4Blame(claimRequestId, claim, actor);
|
||||
@@ -3263,8 +3276,6 @@ export class ExpertClaimService {
|
||||
) {
|
||||
if (actor.role !== RoleEnum.FIELD_EXPERT) requireActorClientKey(actor);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
const blame = await this.blameRequestDbService.findById(claim.blameRequestId);
|
||||
|
||||
if (!claim) {
|
||||
throw new NotFoundException(
|
||||
this.expertReplySubmissionError(
|
||||
@@ -3274,6 +3285,18 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
const blame = await this.blameRequestDbService.findById(
|
||||
claim.blameRequestId,
|
||||
);
|
||||
if (!blame) {
|
||||
throw new NotFoundException(
|
||||
this.expertReplySubmissionError(
|
||||
"پرونده تعیین خسارت مرتبط یافت نشد.",
|
||||
"BLAME_NOT_FOUND",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await this.assertExpertActorOnClaim(claim, actor);
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||
@@ -3304,12 +3327,33 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
if (reply.carPrice && blame.type !== BlameRequestType.CAR_BODY) {
|
||||
throw new ForbiddenException("قیمت روز خودرو فقط در پرونده های بدنه باید بررسی شود")
|
||||
}
|
||||
const carPriceWasProvided = reply.carPrice != null;
|
||||
let normalizedCurrentCarPrice: string | undefined;
|
||||
|
||||
if (blame.type === BlameRequestType.THIRD_PARTY && reply.carPrice) {
|
||||
throw new ForbiddenException("قیمت روز خودرو فقط در پرونده های بدنه باید بررسی شود")
|
||||
if (blame.type === BlameRequestType.CAR_BODY) {
|
||||
const parsedCurrentCarPrice = parseMoneyAmountToman(reply.carPrice);
|
||||
if (
|
||||
parsedCurrentCarPrice == null ||
|
||||
!Number.isSafeInteger(parsedCurrentCarPrice) ||
|
||||
parsedCurrentCarPrice <= 0
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
this.expertReplySubmissionError(
|
||||
"قیمت روز خودرو در پرونده بدنه الزامی است و باید مبلغی صحیح و بیشتر از صفر باشد.",
|
||||
"CAR_PRICE_REQUIRED",
|
||||
{ field: "carPrice" },
|
||||
),
|
||||
);
|
||||
}
|
||||
normalizedCurrentCarPrice = normalizeMoneyAmountString(reply.carPrice!);
|
||||
} else if (carPriceWasProvided) {
|
||||
throw new ForbiddenException(
|
||||
this.expertReplySubmissionError(
|
||||
"قیمت روز خودرو فقط برای پرونده بدنه قابل ثبت است.",
|
||||
"CAR_PRICE_NOT_ALLOWED",
|
||||
{ field: "carPrice" },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const pricingValidationError = getExpertReplyPricingValidationError(
|
||||
@@ -3540,7 +3584,9 @@ export class ExpertClaimService {
|
||||
"evaluation.ownerInsurerApproval": "",
|
||||
"evaluation.ownerPricedPartsApproval": "",
|
||||
},
|
||||
"vehicle.price": reply.carPrice,
|
||||
...(normalizedCurrentCarPrice
|
||||
? { "vehicle.price": normalizedCurrentCarPrice }
|
||||
: {}),
|
||||
"workflow.currentStep": currentStep,
|
||||
"workflow.nextStep": nextWorkflowStep,
|
||||
[`evaluation.${replyField}`]: replyPayload,
|
||||
@@ -5050,6 +5096,10 @@ export class ExpertClaimService {
|
||||
buildFileLink,
|
||||
resolveStoredFileUrl,
|
||||
});
|
||||
const damagedPartsHistory = serializeDamagedPartSelectionHistory({
|
||||
history: (claim.damage as any)?.partSelectionHistory,
|
||||
currentSelectedParts: selectedNormExpert,
|
||||
});
|
||||
|
||||
// Vehicle payload — fall back to blame inquiry if claim vehicle is sparse
|
||||
let vehiclePayload = claim.vehicle as any;
|
||||
@@ -5229,6 +5279,7 @@ export class ExpertClaimService {
|
||||
: undefined,
|
||||
carAngles,
|
||||
damagedParts,
|
||||
damagedPartsHistory,
|
||||
awaitingFactorValidation: isFactorValidationPending,
|
||||
requiresFileMakerApproval: !!(claim as any).requiresFileMakerApproval,
|
||||
fileMakerRejectionCount: (claim as any).fileMakerRejectionCount ?? 0,
|
||||
@@ -5606,33 +5657,58 @@ export class ExpertClaimService {
|
||||
);
|
||||
const mergedExpertAdded = [...existingExpertAdded, ...expertAddedToAppend];
|
||||
|
||||
const changedAt = new Date();
|
||||
const partSelectionRevision = buildDamagedPartSelectionRevision({
|
||||
revisionId: new Types.ObjectId().toString(),
|
||||
changedAt,
|
||||
changedBy: {
|
||||
actorId: actor.sub,
|
||||
actorName: actor.fullName,
|
||||
actorType: "damage_expert",
|
||||
},
|
||||
expertProfileSnapshot: damagedPartsEditSnapshot,
|
||||
previousParts: previousNorm,
|
||||
selectedParts: nextNorm,
|
||||
previousMedia: prevMedia,
|
||||
});
|
||||
|
||||
const $set: Record<string, unknown> = {
|
||||
"damage.selectedParts": nextNorm,
|
||||
"media.damagedParts": nextMedia,
|
||||
"damage.expertAddedParts": mergedExpertAdded,
|
||||
};
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set,
|
||||
$push: {
|
||||
history: {
|
||||
type: "EXPERT_DAMAGED_PARTS_UPDATED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(actor.sub),
|
||||
actorName: actor.fullName,
|
||||
actorType: "damage_expert",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
previousSelectedParts: previous,
|
||||
selectedParts: nextNorm,
|
||||
expertAddedParts: mergedExpertAdded,
|
||||
...(damagedPartsEditSnapshot && {
|
||||
expertProfileSnapshot: damagedPartsEditSnapshot,
|
||||
}),
|
||||
},
|
||||
const push: Record<string, unknown> = {
|
||||
history: {
|
||||
type: "EXPERT_DAMAGED_PARTS_UPDATED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(actor.sub),
|
||||
actorName: actor.fullName,
|
||||
actorType: "damage_expert",
|
||||
},
|
||||
timestamp: changedAt,
|
||||
metadata: {
|
||||
previousSelectedParts: previous,
|
||||
selectedParts: nextNorm,
|
||||
expertAddedParts: mergedExpertAdded,
|
||||
...(partSelectionRevision && {
|
||||
partSelectionRevisionId: partSelectionRevision.revisionId,
|
||||
removedParts: partSelectionRevision.removedParts,
|
||||
addedParts: partSelectionRevision.addedParts,
|
||||
}),
|
||||
...(damagedPartsEditSnapshot && {
|
||||
expertProfileSnapshot: damagedPartsEditSnapshot,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
if (partSelectionRevision) {
|
||||
push["damage.partSelectionHistory"] = partSelectionRevision;
|
||||
}
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set,
|
||||
$push: push,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -5640,6 +5716,7 @@ export class ExpertClaimService {
|
||||
selectedParts: nextNorm,
|
||||
previousSelectedParts: previous,
|
||||
expertAddedParts: mergedExpertAdded,
|
||||
partSelectionRevision,
|
||||
message: "Damaged parts updated successfully.",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user