1
0
forked from Yara724/api

Fix v3 mirror flow

This commit is contained in:
SepehrYahyaee
2026-06-22 17:30:58 +03:30
parent 83e82ea68e
commit db569db9d1
15 changed files with 1004 additions and 248 deletions

View File

@@ -309,6 +309,44 @@ export class ClaimRequestManagementService {
return true;
}
/** V3 initial document step — excludes capture-phase vehicle evidence. */
private v3PreCaptureDocumentKeys(isCarBody: boolean): string[] {
return this.requiredDocumentKeysV2(isCarBody).filter(
(k) => !isCapturePhaseDamagedPartyDocKey(k),
);
}
private allV3PreCaptureDocumentsComplete(
claimCase: any,
isCarBody: boolean,
assumeUploadedKey?: string,
): boolean {
for (const k of this.v3PreCaptureDocumentKeys(isCarBody)) {
const ok =
k === assumeUploadedKey
? true
: this.isRequiredDocumentUploadedOnClaim(claimCase, k);
if (!ok) return false;
}
return true;
}
private countRemainingV3PreCaptureDocuments(
claimCase: any,
isCarBody: boolean,
assumeUploadedKey?: string,
): number {
let n = 0;
for (const k of this.v3PreCaptureDocumentKeys(isCarBody)) {
const ok =
k === assumeUploadedKey
? true
: this.isRequiredDocumentUploadedOnClaim(claimCase, k);
if (!ok) n++;
}
return n;
}
private countRemainingV2OwnerDocuments(
claimCase: any,
isCarBody: boolean,
@@ -5985,6 +6023,7 @@ export class ClaimRequestManagementService {
file: Express.Multer.File,
currentUserId: string,
actor?: { sub: string; role?: string },
options?: { v3InPersonFlow?: boolean },
): Promise<UploadRequiredDocumentV2ResponseDto> {
try {
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
@@ -6042,6 +6081,21 @@ export class ClaimRequestManagementService {
`Invalid workflow step. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS}, ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES} (capture-phase documents only), or ${ClaimWorkflowStep.USER_EXPERT_RESEND}, but current step is ${claimCase.workflow?.currentStep}`,
);
}
if (options?.v3InPersonFlow) {
if (body.documentKey === ClaimRequiredDocumentType.CAR_GREEN_CARD) {
throw new BadRequestException(
"Car green card must be uploaded during other-parts selection, not in the documents step.",
);
}
if (
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS &&
isCapturePhaseDamagedPartyDocKey(body.documentKey)
) {
throw new BadRequestException(
`Chassis, engine, and metal plate photos must be uploaded during capture (after parts and angles), not in the initial documents step (${CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.join(", ")}).`,
);
}
}
}
if (isResendUpload) {
@@ -6160,8 +6214,9 @@ export class ClaimRequestManagementService {
$set["status"] = ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS;
$set["workflow.currentStep"] =
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
$set["workflow.nextStep"] =
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
$set["workflow.nextStep"] = options?.v3InPersonFlow
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
completedStepsEntries.push(
ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
@@ -6183,18 +6238,61 @@ export class ClaimRequestManagementService {
}
}
} else {
allDocumentsUploaded = this.allV2OwnerDocumentsComplete(
const v3InitialDocsComplete =
!!options?.v3InPersonFlow &&
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS &&
this.allV3PreCaptureDocumentsComplete(
claimCase,
isCarBodyUpload,
body.documentKey,
);
remaining = this.countRemainingV2OwnerDocuments(
allDocumentsUploaded = options?.v3InPersonFlow
? v3InitialDocsComplete
: this.allV2OwnerDocumentsComplete(
claimCase,
isCarBodyUpload,
body.documentKey,
);
remaining = options?.v3InPersonFlow
? this.countRemainingV3PreCaptureDocuments(
claimCase,
isCarBodyUpload,
body.documentKey,
)
: this.countRemainingV2OwnerDocuments(
claimCase,
isCarBodyUpload,
body.documentKey,
);
if (allDocumentsUploaded) {
if (options?.v3InPersonFlow) {
$set["status"] = ClaimCaseStatus.SELECTING_OUTER_PARTS;
$set["workflow.currentStep"] =
ClaimWorkflowStep.SELECT_OUTER_PARTS;
$set["workflow.nextStep"] =
ClaimWorkflowStep.SELECT_OTHER_PARTS;
completedStepsEntries.push(
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
);
historyEntries.push({
type: "STEP_COMPLETED",
actor: {
actorId: new Types.ObjectId(currentUserId),
actorName: claimCase.owner?.fullName || "User",
actorType: "user",
},
timestamp: new Date(),
metadata: {
stepKey: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
description:
"Initial required documents uploaded. Proceed to outer part selection.",
v3InPersonFlow: true,
},
});
} else {
$set["status"] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT;
$set["claimStatus"] = ClaimStatus.PENDING;
$set["workflow.currentStep"] =
@@ -6237,6 +6335,7 @@ export class ClaimRequestManagementService {
}
}
}
}
// Build the final MongoDB update — all operators explicit and clean
const updatePayload: Record<string, unknown> = { $set };
@@ -6295,7 +6394,9 @@ export class ClaimRequestManagementService {
}).sequencePhase,
)
: allDocumentsUploaded
? "All documents uploaded successfully. Your claim is now ready for damage expert review."
? options?.v3InPersonFlow
? "Initial required documents uploaded. Proceed to outer part selection."
: "All documents uploaded successfully. Your claim is now ready for damage expert review."
: `Document uploaded successfully. ${remaining} documents remaining.`;
return {
@@ -6308,7 +6409,9 @@ export class ClaimRequestManagementService {
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES
: allDocumentsUploaded
? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
? options?.v3InPersonFlow
? ClaimWorkflowStep.SELECT_OUTER_PARTS
: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
message,
};
@@ -7852,6 +7955,11 @@ export class ClaimRequestManagementService {
}
}
private v3PartsFlowNotStarted(claimCase: any): boolean {
const parts = claimCase.damage?.selectedParts;
return !Array.isArray(parts) || parts.length === 0;
}
private async advanceV3ClaimToOuterPartsIfReady(
claimCase: any,
blame: any,
@@ -7859,9 +7967,18 @@ export class ClaimRequestManagementService {
if (claimCase.workflow?.currentStep === ClaimWorkflowStep.SELECT_OUTER_PARTS) {
return;
}
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS) {
const step = claimCase.workflow?.currentStep;
const partsNotStarted = this.v3PartsFlowNotStarted(claimCase);
const prematurelyCompleted =
step === ClaimWorkflowStep.USER_SUBMISSION_COMPLETE && partsNotStarted;
const canAdvance =
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS ||
prematurelyCompleted;
if (!canAdvance) {
throw new BadRequestException(
`Complete required documents before selecting outer parts. Current step: ${claimCase.workflow?.currentStep}`,
`Complete required documents before selecting outer parts. Current step: ${step}`,
);
}
if (!(blame.expert?.decision as any)?.fields?.accidentWay) {
@@ -7869,9 +7986,11 @@ export class ClaimRequestManagementService {
"Submit accident fields before selecting outer parts.",
);
}
await this.claimCaseDbService.findByIdAndUpdate(String(claimCase._id), {
$set: {
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
claimStatus: ClaimStatus.PENDING,
"workflow.currentStep": ClaimWorkflowStep.SELECT_OUTER_PARTS,
"workflow.nextStep": ClaimWorkflowStep.SELECT_OTHER_PARTS,
},
@@ -7906,22 +8025,13 @@ export class ClaimRequestManagementService {
const blame = await this.assertV3InPersonClaim(claimCase);
this.assertV3ClaimDocumentsPhase(blame);
const step = claimCase.workflow?.currentStep;
if (step !== ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS) {
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: {
status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
"workflow.currentStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
},
});
}
return this.uploadRequiredDocumentV2(
claimRequestId,
body,
file,
currentUserId,
actor,
{ v3InPersonFlow: true },
);
}

View File

@@ -9,6 +9,7 @@ import {
MaxLength,
Min,
} from "class-validator";
import { UNIFIED_FILE_STATUS_KEYS } from "src/helpers/unified-file-status";
/** Allowed sort keys for V2 list endpoints (claims / blame). */
export const LIST_SORT_BY_V2 = [
@@ -75,4 +76,13 @@ export class ListQueryV2Dto {
@IsString()
@MaxLength(64)
search?: string;
@ApiPropertyOptional({
enum: UNIFIED_FILE_STATUS_KEYS,
description:
"Filter by calculated unified file status (blame + claim combined).",
})
@IsOptional()
@IsIn([...UNIFIED_FILE_STATUS_KEYS])
unifiedStatus?: (typeof UNIFIED_FILE_STATUS_KEYS)[number];
}

View File

@@ -0,0 +1,36 @@
import { ApiProperty } from "@nestjs/swagger";
import {
UNIFIED_FILE_STATUS_KEYS,
UnifiedFileStatusDefinition,
UnifiedFileStatusKey,
} from "src/helpers/unified-file-status";
export class UnifiedFileStatusDefinitionDto implements UnifiedFileStatusDefinition {
@ApiProperty({ enum: UNIFIED_FILE_STATUS_KEYS })
key: UnifiedFileStatusKey;
@ApiProperty()
labelEn: string;
@ApiProperty()
labelFa: string;
@ApiProperty()
description: string;
}
export class UnifiedFileStatusReportDto {
@ApiProperty({ type: [UnifiedFileStatusDefinitionDto] })
statuses: UnifiedFileStatusDefinitionDto[];
@ApiProperty({
description: "Counts per unified status key plus `all`",
example: {
all: 42,
IN_PROGRESS: 10,
WAITING_FOR_DAMAGE_EXPERT: 5,
COMPLETED: 12,
},
})
counts: Record<string, number>;
}

View File

@@ -117,6 +117,8 @@ export class AllRequestDtoV2 {
lockTime: string | null;
type: string;
blameStatus: string;
/** Calculated blame + linked claim lifecycle status */
unifiedFileStatus?: string;
partiesInitialForms: { firstParty: string; secondParty: string };
partiesVehicles: { firstPartyVehicle: string; secondPartyVehicle: string };
}

View File

@@ -7,6 +7,7 @@ import { ExpertBlameV2Controller } from "./expert-blame.v2.controller";
import { ExpertBlameService } from "./expert-blame.service";
import { RequestManagementModule } from "src/request-management/request-management.module";
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
import { ClaimRequestManagementModule } from "src/claim-request-management/claim-request-management.module";
@Module({
imports: [
@@ -15,6 +16,7 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
RequestManagementModule,
PlatesModule,
SmsOrchestrationModule,
ClaimRequestManagementModule,
],
controllers: [ExpertBlameController, ExpertBlameV2Controller],
providers: [ExpertBlameService],

View File

@@ -23,7 +23,13 @@ import {
expertPortfolioFileIdsFromActivityEvents,
objectIdsFromStringSet,
} from "src/helpers/expert-portfolio";
import {
countUnifiedFileStatuses,
getUnifiedFileStatusCatalog,
resolveUnifiedFileStatus,
} from "src/helpers/unified-file-status";
import { applyListQueryV2 } from "src/helpers/list-query-v2";
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
ExpertFileAssignResultDto,
@@ -115,8 +121,109 @@ export class ExpertBlameService {
private readonly requestManagementService: RequestManagementService,
private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
private readonly claimCaseDbService: ClaimCaseDbService,
) {}
private async loadClaimsByPublicIds(
publicIds: string[],
): Promise<Map<string, { status?: string }>> {
const unique = [...new Set(publicIds.filter(Boolean))];
if (unique.length === 0) return new Map();
const claims = (await this.claimCaseDbService.find(
{ publicId: { $in: unique } },
{ lean: true, select: "publicId status" },
)) as Array<{ publicId?: string; status?: string }>;
return new Map(
claims.map((c) => [String(c.publicId), { status: c.status }]),
);
}
private filterBlameDocsByUnifiedStatus(
docs: Record<string, unknown>[],
claimByPublicId: Map<string, { status?: string }>,
unifiedStatus?: string,
): Record<string, unknown>[] {
if (!unifiedStatus) return docs;
return docs.filter((doc) => {
const publicId = String((doc as { publicId?: string }).publicId ?? "");
const claim = claimByPublicId.get(publicId);
return (
resolveUnifiedFileStatus({
blameStatus: String((doc as { status?: string }).status ?? ""),
claimStatus: claim?.status,
}) === unifiedStatus
);
});
}
async getUnifiedFileStatusReportV2(actor: any) {
const rows = await this.collectBlamePortfolioDocs(actor);
const claimByPublicId = await this.loadClaimsByPublicIds(
rows.map((d) => String((d as { publicId?: string }).publicId ?? "")),
);
return {
statuses: getUnifiedFileStatusCatalog(),
counts: countUnifiedFileStatuses(
rows.map((doc) => {
const publicId = String((doc as { publicId?: string }).publicId ?? "");
const claim = claimByPublicId.get(publicId);
return {
blameStatus: String((doc as { status?: string }).status ?? ""),
claimStatus: claim?.status,
};
}),
),
};
}
private async collectBlamePortfolioDocs(
actor: any,
): Promise<Record<string, unknown>[]> {
if (actor.role === RoleEnum.FIELD_EXPERT) {
return (await this.blameRequestDbService.find(
{
expertInitiated: true,
initiatedByFieldExpertId: new Types.ObjectId(actor.sub),
},
{ lean: true },
)) as Record<string, unknown>[];
}
requireActorClientKey(actor);
const expertId = String(actor.sub);
const expertOid = new Types.ObjectId(expertId);
const activityEvents = await this.expertFileActivityDbService.findByExpert(
expertId,
ExpertFileKind.BLAME,
);
const activityFileIds =
expertPortfolioFileIdsFromActivityEvents(activityEvents);
const orClauses: Record<string, unknown>[] = [
{ initiatedByFieldExpertId: expertOid },
{ "expert.decision.decidedByExpertId": expertOid },
{ "expert.resend.requestedByExpertId": expertOid },
{ "workflow.lockedBy.actorId": expertOid },
];
const activityOids = objectIdsFromStringSet(activityFileIds);
if (activityOids.length > 0) {
orClauses.push({ _id: { $in: activityOids } });
}
const rows = (await this.blameRequestDbService.find(
{ blameStatus: BlameStatus.DISAGREEMENT, $or: orClauses },
{ lean: true },
)) as Record<string, unknown>[];
const seen = new Set<string>();
const out: Record<string, unknown>[] = [];
for (const doc of rows) {
const id = String(doc._id ?? "");
if (seen.has(id)) continue;
if (!blameDocInExpertPortfolio(doc, actor, activityFileIds)) continue;
seen.add(id);
out.push(doc);
}
return out;
}
/** Load immutable profile fields from `expert` for the acting field expert. */
private async snapshotFieldExpert(actorId: string) {
const expertDoc = await this.expertDbService.findOne({
@@ -386,44 +493,8 @@ export class ExpertBlameService {
}
}
const paged = applyListQueryV2(
visibleCases,
{
publicId: (doc) =>
String((doc as { publicId?: string }).publicId ?? ""),
createdAt: (doc) => (doc as { createdAt?: Date }).createdAt,
requestNo: (doc) =>
String(
(doc as { requestNo?: string }).requestNo ??
(doc as { publicId?: string }).publicId ??
"",
),
status: (doc) => String((doc as { status?: string }).status ?? ""),
searchExtras: (doc) => {
const d = doc as {
_id?: unknown;
blameStatus?: string;
type?: string;
};
return [String(d._id ?? ""), d.blameStatus, d.type].filter(
Boolean,
) as string[];
},
},
query,
);
const items: AllRequestDtoV2[] = paged.list.map((doc) =>
this.mapBlameRequestToListItemV2(doc),
);
return new AllRequestDtoRsV2({
data: items,
total: paged.total,
page: paged.page,
limit: paged.limit,
totalPages: paged.totalPages,
});
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
return pagedResult;
} catch (error) {
if (error instanceof HttpException) throw error;
this.logger.error(
@@ -480,8 +551,27 @@ export class ExpertBlameService {
}
}
const paged = applyListQueryV2(
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
return pagedResult;
}
private async paginateBlameListV2(
visibleCases: Record<string, unknown>[],
query: ListQueryV2Dto,
): Promise<AllRequestDtoRsV2> {
const claimByPublicId = await this.loadClaimsByPublicIds(
visibleCases.map((d) =>
String((d as { publicId?: string }).publicId ?? ""),
),
);
const filtered = this.filterBlameDocsByUnifiedStatus(
visibleCases,
claimByPublicId,
query.unifiedStatus,
);
const paged = applyListQueryV2(
filtered,
{
publicId: (doc) =>
String((doc as { publicId?: string }).publicId ?? ""),
@@ -492,23 +582,42 @@ export class ExpertBlameService {
(doc as { publicId?: string }).publicId ??
"",
),
status: (doc) => String((doc as { status?: string }).status ?? ""),
status: (doc) => {
const publicId = String((doc as { publicId?: string }).publicId ?? "");
const claim = claimByPublicId.get(publicId);
return resolveUnifiedFileStatus({
blameStatus: String((doc as { status?: string }).status ?? ""),
claimStatus: claim?.status,
});
},
searchExtras: (doc) => {
const d = doc as {
_id?: unknown;
blameStatus?: string;
type?: string;
publicId?: string;
status?: string;
};
return [String(d._id ?? ""), d.blameStatus, d.type].filter(
Boolean,
) as string[];
const publicId = String(d.publicId ?? "");
const claim = claimByPublicId.get(publicId);
const unified = resolveUnifiedFileStatus({
blameStatus: String(d.status ?? ""),
claimStatus: claim?.status,
});
return [
String(d._id ?? ""),
d.blameStatus,
d.type,
unified,
claim?.status,
].filter(Boolean) as string[];
},
},
query,
);
const items: AllRequestDtoV2[] = paged.list.map((doc) =>
this.mapBlameRequestToListItemV2(doc),
this.mapBlameRequestToListItemV2(doc, claimByPublicId),
);
return new AllRequestDtoRsV2({
@@ -522,6 +631,7 @@ export class ExpertBlameService {
private mapBlameRequestToListItemV2(
doc: Record<string, unknown>,
claimByPublicId?: Map<string, { status?: string }>,
): AllRequestDtoV2 {
const createdAt = doc.createdAt
? new Date(doc.createdAt as string)
@@ -552,10 +662,17 @@ export class ExpertBlameService {
const firstParty = parties.find((p) => p.role === "FIRST");
const secondParty = parties.find((p) => p.role === "SECOND");
const publicId = String(doc.publicId ?? "");
const linkedClaim = claimByPublicId?.get(publicId);
const unifiedFileStatus = resolveUnifiedFileStatus({
blameStatus: String(doc.status ?? ""),
claimStatus: linkedClaim?.status,
});
return {
requestId: String(doc._id),
status: String(doc.status ?? ""),
unifiedFileStatus,
userComment: null,
requestCode: String(doc.publicId ?? ""),
date,

View File

@@ -44,7 +44,7 @@ export class ExpertBlameV2Controller {
description:
"Damage experts (`expert`): tenant-scoped **DISAGREEMENT** queue (available, locked, or decided by you). " +
"Field experts (`field_expert`): all expert-initiated blame files they created (LINK + IN_PERSON). Use expert-claim for claims after blame completion. " +
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`.",
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus` (calculated blame+claim status).",
})
async findAll(
@CurrentUser() actor: any,
@@ -60,11 +60,31 @@ export class ExpertBlameV2Controller {
}
}
@Get("report/unified-file-statuses")
@ApiOperation({
summary: "Unified file status catalog + counts (blame + linked claim)",
description:
"Calculatable statuses for this expert's blame portfolio. Filter lists with `unifiedStatus` query param.",
})
async getUnifiedFileStatusReportV2(@CurrentUser() actor: any) {
try {
return await this.expertBlameService.getUnifiedFileStatusReportV2(actor);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(
error instanceof Error
? error.message
: "Failed to load unified file status report",
);
}
}
@Get("report/status-counts")
@ApiOperation({
summary: "Count blame cases by grouped status bucket (this field expert)",
deprecated: true,
description:
"Counts only files in the acting experts portfolio: expertFileActivities (checked or handled), workflow lock, expert decision, or expert-initiated blame. IN_PROGRESS groups OPEN and WAITING_FOR_SECOND_PARTY. Other keys match CaseStatus. Does not include the open WAITING_FOR_EXPERT queue unless this expert has touched the file.",
"Legacy blame-only buckets. Prefer GET report/unified-file-statuses for blame+claim calculatable statuses.",
})
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
try {

View File

@@ -15,6 +15,12 @@ export class ClaimListItemV2Dto {
})
status: string;
@ApiPropertyOptional({
description: "Calculated unified blame+claim lifecycle status",
example: "WAITING_FOR_DAMAGE_EXPERT",
})
unifiedFileStatus?: string;
@ApiProperty({ description: 'Workflow step', example: 'USER_SUBMISSION_COMPLETE' })
currentStep: string;

View File

@@ -142,6 +142,11 @@ import {
import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { applyListQueryV2 } from "src/helpers/list-query-v2";
import {
countUnifiedFileStatuses,
getUnifiedFileStatusCatalog,
resolveUnifiedFileStatus,
} from "src/helpers/unified-file-status";
import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher";
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
@@ -3411,6 +3416,144 @@ export class ExpertClaimService {
return buckets;
}
async getUnifiedFileStatusReportV2(actor: any) {
const { claims, blameById } =
await this.collectClaimPortfolioWithBlame(actor);
return {
statuses: getUnifiedFileStatusCatalog(),
counts: countUnifiedFileStatuses(
claims.map((c) => {
const blame = c.blameRequestId
? blameById.get(String(c.blameRequestId))
: undefined;
return {
blameStatus: blame?.status,
claimStatus: c.status,
};
}),
),
};
}
private async collectClaimPortfolioWithBlame(actor: any): Promise<{
claims: any[];
blameById: Map<string, any>;
}> {
let claims: any[] = [];
if (actor.role === RoleEnum.FIELD_EXPERT) {
const expertOid = new Types.ObjectId(actor.sub);
const expertBlameIds = await this.blameRequestDbService
.find(
{ expertInitiated: true, initiatedByFieldExpertId: expertOid },
{ select: "_id", lean: true },
)
.then((docs) => docs.map((d) => (d as { _id: unknown })._id));
const claimOr: Record<string, unknown>[] = [
{ initiatedByFieldExpertId: expertOid },
];
if (expertBlameIds.length > 0) {
claimOr.push({ blameRequestId: { $in: expertBlameIds } });
}
claims = (await this.claimCaseDbService.find({ $or: claimOr })) as any[];
} else {
const clientKey = requireActorClientKey(actor);
const expertId = String(actor.sub);
const expertOid = new Types.ObjectId(expertId);
const activityEvents = await this.expertFileActivityDbService.findByExpert(
expertId,
ExpertFileKind.CLAIM,
);
const activityFileIds =
expertPortfolioFileIdsFromActivityEvents(activityEvents);
const orClauses: Record<string, unknown>[] = [
{ "workflow.lockedBy.actorId": expertOid },
{ "evaluation.damageExpertReply.actorDetail.actorId": expertOid },
{ "evaluation.damageExpertReplyFinal.actorDetail.actorId": expertOid },
{ "evaluation.damageExpertResend.requestedByExpertId": expertOid },
];
const activityOids = objectIdsFromStringSet(activityFileIds);
if (activityOids.length > 0) {
orClauses.push({ _id: { $in: activityOids } });
}
const rows =
orClauses.length === 0
? []
: await this.claimCaseDbService.find({ $or: orClauses }, { lean: true });
const seen = new Set<string>();
for (const doc of rows as Record<string, unknown>[]) {
const id = String(doc._id ?? "");
if (seen.has(id)) continue;
if (
!claimDocInExpertPortfolio(doc, expertId, activityFileIds, clientKey)
) {
continue;
}
seen.add(id);
claims.push(doc);
}
}
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: "status publicId" },
)) as any[])
: [];
const blameById = new Map<string, any>(
blames.map((b) => [String(b._id), b]),
);
return { claims, blameById };
}
private paginateClaimListV2(
list: ClaimListItemV2Dto[],
query: ListQueryV2Dto,
): GetClaimListV2ResponseDto {
let filtered = list;
if (query.unifiedStatus) {
filtered = list.filter(
(item) => item.unifiedFileStatus === query.unifiedStatus,
);
}
const paged = applyListQueryV2(
filtered,
{
publicId: (r) => r.publicId,
createdAt: (r) => r.createdAt,
requestNo: (r) => r.publicId,
status: (r) => r.unifiedFileStatus ?? r.status,
searchExtras: (r) =>
[
r.claimRequestId,
r.currentStep,
r.vehicle?.carName,
r.vehicle?.carModel,
r.blameRequestType,
r.unifiedFileStatus,
r.status,
].filter(Boolean) as string[],
},
query,
);
return {
list: paged.list,
total: paged.total,
page: paged.page,
limit: paged.limit,
totalPages: paged.totalPages,
};
}
/**
* V2: Get claim list for damage expert
*
@@ -3581,6 +3724,10 @@ export class ExpertClaimService {
claimRequestId: c._id.toString(),
publicId: c.publicId,
status: statusForList,
unifiedFileStatus: resolveUnifiedFileStatus({
blameStatus: blame?.status,
claimStatus: c.status,
}),
currentStep: c.workflow?.currentStep || "",
locked: lockActive,
lockedBy:
@@ -3601,32 +3748,7 @@ 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,
);
return {
list: paged.list,
total: paged.total,
page: paged.page,
limit: paged.limit,
totalPages: paged.totalPages,
};
return this.paginateClaimListV2(list, query);
}
/**
@@ -3687,6 +3809,10 @@ export class ExpertClaimService {
claimRequestId: c._id.toString(),
publicId: c.publicId,
status: c.status,
unifiedFileStatus: resolveUnifiedFileStatus({
blameStatus: blame?.status,
claimStatus: c.status,
}),
currentStep: c.workflow?.currentStep || "",
locked: lockActive,
lockedBy:
@@ -3707,31 +3833,7 @@ 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,
].filter(Boolean) as string[],
},
query,
);
return {
list: paged.list,
total: paged.total,
page: paged.page,
limit: paged.limit,
totalPages: paged.totalPages,
};
return this.paginateClaimListV2(list, query);
}
/**

View File

@@ -65,11 +65,22 @@ export class ExpertClaimV2Controller {
private readonly claimRequestManagementService: ClaimRequestManagementService,
) {}
@Get("report/unified-file-statuses")
@ApiOperation({
summary: "Unified file status catalog + counts (claim + linked blame)",
description:
"Calculatable statuses for this expert's claim portfolio. Filter lists with `unifiedStatus` query param.",
})
async getUnifiedFileStatusReportV2(@CurrentUser() actor: any) {
return await this.expertClaimService.getUnifiedFileStatusReportV2(actor);
}
@Get("report/status-counts")
@ApiOperation({
summary: "Count claim cases by grouped status bucket (this damage expert)",
deprecated: true,
description:
"Counts only files in the acting experts portfolio: expertFileActivities (checked or handled), current workflow lock, or a prior damageExpertReply / damageExpertReplyFinal by this expert. IN_PROGRESS groups user-phase statuses before submission is complete (CREATED through CAPTURING_PART_DAMAGES). Other keys match ClaimCaseStatus. Does not include the open tenant queue unless this expert has touched the file.",
"Legacy claim-only buckets. Prefer GET report/unified-file-statuses for blame+claim calculatable statuses.",
})
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
return await this.expertClaimService.getStatusReportBucketsV2(actor);
@@ -116,7 +127,7 @@ export class ExpertClaimV2Controller {
summary: "List available claim requests for damage expert",
description:
"Damage experts: tenant queue (`WAITING_FOR_DAMAGE_EXPERT`), locked/in-progress, factor validation. " +
"Field experts: all claims linked to blame files they initiated (`initiatedByFieldExpertId` or matching `blameRequestId`). Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`.",
"Field experts: all claims linked to blame files they initiated (`initiatedByFieldExpertId` or matching `blameRequestId`). Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`, `unifiedStatus`.",
})
@ApiResponse({ status: 200, type: GetClaimListV2ResponseDto })
async getClaimListV2(

View File

@@ -26,6 +26,7 @@ import { CurrentUser } from "src/decorators/user.decorator";
import { FileRating } from "src/request-management/entities/schema/request-management.schema";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { ExpertInsurerService } from "./expert-insurer.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { CreateBranchDto } from "src/client/dto/create-branch.dto";
import {
CreateBlameExpertByInsurerDto,
@@ -162,9 +163,46 @@ export class ExpertInsurerController {
}
@Get("files")
async getAllFiles(@CurrentUser() insurer) {
@ApiOperation({
summary: "List insurer files (blame + claim merged by publicId)",
description:
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`.",
})
async getAllFiles(
@CurrentUser() insurer,
@Query() query: ListQueryV2Dto,
) {
return await this.expertInsurerService.retrieveAllFilesOfClient(
insurer.clientKey,
query,
);
}
@Get("report/unified-file-statuses")
@ApiOperation({
summary: "Unified file status catalog + counts (blame + claim)",
description:
"Returns calculatable unified statuses and per-status counts for this insurer's files. Use `unifiedStatus` on GET files to filter.",
})
@ApiQuery({
name: "from",
required: false,
description: "Optional start datetime (ISO string)",
})
@ApiQuery({
name: "to",
required: false,
description: "Optional end datetime (ISO string)",
})
async getUnifiedFileStatusReport(
@CurrentUser() actor,
@Query("from") from?: string,
@Query("to") to?: string,
) {
return await this.expertInsurerService.getInsurerUnifiedFileStatusReport(
actor,
from,
to,
);
}
@@ -265,6 +303,11 @@ export class ExpertInsurerController {
}
@Get("report/status-counts")
@ApiOperation({
summary: "Legacy status counts (mapped from unified statuses)",
deprecated: true,
description: "Prefer GET report/unified-file-statuses for full calculatable blame+claim statuses.",
})
@ApiQuery({
name: "from",
required: false,

View File

@@ -39,6 +39,13 @@ import { toJalaliDateAndTime } from "src/helpers/date-jalali";
import { enrichBlamePartiesForAgreementView } from "src/helpers/blame-party-agreement-decision";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { applyListQueryV2 } from "src/helpers/list-query-v2";
import {
countUnifiedFileStatuses,
getUnifiedFileStatusCatalog,
resolveUnifiedFileStatus,
} from "src/helpers/unified-file-status";
import {
getClaimCarAngleCaptureBlob,
getDamagedPartCaptureBlob,
@@ -1145,13 +1152,10 @@ export class ExpertInsurerService {
};
}
async retrieveAllFilesOfClient(insurerId: string) {
const id = this.getClientId(insurerId);
const idStr = String(id);
private async buildMergedInsurerFiles(clientObjectId: Types.ObjectId) {
const [blameFiles, claimFiles] = await Promise.all([
this.getClientBlameFiles(id),
this.getClientClaimFiles(id),
this.getClientBlameFiles(clientObjectId),
this.getClientClaimFiles(clientObjectId),
]);
const byPublicId = new Map<
@@ -1192,7 +1196,6 @@ export class ExpertInsurerService {
}
>();
// Index blame files first
for (const b of blameFiles) {
const key = String((b as any).publicId || (b as any)._id || "");
if (!key) continue;
@@ -1216,8 +1219,6 @@ export class ExpertInsurerService {
byPublicId.set(key, prev);
}
// For claim files whose blame wasn't in blameFiles (different insurer's blame),
// fetch the linked blame directly so we can get vehicle + parties data
const claimOnlyBlameIds = claimFiles
.filter((c) => {
const key = String((c as any).publicId || (c as any)._id || "");
@@ -1243,13 +1244,11 @@ export class ExpertInsurerService {
linkedBlames.map((b) => [String(b.publicId), b]),
);
// Index claim files
for (const c of claimFiles) {
const key = String((c as any).publicId || (c as any)._id || "");
if (!key) continue;
const prev = byPublicId.get(key) ?? { publicId: key };
// If no blame entry yet, use the linked blame for parties/vehicle/fileType
if (!prev.blame) {
const linkedBlame = linkedBlameByPublicId.get(key);
if (linkedBlame) {
@@ -1293,26 +1292,85 @@ export class ExpertInsurerService {
byPublicId.set(key, prev);
}
const files = Array.from(byPublicId.values())
.map((f) => ({
return Array.from(byPublicId.values()).map((f) => ({
publicId: f.publicId,
fileType: f.fileType,
hasClaim: !!f.claim,
unifiedFileStatus: resolveUnifiedFileStatus({
blameStatus: f.blame?.status,
claimStatus: f.claim?.status,
}),
parties: f.parties,
blame: f.blame,
claim: f.claim,
createdAt: f.createdAt,
updatedAt: f.updatedAt,
}))
.sort((a, b) => {
const at = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
const bt = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
return bt - at;
});
}));
}
async retrieveAllFilesOfClient(
insurerId: string,
query: ListQueryV2Dto = {},
) {
const id = this.getClientId(insurerId);
let files = await this.buildMergedInsurerFiles(id);
if (query.unifiedStatus) {
files = files.filter(
(f) => f.unifiedFileStatus === query.unifiedStatus,
);
}
const paged = applyListQueryV2(
files,
{
publicId: (f) => f.publicId,
createdAt: (f) => f.updatedAt ?? f.createdAt,
requestNo: (f) => f.publicId,
status: (f) => f.unifiedFileStatus,
searchExtras: (f) =>
[
f.fileType,
f.blame?.requestNo,
f.claim?.requestNo,
f.blame?.status,
f.claim?.status,
f.unifiedFileStatus,
].filter(Boolean) as string[],
},
query,
);
return {
total: files.length,
files,
total: paged.total,
page: paged.page,
limit: paged.limit,
totalPages: paged.totalPages,
files: paged.list,
};
}
async getInsurerUnifiedFileStatusReport(
actor: any,
from?: string,
to?: string,
) {
const clientObjectId = this.getClientId(actor);
const { fromDate, toDate } = this.parseDateRange(from, to);
const files = await this.buildMergedInsurerFiles(clientObjectId);
const inRange = files.filter((f) =>
this.isInDateRange(f.createdAt, fromDate, toDate),
);
return {
statuses: getUnifiedFileStatusCatalog(),
counts: countUnifiedFileStatuses(
inRange.map((f) => ({
blameStatus: f.blame?.status,
claimStatus: f.claim?.status,
})),
),
};
}
@@ -1888,72 +1946,17 @@ export class ExpertInsurerService {
under_review: number;
waiting_for_documents_resend: number;
}> {
const clientObjectId = this.getClientId(actor);
const { fromDate, toDate } = this.parseDateRange(from, to);
const [blameFilesRaw, claimFilesRaw] = await Promise.all([
this.getClientBlameFiles(clientObjectId),
this.getClientClaimFiles(clientObjectId),
]);
const blameFiles = blameFilesRaw.filter((f) =>
this.isInDateRange((f as any)?.createdAt, fromDate, toDate),
);
const claimFiles = claimFilesRaw.filter((f) =>
this.isInDateRange((f as any)?.createdAt, fromDate, toDate),
);
const fileMap = new Map<string, { blame?: any; claim?: any }>();
for (const b of blameFiles) {
const key = String((b as any).publicId || (b as any)._id || "");
if (!key) continue;
const prev = fileMap.get(key) ?? {};
prev.blame = b;
fileMap.set(key, prev);
}
for (const c of claimFiles) {
const key = String((c as any).publicId || (c as any)._id || "");
if (!key) continue;
const prev = fileMap.get(key) ?? {};
prev.claim = c;
fileMap.set(key, prev);
}
let completed = 0;
let underReview = 0;
let waitingForDocumentsResend = 0;
for (const entry of fileMap.values()) {
const blameStatus = entry.blame?.status;
const claimStatus = entry.claim?.status;
if (claimStatus === ClaimCaseStatus.COMPLETED) {
completed += 1;
}
const blameUnderReview = blameStatus === CaseStatus.WAITING_FOR_EXPERT;
const claimUnderReview =
claimStatus === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT ||
claimStatus === ClaimCaseStatus.EXPERT_REVIEWING;
if (blameUnderReview || claimUnderReview) {
underReview += 1;
}
const blameResend =
blameStatus === CaseStatus.WAITING_FOR_DOCUMENT_RESEND;
const claimResend =
claimStatus === ClaimCaseStatus.WAITING_FOR_USER_RESEND;
if (blameResend || claimResend) {
waitingForDocumentsResend += 1;
}
}
const report = await this.getInsurerUnifiedFileStatusReport(actor, from, to);
const counts = report.counts;
return {
all: fileMap.size,
completed,
under_review: underReview,
waiting_for_documents_resend: waitingForDocumentsResend,
all: counts.all ?? 0,
completed: counts.COMPLETED ?? 0,
under_review:
(counts.WAITING_FOR_BLAME_EXPERT ?? 0) +
(counts.WAITING_FOR_DAMAGE_EXPERT ?? 0) +
(counts.EXPERT_REVIEWING ?? 0) +
(counts.EXPERT_VALIDATING_FACTORS ?? 0),
waiting_for_documents_resend: counts.WAITING_FOR_DOCUMENT_RESEND ?? 0,
};
}
}

View File

@@ -0,0 +1,46 @@
import {
countUnifiedFileStatuses,
resolveUnifiedFileStatus,
} from "./unified-file-status";
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
describe("resolveUnifiedFileStatus", () => {
it("prefers claim completed over blame in progress", () => {
expect(
resolveUnifiedFileStatus({
blameStatus: CaseStatus.WAITING_FOR_EXPERT,
claimStatus: ClaimCaseStatus.COMPLETED,
}),
).toBe("COMPLETED");
});
it("maps blame expert queue", () => {
expect(
resolveUnifiedFileStatus({
blameStatus: CaseStatus.WAITING_FOR_EXPERT,
}),
).toBe("WAITING_FOR_BLAME_EXPERT");
});
it("maps claim damage expert queue", () => {
expect(
resolveUnifiedFileStatus({
blameStatus: CaseStatus.COMPLETED,
claimStatus: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
}),
).toBe("WAITING_FOR_DAMAGE_EXPERT");
});
it("counts merged entries", () => {
const counts = countUnifiedFileStatuses([
{ blameStatus: CaseStatus.OPEN },
{
blameStatus: CaseStatus.COMPLETED,
claimStatus: ClaimCaseStatus.CAPTURING_PART_DAMAGES,
},
]);
expect(counts.all).toBe(2);
expect(counts.IN_PROGRESS).toBe(2);
});
});

View File

@@ -0,0 +1,221 @@
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
/** Calculated lifecycle status for a publicId (blame + claim combined). */
export const UNIFIED_FILE_STATUS_KEYS = [
"IN_PROGRESS",
"WAITING_FOR_SECOND_PARTY",
"WAITING_FOR_BLAME_EXPERT",
"WAITING_FOR_SIGNATURES",
"WAITING_FOR_DOCUMENT_RESEND",
"WAITING_FOR_DAMAGE_EXPERT",
"EXPERT_REVIEWING",
"EXPERT_VALIDATING_FACTORS",
"INSURER_REVIEW",
"COMPLETED",
"CANCELLED",
"REJECTED",
"STOPPED",
] as const;
export type UnifiedFileStatusKey = (typeof UNIFIED_FILE_STATUS_KEYS)[number];
export type UnifiedFileStatusInput = {
blameStatus?: string | null;
claimStatus?: string | null;
};
const CLAIM_USER_PHASE = new Set<string>([
ClaimCaseStatus.CREATED,
ClaimCaseStatus.SELECTING_OUTER_PARTS,
ClaimCaseStatus.SELECTING_OTHER_PARTS,
ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
ClaimCaseStatus.CAPTURING_PART_DAMAGES,
]);
const INSURER_REVIEW_CLAIM_STATUSES = new Set<string>([
ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING,
ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING,
]);
export type UnifiedFileStatusDefinition = {
key: UnifiedFileStatusKey;
labelEn: string;
labelFa: string;
description: string;
};
export const UNIFIED_FILE_STATUS_CATALOG: UnifiedFileStatusDefinition[] = [
{
key: "IN_PROGRESS",
labelEn: "In progress (user)",
labelFa: "در حال تکمیل توسط کاربر",
description:
"Parties are still filling blame steps and/or the damaged party is completing the claim (parts, documents, capture).",
},
{
key: "WAITING_FOR_SECOND_PARTY",
labelEn: "Waiting for second party",
labelFa: "در انتظار طرف دوم",
description: "Blame file is waiting for the second party to join or complete steps.",
},
{
key: "WAITING_FOR_BLAME_EXPERT",
labelEn: "Waiting for blame expert",
labelFa: "در انتظار کارشناس مقصر",
description: "Blame case is in the expert disagreement / guilt-decision queue.",
},
{
key: "WAITING_FOR_SIGNATURES",
labelEn: "Waiting for signatures",
labelFa: "در انتظار امضا",
description: "Blame file is waiting for party signatures on the expert decision.",
},
{
key: "WAITING_FOR_DOCUMENT_RESEND",
labelEn: "Waiting for document resend",
labelFa: "در انتظار ارسال مجدد مدارک",
description:
"Blame or claim is waiting for the user to resubmit documents or photos requested by an expert.",
},
{
key: "WAITING_FOR_DAMAGE_EXPERT",
labelEn: "Waiting for damage expert",
labelFa: "در انتظار کارشناس خسارت",
description: "Claim is in the damage-expert review queue (not yet locked).",
},
{
key: "EXPERT_REVIEWING",
labelEn: "Damage expert reviewing",
labelFa: "در حال بررسی کارشناس خسارت",
description: "A damage expert has locked the claim and is assessing damage.",
},
{
key: "EXPERT_VALIDATING_FACTORS",
labelEn: "Validating repair factors",
labelFa: "اعتبارسنجی فاکتور تعمیر",
description: "Damage expert is validating uploaded repair-factor documents.",
},
{
key: "INSURER_REVIEW",
labelEn: "Insurer / owner review",
labelFa: "بررسی بیمه‌گر / مالک",
description:
"Expert pricing is done; owner signature, factor upload, or insurer approval is pending.",
},
{
key: "COMPLETED",
labelEn: "Completed",
labelFa: "تکمیل شده",
description: "Claim (and blame when present) is finished successfully.",
},
{
key: "CANCELLED",
labelEn: "Cancelled",
labelFa: "لغو شده",
description: "File was cancelled or auto-closed.",
},
{
key: "REJECTED",
labelEn: "Rejected",
labelFa: "رد شده",
description: "Claim was rejected.",
},
{
key: "STOPPED",
labelEn: "Stopped",
labelFa: "متوقف شده",
description: "Blame file was stopped.",
},
];
export function initialUnifiedFileStatusCounts(): Record<
UnifiedFileStatusKey | "all",
number
> {
const out: Record<string, number> = { all: 0 };
for (const key of UNIFIED_FILE_STATUS_KEYS) {
out[key] = 0;
}
return out as Record<UnifiedFileStatusKey | "all", number>;
}
/**
* Single calculatable status for a shared publicId from raw blame + claim statuses.
* Claim pipeline takes precedence once a claim exists and has left the user-only phase.
*/
export function resolveUnifiedFileStatus(
input: UnifiedFileStatusInput,
): UnifiedFileStatusKey {
const blame = input.blameStatus?.trim() || undefined;
const claim = input.claimStatus?.trim() || undefined;
if (claim === ClaimCaseStatus.COMPLETED) return "COMPLETED";
if (claim === ClaimCaseStatus.REJECTED) return "REJECTED";
if (
claim === ClaimCaseStatus.CANCELLED ||
blame === CaseStatus.CANCELLED ||
blame === CaseStatus.AUTO_CLOSED
) {
return "CANCELLED";
}
if (blame === CaseStatus.STOPPED) return "STOPPED";
if (
blame === CaseStatus.WAITING_FOR_DOCUMENT_RESEND ||
claim === ClaimCaseStatus.WAITING_FOR_USER_RESEND
) {
return "WAITING_FOR_DOCUMENT_RESEND";
}
if (claim === ClaimCaseStatus.EXPERT_REVIEWING) return "EXPERT_REVIEWING";
if (claim === ClaimCaseStatus.EXPERT_VALIDATING_REPAIR_FACTORS) {
return "EXPERT_VALIDATING_FACTORS";
}
if (claim && INSURER_REVIEW_CLAIM_STATUSES.has(claim)) {
return "INSURER_REVIEW";
}
if (claim === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT) {
return "WAITING_FOR_DAMAGE_EXPERT";
}
if (blame === CaseStatus.WAITING_FOR_EXPERT) return "WAITING_FOR_BLAME_EXPERT";
if (blame === CaseStatus.WAITING_FOR_SIGNATURES) {
return "WAITING_FOR_SIGNATURES";
}
if (blame === CaseStatus.WAITING_FOR_SECOND_PARTY) {
return "WAITING_FOR_SECOND_PARTY";
}
if (blame === CaseStatus.COMPLETED && !claim) return "COMPLETED";
if (
blame === CaseStatus.OPEN ||
(claim && CLAIM_USER_PHASE.has(claim)) ||
(blame === CaseStatus.COMPLETED && claim && CLAIM_USER_PHASE.has(claim))
) {
return "IN_PROGRESS";
}
if (claim) return "IN_PROGRESS";
if (blame) return "IN_PROGRESS";
return "IN_PROGRESS";
}
export function countUnifiedFileStatuses(
entries: UnifiedFileStatusInput[],
): Record<UnifiedFileStatusKey | "all", number> {
const buckets = initialUnifiedFileStatusCounts();
for (const entry of entries) {
const key = resolveUnifiedFileStatus(entry);
buckets.all++;
buckets[key]++;
}
return buckets;
}
export function getUnifiedFileStatusCatalog(): UnifiedFileStatusDefinition[] {
return UNIFIED_FILE_STATUS_CATALOG;
}

View File

@@ -8284,7 +8284,7 @@ export class RequestManagementService {
inquiries: (req as any).inquiries ?? {},
workflow: {
currentStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
nextStep: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
nextStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
locked: false,
},
@@ -8881,6 +8881,33 @@ export class RequestManagementService {
}
await (req as any).save();
await this.claimCaseDbService.findByIdAndUpdate(String(claim._id), {
$set: {
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
claimStatus: ClaimStatus.PENDING,
"workflow.currentStep": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
},
$push: {
"workflow.completedSteps": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
history: {
type: "STEP_COMPLETED",
actor: {
actorId: new Types.ObjectId(expert.sub),
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
actorType: "field_expert",
},
timestamp: new Date(),
metadata: {
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
description:
"V3 field workflow complete. Claim ready for damage expert review.",
v3InPersonFlow: true,
},
},
},
});
return {
requestId: String(req._id),
publicId: req.publicId,