forked from Yara724/api
Merge pull request 'Fix v3 mirror flow' (#147) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#147
This commit is contained in:
@@ -309,6 +309,44 @@ export class ClaimRequestManagementService {
|
|||||||
return true;
|
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(
|
private countRemainingV2OwnerDocuments(
|
||||||
claimCase: any,
|
claimCase: any,
|
||||||
isCarBody: boolean,
|
isCarBody: boolean,
|
||||||
@@ -5985,6 +6023,7 @@ export class ClaimRequestManagementService {
|
|||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
actor?: { sub: string; role?: string },
|
actor?: { sub: string; role?: string },
|
||||||
|
options?: { v3InPersonFlow?: boolean },
|
||||||
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||||
try {
|
try {
|
||||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
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}`,
|
`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) {
|
if (isResendUpload) {
|
||||||
@@ -6160,8 +6214,9 @@ export class ClaimRequestManagementService {
|
|||||||
$set["status"] = ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS;
|
$set["status"] = ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS;
|
||||||
$set["workflow.currentStep"] =
|
$set["workflow.currentStep"] =
|
||||||
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
|
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
|
||||||
$set["workflow.nextStep"] =
|
$set["workflow.nextStep"] = options?.v3InPersonFlow
|
||||||
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
||||||
|
: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||||
|
|
||||||
completedStepsEntries.push(
|
completedStepsEntries.push(
|
||||||
ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
@@ -6183,30 +6238,46 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
allDocumentsUploaded = this.allV2OwnerDocumentsComplete(
|
const v3InitialDocsComplete =
|
||||||
claimCase,
|
!!options?.v3InPersonFlow &&
|
||||||
isCarBodyUpload,
|
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS &&
|
||||||
body.documentKey,
|
this.allV3PreCaptureDocumentsComplete(
|
||||||
);
|
claimCase,
|
||||||
remaining = this.countRemainingV2OwnerDocuments(
|
isCarBodyUpload,
|
||||||
claimCase,
|
body.documentKey,
|
||||||
isCarBodyUpload,
|
);
|
||||||
body.documentKey,
|
|
||||||
);
|
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 (allDocumentsUploaded) {
|
||||||
$set["status"] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT;
|
if (options?.v3InPersonFlow) {
|
||||||
$set["claimStatus"] = ClaimStatus.PENDING;
|
$set["status"] = ClaimCaseStatus.SELECTING_OUTER_PARTS;
|
||||||
$set["workflow.currentStep"] =
|
$set["workflow.currentStep"] =
|
||||||
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
ClaimWorkflowStep.SELECT_OUTER_PARTS;
|
||||||
$set["workflow.nextStep"] =
|
$set["workflow.nextStep"] =
|
||||||
ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
ClaimWorkflowStep.SELECT_OTHER_PARTS;
|
||||||
|
|
||||||
completedStepsEntries.push(
|
completedStepsEntries.push(
|
||||||
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
);
|
);
|
||||||
historyEntries.push(
|
historyEntries.push({
|
||||||
{
|
|
||||||
type: "STEP_COMPLETED",
|
type: "STEP_COMPLETED",
|
||||||
actor: {
|
actor: {
|
||||||
actorId: new Types.ObjectId(currentUserId),
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
@@ -6216,24 +6287,52 @@ export class ClaimRequestManagementService {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
metadata: {
|
metadata: {
|
||||||
stepKey: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
stepKey: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
description: "All required documents uploaded",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "STEP_COMPLETED",
|
|
||||||
actor: {
|
|
||||||
actorId: new Types.ObjectId(currentUserId),
|
|
||||||
actorName: claimCase.owner?.fullName || "User",
|
|
||||||
actorType: "user",
|
|
||||||
},
|
|
||||||
timestamp: new Date(),
|
|
||||||
metadata: {
|
|
||||||
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
|
||||||
description:
|
description:
|
||||||
"User submission complete. Claim ready for damage expert review.",
|
"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"] =
|
||||||
|
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||||
|
$set["workflow.nextStep"] =
|
||||||
|
ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||||
|
|
||||||
|
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: "All required documents uploaded",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "STEP_COMPLETED",
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || "User",
|
||||||
|
actorType: "user",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||||
|
description:
|
||||||
|
"User submission complete. Claim ready for damage expert review.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6295,7 +6394,9 @@ export class ClaimRequestManagementService {
|
|||||||
}).sequencePhase,
|
}).sequencePhase,
|
||||||
)
|
)
|
||||||
: allDocumentsUploaded
|
: 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.`;
|
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -6308,7 +6409,9 @@ export class ClaimRequestManagementService {
|
|||||||
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
||||||
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES
|
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES
|
||||||
: allDocumentsUploaded
|
: allDocumentsUploaded
|
||||||
? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
|
? options?.v3InPersonFlow
|
||||||
|
? ClaimWorkflowStep.SELECT_OUTER_PARTS
|
||||||
|
: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
|
||||||
: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
message,
|
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(
|
private async advanceV3ClaimToOuterPartsIfReady(
|
||||||
claimCase: any,
|
claimCase: any,
|
||||||
blame: any,
|
blame: any,
|
||||||
@@ -7859,9 +7967,18 @@ export class ClaimRequestManagementService {
|
|||||||
if (claimCase.workflow?.currentStep === ClaimWorkflowStep.SELECT_OUTER_PARTS) {
|
if (claimCase.workflow?.currentStep === ClaimWorkflowStep.SELECT_OUTER_PARTS) {
|
||||||
return;
|
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(
|
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) {
|
if (!(blame.expert?.decision as any)?.fields?.accidentWay) {
|
||||||
@@ -7869,9 +7986,11 @@ export class ClaimRequestManagementService {
|
|||||||
"Submit accident fields before selecting outer parts.",
|
"Submit accident fields before selecting outer parts.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(String(claimCase._id), {
|
await this.claimCaseDbService.findByIdAndUpdate(String(claimCase._id), {
|
||||||
$set: {
|
$set: {
|
||||||
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
claimStatus: ClaimStatus.PENDING,
|
||||||
"workflow.currentStep": ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
"workflow.currentStep": ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||||
"workflow.nextStep": ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
"workflow.nextStep": ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
},
|
},
|
||||||
@@ -7906,22 +8025,13 @@ export class ClaimRequestManagementService {
|
|||||||
const blame = await this.assertV3InPersonClaim(claimCase);
|
const blame = await this.assertV3InPersonClaim(claimCase);
|
||||||
this.assertV3ClaimDocumentsPhase(blame);
|
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(
|
return this.uploadRequiredDocumentV2(
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
body,
|
body,
|
||||||
file,
|
file,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
actor,
|
actor,
|
||||||
|
{ v3InPersonFlow: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
MaxLength,
|
MaxLength,
|
||||||
Min,
|
Min,
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
|
import { UNIFIED_FILE_STATUS_KEYS } from "src/helpers/unified-file-status";
|
||||||
|
|
||||||
/** Allowed sort keys for V2 list endpoints (claims / blame). */
|
/** Allowed sort keys for V2 list endpoints (claims / blame). */
|
||||||
export const LIST_SORT_BY_V2 = [
|
export const LIST_SORT_BY_V2 = [
|
||||||
@@ -75,4 +76,13 @@ export class ListQueryV2Dto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(64)
|
@MaxLength(64)
|
||||||
search?: string;
|
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];
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/common/dto/unified-file-status-report.dto.ts
Normal file
36
src/common/dto/unified-file-status-report.dto.ts
Normal 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>;
|
||||||
|
}
|
||||||
@@ -117,6 +117,8 @@ export class AllRequestDtoV2 {
|
|||||||
lockTime: string | null;
|
lockTime: string | null;
|
||||||
type: string;
|
type: string;
|
||||||
blameStatus: string;
|
blameStatus: string;
|
||||||
|
/** Calculated blame + linked claim lifecycle status */
|
||||||
|
unifiedFileStatus?: string;
|
||||||
partiesInitialForms: { firstParty: string; secondParty: string };
|
partiesInitialForms: { firstParty: string; secondParty: string };
|
||||||
partiesVehicles: { firstPartyVehicle: string; secondPartyVehicle: string };
|
partiesVehicles: { firstPartyVehicle: string; secondPartyVehicle: string };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ExpertBlameV2Controller } from "./expert-blame.v2.controller";
|
|||||||
import { ExpertBlameService } from "./expert-blame.service";
|
import { ExpertBlameService } from "./expert-blame.service";
|
||||||
import { RequestManagementModule } from "src/request-management/request-management.module";
|
import { RequestManagementModule } from "src/request-management/request-management.module";
|
||||||
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
||||||
|
import { ClaimRequestManagementModule } from "src/claim-request-management/claim-request-management.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -15,6 +16,7 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
|
|||||||
RequestManagementModule,
|
RequestManagementModule,
|
||||||
PlatesModule,
|
PlatesModule,
|
||||||
SmsOrchestrationModule,
|
SmsOrchestrationModule,
|
||||||
|
ClaimRequestManagementModule,
|
||||||
],
|
],
|
||||||
controllers: [ExpertBlameController, ExpertBlameV2Controller],
|
controllers: [ExpertBlameController, ExpertBlameV2Controller],
|
||||||
providers: [ExpertBlameService],
|
providers: [ExpertBlameService],
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ import {
|
|||||||
expertPortfolioFileIdsFromActivityEvents,
|
expertPortfolioFileIdsFromActivityEvents,
|
||||||
objectIdsFromStringSet,
|
objectIdsFromStringSet,
|
||||||
} from "src/helpers/expert-portfolio";
|
} from "src/helpers/expert-portfolio";
|
||||||
|
import {
|
||||||
|
countUnifiedFileStatuses,
|
||||||
|
getUnifiedFileStatusCatalog,
|
||||||
|
resolveUnifiedFileStatus,
|
||||||
|
} from "src/helpers/unified-file-status";
|
||||||
import { applyListQueryV2 } from "src/helpers/list-query-v2";
|
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 { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
import {
|
import {
|
||||||
ExpertFileAssignResultDto,
|
ExpertFileAssignResultDto,
|
||||||
@@ -115,8 +121,109 @@ export class ExpertBlameService {
|
|||||||
private readonly requestManagementService: RequestManagementService,
|
private readonly requestManagementService: RequestManagementService,
|
||||||
private readonly smsOrchestrationService: SmsOrchestrationService,
|
private readonly smsOrchestrationService: SmsOrchestrationService,
|
||||||
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
|
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. */
|
/** Load immutable profile fields from `expert` for the acting field expert. */
|
||||||
private async snapshotFieldExpert(actorId: string) {
|
private async snapshotFieldExpert(actorId: string) {
|
||||||
const expertDoc = await this.expertDbService.findOne({
|
const expertDoc = await this.expertDbService.findOne({
|
||||||
@@ -386,44 +493,8 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const paged = applyListQueryV2(
|
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
|
||||||
visibleCases,
|
return pagedResult;
|
||||||
{
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
this.logger.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,
|
visibleCases,
|
||||||
|
claimByPublicId,
|
||||||
|
query.unifiedStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
const paged = applyListQueryV2(
|
||||||
|
filtered,
|
||||||
{
|
{
|
||||||
publicId: (doc) =>
|
publicId: (doc) =>
|
||||||
String((doc as { publicId?: string }).publicId ?? ""),
|
String((doc as { publicId?: string }).publicId ?? ""),
|
||||||
@@ -492,23 +582,42 @@ export class ExpertBlameService {
|
|||||||
(doc as { publicId?: string }).publicId ??
|
(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) => {
|
searchExtras: (doc) => {
|
||||||
const d = doc as {
|
const d = doc as {
|
||||||
_id?: unknown;
|
_id?: unknown;
|
||||||
blameStatus?: string;
|
blameStatus?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
publicId?: string;
|
||||||
|
status?: string;
|
||||||
};
|
};
|
||||||
return [String(d._id ?? ""), d.blameStatus, d.type].filter(
|
const publicId = String(d.publicId ?? "");
|
||||||
Boolean,
|
const claim = claimByPublicId.get(publicId);
|
||||||
) as string[];
|
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,
|
query,
|
||||||
);
|
);
|
||||||
|
|
||||||
const items: AllRequestDtoV2[] = paged.list.map((doc) =>
|
const items: AllRequestDtoV2[] = paged.list.map((doc) =>
|
||||||
this.mapBlameRequestToListItemV2(doc),
|
this.mapBlameRequestToListItemV2(doc, claimByPublicId),
|
||||||
);
|
);
|
||||||
|
|
||||||
return new AllRequestDtoRsV2({
|
return new AllRequestDtoRsV2({
|
||||||
@@ -522,6 +631,7 @@ export class ExpertBlameService {
|
|||||||
|
|
||||||
private mapBlameRequestToListItemV2(
|
private mapBlameRequestToListItemV2(
|
||||||
doc: Record<string, unknown>,
|
doc: Record<string, unknown>,
|
||||||
|
claimByPublicId?: Map<string, { status?: string }>,
|
||||||
): AllRequestDtoV2 {
|
): AllRequestDtoV2 {
|
||||||
const createdAt = doc.createdAt
|
const createdAt = doc.createdAt
|
||||||
? new Date(doc.createdAt as string)
|
? new Date(doc.createdAt as string)
|
||||||
@@ -552,10 +662,17 @@ export class ExpertBlameService {
|
|||||||
|
|
||||||
const firstParty = parties.find((p) => p.role === "FIRST");
|
const firstParty = parties.find((p) => p.role === "FIRST");
|
||||||
const secondParty = parties.find((p) => p.role === "SECOND");
|
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 {
|
return {
|
||||||
requestId: String(doc._id),
|
requestId: String(doc._id),
|
||||||
status: String(doc.status ?? ""),
|
status: String(doc.status ?? ""),
|
||||||
|
unifiedFileStatus,
|
||||||
userComment: null,
|
userComment: null,
|
||||||
requestCode: String(doc.publicId ?? ""),
|
requestCode: String(doc.publicId ?? ""),
|
||||||
date,
|
date,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class ExpertBlameV2Controller {
|
|||||||
description:
|
description:
|
||||||
"Damage experts (`expert`): tenant-scoped **DISAGREEMENT** queue (available, locked, or decided by you). " +
|
"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. " +
|
"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(
|
async findAll(
|
||||||
@CurrentUser() actor: any,
|
@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")
|
@Get("report/status-counts")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Count blame cases by grouped status bucket (this field expert)",
|
summary: "Count blame cases by grouped status bucket (this field expert)",
|
||||||
|
deprecated: true,
|
||||||
description:
|
description:
|
||||||
"Counts only files in the acting expert’s 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) {
|
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ export class ClaimListItemV2Dto {
|
|||||||
})
|
})
|
||||||
status: string;
|
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' })
|
@ApiProperty({ description: 'Workflow step', example: 'USER_SUBMISSION_COMPLETE' })
|
||||||
currentStep: string;
|
currentStep: string;
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,11 @@ import {
|
|||||||
import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits";
|
import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits";
|
||||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
import { applyListQueryV2 } from "src/helpers/list-query-v2";
|
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 { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher";
|
||||||
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
|
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
|
||||||
|
|
||||||
@@ -3411,6 +3416,144 @@ export class ExpertClaimService {
|
|||||||
return buckets;
|
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
|
* V2: Get claim list for damage expert
|
||||||
*
|
*
|
||||||
@@ -3581,6 +3724,10 @@ export class ExpertClaimService {
|
|||||||
claimRequestId: c._id.toString(),
|
claimRequestId: c._id.toString(),
|
||||||
publicId: c.publicId,
|
publicId: c.publicId,
|
||||||
status: statusForList,
|
status: statusForList,
|
||||||
|
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||||
|
blameStatus: blame?.status,
|
||||||
|
claimStatus: c.status,
|
||||||
|
}),
|
||||||
currentStep: c.workflow?.currentStep || "",
|
currentStep: c.workflow?.currentStep || "",
|
||||||
locked: lockActive,
|
locked: lockActive,
|
||||||
lockedBy:
|
lockedBy:
|
||||||
@@ -3601,32 +3748,7 @@ export class ExpertClaimService {
|
|||||||
};
|
};
|
||||||
}) as ClaimListItemV2Dto[];
|
}) as ClaimListItemV2Dto[];
|
||||||
|
|
||||||
const paged = applyListQueryV2(
|
return this.paginateClaimListV2(list, query);
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3687,6 +3809,10 @@ export class ExpertClaimService {
|
|||||||
claimRequestId: c._id.toString(),
|
claimRequestId: c._id.toString(),
|
||||||
publicId: c.publicId,
|
publicId: c.publicId,
|
||||||
status: c.status,
|
status: c.status,
|
||||||
|
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||||
|
blameStatus: blame?.status,
|
||||||
|
claimStatus: c.status,
|
||||||
|
}),
|
||||||
currentStep: c.workflow?.currentStep || "",
|
currentStep: c.workflow?.currentStep || "",
|
||||||
locked: lockActive,
|
locked: lockActive,
|
||||||
lockedBy:
|
lockedBy:
|
||||||
@@ -3707,31 +3833,7 @@ export class ExpertClaimService {
|
|||||||
};
|
};
|
||||||
}) as ClaimListItemV2Dto[];
|
}) as ClaimListItemV2Dto[];
|
||||||
|
|
||||||
const paged = applyListQueryV2(
|
return this.paginateClaimListV2(list, query);
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -65,11 +65,22 @@ export class ExpertClaimV2Controller {
|
|||||||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
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")
|
@Get("report/status-counts")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Count claim cases by grouped status bucket (this damage expert)",
|
summary: "Count claim cases by grouped status bucket (this damage expert)",
|
||||||
|
deprecated: true,
|
||||||
description:
|
description:
|
||||||
"Counts only files in the acting expert’s 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) {
|
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
|
||||||
return await this.expertClaimService.getStatusReportBucketsV2(actor);
|
return await this.expertClaimService.getStatusReportBucketsV2(actor);
|
||||||
@@ -116,7 +127,7 @@ export class ExpertClaimV2Controller {
|
|||||||
summary: "List available claim requests for damage expert",
|
summary: "List available claim requests for damage expert",
|
||||||
description:
|
description:
|
||||||
"Damage experts: tenant queue (`WAITING_FOR_DAMAGE_EXPERT`), locked/in-progress, factor validation. " +
|
"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 })
|
@ApiResponse({ status: 200, type: GetClaimListV2ResponseDto })
|
||||||
async getClaimListV2(
|
async getClaimListV2(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { CurrentUser } from "src/decorators/user.decorator";
|
|||||||
import { FileRating } from "src/request-management/entities/schema/request-management.schema";
|
import { FileRating } from "src/request-management/entities/schema/request-management.schema";
|
||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
import { ExpertInsurerService } from "./expert-insurer.service";
|
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 { CreateBranchDto } from "src/client/dto/create-branch.dto";
|
||||||
import {
|
import {
|
||||||
CreateBlameExpertByInsurerDto,
|
CreateBlameExpertByInsurerDto,
|
||||||
@@ -162,9 +163,46 @@ export class ExpertInsurerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("files")
|
@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(
|
return await this.expertInsurerService.retrieveAllFilesOfClient(
|
||||||
insurer.clientKey,
|
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")
|
@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({
|
@ApiQuery({
|
||||||
name: "from",
|
name: "from",
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ import { toJalaliDateAndTime } from "src/helpers/date-jalali";
|
|||||||
import { enrichBlamePartiesForAgreementView } from "src/helpers/blame-party-agreement-decision";
|
import { enrichBlamePartiesForAgreementView } from "src/helpers/blame-party-agreement-decision";
|
||||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.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 {
|
import {
|
||||||
getClaimCarAngleCaptureBlob,
|
getClaimCarAngleCaptureBlob,
|
||||||
getDamagedPartCaptureBlob,
|
getDamagedPartCaptureBlob,
|
||||||
@@ -1145,13 +1152,10 @@ export class ExpertInsurerService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieveAllFilesOfClient(insurerId: string) {
|
private async buildMergedInsurerFiles(clientObjectId: Types.ObjectId) {
|
||||||
const id = this.getClientId(insurerId);
|
|
||||||
const idStr = String(id);
|
|
||||||
|
|
||||||
const [blameFiles, claimFiles] = await Promise.all([
|
const [blameFiles, claimFiles] = await Promise.all([
|
||||||
this.getClientBlameFiles(id),
|
this.getClientBlameFiles(clientObjectId),
|
||||||
this.getClientClaimFiles(id),
|
this.getClientClaimFiles(clientObjectId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const byPublicId = new Map<
|
const byPublicId = new Map<
|
||||||
@@ -1192,7 +1196,6 @@ export class ExpertInsurerService {
|
|||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
// Index blame files first
|
|
||||||
for (const b of blameFiles) {
|
for (const b of blameFiles) {
|
||||||
const key = String((b as any).publicId || (b as any)._id || "");
|
const key = String((b as any).publicId || (b as any)._id || "");
|
||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
@@ -1216,8 +1219,6 @@ export class ExpertInsurerService {
|
|||||||
byPublicId.set(key, prev);
|
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
|
const claimOnlyBlameIds = claimFiles
|
||||||
.filter((c) => {
|
.filter((c) => {
|
||||||
const key = String((c as any).publicId || (c as any)._id || "");
|
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]),
|
linkedBlames.map((b) => [String(b.publicId), b]),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Index claim files
|
|
||||||
for (const c of claimFiles) {
|
for (const c of claimFiles) {
|
||||||
const key = String((c as any).publicId || (c as any)._id || "");
|
const key = String((c as any).publicId || (c as any)._id || "");
|
||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
const prev = byPublicId.get(key) ?? { publicId: key };
|
const prev = byPublicId.get(key) ?? { publicId: key };
|
||||||
|
|
||||||
// If no blame entry yet, use the linked blame for parties/vehicle/fileType
|
|
||||||
if (!prev.blame) {
|
if (!prev.blame) {
|
||||||
const linkedBlame = linkedBlameByPublicId.get(key);
|
const linkedBlame = linkedBlameByPublicId.get(key);
|
||||||
if (linkedBlame) {
|
if (linkedBlame) {
|
||||||
@@ -1293,26 +1292,85 @@ export class ExpertInsurerService {
|
|||||||
byPublicId.set(key, prev);
|
byPublicId.set(key, prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = Array.from(byPublicId.values())
|
return Array.from(byPublicId.values()).map((f) => ({
|
||||||
.map((f) => ({
|
publicId: f.publicId,
|
||||||
publicId: f.publicId,
|
fileType: f.fileType,
|
||||||
fileType: f.fileType,
|
hasClaim: !!f.claim,
|
||||||
hasClaim: !!f.claim,
|
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||||
parties: f.parties,
|
blameStatus: f.blame?.status,
|
||||||
blame: f.blame,
|
claimStatus: f.claim?.status,
|
||||||
claim: f.claim,
|
}),
|
||||||
createdAt: f.createdAt,
|
parties: f.parties,
|
||||||
updatedAt: f.updatedAt,
|
blame: f.blame,
|
||||||
}))
|
claim: f.claim,
|
||||||
.sort((a, b) => {
|
createdAt: f.createdAt,
|
||||||
const at = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
updatedAt: f.updatedAt,
|
||||||
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 {
|
return {
|
||||||
total: files.length,
|
total: paged.total,
|
||||||
files,
|
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;
|
under_review: number;
|
||||||
waiting_for_documents_resend: number;
|
waiting_for_documents_resend: number;
|
||||||
}> {
|
}> {
|
||||||
const clientObjectId = this.getClientId(actor);
|
const report = await this.getInsurerUnifiedFileStatusReport(actor, from, to);
|
||||||
const { fromDate, toDate } = this.parseDateRange(from, to);
|
const counts = report.counts;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
all: fileMap.size,
|
all: counts.all ?? 0,
|
||||||
completed,
|
completed: counts.COMPLETED ?? 0,
|
||||||
under_review: underReview,
|
under_review:
|
||||||
waiting_for_documents_resend: waitingForDocumentsResend,
|
(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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/helpers/unified-file-status.spec.ts
Normal file
46
src/helpers/unified-file-status.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
221
src/helpers/unified-file-status.ts
Normal file
221
src/helpers/unified-file-status.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -8284,7 +8284,7 @@ export class RequestManagementService {
|
|||||||
inquiries: (req as any).inquiries ?? {},
|
inquiries: (req as any).inquiries ?? {},
|
||||||
workflow: {
|
workflow: {
|
||||||
currentStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
currentStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
nextStep: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
nextStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||||
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||||
locked: false,
|
locked: false,
|
||||||
},
|
},
|
||||||
@@ -8881,6 +8881,33 @@ export class RequestManagementService {
|
|||||||
}
|
}
|
||||||
await (req as any).save();
|
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 {
|
return {
|
||||||
requestId: String(req._id),
|
requestId: String(req._id),
|
||||||
publicId: req.publicId,
|
publicId: req.publicId,
|
||||||
|
|||||||
Reference in New Issue
Block a user