forked from Yara724/api
daghi + expert and damage expert name in decision +
signature required added
This commit is contained in:
@@ -59,6 +59,8 @@ import {
|
||||
enrichBlamePartiesForAgreementView,
|
||||
} from "src/helpers/blame-party-agreement-decision";
|
||||
import { resendRequestHasPayload } from "src/helpers/claim-expert-resend";
|
||||
import { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot";
|
||||
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
|
||||
|
||||
@Injectable()
|
||||
export class ExpertClaimService {
|
||||
@@ -183,6 +185,12 @@ export class ExpertClaimService {
|
||||
private readonly branchDbService: BranchDbService,
|
||||
) { }
|
||||
|
||||
/** Load immutable profile fields from `damage-expert` for the acting expert. */
|
||||
private async snapshotDamageExpert(actorId: string) {
|
||||
const doc = await this.damageExpertDbService.findById(actorId);
|
||||
return snapshotFromDamageExpert(doc as DamageExpertModel);
|
||||
}
|
||||
|
||||
/** Map blame party `Vehicle` (name/model/type) to expert panel shape. */
|
||||
private expertVehicleFromPartyVehicle(v: any): {
|
||||
carName?: string;
|
||||
@@ -474,6 +482,7 @@ export class ExpertClaimService {
|
||||
}
|
||||
|
||||
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
||||
const lockSnapshot = await this.snapshotDamageExpert(actorDetail.sub);
|
||||
|
||||
await this.claimRequestManagementDbService.findOneAndUpdate(
|
||||
new Types.ObjectId(requestId),
|
||||
@@ -486,6 +495,7 @@ export class ExpertClaimService {
|
||||
actorLocked: {
|
||||
actorId: new Types.ObjectId(actorDetail.sub),
|
||||
fullName: actorDetail.fullName,
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
},
|
||||
$push: {
|
||||
@@ -535,6 +545,48 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
/** Same rules as V1 `submitReplyRequest`: daghi option + conditional price / branchId; `branchId` stored as ObjectId. */
|
||||
private validateAndNormalizeDaghiForExpertReplyV2(
|
||||
parts: import("./dto/expert-claim-v2.dto").PartPricingV2Dto[],
|
||||
) {
|
||||
for (const part of parts) {
|
||||
if (!part.daghi || !part.daghi.option) {
|
||||
throw new BadRequestException(
|
||||
`Daghi option is required for part ${part.partId}`,
|
||||
);
|
||||
}
|
||||
if (part.daghi.option === DaghiOption.RECYCLED_PARTS_VALUE) {
|
||||
if (!part.daghi.price) {
|
||||
throw new BadRequestException(
|
||||
`Price is required for part ${part.partId} when option is '${DaghiOption.RECYCLED_PARTS_VALUE}'`,
|
||||
);
|
||||
}
|
||||
} else if (part.daghi.option === DaghiOption.DELIVER_DAMAGED_PART) {
|
||||
if (!part.daghi.branchId) {
|
||||
throw new BadRequestException(
|
||||
`Branch ID is required for part ${part.partId} when option is '${DaghiOption.DELIVER_DAMAGED_PART}'`,
|
||||
);
|
||||
}
|
||||
if (!Types.ObjectId.isValid(part.daghi.branchId)) {
|
||||
throw new BadRequestException(
|
||||
`Invalid branch ID format for part ${part.partId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts.map((part) => ({
|
||||
...part,
|
||||
daghi: {
|
||||
option: part.daghi.option,
|
||||
...(part.daghi.price && { price: part.daghi.price }),
|
||||
...(part.daghi.branchId && {
|
||||
branchId: new Types.ObjectId(part.daghi.branchId),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private findClosestCar(input: string, cars: { carName: string }[]) {
|
||||
if (!input || !cars || cars.length === 0) {
|
||||
this.logger.debug(
|
||||
@@ -1298,6 +1350,8 @@ export class ExpertClaimService {
|
||||
},
|
||||
}));
|
||||
|
||||
const expertProfileSnapshot = await this.snapshotDamageExpert(userId.sub);
|
||||
|
||||
const replyPayload = {
|
||||
description: reply.description,
|
||||
parts: processedParts,
|
||||
@@ -1306,6 +1360,7 @@ export class ExpertClaimService {
|
||||
actorName: userId.fullName,
|
||||
actorId: userId.sub,
|
||||
},
|
||||
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||
};
|
||||
|
||||
const updatePayload = {
|
||||
@@ -1378,6 +1433,8 @@ export class ExpertClaimService {
|
||||
if (request.damageExpertReplyFinal)
|
||||
throw new ForbiddenException("request already has final reply");
|
||||
|
||||
const resendSnapshot = await this.snapshotDamageExpert(userId);
|
||||
|
||||
await this.claimRequestManagementDbService.findAndUpdate(requestId, {
|
||||
lockFile: false,
|
||||
claimStatus: ReqClaimStatus.WaitingForUserToResend,
|
||||
@@ -1385,6 +1442,7 @@ export class ExpertClaimService {
|
||||
resendDescription: body.resendDescription,
|
||||
resendDocuments: body.resendDocuments,
|
||||
resendCarParts: body.resendCarParts,
|
||||
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||
},
|
||||
$push: {
|
||||
actorsChecker: {
|
||||
@@ -1668,6 +1726,16 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
const factorSnap = await this.snapshotDamageExpert(expertId);
|
||||
if (factorSnap && decisions.length > 0) {
|
||||
await this.claimRequestManagementDbService.findOneAndUpdate(
|
||||
{ _id: new Types.ObjectId(claimId) },
|
||||
{
|
||||
$set: { factorValidationExpertProfileSnapshot: factorSnap },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const updatedClaim =
|
||||
await this.claimRequestManagementDbService.findOne(claimId);
|
||||
const updatedReply =
|
||||
@@ -1748,6 +1816,8 @@ export class ExpertClaimService {
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
|
||||
const factorValidationSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
if (
|
||||
claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL ||
|
||||
claim.claimStatus !== ClaimStatus.UNDER_REVIEW ||
|
||||
@@ -1828,6 +1898,11 @@ export class ExpertClaimService {
|
||||
throw new BadRequestException("No valid factor decisions to apply.");
|
||||
}
|
||||
|
||||
if (factorValidationSnapshot) {
|
||||
$set["evaluation.factorValidationExpertProfileSnapshot"] =
|
||||
factorValidationSnapshot;
|
||||
}
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set,
|
||||
});
|
||||
@@ -1941,10 +2016,15 @@ export class ExpertClaimService {
|
||||
throw new NotFoundException("Blame not found");
|
||||
}
|
||||
|
||||
const visitSnapshot = await this.snapshotDamageExpert(actorDetail.sub);
|
||||
|
||||
const updated = await this.claimRequestManagementDbService.findAndUpdate(
|
||||
requestId,
|
||||
{
|
||||
claimStatus: ReqClaimStatus.InPersonVisit,
|
||||
...(visitSnapshot && {
|
||||
damageExpertInPersonVisitProfileSnapshot: visitSnapshot,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2013,6 +2093,8 @@ export class ExpertClaimService {
|
||||
return { claimRequestId, locked: true, message: 'Already locked by you' };
|
||||
}
|
||||
|
||||
const lockSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
||||
claimStatus: ClaimStatus.UNDER_REVIEW,
|
||||
@@ -2022,6 +2104,7 @@ export class ExpertClaimService {
|
||||
actorId: new Types.ObjectId(actor.sub),
|
||||
actorName: actor.fullName,
|
||||
actorRole: 'damage_expert',
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
$push: {
|
||||
@@ -2095,6 +2178,8 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
const resendSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.WAITING_FOR_USER_RESEND,
|
||||
@@ -2106,6 +2191,7 @@ export class ExpertClaimService {
|
||||
resendDescription: desc || undefined,
|
||||
resendDocuments: docs,
|
||||
resendCarParts: parts,
|
||||
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||
},
|
||||
},
|
||||
$unset: {
|
||||
@@ -2148,6 +2234,7 @@ export class ExpertClaimService {
|
||||
* - Must be locked by this expert (workflow.lockedBy.actorId === actor.sub)
|
||||
* - Must be in EXPERT_REVIEWING status
|
||||
* - Total payment across all parts must not exceed 30,000,000
|
||||
* - Each part must include `daghi` (option + conditional price/branchId) like V1
|
||||
*
|
||||
* On success:
|
||||
* - Stores reply in evaluation.damageExpertReply
|
||||
@@ -2201,6 +2288,11 @@ export class ExpertClaimService {
|
||||
});
|
||||
}
|
||||
|
||||
const processedParts =
|
||||
reply.parts?.length > 0
|
||||
? this.validateAndNormalizeDaghiForExpertReplyV2(reply.parts)
|
||||
: [];
|
||||
|
||||
const needsFactorUpload = reply.parts?.some((p) => p.factorNeeded === true) ?? false;
|
||||
|
||||
const objectionSubmitted = !!claim.evaluation?.objection?.submittedAt;
|
||||
@@ -2220,14 +2312,17 @@ export class ExpertClaimService {
|
||||
? ClaimWorkflowStep.EXPERT_FINAL_REPLY
|
||||
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||
|
||||
const expertProfileSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
const replyPayload = {
|
||||
description: reply.description,
|
||||
parts: reply.parts,
|
||||
parts: processedParts,
|
||||
submittedAt: new Date(),
|
||||
actorDetail: {
|
||||
actorId: actor.sub,
|
||||
actorName: actor.fullName,
|
||||
},
|
||||
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||
};
|
||||
|
||||
const nextStep = needsFactorUpload
|
||||
@@ -2262,7 +2357,7 @@ export class ExpertClaimService {
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
factorNeeded: needsFactorUpload,
|
||||
partsCount: reply.parts?.length ?? 0,
|
||||
partsCount: processedParts.length,
|
||||
isFinalReplyAfterObjection,
|
||||
replyField,
|
||||
},
|
||||
@@ -2325,10 +2420,15 @@ export class ExpertClaimService {
|
||||
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,
|
||||
...(note ? { 'evaluation.visitLocation': note } : {}),
|
||||
...(visitSnapshot && {
|
||||
'evaluation.inPersonVisitExpertProfileSnapshot': visitSnapshot,
|
||||
}),
|
||||
$push: {
|
||||
history: {
|
||||
event: 'IN_PERSON_VISIT_REQUESTED',
|
||||
@@ -2815,6 +2915,8 @@ export class ExpertClaimService {
|
||||
throw new ForbiddenException("This claim is locked by another expert");
|
||||
}
|
||||
|
||||
const damagedPartsEditSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
const previous = Array.isArray((claim as any).damage?.selectedParts)
|
||||
? [...(claim as any).damage.selectedParts]
|
||||
: [];
|
||||
@@ -2848,6 +2950,9 @@ export class ExpertClaimService {
|
||||
metadata: {
|
||||
previousSelectedParts: previous,
|
||||
selectedParts,
|
||||
...(damagedPartsEditSnapshot && {
|
||||
expertProfileSnapshot: damagedPartsEditSnapshot,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user