forked from Yara724/api
Merge pull request 'YARA-867, YARA-868' (#50) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#50
This commit is contained in:
@@ -2805,6 +2805,27 @@ export class ClaimRequestManagementService {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Branch ids referenced on expert-priced parts (`daghi` object with `branchId`). */
|
||||||
|
private collectBranchIdsFromClaimExpertReply(reply: {
|
||||||
|
parts?: unknown[];
|
||||||
|
}): Set<string> {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
const parts = reply?.parts;
|
||||||
|
if (!Array.isArray(parts)) {
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
for (const p of parts) {
|
||||||
|
const d = (p as { daghi?: unknown })?.daghi;
|
||||||
|
if (d && typeof d === "object" && d !== null && "branchId" in d) {
|
||||||
|
const bid = (d as { branchId?: unknown }).branchId;
|
||||||
|
if (bid != null && Types.ObjectId.isValid(String(bid))) {
|
||||||
|
ids.add(String(bid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fanavaran Submit - Map data from claim-request-management and request-management to third-party-car-financial-claims format
|
* Fanavaran Submit - Map data from claim-request-management and request-management to third-party-car-financial-claims format
|
||||||
*/
|
*/
|
||||||
@@ -5139,6 +5160,7 @@ export class ClaimRequestManagementService {
|
|||||||
async submitOwnerInsurerApprovalSignV2(
|
async submitOwnerInsurerApprovalSignV2(
|
||||||
claimRequestId: string,
|
claimRequestId: string,
|
||||||
agree: boolean,
|
agree: boolean,
|
||||||
|
branchId: string,
|
||||||
signFile: Express.Multer.File,
|
signFile: Express.Multer.File,
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
actor?: { sub: string; role?: string; fullName?: string },
|
actor?: { sub: string; role?: string; fullName?: string },
|
||||||
@@ -5200,6 +5222,34 @@ export class ClaimRequestManagementService {
|
|||||||
throw new BadRequestException("No expert reply is available to sign off on yet.");
|
throw new BadRequestException("No expert reply is available to sign off on yet.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawBranchId = (branchId ?? "").trim();
|
||||||
|
if (!rawBranchId || !Types.ObjectId.isValid(rawBranchId)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"branchId is required and must be a valid MongoDB ObjectId.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const branchDoc = await this.branchDbService.findById(rawBranchId);
|
||||||
|
if (!branchDoc) {
|
||||||
|
throw new NotFoundException(`Branch with ID ${rawBranchId} not found.`);
|
||||||
|
}
|
||||||
|
const ownerClientId = claimCase.owner?.clientId;
|
||||||
|
if (!ownerClientId || String(branchDoc.clientKey) !== String(ownerClientId)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This branch does not belong to the insurer for this claim.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const branchIdsOnPricing = this.collectBranchIdsFromClaimExpertReply(
|
||||||
|
activeReply as { parts?: unknown[] },
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
branchIdsOnPricing.size > 0 &&
|
||||||
|
!branchIdsOnPricing.has(String(rawBranchId))
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"branchId must match a branch used in the expert pricing for this claim.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const signDoc = await this.claimSignDbService.create({
|
const signDoc = await this.claimSignDbService.create({
|
||||||
fileName: signFile.filename,
|
fileName: signFile.filename,
|
||||||
userId: effectiveUserId,
|
userId: effectiveUserId,
|
||||||
@@ -5210,6 +5260,7 @@ export class ClaimRequestManagementService {
|
|||||||
const signedAt = new Date();
|
const signedAt = new Date();
|
||||||
const approvalPayload = {
|
const approvalPayload = {
|
||||||
agree,
|
agree,
|
||||||
|
branchId: new Types.ObjectId(rawBranchId),
|
||||||
signDetailId: signId,
|
signDetailId: signId,
|
||||||
signedAt,
|
signedAt,
|
||||||
};
|
};
|
||||||
@@ -5233,7 +5284,7 @@ export class ClaimRequestManagementService {
|
|||||||
type: "OWNER_REJECTED_INSURER_APPROVAL_PRICING",
|
type: "OWNER_REJECTED_INSURER_APPROVAL_PRICING",
|
||||||
actor: historyActor,
|
actor: historyActor,
|
||||||
timestamp: signedAt,
|
timestamp: signedAt,
|
||||||
metadata: {},
|
metadata: { branchId: rawBranchId },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -5262,7 +5313,7 @@ export class ClaimRequestManagementService {
|
|||||||
type: "OWNER_SIGNED_INSURER_APPROVAL",
|
type: "OWNER_SIGNED_INSURER_APPROVAL",
|
||||||
actor: historyActor,
|
actor: historyActor,
|
||||||
timestamp: signedAt,
|
timestamp: signedAt,
|
||||||
metadata: {},
|
metadata: { branchId: rawBranchId },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export class ClaimRequestManagementV2Controller {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get Claim Details (V2)",
|
summary: "Get Claim Details (V2)",
|
||||||
description:
|
description:
|
||||||
"Get claim details for current actor. USER receives only minimal own-needed payload (no extra owner/security fields). FIELD_EXPERT keeps full view for expert workflows.",
|
"Get claim details for current actor. USER (claim owner) receives the same core claim payload including `owner` (userId, clientId, optional userClientKey). FIELD_EXPERT additionally receives unmasked money and userRating when present.",
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -245,21 +245,27 @@ export class ClaimRequestManagementV2Controller {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Sign final claim pricing (owner)",
|
summary: "Sign final claim pricing (owner)",
|
||||||
description:
|
description:
|
||||||
"Multipart: `sign` (signature image) and `agree` (boolean). Allowed only when `status` is WAITING_FOR_INSURER_APPROVAL, " +
|
"Multipart: `sign` (signature image), `agree` (boolean), and `branchId` (Mongo ObjectId of the insurer branch the file is reviewed under). " +
|
||||||
|
"Allowed only when `status` is WAITING_FOR_INSURER_APPROVAL, " +
|
||||||
"`workflow.currentStep` is INSURER_REVIEW (not EXPERT_COST_EVALUATION), and `claimStatus` is APPROVED. " +
|
"`workflow.currentStep` is INSURER_REVIEW (not EXPERT_COST_EVALUATION), and `claimStatus` is APPROVED. " +
|
||||||
"If `agree` is true, the case moves to COMPLETED; if false, to REJECTED.",
|
"If `agree` is true, the case moves to COMPLETED; if false, to REJECTED.",
|
||||||
})
|
})
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
description: "Signature file and agreement",
|
description: "Signature file, agreement, and branch",
|
||||||
schema: {
|
schema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["sign", "agree"],
|
required: ["sign", "agree", "branchId"],
|
||||||
properties: {
|
properties: {
|
||||||
sign: { type: "string", format: "binary", description: "Signature image" },
|
sign: { type: "string", format: "binary", description: "Signature image" },
|
||||||
agree: {
|
agree: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
description: "true to accept expert pricing and complete the claim",
|
description: "true to accept expert pricing and complete the claim",
|
||||||
},
|
},
|
||||||
|
branchId: {
|
||||||
|
type: "string",
|
||||||
|
description: "Insurer branch id (must belong to the claim owner's insurer; if pricing lists branch options, must match one of them)",
|
||||||
|
example: "507f1f77bcf86cd799439011",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -285,6 +291,7 @@ export class ClaimRequestManagementV2Controller {
|
|||||||
async submitOwnerInsurerApprovalSignV2(
|
async submitOwnerInsurerApprovalSignV2(
|
||||||
@Param("claimRequestId") claimRequestId: string,
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
@Body("agree") agree: string | boolean,
|
@Body("agree") agree: string | boolean,
|
||||||
|
@Body("branchId") branchId: string,
|
||||||
@CurrentUser() user: any,
|
@CurrentUser() user: any,
|
||||||
@UploadedFile() sign: Express.Multer.File,
|
@UploadedFile() sign: Express.Multer.File,
|
||||||
) {
|
) {
|
||||||
@@ -297,6 +304,7 @@ export class ClaimRequestManagementV2Controller {
|
|||||||
return await this.claimRequestManagementService.submitOwnerInsurerApprovalSignV2(
|
return await this.claimRequestManagementService.submitOwnerInsurerApprovalSignV2(
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
agreed,
|
agreed,
|
||||||
|
typeof branchId === "string" ? branchId : "",
|
||||||
sign,
|
sign,
|
||||||
user.sub,
|
user.sub,
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ export class ClaimOwnerInsurerApproval {
|
|||||||
@Prop({ type: Boolean, required: true })
|
@Prop({ type: Boolean, required: true })
|
||||||
agree: boolean;
|
agree: boolean;
|
||||||
|
|
||||||
|
/** Branch the owner is signing for (must belong to their insurer; aligned with expert daghi options when present). */
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
branchId?: Types.ObjectId;
|
||||||
|
|
||||||
@Prop({ type: Types.ObjectId })
|
@Prop({ type: Types.ObjectId })
|
||||||
signDetailId?: Types.ObjectId;
|
signDetailId?: Types.ObjectId;
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,16 @@ export class ClaimDetailV2ResponseDto {
|
|||||||
damageExpertReplyFinal?: unknown;
|
damageExpertReplyFinal?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
'Owner objection to expert-priced parts (`evaluation.objection`): disputed lines + optional new parts + submission time.',
|
||||||
|
})
|
||||||
|
objection?: {
|
||||||
|
objectionParts?: Array<Record<string, unknown>>;
|
||||||
|
newParts?: Array<Record<string, unknown>>;
|
||||||
|
submittedAt?: Date | string;
|
||||||
|
};
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description:
|
description:
|
||||||
'Car walk-around video from `claim-video-capture` (resolved from `media.videoCaptureId`).',
|
'Car walk-around video from `claim-video-capture` (resolved from `media.videoCaptureId`).',
|
||||||
|
|||||||
@@ -245,6 +245,24 @@ export class ExpertClaimService {
|
|||||||
return String(actor.clientKey ?? "");
|
return String(actor.clientKey ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `preLockQueueSnapshot` must only store {@link ClaimWorkflowStep} values.
|
||||||
|
* Legacy / mistaken data sometimes put {@link ClaimCaseStatus} (e.g. WAITING_FOR_DAMAGE_EXPERT)
|
||||||
|
* in `workflow.currentStep`, which would fail Mongoose enum validation when snapshotted.
|
||||||
|
*/
|
||||||
|
private normalizeClaimWorkflowStepForSnapshot(
|
||||||
|
value: unknown,
|
||||||
|
fallback: ClaimWorkflowStep,
|
||||||
|
): ClaimWorkflowStep {
|
||||||
|
if (
|
||||||
|
typeof value === "string" &&
|
||||||
|
(Object.values(ClaimWorkflowStep) as string[]).includes(value)
|
||||||
|
) {
|
||||||
|
return value as ClaimWorkflowStep;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
/** Owner mobile: linked blame party phone when available, else `users.mobile`. */
|
/** 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;
|
if (!claim?.owner?.userId) return undefined;
|
||||||
@@ -2226,20 +2244,30 @@ export class ExpertClaimService {
|
|||||||
},
|
},
|
||||||
'workflow.preLockQueueSnapshot': {
|
'workflow.preLockQueueSnapshot': {
|
||||||
claimStatus: claim.claimStatus,
|
claimStatus: claim.claimStatus,
|
||||||
currentStep:
|
currentStep: this.normalizeClaimWorkflowStepForSnapshot(
|
||||||
claim.workflow?.currentStep ?? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
claim.workflow?.currentStep,
|
||||||
|
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||||
|
),
|
||||||
...(claim.workflow?.nextStep != null
|
...(claim.workflow?.nextStep != null
|
||||||
? { nextStep: claim.workflow.nextStep }
|
? {
|
||||||
|
nextStep: this.normalizeClaimWorkflowStepForSnapshot(
|
||||||
|
claim.workflow.nextStep,
|
||||||
|
ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
|
),
|
||||||
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
$push: {
|
$push: {
|
||||||
history: {
|
history: {
|
||||||
event: 'CLAIM_LOCKED',
|
type: "CLAIM_LOCKED",
|
||||||
performedBy: actor.sub,
|
actor: {
|
||||||
performedByName: actor.fullName,
|
actorId: new Types.ObjectId(actor.sub),
|
||||||
performedAt: new Date(),
|
actorName: actor.fullName,
|
||||||
note: `Claim locked by damage expert ${actor.fullName}`,
|
actorType: "damage_expert",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: { note: `Claim locked by damage expert ${actor.fullName}` },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -2657,11 +2685,16 @@ export class ExpertClaimService {
|
|||||||
}),
|
}),
|
||||||
$push: {
|
$push: {
|
||||||
history: {
|
history: {
|
||||||
event: 'IN_PERSON_VISIT_REQUESTED',
|
type: "IN_PERSON_VISIT_REQUESTED",
|
||||||
performedBy: actor.sub,
|
actor: {
|
||||||
performedByName: actor.fullName,
|
actorId: new Types.ObjectId(actor.sub),
|
||||||
performedAt: new Date(),
|
actorName: actor.fullName,
|
||||||
note: note || 'Expert requested in-person visit',
|
actorType: "damage_expert",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
note: note || "Expert requested in-person visit",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -2678,7 +2711,7 @@ export class ExpertClaimService {
|
|||||||
claimRequestId,
|
claimRequestId,
|
||||||
status: claim.status,
|
status: claim.status,
|
||||||
claimStatus: ClaimStatus.NEEDS_REVISION,
|
claimStatus: ClaimStatus.NEEDS_REVISION,
|
||||||
message: 'In-person visit requested. User will be notified.',
|
message: "In-person visit requested. User will be notified.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2723,6 +2756,11 @@ export class ExpertClaimService {
|
|||||||
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
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 },
|
||||||
|
},
|
||||||
// This expert's own locked/in-progress claims
|
// This expert's own locked/in-progress claims
|
||||||
{
|
{
|
||||||
'workflow.locked': true,
|
'workflow.locked': true,
|
||||||
@@ -2919,6 +2957,34 @@ export class ExpertClaimService {
|
|||||||
return docRec;
|
return docRec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Strip bulky insurer-inquiry payloads (`raw` / `mapped`) from vehicle.inquiry. */
|
||||||
|
private sanitizeVehicleInquiryForApi(vehicle: any) {
|
||||||
|
if (!vehicle || typeof vehicle !== "object") return vehicle;
|
||||||
|
const inquiry = vehicle.inquiry;
|
||||||
|
if (!inquiry || typeof inquiry !== "object") return vehicle;
|
||||||
|
const { raw, mapped, ...safeInquiry } = inquiry as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
...vehicle,
|
||||||
|
inquiry: safeInquiry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Linked blame snapshot for damage-expert detail: keep API response lean. */
|
||||||
|
private sanitizeBlameCaseForClaimDetailApi(
|
||||||
|
blame: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const out = { ...blame };
|
||||||
|
const parties = out.parties;
|
||||||
|
if (Array.isArray(parties)) {
|
||||||
|
out.parties = parties.map((p: any) =>
|
||||||
|
p && typeof p === "object"
|
||||||
|
? { ...p, vehicle: this.sanitizeVehicleInquiryForApi(p.vehicle) }
|
||||||
|
: p,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/** JSON-safe `expert.decision` for API responses (ObjectIds → string). */
|
/** JSON-safe `expert.decision` for API responses (ObjectIds → string). */
|
||||||
private serializeBlameExpertDecisionForClaimDetail(
|
private serializeBlameExpertDecisionForClaimDetail(
|
||||||
decision: unknown,
|
decision: unknown,
|
||||||
@@ -3025,8 +3091,9 @@ export class ExpertClaimService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist unlock when claim V2 workflow lock TTL has passed.
|
* Persist unlock when claim V2 workflow lock TTL has passed.
|
||||||
* If the expert never submitted a damage reply, restores queue fields from
|
* When the case was in {@link ClaimCaseStatus.EXPERT_REVIEWING}, restores queue fields from
|
||||||
* `workflow.preLockQueueSnapshot` (or defaults) so the case shows as WAITING_FOR_DAMAGE_EXPERT again.
|
* `workflow.preLockQueueSnapshot` (or defaults) and sets status back to WAITING_FOR_DAMAGE_EXPERT
|
||||||
|
* so the claim reappears in the open expert queue (same intent as blame list expiry + in-memory unlock).
|
||||||
*/
|
*/
|
||||||
private async expireClaimWorkflowLockV2IfStale(
|
private async expireClaimWorkflowLockV2IfStale(
|
||||||
claimRequestId: string,
|
claimRequestId: string,
|
||||||
@@ -3047,7 +3114,6 @@ export class ExpertClaimService {
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!expiredAt && !lockedAt) return false;
|
|
||||||
|
|
||||||
const lockedById = claim.workflow.lockedBy?.actorId;
|
const lockedById = claim.workflow.lockedBy?.actorId;
|
||||||
const filter: Record<string, unknown> = {
|
const filter: Record<string, unknown> = {
|
||||||
@@ -3059,12 +3125,10 @@ export class ExpertClaimService {
|
|||||||
} else if (lockedAt) {
|
} else if (lockedAt) {
|
||||||
filter["workflow.lockedAt"] = lockedAt;
|
filter["workflow.lockedAt"] = lockedAt;
|
||||||
} else if (lockedById) {
|
} else if (lockedById) {
|
||||||
filter["workflow.lockedBy.actorId"] = lockedById;
|
filter["workflow.lockedBy.actorId"] = new Types.ObjectId(String(lockedById));
|
||||||
}
|
}
|
||||||
|
|
||||||
const needsQueueRestore =
|
const needsQueueRestore = claim.status === ClaimCaseStatus.EXPERT_REVIEWING;
|
||||||
claim.status === ClaimCaseStatus.EXPERT_REVIEWING &&
|
|
||||||
!this.claimHasDamageExpertReply(claim);
|
|
||||||
|
|
||||||
const snap = claim.workflow?.preLockQueueSnapshot as
|
const snap = claim.workflow?.preLockQueueSnapshot as
|
||||||
| {
|
| {
|
||||||
@@ -3075,11 +3139,16 @@ export class ExpertClaimService {
|
|||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
const restoreClaimStatus = snap?.claimStatus ?? ClaimStatus.PENDING;
|
const restoreClaimStatus = snap?.claimStatus ?? ClaimStatus.PENDING;
|
||||||
const restoreCurrent =
|
const restoreCurrent = this.normalizeClaimWorkflowStepForSnapshot(
|
||||||
snap?.currentStep ?? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
snap?.currentStep,
|
||||||
|
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||||
|
);
|
||||||
const restoreNext =
|
const restoreNext =
|
||||||
snap?.nextStep != null
|
snap?.nextStep != null
|
||||||
? snap.nextStep
|
? this.normalizeClaimWorkflowStepForSnapshot(
|
||||||
|
snap.nextStep,
|
||||||
|
ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
|
)
|
||||||
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||||
|
|
||||||
const lockFieldsUnset = {
|
const lockFieldsUnset = {
|
||||||
@@ -3136,6 +3205,7 @@ export class ExpertClaimService {
|
|||||||
actor: any,
|
actor: any,
|
||||||
): Promise<ClaimDetailV2ResponseDto> {
|
): Promise<ClaimDetailV2ResponseDto> {
|
||||||
const actorId = actor.sub;
|
const actorId = actor.sub;
|
||||||
|
await this.reconcileStaleExpertReviewingClaimLocksForTenant(actor);
|
||||||
await this.expireClaimWorkflowLockV2IfStale(claimRequestId);
|
await this.expireClaimWorkflowLockV2IfStale(claimRequestId);
|
||||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
@@ -3319,6 +3389,21 @@ export class ExpertClaimService {
|
|||||||
? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT
|
? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT
|
||||||
: claim.status;
|
: claim.status;
|
||||||
|
|
||||||
|
const objection = (claim as any).evaluation?.objection
|
||||||
|
? {
|
||||||
|
objectionParts:
|
||||||
|
(claim as any).evaluation.objection.objectionParts ?? [],
|
||||||
|
newParts: (claim as any).evaluation.objection.newParts ?? [],
|
||||||
|
submittedAt: (claim as any).evaluation.objection.submittedAt,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const blameCaseForApi = blameCase
|
||||||
|
? this.sanitizeBlameCaseForClaimDetailApi(
|
||||||
|
blameCase as Record<string, unknown>,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claimRequestId: claim._id.toString(),
|
claimRequestId: claim._id.toString(),
|
||||||
publicId: claim.publicId,
|
publicId: claim.publicId,
|
||||||
@@ -3341,7 +3426,9 @@ export class ExpertClaimService {
|
|||||||
fullName: claim.owner.fullName,
|
fullName: claim.owner.fullName,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
vehicle: vehiclePayload,
|
vehicle: vehiclePayload
|
||||||
|
? this.sanitizeVehicleInquiryForApi(vehiclePayload)
|
||||||
|
: undefined,
|
||||||
...blameFileContext,
|
...blameFileContext,
|
||||||
blameRequestId: claim.blameRequestId?.toString(),
|
blameRequestId: claim.blameRequestId?.toString(),
|
||||||
blameRequestNo: claim.blameRequestNo,
|
blameRequestNo: claim.blameRequestNo,
|
||||||
@@ -3361,8 +3448,9 @@ export class ExpertClaimService {
|
|||||||
damageExpertReplyFinal: (claim as any).evaluation?.damageExpertReplyFinal,
|
damageExpertReplyFinal: (claim as any).evaluation?.damageExpertReplyFinal,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
objection,
|
||||||
videoCapture,
|
videoCapture,
|
||||||
blameCase: blameCase ?? undefined,
|
blameCase: blameCaseForApi,
|
||||||
blameExpertDecision,
|
blameExpertDecision,
|
||||||
createdAt: (claim as any).createdAt,
|
createdAt: (claim as any).createdAt,
|
||||||
updatedAt: (claim as any).updatedAt,
|
updatedAt: (claim as any).updatedAt,
|
||||||
@@ -3405,31 +3493,41 @@ export class ExpertClaimService {
|
|||||||
: [];
|
: [];
|
||||||
const selectedParts = body.selectedParts as string[];
|
const selectedParts = body.selectedParts as string[];
|
||||||
|
|
||||||
if (!(claim as any).damage) (claim as any).damage = {};
|
const $set: Record<string, unknown> = {
|
||||||
(claim as any).damage.selectedParts = selectedParts;
|
"damage.selectedParts": selectedParts,
|
||||||
|
};
|
||||||
|
|
||||||
// Keep damaged-parts captures consistent with updated selection.
|
|
||||||
const damagedPartsMedia = (claim as any).media?.damagedParts;
|
const damagedPartsMedia = (claim as any).media?.damagedParts;
|
||||||
if (damagedPartsMedia) {
|
if (damagedPartsMedia) {
|
||||||
const selectedSet = new Set(selectedParts);
|
const selectedSet = new Set(selectedParts);
|
||||||
if (damagedPartsMedia instanceof Map) {
|
if (damagedPartsMedia instanceof Map) {
|
||||||
for (const key of Array.from(damagedPartsMedia.keys())) {
|
const m = new Map(damagedPartsMedia as Map<string, unknown>);
|
||||||
if (!selectedSet.has(key)) damagedPartsMedia.delete(key);
|
for (const key of Array.from(m.keys())) {
|
||||||
|
if (!selectedSet.has(key)) m.delete(key);
|
||||||
}
|
}
|
||||||
|
$set["media.damagedParts"] = Object.fromEntries(m.entries());
|
||||||
} else {
|
} else {
|
||||||
for (const key of Object.keys(damagedPartsMedia)) {
|
const o = {
|
||||||
if (!selectedSet.has(key)) delete damagedPartsMedia[key];
|
...(damagedPartsMedia as Record<string, unknown>),
|
||||||
|
};
|
||||||
|
for (const key of Object.keys(o)) {
|
||||||
|
if (!selectedSet.has(key)) delete o[key];
|
||||||
}
|
}
|
||||||
|
$set["media.damagedParts"] = o;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray((claim as any).history)) (claim as any).history = [];
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
(claim as any).history.push({
|
$set,
|
||||||
event: "EXPERT_DAMAGED_PARTS_UPDATED",
|
$push: {
|
||||||
performedBy: actor.sub,
|
history: {
|
||||||
performedByName: actor.fullName,
|
type: "EXPERT_DAMAGED_PARTS_UPDATED",
|
||||||
performedAt: new Date(),
|
actor: {
|
||||||
note: "Damage expert edited selected damaged parts",
|
actorId: new Types.ObjectId(actor.sub),
|
||||||
|
actorName: actor.fullName,
|
||||||
|
actorType: "damage_expert",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
metadata: {
|
metadata: {
|
||||||
previousSelectedParts: previous,
|
previousSelectedParts: previous,
|
||||||
selectedParts,
|
selectedParts,
|
||||||
@@ -3437,10 +3535,10 @@ export class ExpertClaimService {
|
|||||||
expertProfileSnapshot: damagedPartsEditSnapshot,
|
expertProfileSnapshot: damagedPartsEditSnapshot,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await (claim as any).save();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claimRequestId: claim._id.toString(),
|
claimRequestId: claim._id.toString(),
|
||||||
selectedParts,
|
selectedParts,
|
||||||
|
|||||||
Reference in New Issue
Block a user