user owner guidence added + status and steps fixed

This commit is contained in:
Soheil Hajizadeh
2026-05-02 01:50:45 +03:30
parent 908292b0c3
commit e1115b0632
10 changed files with 842 additions and 101 deletions

View File

@@ -0,0 +1,339 @@
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,
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 (
claim.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL &&
claim.workflow?.currentStep === ClaimWorkflowStep.INSURER_REVIEW &&
!claim.evaluation?.ownerInsurerApproval?.signedAt
) {
if (!objectionDisallowedDueToOutstandingFactorWorkflow(claim)) {
const active = getActiveV2ExpertReply(
claim as { evaluation?: Record<string, unknown> },
);
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 (claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL) {
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<string, unknown> },
);
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,
};
}

View File

@@ -0,0 +1,67 @@
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";
export interface ClaimPricingPartLite {
factorNeeded?: boolean;
partId?: string;
}
export function getActiveV2ExpertReply(claim: { evaluation?: Record<string, unknown> }): {
replyKey: "damageExpertReplyFinal" | "damageExpertReply";
reply: Record<string, unknown>;
parts: ClaimPricingPartLite[];
} | null {
const fin = claim.evaluation?.damageExpertReplyFinal as
| { parts?: ClaimPricingPartLite[]; submittedAt?: Date }
| undefined;
const ini = claim.evaluation?.damageExpertReply as
| { parts?: ClaimPricingPartLite[]; submittedAt?: Date }
| undefined;
if (fin?.submittedAt && fin?.parts?.length) {
return { replyKey: "damageExpertReplyFinal", reply: fin as Record<string, unknown>, parts: fin.parts };
}
if (ini?.submittedAt && ini?.parts?.length) {
return { replyKey: "damageExpertReply", reply: ini as Record<string, unknown>, parts: ini.parts };
}
return null;
}
export function classifyV2ExpertPricingParts(parts: ClaimPricingPartLite[]) {
const factorNeeded = parts.filter((p) => p.factorNeeded === true);
const priced = parts.filter((p) => p.factorNeeded !== true);
return {
pricedPartsCount: priced.length,
factorNeededPartsCount: factorNeeded.length,
mixedFactorAndPrice:
factorNeeded.length > 0 && priced.length > 0,
allFactorLines: factorNeeded.length > 0 && priced.length === 0,
noFactors: factorNeeded.length === 0,
};
}
export function claimIsV2ExpertFactorUploadStep(claim: { workflow?: { currentStep?: string } }) {
return claim.workflow?.currentStep === ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS;
}
export function claimIsV2PendingFactorExpertValidation(claim: {
status?: ClaimCaseStatus;
claimStatus?: ClaimStatus;
workflow?: { currentStep?: string };
}) {
return (
claim.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL &&
claim.claimStatus === ClaimStatus.UNDER_REVIEW &&
claim.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION
);
}
export function objectionDisallowedDueToOutstandingFactorWorkflow(claim: {
workflow?: { currentStep?: string };
}) {
const step = claim.workflow?.currentStep;
return (
step === ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS ||
step === ClaimWorkflowStep.EXPERT_COST_EVALUATION
);
}