forked from Yara724/api
Added expert field mirror flow
This commit is contained in:
@@ -46,8 +46,11 @@ import { ClaimListDtoRs } from "./dto/claim-list-rs.dto";
|
||||
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import {
|
||||
assertClaimCaseForTenant,
|
||||
assertClaimCaseForExpertActor,
|
||||
claimCaseInitiatedByFieldExpert,
|
||||
claimCaseTouchesClient,
|
||||
requireActorClientKey,
|
||||
} from "src/helpers/tenant-scope";
|
||||
@@ -2148,7 +2151,7 @@ export class ExpertClaimService {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
}
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
assertClaimCaseForExpertActor(claim, actor);
|
||||
|
||||
const factorValidationSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
@@ -2389,9 +2392,9 @@ export class ExpertClaimService {
|
||||
*/
|
||||
async assignClaimForReviewV2(
|
||||
claimRequestId: string,
|
||||
actor: { sub: string; fullName?: string; clientKey?: string },
|
||||
actor: { sub: string; fullName?: string; clientKey?: string; role?: string },
|
||||
): Promise<ExpertFileAssignResultDto> {
|
||||
requireActorClientKey(actor);
|
||||
if ((actor as any).role !== RoleEnum.FIELD_EXPERT) requireActorClientKey(actor);
|
||||
await this.expireClaimWorkflowLockV2IfStale(claimRequestId);
|
||||
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
@@ -2399,7 +2402,7 @@ export class ExpertClaimService {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
}
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
assertClaimCaseForExpertActor(claim, actor);
|
||||
|
||||
const isFactorValidationLock =
|
||||
claimIsAwaitingExpertFactorValidationV2(claim);
|
||||
@@ -2692,7 +2695,7 @@ export class ExpertClaimService {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
}
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
assertClaimCaseForExpertActor(claim, actor);
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||
throw new BadRequestException(
|
||||
@@ -2850,14 +2853,14 @@ export class ExpertClaimService {
|
||||
reply: import("./dto/expert-claim-v2.dto").SubmitExpertReplyV2Dto,
|
||||
actor: any,
|
||||
) {
|
||||
requireActorClientKey(actor);
|
||||
if (actor.role !== RoleEnum.FIELD_EXPERT) requireActorClientKey(actor);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claim) {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
}
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
assertClaimCaseForExpertActor(claim, actor);
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||
throw new BadRequestException(
|
||||
@@ -3218,6 +3221,28 @@ export class ExpertClaimService {
|
||||
* Does not include the open tenant queue (`WAITING_FOR_DAMAGE_EXPERT`) unless this expert touched the file.
|
||||
*/
|
||||
async getStatusReportBucketsV2(actor: any): Promise<Record<string, number>> {
|
||||
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
||||
const rows = (await this.claimCaseDbService.find(
|
||||
{ initiatedByFieldExpertId: new Types.ObjectId(actor.sub) },
|
||||
{ lean: true },
|
||||
)) as any[];
|
||||
const buckets = initialClaimExpertReportBuckets();
|
||||
for (const doc of rows) {
|
||||
const key = claimCaseStatusToReportBucket(String(doc.status ?? ""));
|
||||
buckets.all++;
|
||||
buckets[key] = (buckets[key] ?? 0) + 1;
|
||||
}
|
||||
buckets["PENDING_ON_USER"] =
|
||||
(buckets[ClaimCaseStatus.WAITING_FOR_USER_RESEND] ?? 0) +
|
||||
(buckets[ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL] ?? 0) +
|
||||
(buckets[ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN] ?? 0) +
|
||||
(buckets[ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING] ?? 0) +
|
||||
(buckets[ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING] ?? 0);
|
||||
buckets["CHECK_NEEDED"] =
|
||||
buckets["PENDING_ON_USER"] +
|
||||
(buckets[ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT] ?? 0);
|
||||
return buckets;
|
||||
}
|
||||
const clientKey = requireActorClientKey(actor);
|
||||
const expertId = String(actor.sub);
|
||||
const expertOid = new Types.ObjectId(expertId);
|
||||
@@ -3290,6 +3315,9 @@ export class ExpertClaimService {
|
||||
actor: any,
|
||||
query: ListQueryV2Dto = {},
|
||||
): Promise<GetClaimListV2ResponseDto> {
|
||||
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
||||
return this.getFieldExpertClaimListV2(actor, query);
|
||||
}
|
||||
requireActorClientKey(actor);
|
||||
const actorId = actor.sub;
|
||||
const clientKey = actor.clientKey as string;
|
||||
@@ -3493,6 +3521,96 @@ export class ExpertClaimService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim list for a FIELD_EXPERT — returns only the claims they personally
|
||||
* initiated (expert-initiated IN_PERSON blame → createClaimFromBlameForExpertV2).
|
||||
*/
|
||||
private async getFieldExpertClaimListV2(
|
||||
actor: any,
|
||||
query: ListQueryV2Dto = {},
|
||||
): Promise<GetClaimListV2ResponseDto> {
|
||||
const claims = (await this.claimCaseDbService.find({
|
||||
initiatedByFieldExpertId: new Types.ObjectId(actor.sub),
|
||||
})) as any[];
|
||||
|
||||
const blameIds = [
|
||||
...new Set(
|
||||
claims
|
||||
.map((c) => c.blameRequestId?.toString())
|
||||
.filter((id): id is string => !!id),
|
||||
),
|
||||
];
|
||||
const blames =
|
||||
blameIds.length > 0
|
||||
? ((await this.blameRequestDbService.find(
|
||||
{ _id: { $in: blameIds.map((id) => new Types.ObjectId(id)) } },
|
||||
{ lean: true, select: "type parties expert.decision blameStatus" },
|
||||
)) as any[])
|
||||
: [];
|
||||
const blameById = new Map<string, any>(
|
||||
blames.map((b) => [String(b._id), b]),
|
||||
);
|
||||
|
||||
const list = claims.map((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)
|
||||
);
|
||||
return {
|
||||
claimRequestId: c._id.toString(),
|
||||
publicId: c.publicId,
|
||||
status: c.status,
|
||||
currentStep: c.workflow?.currentStep || "",
|
||||
locked: lockActive,
|
||||
lockedBy:
|
||||
lockActive && c.workflow?.lockedBy
|
||||
? {
|
||||
actorId: c.workflow.lockedBy.actorId?.toString(),
|
||||
actorName: c.workflow.lockedBy.actorName,
|
||||
lockedAt: (c.workflow as any).lockedAt?.toISOString?.(),
|
||||
expiredAt: (c.workflow as any).expiredAt?.toISOString?.(),
|
||||
}
|
||||
: undefined,
|
||||
vehicle: v
|
||||
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
||||
: undefined,
|
||||
...fileCtx,
|
||||
createdAt: c.createdAt,
|
||||
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
||||
};
|
||||
}) 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,
|
||||
].filter(Boolean) as string[],
|
||||
},
|
||||
query,
|
||||
);
|
||||
|
||||
return {
|
||||
list: paged.list,
|
||||
total: paged.total,
|
||||
page: paged.page,
|
||||
limit: paged.limit,
|
||||
totalPages: paged.totalPages,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load linked blame case for damage-expert claim detail (same enrichment as expert-blame `findOneV2`:
|
||||
* party evidence `videoUrl` / `voiceUrls`, Jalali date strings).
|
||||
@@ -3668,7 +3786,9 @@ export class ExpertClaimService {
|
||||
private async reconcileStaleExpertReviewingClaimLocksForTenant(actor: {
|
||||
sub: string;
|
||||
clientKey?: string;
|
||||
role?: string;
|
||||
}): Promise<void> {
|
||||
if ((actor as any).role === RoleEnum.FIELD_EXPERT) return;
|
||||
const clientKey = requireActorClientKey(actor);
|
||||
const lockTtlMs = this.claimV2WorkflowLockTtlMs;
|
||||
const now = new Date();
|
||||
@@ -3828,7 +3948,9 @@ export class ExpertClaimService {
|
||||
actor: any,
|
||||
): Promise<ClaimDetailV2ResponseDto> {
|
||||
const actorId = actor.sub;
|
||||
await this.reconcileStaleExpertReviewingClaimLocksForTenant(actor);
|
||||
if (actor.role !== RoleEnum.FIELD_EXPERT) {
|
||||
await this.reconcileStaleExpertReviewingClaimLocksForTenant(actor);
|
||||
}
|
||||
await this.expireClaimWorkflowLockV2IfStale(claimRequestId);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
@@ -3836,13 +3958,22 @@ export class ExpertClaimService {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
}
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
assertClaimCaseForExpertActor(claim, actor);
|
||||
|
||||
// Variables used both in the gate block and in the detail-build section below
|
||||
const isDamageExpertPhase =
|
||||
claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT ||
|
||||
claim.status === ClaimCaseStatus.EXPERT_REVIEWING;
|
||||
const isFactorValidationPending =
|
||||
claimIsAwaitingExpertFactorValidationV2(claim);
|
||||
|
||||
// Field experts can always view their own initiated claims regardless of status
|
||||
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
||||
if (!claimCaseInitiatedByFieldExpert(claim, actor)) {
|
||||
throw new ForbiddenException("This claim was not initiated by you.");
|
||||
}
|
||||
// Fall through to the detail build below (skip the damage-expert gate)
|
||||
} else {
|
||||
const isResendPending =
|
||||
claim.status === ClaimCaseStatus.WAITING_FOR_USER_RESEND;
|
||||
|
||||
@@ -3891,6 +4022,7 @@ export class ExpertClaimService {
|
||||
"You do not have permission to view this claim.",
|
||||
);
|
||||
}
|
||||
} // end damage-expert gate (else block)
|
||||
|
||||
// Build requiredDocuments map
|
||||
const requiredDocs = claim.requiredDocuments as any;
|
||||
@@ -4134,8 +4266,8 @@ export class ExpertClaimService {
|
||||
|
||||
/** V2 price-drop: claim locked by this expert during damage assessment. */
|
||||
private assertExpertCanEditClaimDuringReviewV2(claim: any, actor: any): void {
|
||||
requireActorClientKey(actor);
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
if (actor.role !== RoleEnum.FIELD_EXPERT) requireActorClientKey(actor);
|
||||
assertClaimCaseForExpertActor(claim, actor);
|
||||
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||
throw new BadRequestException(
|
||||
`Claim must be EXPERT_REVIEWING to edit price drop. Current status: ${claim.status}`,
|
||||
@@ -4364,12 +4496,12 @@ export class ExpertClaimService {
|
||||
body: UpdateClaimDamagedPartsV2Dto,
|
||||
actor: any,
|
||||
) {
|
||||
requireActorClientKey(actor);
|
||||
if (actor.role !== RoleEnum.FIELD_EXPERT) requireActorClientKey(actor);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim) {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
}
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
assertClaimCaseForExpertActor(claim, actor);
|
||||
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||
throw new BadRequestException(
|
||||
`Claim is not in EXPERT_REVIEWING state. Current status: ${claim.status}`,
|
||||
|
||||
Reference in New Issue
Block a user