status modfied

This commit is contained in:
Soheil Hajizadeh
2026-05-04 21:19:11 +03:30
parent e1115b0632
commit 972b4b8719
11 changed files with 153 additions and 55 deletions

View File

@@ -17,8 +17,24 @@ export enum ClaimCaseStatus {
// Expert flow // Expert flow
WAITING_FOR_DAMAGE_EXPERT = "WAITING_FOR_DAMAGE_EXPERT", WAITING_FOR_DAMAGE_EXPERT = "WAITING_FOR_DAMAGE_EXPERT",
EXPERT_REVIEWING = "EXPERT_REVIEWING", EXPERT_REVIEWING = "EXPERT_REVIEWING",
/**
* @deprecated Prefer specific post-expert statuses below. Kept for existing DB rows and reads.
* Historically used for all owner/insurer steps after expert pricing.
*/
WAITING_FOR_INSURER_APPROVAL = "WAITING_FOR_INSURER_APPROVAL", WAITING_FOR_INSURER_APPROVAL = "WAITING_FOR_INSURER_APPROVAL",
/** Expert reply has only priced lines (`factorNeeded=false` everywhere). Owner final accept/reject at `INSURER_REVIEW`. Also set after expert finishes repair-factor validation when the case returns to final owner sign-off. */
INSURER_REVIEW_AWAITING_OWNER_SIGN = "INSURER_REVIEW_AWAITING_OWNER_SIGN",
/** Expert reply mixes priced lines and factor-needed lines: owner signs priced lines, then uploads factors (status stays through both sub-steps). */
INSURER_REVIEW_MIXED_FACTORS_PENDING = "INSURER_REVIEW_MIXED_FACTORS_PENDING",
/** Expert reply requires a repair-factor file for every line before cost validation. */
OWNER_REPAIR_FACTOR_UPLOAD_PENDING = "OWNER_REPAIR_FACTOR_UPLOAD_PENDING",
/** All required factor files are uploaded; damage expert validates factors (`UNDER_REVIEW` @ `EXPERT_COST_EVALUATION`). */
EXPERT_VALIDATING_REPAIR_FACTORS = "EXPERT_VALIDATING_REPAIR_FACTORS",
// Final states // Final states
COMPLETED = "COMPLETED", COMPLETED = "COMPLETED",
CANCELLED = "CANCELLED", CANCELLED = "CANCELLED",

View File

@@ -85,6 +85,8 @@ import {
partKeyAllowedForExpertResend, partKeyAllowedForExpertResend,
} from "src/helpers/claim-expert-resend"; } from "src/helpers/claim-expert-resend";
import { import {
claimCaseStatusAllowsOwnerFactorUploadDuringRevision,
claimCaseStatusAllowsOwnerInsurerSignStep,
classifyV2ExpertPricingParts, classifyV2ExpertPricingParts,
getActiveV2ExpertReply, getActiveV2ExpertReply,
objectionDisallowedDueToOutstandingFactorWorkflow, objectionDisallowedDueToOutstandingFactorWorkflow,
@@ -2369,9 +2371,9 @@ export class ClaimRequestManagementService {
); );
} }
if (claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL) { if (!claimCaseStatusAllowsOwnerFactorUploadDuringRevision(claim.status)) {
throw new BadRequestException( throw new BadRequestException(
"Factors can only be uploaded while the insurer-approval phase is pending.", "Factors can only be uploaded while the claim is in a repair-factor upload phase (see ClaimCaseStatus).",
); );
} }
if (claim.claimStatus !== ClaimStatus.NEEDS_REVISION) { if (claim.claimStatus !== ClaimStatus.NEEDS_REVISION) {
@@ -2487,6 +2489,7 @@ export class ClaimRequestManagementService {
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: { $set: {
status: ClaimCaseStatus.EXPERT_VALIDATING_REPAIR_FACTORS,
claimStatus: ClaimStatus.UNDER_REVIEW, claimStatus: ClaimStatus.UNDER_REVIEW,
"workflow.currentStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION, "workflow.currentStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION,
"workflow.nextStep": ClaimWorkflowStep.INSURER_REVIEW, "workflow.nextStep": ClaimWorkflowStep.INSURER_REVIEW,
@@ -5064,7 +5067,7 @@ export class ClaimRequestManagementService {
/** /**
* V2 API: User objection (ClaimCase + evaluation.objection payload). * V2 API: User objection (ClaimCase + evaluation.objection payload).
* *
* Priced-flow: WAITING_FOR_INSURER_APPROVAL @ INSURER_REVIEW — either NEEDS_REVISION (mixed-priced gate before factor upload) * Priced-flow: insurer-review post-expert statuses (`INSURER_REVIEW_AWAITING_OWNER_SIGN`, `INSURER_REVIEW_MIXED_FACTORS_PENDING`, or legacy `WAITING_FOR_INSURER_APPROVAL`) @ `INSURER_REVIEW` — either NEEDS_REVISION (mixed-priced gate before factor upload)
* or APPROVED (final totals after pricing / factor validation); never while uploading factors or while expert validates. * or APPROVED (final totals after pricing / factor validation); never while uploading factors or while expert validates.
* Legacy: objection during unpaid expert resend (`WAITING_FOR_USER_RESEND` @ `USER_EXPERT_RESEND`). * Legacy: objection during unpaid expert resend (`WAITING_FOR_USER_RESEND` @ `USER_EXPERT_RESEND`).
* Post: resets owner signatures when applicable and returns claim to WAITING_FOR_DAMAGE_EXPERT. * Post: resets owner signatures when applicable and returns claim to WAITING_FOR_DAMAGE_EXPERT.
@@ -5121,7 +5124,7 @@ export class ClaimRequestManagementService {
let pricingObjectionEligible = false; let pricingObjectionEligible = false;
if ( if (
claimCase.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL && claimCaseStatusAllowsOwnerInsurerSignStep(claimCase.status) &&
claimCase.workflow?.currentStep === ClaimWorkflowStep.INSURER_REVIEW && claimCase.workflow?.currentStep === ClaimWorkflowStep.INSURER_REVIEW &&
!claimCase.evaluation?.ownerInsurerApproval?.signedAt !claimCase.evaluation?.ownerInsurerApproval?.signedAt
) { ) {
@@ -5321,11 +5324,11 @@ export class ClaimRequestManagementService {
/** /**
* V2: Claim owner signature on expert pricing. * V2: Claim owner signature on expert pricing.
* *
* Phase 1 (mixed priced + factor-needed lines): WAITING_FOR_INSURER_APPROVAL, NEEDS_REVISION, INSURER_REVIEW, * Phase 1 (mixed priced + factor-needed lines): `INSURER_REVIEW_MIXED_FACTORS_PENDING` (or legacy `WAITING_FOR_INSURER_APPROVAL`), NEEDS_REVISION, INSURER_REVIEW,
* owner has not yet accepted priced lines — agreeing records `evaluation.ownerPricedPartsApproval` and moves the * owner has not yet accepted priced lines — agreeing records `evaluation.ownerPricedPartsApproval` and moves the
* workflow to OWNER_UPLOAD_FACTOR_DOCUMENTS. Rejecting rejects the whole case. * workflow to OWNER_UPLOAD_FACTOR_DOCUMENTS. Rejecting rejects the whole case.
* *
* Phase 2 (final): WAITING_FOR_INSURER_APPROVAL, APPROVED, INSURER_REVIEW — full accept/reject completes the case. * Phase 2 (final): INSURER_REVIEW_AWAITING_OWNER_SIGN (or legacy WAITING_FOR_INSURER_APPROVAL), APPROVED, INSURER_REVIEW — full accept/reject completes the case.
*/ */
async submitOwnerInsurerApprovalSignV2( async submitOwnerInsurerApprovalSignV2(
claimRequestId: string, claimRequestId: string,
@@ -5366,7 +5369,7 @@ export class ClaimRequestManagementService {
); );
} }
if (claimCase.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL) { if (!claimCaseStatusAllowsOwnerInsurerSignStep(claimCase.status)) {
throw new BadRequestException( throw new BadRequestException(
`Claim is not waiting for your insurer-approval signature. Current status: ${String(claimCase.status)}`, `Claim is not waiting for your insurer-approval signature. Current status: ${String(claimCase.status)}`,
); );
@@ -5515,7 +5518,7 @@ export class ClaimRequestManagementService {
message: message:
"Priced repair lines accepted. Upload repair factor files for each factor-needed part; the claim will return to the damage expert for cost validation.", "Priced repair lines accepted. Upload repair factor files for each factor-needed part; the claim will return to the damage expert for cost validation.",
claimRequestId, claimRequestId,
status: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL, status: claimCase.status,
claimStatus: ClaimStatus.NEEDS_REVISION, claimStatus: ClaimStatus.NEEDS_REVISION,
currentStep: ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS, currentStep: ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS,
accepted: true, accepted: true,

View File

@@ -156,7 +156,7 @@ export class ClaimRequestManagementV2Controller {
@ApiOperation({ @ApiOperation({
summary: "Submit user objection (V2)", summary: "Submit user objection (V2)",
description: description:
"**Windows:** (1) **Insurer-review:** `WAITING_FOR_INSURER_APPROVAL` + `workflow.currentStep=INSURER_REVIEW` without a recorded **final** `evaluation.ownerInsurerApproval` signature — including **mixed** priced+factor gate (`NEEDS_REVISION` before factor upload signature) or **final** totals (`claimStatus=APPROVED`). Not allowed while `OWNER_UPLOAD_FACTOR_DOCUMENTS` or `EXPERT_COST_EVALUATION` (finish factor uploads / validation first).\n" + "**Windows:** (1) **Insurer-review:** `ClaimCaseStatus` in **`INSURER_REVIEW_AWAITING_OWNER_SIGN`**, **`INSURER_REVIEW_MIXED_FACTORS_PENDING`**, or legacy **`WAITING_FOR_INSURER_APPROVAL`**, with `workflow.currentStep=INSURER_REVIEW` and no recorded **final** `evaluation.ownerInsurerApproval` — including **mixed** priced+factor gate (`NEEDS_REVISION` before priced-line signature for factors) or **final** totals (`claimStatus=APPROVED`). Not allowed while uploading factors (`OWNER_UPLOAD_FACTOR_DOCUMENTS`) or expert validation (`EXPERT_COST_EVALUATION`/`EXPERT_VALIDATING_REPAIR_FACTORS`).\n" +
"(2) **Legacy resend:** active expert resend (`WAITING_FOR_USER_RESEND` @ `USER_EXPERT_RESEND`).\n\n" + "(2) **Legacy resend:** active expert resend (`WAITING_FOR_USER_RESEND` @ `USER_EXPERT_RESEND`).\n\n" +
"`objectionParts` may only reference **priced** repair lines (`factorNeeded=false`). Factor-only lines cannot be disputed until they have expert pricing.\n\n" + "`objectionParts` may only reference **priced** repair lines (`factorNeeded=false`). Factor-only lines cannot be disputed until they have expert pricing.\n\n" +
"After **`damageExpertReplyFinal`** exists (final reply following a prior objection), **no second objection** — owner uses **owner-insurer-approval/sign** to accept/reject and close the case.\n\n" + "After **`damageExpertReplyFinal`** exists (final reply following a prior objection), **no second objection** — owner uses **owner-insurer-approval/sign** to accept/reject and close the case.\n\n" +
@@ -247,7 +247,7 @@ export class ClaimRequestManagementV2Controller {
@ApiOperation({ @ApiOperation({
summary: "Sign priced lines or final claim pricing (owner)", summary: "Sign priced lines or final claim pricing (owner)",
description: description:
"Multipart: `sign`, `agree`, `branchId`. Always requires `status=WAITING_FOR_INSURER_APPROVAL` and `workflow.currentStep=INSURER_REVIEW` (never during `EXPERT_COST_EVALUATION`).\n\n" + "Multipart: `sign`, `agree`, `branchId`. Requires `ClaimCaseStatus` **`INSURER_REVIEW_AWAITING_OWNER_SIGN`**, **`INSURER_REVIEW_MIXED_FACTORS_PENDING`**, or legacy **`WAITING_FOR_INSURER_APPROVAL`**, and `workflow.currentStep=INSURER_REVIEW` (not during owner factor upload or `EXPERT_COST_EVALUATION`).\n\n" +
"**Phase A — Mixed reply, priced lines only:** `claimStatus=NEEDS_REVISION`, no `evaluation.ownerPricedPartsApproval` yet. `agree=true` records that signature and moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` for factor uploads; `agree=false` rejects the whole case (`REJECTED`).\n\n" + "**Phase A — Mixed reply, priced lines only:** `claimStatus=NEEDS_REVISION`, no `evaluation.ownerPricedPartsApproval` yet. `agree=true` records that signature and moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` for factor uploads; `agree=false` rejects the whole case (`REJECTED`).\n\n" +
"**Phase B — Final:** `claimStatus=APPROVED`, no `evaluation.ownerInsurerApproval` yet. `agree=true` → `COMPLETED`; `agree=false` → `REJECTED`.\n\n" + "**Phase B — Final:** `claimStatus=APPROVED`, no `evaluation.ownerInsurerApproval` yet. `agree=true` → `COMPLETED`; `agree=false` → `REJECTED`.\n\n" +
"Response may include `phase`: `PRICED_PARTS_FOR_FACTORS` or `FINAL_APPROVAL` for UI state.", "Response may include `phase`: `PRICED_PARTS_FOR_FACTORS` or `FINAL_APPROVAL` for UI state.",
@@ -895,9 +895,9 @@ Returns status of each item (uploaded/captured or not).
summary: "Upload repair factor file for a factor-needed part (V2)", summary: "Upload repair factor file for a factor-needed part (V2)",
description: description:
"Part must have `factorNeeded: true` on the active reply (`evaluation.damageExpertReply` or `evaluation.damageExpertReplyFinal`). One file per part.\n\n" + "Part must have `factorNeeded: true` on the active reply (`evaluation.damageExpertReply` or `evaluation.damageExpertReplyFinal`). One file per part.\n\n" +
"**Requires:** `status=WAITING_FOR_INSURER_APPROVAL`, `claimStatus=NEEDS_REVISION`, `workflow.currentStep=OWNER_UPLOAD_FACTOR_DOCUMENTS`.\n\n" + "**Requires:** `ClaimCaseStatus` **`OWNER_REPAIR_FACTOR_UPLOAD_PENDING`** or **`INSURER_REVIEW_MIXED_FACTORS_PENDING`** (or legacy **`WAITING_FOR_INSURER_APPROVAL`**), `claimStatus=NEEDS_REVISION`, `workflow.currentStep=OWNER_UPLOAD_FACTOR_DOCUMENTS`.\n\n" +
"**Mixed priced+factor replies:** owner must complete **owner-insurer-approval/sign** (priced-line phase) first so `evaluation.ownerPricedPartsApproval` exists.\n\n" + "**Mixed priced+factor replies:** owner must complete **owner-insurer-approval/sign** (priced-line phase) first so `evaluation.ownerPricedPartsApproval` exists.\n\n" +
"When every `factorNeeded` line has `factorLink`, the case moves to `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_COST_EVALUATION` for damage expert validation.", "When every `factorNeeded` line has `factorLink`, the case moves to **`EXPERT_VALIDATING_REPAIR_FACTORS`**, `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_COST_EVALUATION` for damage expert validation.",
}) })
@ApiParam({ name: "claimRequestId", description: "Claim case ID" }) @ApiParam({ name: "claimRequestId", description: "Claim case ID" })
@ApiParam({ name: "partId", description: "Part id from expert reply" }) @ApiParam({ name: "partId", description: "Part id from expert reply" })

View File

@@ -83,7 +83,11 @@ export class ClaimDetailsV2ResponseDto {
@ApiProperty({ description: 'Request number' }) @ApiProperty({ description: 'Request number' })
requestNo: string; requestNo: string;
@ApiProperty({ description: 'Overall case status' }) @ApiProperty({
description:
"ClaimCaseStatus; see also `ownerGuidance` for UX. Post-expert: INSURER_REVIEW_AWAITING_OWNER_SIGN | INSURER_REVIEW_MIXED_FACTORS_PENDING | OWNER_REPAIR_FACTOR_UPLOAD_PENDING | EXPERT_VALIDATING_REPAIR_FACTORS; legacy WAITING_FOR_INSURER_APPROVAL may still appear.",
example: "OWNER_REPAIR_FACTOR_UPLOAD_PENDING",
})
status: string; status: string;
@ApiProperty({ description: 'Claim damage status' }) @ApiProperty({ description: 'Claim damage status' })

View File

@@ -10,7 +10,11 @@ export class ClaimListItemV2Dto {
@ApiProperty({ description: 'Claim request number', example: 'CL12345' }) @ApiProperty({ description: 'Claim request number', example: 'CL12345' })
requestNo: string; requestNo: string;
@ApiProperty({ description: 'Overall case status', example: 'WAITING_FOR_DAMAGE_EXPERT' }) @ApiProperty({
description:
"ClaimCaseStatus. Post-expert owner phase includes: INSURER_REVIEW_AWAITING_OWNER_SIGN (priced lines only → final owner sign); INSURER_REVIEW_MIXED_FACTORS_PENDING (priced + factor lines); OWNER_REPAIR_FACTOR_UPLOAD_PENDING (all lines factor-needed); EXPERT_VALIDATING_REPAIR_FACTORS (all factors uploaded, expert validating). Legacy DB rows may still use WAITING_FOR_INSURER_APPROVAL for those flows.",
example: "INSURER_REVIEW_AWAITING_OWNER_SIGN",
})
status: string; status: string;
@ApiProperty({ description: 'Claim damage determination status', example: 'PENDING' }) @ApiProperty({ description: 'Claim damage determination status', example: 'PENDING' })

View File

@@ -76,7 +76,7 @@ export class ClaimUserComment {
export const ClaimUserCommentSchema = export const ClaimUserCommentSchema =
SchemaFactory.createForClass(ClaimUserComment); SchemaFactory.createForClass(ClaimUserComment);
/** Owner acceptance + signature after expert pricing (WAITING_FOR_INSURER_APPROVAL @ INSURER_REVIEW). */ /** Owner acceptance + signature after expert pricing (post-expert insurer `INSURER_REVIEW` — `INSURER_REVIEW_AWAITING_OWNER_SIGN` / `INSURER_REVIEW_MIXED_FACTORS_PENDING` / legacy `WAITING_FOR_INSURER_APPROVAL`). */
@Schema({ _id: false }) @Schema({ _id: false })
export class ClaimOwnerInsurerApproval { export class ClaimOwnerInsurerApproval {
@Prop({ type: Boolean, required: true }) @Prop({ type: Boolean, required: true })

View File

@@ -8,7 +8,11 @@ export class ClaimListItemV2Dto {
@ApiProperty({ description: 'Public ID shared across blame+claim', example: 'A14235' }) @ApiProperty({ description: 'Public ID shared across blame+claim', example: 'A14235' })
publicId: string; publicId: string;
@ApiProperty({ description: 'Overall case status', example: 'WAITING_FOR_DAMAGE_EXPERT' }) @ApiProperty({
description:
"ClaimCaseStatus (expert list may normalize EXPERT_REVIEWING → WAITING_FOR_DAMAGE_EXPERT). Post-expert owner phase: INSURER_REVIEW_* / OWNER_REPAIR_FACTOR_UPLOAD_PENDING / EXPERT_VALIDATING_REPAIR_FACTORS; see expert reply & validate-factors docs.",
example: "WAITING_FOR_DAMAGE_EXPERT",
})
status: string; status: string;
@ApiProperty({ description: 'Workflow step', example: 'USER_SUBMISSION_COMPLETE' }) @ApiProperty({ description: 'Workflow step', example: 'USER_SUBMISSION_COMPLETE' })
@@ -51,7 +55,7 @@ export class ClaimListItemV2Dto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
'True when the claim is in the expert factor validation queue (all factors uploaded).', "True in the expert repair-factor validation queue: `status=EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`) with `claimStatus=UNDER_REVIEW` and `currentStep=EXPERT_COST_EVALUATION`.",
}) })
awaitingFactorValidation?: boolean; awaitingFactorValidation?: boolean;
} }

View File

@@ -66,7 +66,9 @@ import {
} from "src/helpers/blame-party-agreement-decision"; } from "src/helpers/blame-party-agreement-decision";
import { resendRequestHasPayload } from "src/helpers/claim-expert-resend"; import { resendRequestHasPayload } from "src/helpers/claim-expert-resend";
import { import {
claimCaseStatusAfterExpertReplyV2,
classifyV2ExpertPricingParts, classifyV2ExpertPricingParts,
claimIsAwaitingExpertFactorValidationV2,
} from "src/helpers/claim-v2-expert-reply-workflow"; } from "src/helpers/claim-v2-expert-reply-workflow";
import { import {
getClaimCarAngleCaptureBlob, getClaimCarAngleCaptureBlob,
@@ -1911,7 +1913,7 @@ export class ExpertClaimService {
* V2 (ClaimCase): validate user-uploaded repair factors on the active expert reply. * V2 (ClaimCase): validate user-uploaded repair factors on the active expert reply.
* Preconditions: all `factorNeeded` parts have `factorLink`; case is UNDER_REVIEW at EXPERT_COST_EVALUATION. * Preconditions: all `factorNeeded` parts have `factorLink`; case is UNDER_REVIEW at EXPERT_COST_EVALUATION.
* — All approved → claimStatus APPROVED, INSURER_REVIEW: owner accepts/rejects to close. * — All approved → claimStatus APPROVED, INSURER_REVIEW: owner accepts/rejects to close.
* — Any rejected (expert repriced) → WAITING_FOR_INSURER_APPROVAL, APPROVED, INSURER_REVIEW until owner accepts/rejects. * — Any rejected (expert repriced) → INSURER_REVIEW_AWAITING_OWNER_SIGN, APPROVED, INSURER_REVIEW until owner accepts/rejects.
*/ */
async validateClaimFactorsV2( async validateClaimFactorsV2(
claimRequestId: string, claimRequestId: string,
@@ -1927,11 +1929,7 @@ export class ExpertClaimService {
const factorValidationSnapshot = await this.snapshotDamageExpert(actor.sub); const factorValidationSnapshot = await this.snapshotDamageExpert(actor.sub);
if ( if (!claimIsAwaitingExpertFactorValidationV2(claim)) {
claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL ||
claim.claimStatus !== ClaimStatus.UNDER_REVIEW ||
claim.workflow?.currentStep !== ClaimWorkflowStep.EXPERT_COST_EVALUATION
) {
throw new BadRequestException( throw new BadRequestException(
"Claim is not awaiting expert factor validation (UNDER_REVIEW at EXPERT_COST_EVALUATION).", "Claim is not awaiting expert factor validation (UNDER_REVIEW at EXPERT_COST_EVALUATION).",
); );
@@ -2075,7 +2073,7 @@ export class ExpertClaimService {
if (anyRejected) { if (anyRejected) {
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: { $set: {
status: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL, status: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
claimStatus: ClaimStatus.APPROVED, claimStatus: ClaimStatus.APPROVED,
"workflow.currentStep": ClaimWorkflowStep.INSURER_REVIEW, "workflow.currentStep": ClaimWorkflowStep.INSURER_REVIEW,
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED, "workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
@@ -2108,7 +2106,7 @@ export class ExpertClaimService {
"Factors reviewed with repricing. Owner must accept or reject via owner-insurer-approval/sign.", "Factors reviewed with repricing. Owner must accept or reject via owner-insurer-approval/sign.",
claimRequestId, claimRequestId,
claimStatus: ClaimStatus.APPROVED, claimStatus: ClaimStatus.APPROVED,
caseStatus: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL, caseStatus: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
outcome: "REJECTED_REPRICED_OWNER_SIGN", outcome: "REJECTED_REPRICED_OWNER_SIGN",
}; };
} }
@@ -2155,7 +2153,7 @@ export class ExpertClaimService {
"All factors approved. The claim proceeds to insurer review (same as non-factor expert reply).", "All factors approved. The claim proceeds to insurer review (same as non-factor expert reply).",
claimRequestId, claimRequestId,
claimStatus: ClaimStatus.APPROVED, claimStatus: ClaimStatus.APPROVED,
caseStatus: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL, caseStatus: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
outcome: "ALL_APPROVED_INSURER_REVIEW", outcome: "ALL_APPROVED_INSURER_REVIEW",
}; };
} }
@@ -2482,7 +2480,7 @@ export class ExpertClaimService {
* On success: * On success:
* - Stores reply; clears owner signature fields (`ownerInsurerApproval`, `ownerPricedPartsApproval`) * - Stores reply; clears owner signature fields (`ownerInsurerApproval`, `ownerPricedPartsApproval`)
* - Unlocks the workflow * - Unlocks the workflow
* - Pricing-only (`factorNeeded=false` everywhere): WAITING_FOR_INSURER_APPROVAL, APPROVED, INSURER_REVIEW → owner final sign/reject. * - Pricing-only (`factorNeeded=false` everywhere): INSURER_REVIEW_AWAITING_OWNER_SIGN, APPROVED, INSURER_REVIEW → owner final sign/reject.
* - All lines `factorNeeded`: NEEDS_REVISION, OWNER_UPLOAD_FACTOR_DOCUMENTS → uploads → UNDER_REVIEW, EXPERT_COST_EVALUATION (validate factors). * - All lines `factorNeeded`: NEEDS_REVISION, OWNER_UPLOAD_FACTOR_DOCUMENTS → uploads → UNDER_REVIEW, EXPERT_COST_EVALUATION (validate factors).
* - Mixed priced + factor: NEEDS_REVISION, INSURER_REVIEW with next OWNER_UPLOAD_FACTOR_DOCUMENTS → owner signs priced lines first → upload factors → expert validates → final sign. * - Mixed priced + factor: NEEDS_REVISION, INSURER_REVIEW with next OWNER_UPLOAD_FACTOR_DOCUMENTS → owner signs priced lines first → upload factors → expert validates → final sign.
*/ */
@@ -2576,7 +2574,7 @@ export class ExpertClaimService {
let currentStep = ClaimWorkflowStep.INSURER_REVIEW; let currentStep = ClaimWorkflowStep.INSURER_REVIEW;
let nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED; let nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED;
const nextCaseStatus = ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL; const nextCaseStatus = claimCaseStatusAfterExpertReplyV2(processedParts);
let nextClaimStatus = ClaimStatus.APPROVED; let nextClaimStatus = ClaimStatus.APPROVED;
if (needsFactorUpload) { if (needsFactorUpload) {
@@ -2780,7 +2778,7 @@ export class ExpertClaimService {
* Returns claims that are (then filtered to the actor's insurer via `claimCaseTouchesClient`): * Returns claims that are (then filtered to the actor's insurer via `claimCaseTouchesClient`):
* 1. WAITING_FOR_DAMAGE_EXPERT and not locked (open queue) * 1. WAITING_FOR_DAMAGE_EXPERT and not locked (open queue)
* 2. Locked by this expert (in-progress) * 2. Locked by this expert (in-progress)
* 3. WAITING_FOR_INSURER_APPROVAL + UNDER_REVIEW at EXPERT_COST_EVALUATION (factor validation queue) * 3. EXPERT_VALIDATING_REPAIR_FACTORS (or legacy WAITING_FOR_INSURER_APPROVAL) + UNDER_REVIEW at EXPERT_COST_EVALUATION (factor validation queue)
*/ */
async getClaimListV2(actor: any): Promise<GetClaimListV2ResponseDto> { async getClaimListV2(actor: any): Promise<GetClaimListV2ResponseDto> {
requireActorClientKey(actor); requireActorClientKey(actor);
@@ -2807,12 +2805,21 @@ export class ExpertClaimService {
'workflow.lockedBy.actorId': new Types.ObjectId(actorId), 'workflow.lockedBy.actorId': new Types.ObjectId(actorId),
}, },
// User uploaded all factors; expert must approve/reject (unlocked queue) // User uploaded all factors; expert must approve/reject (unlocked queue)
{
$or: [
{
status: ClaimCaseStatus.EXPERT_VALIDATING_REPAIR_FACTORS,
claimStatus: ClaimStatus.UNDER_REVIEW,
'workflow.currentStep': ClaimWorkflowStep.EXPERT_COST_EVALUATION,
},
{ {
status: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL, status: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
claimStatus: ClaimStatus.UNDER_REVIEW, claimStatus: ClaimStatus.UNDER_REVIEW,
'workflow.currentStep': ClaimWorkflowStep.EXPERT_COST_EVALUATION, 'workflow.currentStep': ClaimWorkflowStep.EXPERT_COST_EVALUATION,
}, },
], ],
},
],
}); });
const filtered = (claims as any[]).filter((c) => const filtered = (claims as any[]).filter((c) =>
@@ -2867,10 +2874,7 @@ export class ExpertClaimService {
); );
const list = filtered.map((c) => { const list = filtered.map((c) => {
const awaitingFactorValidation = const awaitingFactorValidation = claimIsAwaitingExpertFactorValidationV2(c);
c.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL &&
c.claimStatus === ClaimStatus.UNDER_REVIEW &&
c.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION;
const v = this.vehicleForExpertFromClaimAndBlameMap(c, blameById); const v = this.vehicleForExpertFromClaimAndBlameMap(c, blameById);
const blame = c.blameRequestId const blame = c.blameRequestId
? blameById.get(c.blameRequestId.toString()) ? blameById.get(c.blameRequestId.toString())
@@ -3237,7 +3241,7 @@ export class ExpertClaimService {
* Validations: * Validations:
* - Claim must exist and belong to the actor's insurer (`assertClaimCaseForTenant`) * - Claim must exist and belong to the actor's insurer (`assertClaimCaseForTenant`)
* - Allowed when: WAITING_FOR_DAMAGE_EXPERT (queue), or EXPERT_REVIEWING (after lock — same expert must be able to reopen detail), * - Allowed when: WAITING_FOR_DAMAGE_EXPERT (queue), or EXPERT_REVIEWING (after lock — same expert must be able to reopen detail),
* or factor-validation queue (WAITING_FOR_INSURER_APPROVAL + UNDER_REVIEW + EXPERT_COST_EVALUATION) * or factor-validation queue (EXPERT_VALIDATING_REPAIR_FACTORS / legacy WAITING_FOR_INSURER_APPROVAL + UNDER_REVIEW + EXPERT_COST_EVALUATION)
* - If an active workflow lock exists (15 min from lockedAt): only the locking expert; after expiry, any expert (tenant) may view * - If an active workflow lock exists (15 min from lockedAt): only the locking expert; after expiry, any expert (tenant) may view
*/ */
async getClaimDetailV2( async getClaimDetailV2(
@@ -3259,9 +3263,7 @@ export class ExpertClaimService {
claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT || claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT ||
claim.status === ClaimCaseStatus.EXPERT_REVIEWING; claim.status === ClaimCaseStatus.EXPERT_REVIEWING;
const isFactorValidationPending = const isFactorValidationPending =
claim.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL && claimIsAwaitingExpertFactorValidationV2(claim);
claim.claimStatus === ClaimStatus.UNDER_REVIEW &&
claim.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION;
if (!isDamageExpertPhase && !isFactorValidationPending) { if (!isDamageExpertPhase && !isFactorValidationPending) {
throw new ForbiddenException( throw new ForbiddenException(

View File

@@ -48,7 +48,7 @@ export class ExpertClaimV2Controller {
@ApiOperation({ @ApiOperation({
summary: "List available claim requests for damage expert", summary: "List available claim requests for damage expert",
description: description:
"Returns claims that are WAITING_FOR_DAMAGE_EXPERT and not locked by another expert, this expert's locked/in-progress claims, and claims awaiting factor validation (UNDER_REVIEW at EXPERT_COST_EVALUATION).", "Returns claims that are WAITING_FOR_DAMAGE_EXPERT and not locked by another expert, this expert's locked/in-progress claims, and claims awaiting factor validation (`status=EXPERT_VALIDATING_REPAIR_FACTORS` or legacy `WAITING_FOR_INSURER_APPROVAL` with UNDER_REVIEW at EXPERT_COST_EVALUATION).",
}) })
async getClaimListV2(@CurrentUser() actor) { async getClaimListV2(@CurrentUser() actor) {
return await this.expertClaimService.getClaimListV2(actor); return await this.expertClaimService.getClaimListV2(actor);
@@ -87,10 +87,11 @@ export class ExpertClaimV2Controller {
summary: "Submit expert damage assessment reply", summary: "Submit expert damage assessment reply",
description: description:
"**Preconditions:** claim locked by this expert (`EXPERT_REVIEWING`). **Unlocks** the claim. Each part needs `daghi` (V1 rules) and may set `factorNeeded` (repair factor file required from owner) or priced lines only. **Cap:** sum of `totalPayment` ≤ 30,000,000. Clears any prior `evaluation.ownerInsurerApproval` / `ownerPricedPartsApproval`.\n\n" + "**Preconditions:** claim locked by this expert (`EXPERT_REVIEWING`). **Unlocks** the claim. Each part needs `daghi` (V1 rules) and may set `factorNeeded` (repair factor file required from owner) or priced lines only. **Cap:** sum of `totalPayment` ≤ 30,000,000. Clears any prior `evaluation.ownerInsurerApproval` / `ownerPricedPartsApproval`.\n\n" +
"**Frontend routing by outcome:**\n" + "**Frontend routing by `ClaimCaseStatus` (`status`):**\n" +
"- **All parts `factorNeeded`:** `status=WAITING_FOR_INSURER_APPROVAL`, `claimStatus=NEEDS_REVISION`, `workflow.currentStep=OWNER_UPLOAD_FACTOR_DOCUMENTS`, `workflow.nextStep=EXPERT_COST_EVALUATION` → owner uploads all factors, then `claimStatus` becomes `UNDER_REVIEW` and `currentStep=EXPERT_COST_EVALUATION` for expert validate-factors.\n" + "- **All parts `factorNeeded`:** `OWNER_REPAIR_FACTOR_UPLOAD_PENDING`, `claimStatus=NEEDS_REVISION`, `workflow.currentStep=OWNER_UPLOAD_FACTOR_DOCUMENTS`, `workflow.nextStep=EXPERT_COST_EVALUATION` → owner uploads all factors; then `status` becomes **`EXPERT_VALIDATING_REPAIR_FACTORS`**, `claimStatus=UNDER_REVIEW`, `currentStep=EXPERT_COST_EVALUATION` for expert **validate-factors**.\n" +
"- **Mixed (some priced, some factorNeeded):** same `status`, `claimStatus=NEEDS_REVISION`, `currentStep=INSURER_REVIEW`, `nextStep=OWNER_UPLOAD_FACTOR_DOCUMENTS` → owner must call **owner-insurer-approval/sign** first (priced-line acceptance), then `currentStep` moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` for factor uploads, then expert validation.\n" + "- **Mixed (some priced, some factorNeeded):** `INSURER_REVIEW_MIXED_FACTORS_PENDING`, `claimStatus=NEEDS_REVISION`, `currentStep=INSURER_REVIEW`, `nextStep=OWNER_UPLOAD_FACTOR_DOCUMENTS` → owner must call **owner-insurer-approval/sign** first (priced-line acceptance); `currentStep` then moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` (same case `status` until factors are done).\n" +
"- **No factors:** `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW`, `nextStep=CLAIM_COMPLETED` → owner final sign/reject only.\n\n" + "- **No factors:** **`INSURER_REVIEW_AWAITING_OWNER_SIGN`**, `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW`, `nextStep=CLAIM_COMPLETED` → owner final sign/reject only.\n\n" +
"**Legacy rows** may still use `WAITING_FOR_INSURER_APPROVAL` instead of the specific values above.\n\n" +
"**After the owner fulfilled an expert resend** (`damageExpertResend.fulfilledAt`), this expert **cannot** initiate **another resend**—use **reply/submit**, in-person visit, or **validate-factors** as appropriate.", "**After the owner fulfilled an expert resend** (`damageExpertResend.fulfilledAt`), this expert **cannot** initiate **another resend**—use **reply/submit**, in-person visit, or **validate-factors** as appropriate.",
}) })
@ApiParam({ name: "claimRequestId" }) @ApiParam({ name: "claimRequestId" })
@@ -145,11 +146,11 @@ export class ExpertClaimV2Controller {
@ApiOperation({ @ApiOperation({
summary: "Validate uploaded repair factors (V2 ClaimCase)", summary: "Validate uploaded repair factors (V2 ClaimCase)",
description: description:
"**Preconditions:** `status=WAITING_FOR_INSURER_APPROVAL`, `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_COST_EVALUATION`, every `factorNeeded` line has `factorLink`.\n\n" + "**Preconditions:** `status=EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`), `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_COST_EVALUATION`, every `factorNeeded` line has `factorLink`.\n\n" +
"**Decisions:** each factor line gets `APPROVED` or `REJECTED`; rejected lines require expert `totalPayment` (or both `price` and `salary`).\n\n" + "**Decisions:** each factor line gets `APPROVED` or `REJECTED`; rejected lines require expert `totalPayment` (or both `price` and `salary`).\n\n" +
"**Outcomes:**\n" + "**Outcomes:**\n" +
"- **All approved:** `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW` → owner must **sign or reject** via `owner-insurer-approval/sign`; agree completes the case.\n" + "- **All approved:** `status=INSURER_REVIEW_AWAITING_OWNER_SIGN`, `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW` → owner must **sign or reject** via `owner-insurer-approval/sign`; agree completes the case.\n" +
"- **Any rejected (repriced):** same waiter + `INSURER_REVIEW` → owner still must **accept or reject** the repriced totals (case is not completed until they sign).\n" + "- **Any rejected (repriced):** same as above (`INSURER_REVIEW_AWAITING_OWNER_SIGN` + `INSURER_REVIEW`) → owner must **accept or reject** the repriced totals.\n" +
"- **Partial batch:** service may return pending until every factor line has a non-pending decision.", "- **Partial batch:** service may return pending until every factor line has a non-pending decision.",
}) })
@ApiParam({ name: "claimRequestId" }) @ApiParam({ name: "claimRequestId" })

View File

@@ -3,6 +3,8 @@ import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatu
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum"; import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
import { import {
classifyV2ExpertPricingParts, classifyV2ExpertPricingParts,
claimCaseStatusAllowsOwnerInsurerSignStep,
claimCaseStatusIsV2OwnerPostExpertPipeline,
getActiveV2ExpertReply, getActiveV2ExpertReply,
objectionDisallowedDueToOutstandingFactorWorkflow, objectionDisallowedDueToOutstandingFactorWorkflow,
} from "src/helpers/claim-v2-expert-reply-workflow"; } from "src/helpers/claim-v2-expert-reply-workflow";
@@ -46,7 +48,7 @@ function objectionWindowForOwner(claim: any): {
let pricingEligible = false; let pricingEligible = false;
if ( if (
claim.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL && claimCaseStatusAllowsOwnerInsurerSignStep(claim.status) &&
claim.workflow?.currentStep === ClaimWorkflowStep.INSURER_REVIEW && claim.workflow?.currentStep === ClaimWorkflowStep.INSURER_REVIEW &&
!claim.evaluation?.ownerInsurerApproval?.signedAt !claim.evaluation?.ownerInsurerApproval?.signedAt
) { ) {
@@ -191,7 +193,7 @@ export function buildClaimDetailsV2OwnerGuidance(
}; };
} }
if (claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL) { if (!claimCaseStatusIsV2OwnerPostExpertPipeline(claim.status)) {
return { return {
phaseKey: "USER_FLOW", phaseKey: "USER_FLOW",
headline: "Continue your claim registration", headline: "Continue your claim registration",

View File

@@ -40,6 +40,72 @@ export function classifyV2ExpertPricingParts(parts: ClaimPricingPartLite[]) {
}; };
} }
const OWNER_POST_EXPERT_INSURER_SIGNATURE_STATUSES: ReadonlySet<string> = new Set([
ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING,
]);
const OWNER_FACTOR_UPLOAD_PHASE_STATUSES: ReadonlySet<string> = new Set([
ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING,
ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING,
]);
const V2_POST_EXPERT_OWNER_PIPELINE_STATUSES: ReadonlySet<string> = new Set([
ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING,
ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING,
ClaimCaseStatus.EXPERT_VALIDATING_REPAIR_FACTORS,
]);
/** Owner multipart sign at `INSURER_REVIEW` (priced-line gate or final acceptance). */
export function claimCaseStatusAllowsOwnerInsurerSignStep(
status: ClaimCaseStatus | string | undefined,
): boolean {
return OWNER_POST_EXPERT_INSURER_SIGNATURE_STATUSES.has(String(status ?? ""));
}
/** Repair-factor upload during `NEEDS_REVISION` @ `OWNER_UPLOAD_FACTOR_DOCUMENTS`. */
export function claimCaseStatusAllowsOwnerFactorUploadDuringRevision(
status: ClaimCaseStatus | string | undefined,
): boolean {
return OWNER_FACTOR_UPLOAD_PHASE_STATUSES.has(String(status ?? ""));
}
/** Owner-facing post-expert flow: signatures, factor uploads, or waiting on expert factor validation. */
export function claimCaseStatusIsV2OwnerPostExpertPipeline(
status: ClaimCaseStatus | string | undefined,
): boolean {
return V2_POST_EXPERT_OWNER_PIPELINE_STATUSES.has(String(status ?? ""));
}
export function claimIsAwaitingExpertFactorValidationV2(claim: {
status?: ClaimCaseStatus | string;
claimStatus?: ClaimStatus;
workflow?: { currentStep?: string };
}): boolean {
return (
claim.claimStatus === ClaimStatus.UNDER_REVIEW &&
claim.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION &&
(claim.status === ClaimCaseStatus.EXPERT_VALIDATING_REPAIR_FACTORS ||
claim.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL)
);
}
export function claimCaseStatusAfterExpertReplyV2(parts: ClaimPricingPartLite[]): ClaimCaseStatus {
const { mixedFactorAndPrice } = classifyV2ExpertPricingParts(parts);
const needsFactorUpload = parts.some((p) => p.factorNeeded === true);
if (!needsFactorUpload) {
return ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN;
}
if (mixedFactorAndPrice) {
return ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING;
}
return ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING;
}
export function claimIsV2ExpertFactorUploadStep(claim: { workflow?: { currentStep?: string } }) { export function claimIsV2ExpertFactorUploadStep(claim: { workflow?: { currentStep?: string } }) {
return claim.workflow?.currentStep === ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS; return claim.workflow?.currentStep === ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS;
} }
@@ -49,11 +115,7 @@ export function claimIsV2PendingFactorExpertValidation(claim: {
claimStatus?: ClaimStatus; claimStatus?: ClaimStatus;
workflow?: { currentStep?: string }; workflow?: { currentStep?: string };
}) { }) {
return ( return claimIsAwaitingExpertFactorValidationV2(claim);
claim.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL &&
claim.claimStatus === ClaimStatus.UNDER_REVIEW &&
claim.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION
);
} }
export function objectionDisallowedDueToOutstandingFactorWorkflow(claim: { export function objectionDisallowedDueToOutstandingFactorWorkflow(claim: {