forked from Yara724/api
user owner guidence added + status and steps fixed
This commit is contained in:
@@ -84,6 +84,12 @@ import {
|
||||
normalizeResendPartKeys,
|
||||
partKeyAllowedForExpertResend,
|
||||
} from "src/helpers/claim-expert-resend";
|
||||
import {
|
||||
classifyV2ExpertPricingParts,
|
||||
getActiveV2ExpertReply,
|
||||
objectionDisallowedDueToOutstandingFactorWorkflow,
|
||||
} from "src/helpers/claim-v2-expert-reply-workflow";
|
||||
import { buildClaimDetailsV2OwnerGuidance } from "src/helpers/claim-details-v2-owner-guidance";
|
||||
import {
|
||||
canonicalizeClaimCarAngleKey,
|
||||
getClaimCarAngleCaptureBlob,
|
||||
@@ -2363,9 +2369,30 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL) {
|
||||
throw new BadRequestException(
|
||||
"Factors can only be uploaded while the insurer-approval phase is pending.",
|
||||
);
|
||||
}
|
||||
if (claim.claimStatus !== ClaimStatus.NEEDS_REVISION) {
|
||||
throw new BadRequestException(
|
||||
"Factors can only be uploaded while the claim is in NEEDS_REVISION (expert requested factor documents). After all factors are uploaded the claim moves to UNDER_REVIEW for expert validation only.",
|
||||
"Factors can only be uploaded while the claim needs owner action (claimStatus NEEDS_REVISION).",
|
||||
);
|
||||
}
|
||||
if (claim.workflow?.currentStep !== ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS) {
|
||||
throw new BadRequestException(
|
||||
`Repair factors are uploaded at workflow step ${ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS} (after accepting priced lines when applicable). Current: ${String(claim.workflow?.currentStep ?? "")}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const flowParts = finalReply.parts ?? [];
|
||||
const pricingShape = classifyV2ExpertPricingParts(flowParts);
|
||||
if (
|
||||
pricingShape.mixedFactorAndPrice &&
|
||||
!claim.evaluation?.ownerPricedPartsApproval?.signedAt
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Sign acceptance of the priced repair lines first (`PUT .../owner-insurer-approval/sign` while claimStatus is NEEDS_REVISION at INSURER_REVIEW), then upload factors for factor-needed parts.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2459,7 +2486,11 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: { claimStatus: ClaimStatus.UNDER_REVIEW },
|
||||
$set: {
|
||||
claimStatus: ClaimStatus.UNDER_REVIEW,
|
||||
"workflow.currentStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION,
|
||||
"workflow.nextStep": ClaimWorkflowStep.INSURER_REVIEW,
|
||||
},
|
||||
$push: {
|
||||
history: {
|
||||
type: "ALL_FACTORS_UPLOADED_PENDING_VALIDATION",
|
||||
@@ -5033,8 +5064,10 @@ export class ClaimRequestManagementService {
|
||||
/**
|
||||
* V2 API: User objection (ClaimCase + evaluation.objection payload).
|
||||
*
|
||||
* Preconditions: damage expert resend and/or first expert reply exists; no prior objection; no final reply yet.
|
||||
* Post: claim returns to expert queue (WAITING_FOR_DAMAGE_EXPERT), objection stored, optional new parts merged into damage.selectedParts.
|
||||
* Priced-flow: 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.
|
||||
* 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.
|
||||
*/
|
||||
async handleUserObjectionV2(
|
||||
claimRequestId: string,
|
||||
@@ -5058,26 +5091,26 @@ export class ClaimRequestManagementService {
|
||||
throw new ForbiddenException('Only the claim owner can submit an objection');
|
||||
}
|
||||
|
||||
const hasExpertResend = this.claimCaseHasExpertResend(claimCase);
|
||||
const hasFirstExpertReply = !!claimCase.evaluation?.damageExpertReply;
|
||||
const hasFinalExpertReply = !!claimCase.evaluation?.damageExpertReplyFinal;
|
||||
|
||||
if (!hasExpertResend && !hasFirstExpertReply) {
|
||||
throw new BadRequestException(
|
||||
'You can only object after the damage expert has submitted an assessment or requested a resend.',
|
||||
);
|
||||
}
|
||||
|
||||
if (hasFinalExpertReply) {
|
||||
throw new ConflictException(
|
||||
'The expert has already submitted the final reply after objection; you cannot submit another objection through this endpoint.',
|
||||
);
|
||||
}
|
||||
|
||||
if (claimCase.evaluation?.objection?.submittedAt) {
|
||||
throw new ConflictException('An objection has already been submitted for this claim.');
|
||||
}
|
||||
|
||||
const hasExpertResend = this.claimCaseHasExpertResend(claimCase);
|
||||
const pendingResendGate =
|
||||
hasExpertResend &&
|
||||
claimCase.status === ClaimCaseStatus.WAITING_FOR_USER_RESEND &&
|
||||
claimCase.workflow?.currentStep === ClaimWorkflowStep.USER_EXPERT_RESEND;
|
||||
|
||||
if (claimCase.evaluation?.damageExpertReplyFinal) {
|
||||
throw new ConflictException(
|
||||
'The expert already submitted the final reply after objection; submit accept/reject via the owner signer.',
|
||||
);
|
||||
}
|
||||
|
||||
const activeExpert = getActiveV2ExpertReply(
|
||||
claimCase as unknown as { evaluation?: Record<string, unknown> },
|
||||
);
|
||||
|
||||
const partsIn = body.objectionParts ?? [];
|
||||
const newPartsIn = body.newParts ?? [];
|
||||
if (partsIn.length === 0 && newPartsIn.length === 0) {
|
||||
@@ -5086,6 +5119,61 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
let pricingObjectionEligible = false;
|
||||
if (
|
||||
claimCase.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL &&
|
||||
claimCase.workflow?.currentStep === ClaimWorkflowStep.INSURER_REVIEW &&
|
||||
!claimCase.evaluation?.ownerInsurerApproval?.signedAt
|
||||
) {
|
||||
if (objectionDisallowedDueToOutstandingFactorWorkflow(claimCase)) {
|
||||
throw new BadRequestException(
|
||||
'Objections are paused while factor files are uploading or the expert is validating repair factors.',
|
||||
);
|
||||
}
|
||||
|
||||
const shape = classifyV2ExpertPricingParts(activeExpert?.parts ?? []);
|
||||
const atMixedPartialGate =
|
||||
claimCase.claimStatus === ClaimStatus.NEEDS_REVISION &&
|
||||
shape.mixedFactorAndPrice &&
|
||||
!claimCase.evaluation?.ownerPricedPartsApproval?.signedAt;
|
||||
const atFinalPricingGate =
|
||||
claimCase.claimStatus === ClaimStatus.APPROVED;
|
||||
pricingObjectionEligible = atMixedPartialGate || atFinalPricingGate;
|
||||
}
|
||||
|
||||
if (!(pendingResendGate || pricingObjectionEligible)) {
|
||||
throw new BadRequestException(
|
||||
'No open objection window. Use this after a priced insurer-review step without a recorded final approval, ' +
|
||||
'or while completing an unpaid expert resend.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!pendingResendGate && pricingObjectionEligible && !activeExpert?.parts?.length) {
|
||||
throw new BadRequestException(
|
||||
'A priced assessment is required before you can object.',
|
||||
);
|
||||
}
|
||||
|
||||
const pricedPartIdSet =
|
||||
activeExpert?.parts?.length ?
|
||||
new Set(
|
||||
activeExpert.parts
|
||||
.filter((p) => p.factorNeeded !== true)
|
||||
.map((p) => String((p as { partId?: string }).partId ?? ""))
|
||||
.filter(Boolean),
|
||||
)
|
||||
: new Set<string>();
|
||||
|
||||
if (partsIn.length && pricingObjectionEligible) {
|
||||
for (const op of partsIn) {
|
||||
if (!pricedPartIdSet.has(String(op.partId))) {
|
||||
throw new BadRequestException(
|
||||
`Only priced repair lines can be disputed (${String(op.partId)} is factor-only or unknown).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const objectionParts = partsIn.map((p) => ({
|
||||
partId: p.partId,
|
||||
reason: p.reason,
|
||||
@@ -5126,6 +5214,10 @@ export class ClaimRequestManagementService {
|
||||
'evaluation.objection': objectionPayload,
|
||||
'damage.selectedParts': mergedSelected,
|
||||
},
|
||||
$unset: {
|
||||
'evaluation.ownerPricedPartsApproval': "",
|
||||
'evaluation.ownerInsurerApproval': "",
|
||||
},
|
||||
$push: {
|
||||
history: {
|
||||
type: 'USER_OBJECTION_SUBMITTED',
|
||||
@@ -5227,9 +5319,13 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Claim owner signs final priced expert reply (single-party), same intent as blame
|
||||
* `PUT …/sign/:requestId` — only when case status is WAITING_FOR_INSURER_APPROVAL and
|
||||
* workflow is at INSURER_REVIEW (not factor-validation queue at EXPERT_COST_EVALUATION).
|
||||
* V2: Claim owner signature on expert pricing.
|
||||
*
|
||||
* Phase 1 (mixed priced + factor-needed lines): WAITING_FOR_INSURER_APPROVAL, NEEDS_REVISION, INSURER_REVIEW,
|
||||
* 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.
|
||||
*
|
||||
* Phase 2 (final): WAITING_FOR_INSURER_APPROVAL, APPROVED, INSURER_REVIEW — full accept/reject completes the case.
|
||||
*/
|
||||
async submitOwnerInsurerApprovalSignV2(
|
||||
claimRequestId: string,
|
||||
@@ -5245,6 +5341,7 @@ export class ClaimRequestManagementService {
|
||||
claimStatus: ClaimStatus;
|
||||
currentStep?: ClaimWorkflowStep;
|
||||
accepted: boolean;
|
||||
phase?: "PRICED_PARTS_FOR_FACTORS" | "FINAL_APPROVAL";
|
||||
}> {
|
||||
if (!Types.ObjectId.isValid(claimRequestId)) {
|
||||
throw new BadRequestException("Invalid claim request id");
|
||||
@@ -5271,7 +5368,7 @@ export class ClaimRequestManagementService {
|
||||
|
||||
if (claimCase.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL) {
|
||||
throw new BadRequestException(
|
||||
`Claim is not waiting for your approval signature. Current status: ${String(claimCase.status)}`,
|
||||
`Claim is not waiting for your insurer-approval signature. Current status: ${String(claimCase.status)}`,
|
||||
);
|
||||
}
|
||||
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.INSURER_REVIEW) {
|
||||
@@ -5279,21 +5376,41 @@ export class ClaimRequestManagementService {
|
||||
`Signing is only allowed at workflow step ${ClaimWorkflowStep.INSURER_REVIEW}. Current: ${String(claimCase.workflow?.currentStep ?? "")}`,
|
||||
);
|
||||
}
|
||||
if (claimCase.claimStatus !== ClaimStatus.APPROVED) {
|
||||
|
||||
const active =
|
||||
claimCase.evaluation?.damageExpertReplyFinal ||
|
||||
claimCase.evaluation?.damageExpertReply;
|
||||
if (!active?.submittedAt) {
|
||||
throw new BadRequestException("No expert reply is available to sign off on yet.");
|
||||
}
|
||||
|
||||
const shape = classifyV2ExpertPricingParts(active.parts ?? []);
|
||||
const pricedBranchReply = {
|
||||
parts: (active.parts ?? []).filter((p: { factorNeeded?: boolean }) => !p.factorNeeded),
|
||||
};
|
||||
|
||||
const isMixedPartialGate =
|
||||
shape.mixedFactorAndPrice &&
|
||||
claimCase.claimStatus === ClaimStatus.NEEDS_REVISION &&
|
||||
!claimCase.evaluation?.ownerPricedPartsApproval?.signedAt;
|
||||
|
||||
const isFinalApprovalGate =
|
||||
claimCase.claimStatus === ClaimStatus.APPROVED &&
|
||||
!claimCase.evaluation?.ownerInsurerApproval?.signedAt;
|
||||
|
||||
if (!isMixedPartialGate && !isFinalApprovalGate) {
|
||||
throw new BadRequestException(
|
||||
`Claim must be insurer-approved before owner signature. Current claimStatus: ${String(claimCase.claimStatus)}`,
|
||||
"No owner signature action is available at this state. " +
|
||||
"If repair factors are pending upload, complete the priced-line signature first (when claimStatus is NEEDS_REVISION). " +
|
||||
"If you already completed the final approval signature, refresh the claim.",
|
||||
);
|
||||
}
|
||||
|
||||
if (claimCase.evaluation?.ownerInsurerApproval?.signedAt) {
|
||||
throw new ConflictException("You have already submitted a signature for this claim.");
|
||||
if (isMixedPartialGate && claimCase.evaluation?.ownerInsurerApproval?.signedAt) {
|
||||
throw new ConflictException("Claim already has a final approval record; refresh the claim state.");
|
||||
}
|
||||
|
||||
const activeReply =
|
||||
claimCase.evaluation?.damageExpertReplyFinal ||
|
||||
claimCase.evaluation?.damageExpertReply;
|
||||
if (!activeReply?.submittedAt) {
|
||||
throw new BadRequestException("No expert reply is available to sign off on yet.");
|
||||
if (isFinalApprovalGate && claimCase.evaluation?.ownerInsurerApproval?.signedAt) {
|
||||
throw new ConflictException("You have already submitted a signature for this claim.");
|
||||
}
|
||||
|
||||
const rawBranchId = (branchId ?? "").trim();
|
||||
@@ -5312,9 +5429,10 @@ export class ClaimRequestManagementService {
|
||||
"This branch does not belong to the insurer for this claim.",
|
||||
);
|
||||
}
|
||||
const branchIdsOnPricing = this.collectBranchIdsFromClaimExpertReply(
|
||||
activeReply as { parts?: unknown[] },
|
||||
);
|
||||
const branchIdsOnPricing =
|
||||
isMixedPartialGate
|
||||
? this.collectBranchIdsFromClaimExpertReply(pricedBranchReply)
|
||||
: this.collectBranchIdsFromClaimExpertReply(active as { parts?: unknown[] });
|
||||
if (
|
||||
branchIdsOnPricing.size > 0 &&
|
||||
!branchIdsOnPricing.has(String(rawBranchId))
|
||||
@@ -5332,7 +5450,7 @@ export class ClaimRequestManagementService {
|
||||
} as any);
|
||||
const signId = new Types.ObjectId(String((signDoc as any)._id));
|
||||
const signedAt = new Date();
|
||||
const approvalPayload = {
|
||||
const scopedApprovalPayload = {
|
||||
agree,
|
||||
branchId: new Types.ObjectId(rawBranchId),
|
||||
signDetailId: signId,
|
||||
@@ -5345,12 +5463,73 @@ export class ClaimRequestManagementService {
|
||||
actorType: "user",
|
||||
};
|
||||
|
||||
/** Mixed reply: priced lines acceptance before factor uploads */
|
||||
if (isMixedPartialGate) {
|
||||
if (!agree) {
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.REJECTED,
|
||||
claimStatus: ClaimStatus.REJECTED,
|
||||
"evaluation.ownerPricedPartsApproval": scopedApprovalPayload,
|
||||
"workflow.nextStep": null,
|
||||
},
|
||||
$push: {
|
||||
history: {
|
||||
type: "OWNER_REJECTED_PRICED_PARTS_BEFORE_FACTORS",
|
||||
actor: historyActor,
|
||||
timestamp: signedAt,
|
||||
metadata: { branchId: rawBranchId },
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
message:
|
||||
"Your rejection has been recorded. This claim is marked rejected and will not proceed.",
|
||||
claimRequestId,
|
||||
status: ClaimCaseStatus.REJECTED,
|
||||
claimStatus: ClaimStatus.REJECTED,
|
||||
currentStep: claimCase.workflow?.currentStep,
|
||||
accepted: false,
|
||||
phase: "PRICED_PARTS_FOR_FACTORS",
|
||||
};
|
||||
}
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
claimStatus: ClaimStatus.NEEDS_REVISION,
|
||||
"evaluation.ownerPricedPartsApproval": scopedApprovalPayload,
|
||||
"workflow.currentStep": ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS,
|
||||
"workflow.nextStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION,
|
||||
},
|
||||
$push: {
|
||||
history: {
|
||||
type: "OWNER_SIGNED_PRICED_PARTS_PENDING_FACTOR_UPLOAD",
|
||||
actor: historyActor,
|
||||
timestamp: signedAt,
|
||||
metadata: { branchId: rawBranchId },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
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.",
|
||||
claimRequestId,
|
||||
status: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
|
||||
claimStatus: ClaimStatus.NEEDS_REVISION,
|
||||
currentStep: ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS,
|
||||
accepted: true,
|
||||
phase: "PRICED_PARTS_FOR_FACTORS",
|
||||
};
|
||||
}
|
||||
|
||||
/** Final accept / reject (no outstanding factor gate at this step) */
|
||||
if (!agree) {
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.REJECTED,
|
||||
claimStatus: ClaimStatus.REJECTED,
|
||||
"evaluation.ownerInsurerApproval": approvalPayload,
|
||||
"evaluation.ownerInsurerApproval": scopedApprovalPayload,
|
||||
"workflow.nextStep": null,
|
||||
},
|
||||
$push: {
|
||||
@@ -5370,6 +5549,7 @@ export class ClaimRequestManagementService {
|
||||
claimStatus: ClaimStatus.REJECTED,
|
||||
currentStep: claimCase.workflow?.currentStep,
|
||||
accepted: false,
|
||||
phase: "FINAL_APPROVAL",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5377,7 +5557,7 @@ export class ClaimRequestManagementService {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.COMPLETED,
|
||||
claimStatus: ClaimStatus.APPROVED,
|
||||
"evaluation.ownerInsurerApproval": approvalPayload,
|
||||
"evaluation.ownerInsurerApproval": scopedApprovalPayload,
|
||||
"workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
},
|
||||
@@ -5399,6 +5579,7 @@ export class ClaimRequestManagementService {
|
||||
claimStatus: ClaimStatus.APPROVED,
|
||||
currentStep: ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
accepted: true,
|
||||
phase: "FINAL_APPROVAL",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5458,7 +5639,8 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Get claim details by ID (owner only)
|
||||
* V2 API: Get claim details by ID (owner — or FIELD_EXPERT with enriched money/rating fields).
|
||||
* Owners receive `ownerGuidance` computed from workflow state.
|
||||
*/
|
||||
async getClaimDetailsV2(
|
||||
claimRequestId: string,
|
||||
@@ -5618,6 +5800,14 @@ export class ClaimRequestManagementService {
|
||||
damageExpertReplyFinal: mappedEvaluation.damageExpertReplyFinal,
|
||||
}
|
||||
: undefined,
|
||||
...(!isExpertViewer
|
||||
? {
|
||||
ownerGuidance: buildClaimDetailsV2OwnerGuidance(
|
||||
claim,
|
||||
claim._id.toString(),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(isExpertViewer
|
||||
? {
|
||||
userRating: claim.userRating
|
||||
|
||||
@@ -85,7 +85,7 @@ export class ClaimRequestManagementV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "Get Claim Details (V2)",
|
||||
description:
|
||||
"Get claim details for current actor. USER (claim owner) receives the same core claim payload including `owner` (userId, clientId, optional userClientKey). FIELD_EXPERT additionally receives unmasked money and userRating when present.",
|
||||
"Returns the claim snapshot for **USER** (owner) or **FIELD_EXPERT** when permitted. Owners get `ownerGuidance`: `{ phaseKey, headline, nextActions[] (method + pathTemplate), objectionAllowed }` mapped from `status` / `claimStatus` / workflow so the client can show the correct screen without duplicating orchestration logic. FIELD_EXPERT does not receive `ownerGuidance`. Core payload includes documents, captures, evaluation replies, optional expert resend, and masked bank info.",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -156,9 +156,11 @@ export class ClaimRequestManagementV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "Submit user objection (V2)",
|
||||
description:
|
||||
"After the damage expert submits a resend request (`damageExpertResend`), the owner may dispute priced parts " +
|
||||
"and/or propose additional damaged parts. Stores a structured payload on `evaluation.objection`, merges " +
|
||||
"`newParts` into `damage.selectedParts`, and moves the case back to `WAITING_FOR_DAMAGE_EXPERT` for re-review.",
|
||||
"**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" +
|
||||
"(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" +
|
||||
"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" +
|
||||
"Stores `evaluation.objection`, clears partial/final owner approval fields, merges `newParts` into `damage.selectedParts`, returns case to `WAITING_FOR_DAMAGE_EXPERT`.",
|
||||
})
|
||||
@ApiParam({
|
||||
name: "claimRequestId",
|
||||
@@ -233,7 +235,7 @@ export class ClaimRequestManagementV2Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Owner signs acceptance of final expert pricing (WAITING_FOR_INSURER_APPROVAL @ INSURER_REVIEW).
|
||||
* V2: Owner signature — priced-line gate (mixed factors) or final accept/reject.
|
||||
*/
|
||||
@Put("request/:claimRequestId/owner-insurer-approval/sign")
|
||||
@ApiParam({
|
||||
@@ -243,12 +245,12 @@ export class ClaimRequestManagementV2Controller {
|
||||
})
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
summary: "Sign final claim pricing (owner)",
|
||||
summary: "Sign priced lines or final claim pricing (owner)",
|
||||
description:
|
||||
"Multipart: `sign` (signature image), `agree` (boolean), and `branchId` (Mongo ObjectId of the insurer branch the file is reviewed under). " +
|
||||
"Allowed only when `status` is WAITING_FOR_INSURER_APPROVAL, " +
|
||||
"`workflow.currentStep` is INSURER_REVIEW (not EXPERT_COST_EVALUATION), and `claimStatus` is APPROVED. " +
|
||||
"If `agree` is true, the case moves to COMPLETED; if false, to REJECTED.",
|
||||
"Multipart: `sign`, `agree`, `branchId`. Always requires `status=WAITING_FOR_INSURER_APPROVAL` and `workflow.currentStep=INSURER_REVIEW` (never during `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 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.",
|
||||
})
|
||||
@ApiBody({
|
||||
description: "Signature file, agreement, and branch",
|
||||
@@ -890,12 +892,12 @@ Returns status of each item (uploaded/captured or not).
|
||||
@Patch("request/reply/:claimRequestId/:partId/upload-factor")
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
summary: "Upload factor file for a priced part (V2)",
|
||||
summary: "Upload repair factor file for a factor-needed part (V2)",
|
||||
description:
|
||||
"Use when the damage expert reply marks `factorNeeded: true` for this `partId`. " +
|
||||
"Stores file in `claim-factors-image`, sets `factorLink` / `factorStatus` on the matching part in " +
|
||||
"`evaluation.damageExpertReply` or `evaluation.damageExpertReplyFinal`. " +
|
||||
"Requires claim `claimStatus` NEEDS_REVISION or UNDER_REVIEW.",
|
||||
"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" +
|
||||
"**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.",
|
||||
})
|
||||
@ApiParam({ name: "claimRequestId", description: "Claim case ID" })
|
||||
@ApiParam({ name: "partId", description: "Part id from expert reply" })
|
||||
|
||||
@@ -1,5 +1,63 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/** Suggested HTTP call for the owner UI (`pathTemplate`: replace placeholders). */
|
||||
export class ClaimDetailsOwnerNextActionV2Dto {
|
||||
@ApiProperty({ description: 'Stable UI key', example: 'FINAL_SIGN' })
|
||||
key: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'HTTP verb', example: 'PUT' })
|
||||
method?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Path under API root',
|
||||
example: 'v2/claim-request-management/request/{claimRequestId}/owner-insurer-approval/sign',
|
||||
})
|
||||
pathTemplate?: string;
|
||||
|
||||
@ApiProperty({ description: 'What this endpoint does for the user' })
|
||||
description: string;
|
||||
}
|
||||
|
||||
/** Server-derived hints: which phase the claim is in and what to offer next (owners only). */
|
||||
export class ClaimDetailsOwnerGuidanceV2Dto {
|
||||
@ApiProperty({
|
||||
description: 'Machine-readable phase',
|
||||
enum: [
|
||||
'COMPLETED',
|
||||
'REJECTED',
|
||||
'CANCELLED',
|
||||
'EXPERT_RESEND',
|
||||
'WAITING_DAMAGE_EXPERT',
|
||||
'EXPERT_REVIEWING',
|
||||
'USER_FLOW',
|
||||
'UPLOAD_FACTORS',
|
||||
'EXPERT_VALIDATING_FACTORS',
|
||||
'SIGN_PRICED_LINES',
|
||||
'INSURER_REVIEW_NEEDS_REVISION',
|
||||
'FINAL_SIGN_OR_REJECT',
|
||||
'INSURER_APPROVAL_FALLBACK',
|
||||
],
|
||||
})
|
||||
phaseKey: string;
|
||||
|
||||
@ApiProperty({ description: 'Short headline for the current phase' })
|
||||
headline: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Longer UX copy' })
|
||||
detail?: string;
|
||||
|
||||
@ApiProperty({ type: [ClaimDetailsOwnerNextActionV2Dto] })
|
||||
nextActions: ClaimDetailsOwnerNextActionV2Dto[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Whether PUT objection is permitted (see server validation for exact rules)',
|
||||
})
|
||||
objectionAllowed?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'How objection relates to priced vs factor-only lines' })
|
||||
objectionHint?: string;
|
||||
}
|
||||
|
||||
/** Active damage-expert resend request (owner must upload or acknowledge). */
|
||||
export class ExpertResendDetailsV2Dto {
|
||||
@ApiPropertyOptional()
|
||||
@@ -97,6 +155,13 @@ export class ClaimDetailsV2ResponseDto {
|
||||
damageExpertReplyFinal?: unknown;
|
||||
};
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: ClaimDetailsOwnerGuidanceV2Dto,
|
||||
description:
|
||||
'Owner-only: derived headline, suggested API routes, and whether objection is plausible. Omitted when the actor is FIELD_EXPERT.',
|
||||
})
|
||||
ownerGuidance?: ClaimDetailsOwnerGuidanceV2Dto;
|
||||
|
||||
@ApiPropertyOptional({ description: 'User satisfaction rating (if submitted)' })
|
||||
userRating?: {
|
||||
progressSpeed: number;
|
||||
|
||||
@@ -95,6 +95,24 @@ export class ClaimOwnerInsurerApproval {
|
||||
export const ClaimOwnerInsurerApprovalSchema =
|
||||
SchemaFactory.createForClass(ClaimOwnerInsurerApproval);
|
||||
|
||||
/** Owner signed acceptance of priced lines only — required before uploading factors when reply is mixed priced + factorNeeded. */
|
||||
@Schema({ _id: false })
|
||||
export class ClaimOwnerPricedPartsApproval {
|
||||
@Prop({ type: Boolean, required: true })
|
||||
agree: boolean;
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
branchId?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
signDetailId?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: Date, default: () => new Date() })
|
||||
signedAt?: Date;
|
||||
}
|
||||
export const ClaimOwnerPricedPartsApprovalSchema =
|
||||
SchemaFactory.createForClass(ClaimOwnerPricedPartsApproval);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimExpertReply {
|
||||
@Prop({ type: String })
|
||||
@@ -276,6 +294,9 @@ export class ClaimEvaluation {
|
||||
@Prop({ type: ClaimOwnerInsurerApprovalSchema })
|
||||
ownerInsurerApproval?: ClaimOwnerInsurerApproval;
|
||||
|
||||
@Prop({ type: ClaimOwnerPricedPartsApprovalSchema })
|
||||
ownerPricedPartsApproval?: ClaimOwnerPricedPartsApproval;
|
||||
|
||||
@Prop({ type: String })
|
||||
visitLocation?: string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user