import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum"; import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum"; import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum"; import { classifyV2ExpertPricingParts, claimCaseStatusAllowsOwnerInsurerSignStep, claimCaseStatusIsV2OwnerPostExpertPipeline, getActiveV2ExpertReply, objectionDisallowedDueToOutstandingFactorWorkflow, } from "src/helpers/claim-v2-expert-reply-workflow"; const BASE = "v2/claim-request-management"; /** One actionable next step for owner UI (`pathTemplate` embeds claim id; replace `{partId}` per line item). */ export interface ClaimDetailsOwnerNextActionV2 { key: string; method?: string; pathTemplate?: string; description: string; } /** Derived hints so the client can route without re-implementing server rules (returned only for claim owners, not FIELD_EXPERT). */ export interface ClaimDetailsOwnerGuidanceV2 { phaseKey: string; headline: string; detail?: string; nextActions: ClaimDetailsOwnerNextActionV2[]; objectionAllowed?: boolean; objectionHint?: string; } function objectionWindowForOwner(claim: any): { pricingEligible: boolean; resendEligible: boolean; } { const r = claim.evaluation?.damageExpertResend; const hasExpertResend = !!( r && (String(r.resendDescription || "").trim() !== "" || (Array.isArray(r.resendDocuments) && r.resendDocuments.length > 0) || (Array.isArray(r.resendCarParts) && r.resendCarParts.length > 0)) ); const pendingResend = hasExpertResend && claim.status === ClaimCaseStatus.WAITING_FOR_USER_RESEND && claim.workflow?.currentStep === ClaimWorkflowStep.USER_EXPERT_RESEND; let pricingEligible = false; if ( claimCaseStatusAllowsOwnerInsurerSignStep(claim.status) && claim.workflow?.currentStep === ClaimWorkflowStep.INSURER_REVIEW && !claim.evaluation?.ownerInsurerApproval?.signedAt ) { if (!objectionDisallowedDueToOutstandingFactorWorkflow(claim)) { const active = getActiveV2ExpertReply( claim as { evaluation?: Record }, ); const shape = classifyV2ExpertPricingParts(active?.parts ?? []); const atMixed = claim.claimStatus === ClaimStatus.NEEDS_REVISION && shape.mixedFactorAndPrice && !claim.evaluation?.ownerPricedPartsApproval?.signedAt; const atFinal = claim.claimStatus === ClaimStatus.APPROVED; pricingEligible = atMixed || atFinal; } } return { pricingEligible, resendEligible: !!pendingResend }; } /** Mirrors server rules for PUT .../objection (read-only hint for UI). */ export function ownerMaySubmitObjectionForClaimDetails(claim: any): boolean { if (claim.evaluation?.objection?.submittedAt) return false; if (claim.evaluation?.damageExpertReplyFinal) return false; const { pricingEligible, resendEligible } = objectionWindowForOwner(claim); if (resendEligible) return true; return ( pricingEligible && !claim.evaluation?.ownerInsurerApproval?.signedAt && !objectionDisallowedDueToOutstandingFactorWorkflow(claim) ); } function objectionHintWhen(allowed: boolean): string | undefined { return allowed ? "Optional: PUT v2/claim-request-management/request/{claimRequestId}/objection — only priced repair lines (`factorNeeded=false` in the expert reply)." : undefined; } /** * Build owner-facing guidance for GET claim details V2. * Caller should omit this field for FIELD_EXPERT viewers. */ export function buildClaimDetailsV2OwnerGuidance( claim: any, claimRequestId: string, ): ClaimDetailsOwnerGuidanceV2 { const id = claimRequestId; const act = (key: string, method: string, path: string, description: string) => ({ key, method, pathTemplate: path, description }); const objectionAllowed = ownerMaySubmitObjectionForClaimDetails(claim); const objectionHint = objectionHintWhen(objectionAllowed); if (claim.status === ClaimCaseStatus.COMPLETED) { return { phaseKey: "COMPLETED", headline: "Claim completed", detail: claim.userRating?.createdAt ? "Rating already submitted." : "You may submit a satisfaction rating once.", nextActions: [ act( "USER_RATING", "PUT", `${BASE}/request/${id}/user-rating`, "Optional satisfaction rating", ), ], objectionAllowed: false, }; } if (claim.status === ClaimCaseStatus.REJECTED || claim.claimStatus === ClaimStatus.REJECTED) { return { phaseKey: "REJECTED", headline: "Claim rejected", detail: "This case is closed as rejected.", nextActions: [], objectionAllowed: false, }; } if (claim.status === ClaimCaseStatus.CANCELLED) { return { phaseKey: "CANCELLED", headline: "Claim cancelled", nextActions: [], objectionAllowed: false, }; } if (claim.status === ClaimCaseStatus.WAITING_FOR_USER_RESEND) { return { phaseKey: "EXPERT_RESEND", headline: "Expert requested more information", detail: "Upload requested documents or damaged-part photos, or acknowledge if the expert gave instructions only.", nextActions: [ act( "UPLOAD_DOC", "POST", `${BASE}/upload-document/${id}`, "Multipart: required document upload", ), act( "CAPTURE_PART", "POST", `${BASE}/capture-part/${id}`, "Multipart: damaged-part or angle capture", ), act( "RESEND_ACK", "POST", `${BASE}/request/${id}/expert-resend/acknowledge`, "If no document/part list — instructions only", ), ], objectionAllowed: ownerMaySubmitObjectionForClaimDetails(claim), objectionHint: objectionHintWhen(ownerMaySubmitObjectionForClaimDetails(claim)), }; } if (claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT) { return { phaseKey: "WAITING_DAMAGE_EXPERT", headline: "Waiting for damage expert", detail: "Your file is in the expert queue.", nextActions: [], objectionAllowed: false, }; } if (claim.status === ClaimCaseStatus.EXPERT_REVIEWING) { return { phaseKey: "EXPERT_REVIEWING", headline: "Expert is reviewing", detail: "A damage expert is assessing your claim.", nextActions: [], objectionAllowed: false, }; } if (!claimCaseStatusIsV2OwnerPostExpertPipeline(claim.status)) { return { phaseKey: "USER_FLOW", headline: "Continue your claim registration", detail: "Complete remaining selection, captures, documents, or bank info.", nextActions: [ act( "CAPTURE_REQUIREMENTS", "GET", `${BASE}/capture-requirements/${id}`, "Shows remaining angles, parts, documents", ), act( "UPLOAD_DOC", "POST", `${BASE}/upload-document/${id}`, "Upload required document when workflow allows", ), act( "CAPTURE_PART", "POST", `${BASE}/capture-part/${id}`, "Upload angle or damaged-part photo when workflow allows", ), ], objectionAllowed: false, }; } const step = claim.workflow?.currentStep; const cs = claim.claimStatus; if (step === ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS && cs === ClaimStatus.NEEDS_REVISION) { return { phaseKey: "UPLOAD_FACTORS", headline: "Upload repair factor files", detail: "One upload per factor-needed part. When all factors are uploaded, the expert validates costs.", nextActions: [ act( "UPLOAD_FACTOR", "PATCH", `${BASE}/request/reply/${id}/{partId}/upload-factor`, "Multipart `file` — use each expert reply `partId` with factorNeeded=true", ), ], objectionAllowed: false, }; } if (step === ClaimWorkflowStep.EXPERT_COST_EVALUATION && cs === ClaimStatus.UNDER_REVIEW) { return { phaseKey: "EXPERT_VALIDATING_FACTORS", headline: "Expert is validating factors", detail: "No owner action until the expert finishes validating repair-factor pricing.", nextActions: [], objectionAllowed: false, }; } if (step === ClaimWorkflowStep.INSURER_REVIEW) { if (cs === ClaimStatus.NEEDS_REVISION) { const active = getActiveV2ExpertReply( claim as { evaluation?: Record }, ); const shape = classifyV2ExpertPricingParts(active?.parts ?? []); if ( shape.mixedFactorAndPrice && !claim.evaluation?.ownerPricedPartsApproval?.signedAt ) { const ow = objectionWindowForOwner(claim); const allowObj = ow.pricingEligible; return { phaseKey: "SIGN_PRICED_LINES", headline: "Sign acceptance of priced lines", detail: "Required before uploading repair-factor files for mixed priced + factor-needed replies.", nextActions: [ act( "SIGN_PRICED_PARTS", "PUT", `${BASE}/request/${id}/owner-insurer-approval/sign`, "Multipart sign + agree + branchId", ), ], objectionAllowed: allowObj, objectionHint: objectionHintWhen(allowObj), }; } /** NEEDS_REVISION at INSURER_REVIEW without mixed gate — all-factor initial sign not used; fallback */ const allowObj = objectionWindowForOwner(claim).pricingEligible; return { phaseKey: "INSURER_REVIEW_NEEDS_REVISION", headline: "Insurer approval — action pending", detail: "If you see priced lines without factors, use sign or objection; otherwise verify workflow data.", nextActions: [ act( "SIGN_OR_CHECK", "PUT", `${BASE}/request/${id}/owner-insurer-approval/sign`, "Sign if prompted by app state", ), ], objectionAllowed: allowObj, objectionHint: objectionHintWhen(allowObj), }; } if (cs === ClaimStatus.APPROVED && !claim.evaluation?.ownerInsurerApproval?.signedAt) { const allowObj = objectionWindowForOwner(claim).pricingEligible; return { phaseKey: "FINAL_SIGN_OR_REJECT", headline: "Accept or reject final pricing", detail: "Sign to complete the claim, or reject. You may object to priced lines instead of signing.", nextActions: [ act( "FINAL_SIGN", "PUT", `${BASE}/request/${id}/owner-insurer-approval/sign`, "Multipart sign + agree + branchId — completes or rejects claim", ), act( "OBJECTION", "PUT", `${BASE}/request/${id}/objection`, "Dispute priced lines before signing", ), ], objectionAllowed: allowObj, objectionHint: objectionHintWhen(allowObj), }; } } return { phaseKey: "INSURER_APPROVAL_FALLBACK", headline: "Awaiting next step", detail: "Inspect `status`, `claimStatus`, `workflow.currentStep`/`nextStep` or contact support.", nextActions: [], objectionAllowed, objectionHint, }; }