forked from Yara724/api
feat: complete in-person claims without final sign
This commit is contained in:
@@ -322,7 +322,8 @@ export class ClaimRequestManagementV2Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Owner signature — priced-line gate (mixed factors) or final accept/reject.
|
||||
* V2–V5: owner signature used only as the priced-line gate for mixed-factor claims.
|
||||
* The final accept/reject phase is retained for legacy rows only.
|
||||
*/
|
||||
@Put("request/:claimRequestId/owner-insurer-approval/sign")
|
||||
@ApiParam({
|
||||
@@ -332,12 +333,12 @@ export class ClaimRequestManagementV2Controller {
|
||||
})
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
summary: "Sign priced lines or final claim pricing (owner)",
|
||||
summary: "Sign priced lines before factor uploads (owner; final phase is legacy only)",
|
||||
description:
|
||||
"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 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.",
|
||||
"**Phase B — Legacy final phase only:** pre-existing rows with `claimStatus=APPROVED` may still be accepted or rejected through this endpoint. New V2–V5 claims complete after expert work (and V5 FileMaker approval) without a final owner signature.\n\n" +
|
||||
"Response may include `phase`: `PRICED_PARTS_FOR_FACTORS` or legacy `FINAL_APPROVAL` for UI state.",
|
||||
})
|
||||
@ApiBody({
|
||||
description: "Signature file, agreement, and branch",
|
||||
|
||||
@@ -102,7 +102,7 @@ export class ClaimDetailsV2ResponseDto {
|
||||
|
||||
@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.",
|
||||
"ClaimCaseStatus; see also `ownerGuidance` for UX. New V2–V5 priced-only claims are COMPLETED after expert work; factor claims use INSURER_REVIEW_MIXED_FACTORS_PENDING | OWNER_REPAIR_FACTOR_UPLOAD_PENDING | EXPERT_VALIDATING_REPAIR_FACTORS. V5 then uses WAITING_FOR_FILE_MAKER_APPROVAL. Legacy WAITING_FOR_INSURER_APPROVAL or INSURER_REVIEW_AWAITING_OWNER_SIGN may still appear.",
|
||||
example: "OWNER_REPAIR_FACTOR_UPLOAD_PENDING",
|
||||
})
|
||||
status: string;
|
||||
|
||||
@@ -12,8 +12,8 @@ export class ClaimListItemV2Dto {
|
||||
|
||||
@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",
|
||||
"ClaimCaseStatus. New V2–V5 priced-only claims become COMPLETED after expert work. Factor claims use INSURER_REVIEW_MIXED_FACTORS_PENDING (priced-line acceptance before factor uploads), OWNER_REPAIR_FACTOR_UPLOAD_PENDING, and EXPERT_VALIDATING_REPAIR_FACTORS. V5 then waits at WAITING_FOR_FILE_MAKER_APPROVAL. Legacy DB rows may still use WAITING_FOR_INSURER_APPROVAL or INSURER_REVIEW_AWAITING_OWNER_SIGN.",
|
||||
example: "COMPLETED",
|
||||
})
|
||||
status: string;
|
||||
|
||||
|
||||
@@ -527,7 +527,7 @@ Returns status of each item (uploaded/captured or not).
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Owner signature on expert pricing ───────────────────────────────────────
|
||||
// ─── Mixed-factor priced-line signature ──────────────────────────────────────
|
||||
|
||||
@Put("claim-sign/:claimRequestId")
|
||||
@ApiParam({ name: "claimRequestId" })
|
||||
@@ -553,11 +553,12 @@ Returns status of each item (uploaded/captured or not).
|
||||
})
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Owner signature on expert pricing (Flow 3 — expert acts on behalf of user)",
|
||||
"Priced-line acceptance before factor uploads (Flow 3 — expert acts on behalf of user)",
|
||||
description:
|
||||
"Field expert submits the damaged party's signature during the final approval stage. " +
|
||||
"Delegates to the same service method as the user sign endpoint; the expert's " +
|
||||
"identity is resolved to the claim owner via `resolveClaimEffectiveUserId`.",
|
||||
"For mixed priced/factor claims only, the field expert records the damaged party's " +
|
||||
"acceptance of priced lines before factor uploads. V2–V5 no longer require a final " +
|
||||
"owner signature: they complete after expert work. The expert's identity is resolved " +
|
||||
"to the claim owner via `resolveClaimEffectiveUserId`.",
|
||||
})
|
||||
@UseInterceptors(
|
||||
FileInterceptor("sign", {
|
||||
|
||||
@@ -2223,10 +2223,16 @@ export class ExpertClaimService {
|
||||
actorType: string;
|
||||
},
|
||||
metadata: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
): Promise<ClaimCaseStatus> {
|
||||
// V5 retains the FileMaker approval gate, but no longer waits for a
|
||||
// damaged-party final signature after that approval.
|
||||
const completionStatus = (claimForTenant as any).requiresFileMakerApproval
|
||||
? ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL
|
||||
: ClaimCaseStatus.COMPLETED;
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.COMPLETED,
|
||||
status: completionStatus,
|
||||
claimStatus: ClaimStatus.APPROVED,
|
||||
"workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
@@ -2248,6 +2254,8 @@ export class ExpertClaimService {
|
||||
eventType: ExpertFileActivityType.HANDLED,
|
||||
idempotencyKey: `claim:${claimRequestId}:factor_validation:${historyType}:${actor.sub}`,
|
||||
});
|
||||
|
||||
return completionStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2420,7 +2428,7 @@ export class ExpertClaimService {
|
||||
};
|
||||
|
||||
if (anyRejected) {
|
||||
await this.completeClaimCaseAfterFactorValidationV2(
|
||||
const completionStatus = await this.completeClaimCaseAfterFactorValidationV2(
|
||||
claimRequestId,
|
||||
claim,
|
||||
actor,
|
||||
@@ -2428,28 +2436,19 @@ export class ExpertClaimService {
|
||||
historyActor,
|
||||
{ replyField },
|
||||
);
|
||||
const fanavaran =
|
||||
await this.claimRequestManagementService.autoSubmitToFanavaranV2OnClaimCompleted(
|
||||
claimRequestId,
|
||||
);
|
||||
// Expertise (stage 4) is intentionally NOT submitted here.
|
||||
// It is only triggered after the owner's final approval signature
|
||||
// (submitOwnerInsurerApprovalSignV2), because until that point the
|
||||
// expert review is not considered final — the user can still object.
|
||||
return {
|
||||
message: this.appendFanavaranAutoSubmitToMessage(
|
||||
"Factors were reviewed with expert repricing on rejected lines. The claim is completed without an owner signature (temporary policy; may require owner acceptance later).",
|
||||
fanavaran,
|
||||
),
|
||||
message:
|
||||
completionStatus === ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL
|
||||
? "Factors were reviewed with expert repricing. The claim is awaiting FileMaker approval."
|
||||
: "Factors were reviewed with expert repricing. The claim is completed without a final owner signature.",
|
||||
claimRequestId,
|
||||
claimStatus: ClaimStatus.APPROVED,
|
||||
caseStatus: ClaimCaseStatus.COMPLETED,
|
||||
caseStatus: completionStatus,
|
||||
outcome: "REJECTED_REPRICED_AUTO_COMPLETED",
|
||||
fanavaran,
|
||||
};
|
||||
}
|
||||
|
||||
await this.completeClaimCaseAfterFactorValidationV2(
|
||||
const completionStatus = await this.completeClaimCaseAfterFactorValidationV2(
|
||||
claimRequestId,
|
||||
claim,
|
||||
actor,
|
||||
@@ -2458,25 +2457,15 @@ export class ExpertClaimService {
|
||||
{ replyField },
|
||||
);
|
||||
|
||||
const fanavaran =
|
||||
await this.claimRequestManagementService.autoSubmitToFanavaranV2OnClaimCompleted(
|
||||
claimRequestId,
|
||||
);
|
||||
// Expertise (stage 4) is intentionally NOT submitted here.
|
||||
// It is only triggered after the owner's final approval signature
|
||||
// (submitOwnerInsurerApprovalSignV2), because until that point the
|
||||
// expert review is not considered final — the user can still object.
|
||||
|
||||
return {
|
||||
message: this.appendFanavaranAutoSubmitToMessage(
|
||||
"All factors were approved by the expert. The claim is completed without an additional owner signature.",
|
||||
fanavaran,
|
||||
),
|
||||
message:
|
||||
completionStatus === ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL
|
||||
? "All factors were approved. The claim is awaiting FileMaker approval."
|
||||
: "All factors were approved by the expert. The claim is completed without a final owner signature.",
|
||||
claimRequestId,
|
||||
claimStatus: ClaimStatus.APPROVED,
|
||||
caseStatus: ClaimCaseStatus.COMPLETED,
|
||||
caseStatus: completionStatus,
|
||||
outcome: "ALL_APPROVED_AUTO_COMPLETED",
|
||||
fanavaran,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3230,9 +3219,9 @@ export class ExpertClaimService {
|
||||
* On success:
|
||||
* - Stores reply; clears owner signature fields (`ownerInsurerApproval`, `ownerPricedPartsApproval`)
|
||||
* - Unlocks the workflow
|
||||
* - Pricing-only (`factorNeeded=false` everywhere): INSURER_REVIEW_AWAITING_OWNER_SIGN, APPROVED, INSURER_REVIEW → owner final sign/reject.
|
||||
* - Pricing-only (`factorNeeded=false` everywhere): COMPLETED, APPROVED, CLAIM_COMPLETED — no final owner signature.
|
||||
* - 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 → complete.
|
||||
*/
|
||||
async submitExpertReplyV2(
|
||||
claimRequestId: string,
|
||||
@@ -3398,14 +3387,23 @@ export class ExpertClaimService {
|
||||
let currentStep = ClaimWorkflowStep.INSURER_REVIEW;
|
||||
let nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED;
|
||||
|
||||
const nextCaseStatus = claimCaseStatusAfterExpertReplyV2(processedParts);
|
||||
const blameForFlow = claim.blameRequestId
|
||||
? await this.blameRequestDbService.findById(String(claim.blameRequestId))
|
||||
: undefined;
|
||||
const skipsFinalOwnerSignature =
|
||||
!!(blameForFlow as any)?.expertInitiated &&
|
||||
(blameForFlow as any)?.creationMethod === "IN_PERSON";
|
||||
const nextCaseStatus = claimCaseStatusAfterExpertReplyV2(
|
||||
processedParts,
|
||||
skipsFinalOwnerSignature,
|
||||
);
|
||||
let nextClaimStatus = ClaimStatus.APPROVED;
|
||||
|
||||
// V5 flow: FileMaker must approve before the owner is asked to sign.
|
||||
// When no factor upload is needed, skip the owner-sign step entirely here
|
||||
// and park the claim at WAITING_FOR_FILE_MAKER_APPROVAL. The owner SMS
|
||||
// and INSURER_REVIEW status are emitted by fileMakerApproveV5 instead.
|
||||
// V5 retains a FileMaker approval gate after expert work. Expert-initiated
|
||||
// in-person V2–V5 flows do not request a final damaged-party signature.
|
||||
const isV5Claim = !!(claim as any).requiresFileMakerApproval;
|
||||
const completesWithoutFactors =
|
||||
!needsFactorUpload && !isV5Claim && skipsFinalOwnerSignature;
|
||||
|
||||
if (needsFactorUpload) {
|
||||
nextClaimStatus = ClaimStatus.NEEDS_REVISION;
|
||||
@@ -3418,7 +3416,10 @@ export class ExpertClaimService {
|
||||
}
|
||||
} else if (isV5Claim) {
|
||||
// V5, no factors: hold at WAITING_FOR_FILE_MAKER_APPROVAL
|
||||
currentStep = ClaimWorkflowStep.INSURER_REVIEW;
|
||||
currentStep = ClaimWorkflowStep.CLAIM_COMPLETED;
|
||||
nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED;
|
||||
} else if (completesWithoutFactors) {
|
||||
currentStep = ClaimWorkflowStep.CLAIM_COMPLETED;
|
||||
nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED;
|
||||
}
|
||||
|
||||
@@ -3436,10 +3437,11 @@ export class ExpertClaimService {
|
||||
}
|
||||
}
|
||||
|
||||
const updatePayload: Record<string, unknown> = {
|
||||
status: isV5Claim && !needsFactorUpload
|
||||
const persistedCaseStatus = isV5Claim && !needsFactorUpload
|
||||
? ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL
|
||||
: nextCaseStatus,
|
||||
: nextCaseStatus;
|
||||
const updatePayload: Record<string, unknown> = {
|
||||
status: persistedCaseStatus,
|
||||
claimStatus: nextClaimStatus,
|
||||
...(expertAddedParts.length > 0
|
||||
? { "damage.selectedParts": mergedSelectedParts }
|
||||
@@ -3454,9 +3456,7 @@ export class ExpertClaimService {
|
||||
"evaluation.ownerPricedPartsApproval": "",
|
||||
},
|
||||
"workflow.currentStep": currentStep,
|
||||
"workflow.nextStep": needsFactorUpload
|
||||
? nextWorkflowStep
|
||||
: ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
"workflow.nextStep": nextWorkflowStep,
|
||||
[`evaluation.${replyField}`]: replyPayload,
|
||||
$push: {
|
||||
"workflow.completedSteps": completedStep,
|
||||
@@ -3495,30 +3495,36 @@ export class ExpertClaimService {
|
||||
idempotencyKey: `claim:${claimRequestId}:handled:${actor.sub}`,
|
||||
});
|
||||
|
||||
// V5: owner SMS is sent by fileMakerApproveV5 after FileMaker approval.
|
||||
const ownerPhoneNotify = await this.resolveClaimOwnerPhone(claim);
|
||||
if (ownerPhoneNotify && !needsFactorUpload && !isV5Claim) {
|
||||
const expertLastName =
|
||||
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
|
||||
await this.smsOrchestrationService.sendSignatureReviewNotice({
|
||||
receptor: ownerPhoneNotify,
|
||||
fileKind: "claim",
|
||||
publicId: claim.publicId,
|
||||
expertLastName,
|
||||
link: this.smsOrchestrationService.buildClaimLink(String(claim._id), "v2"),
|
||||
});
|
||||
// V1 retains its existing final owner-signature notification. V2–V5 do not
|
||||
// send this notification because their completion policy is manual Fanavaran
|
||||
// submission after expert work (and FileMaker approval for V5).
|
||||
if (!needsFactorUpload && !isV5Claim && !skipsFinalOwnerSignature) {
|
||||
const ownerPhoneNotify = await this.resolveClaimOwnerPhone(claim);
|
||||
if (ownerPhoneNotify) {
|
||||
const expertLastName =
|
||||
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
|
||||
await this.smsOrchestrationService.sendSignatureReviewNotice({
|
||||
receptor: ownerPhoneNotify,
|
||||
fileKind: "claim",
|
||||
publicId: claim.publicId,
|
||||
expertLastName,
|
||||
link: this.smsOrchestrationService.buildClaimLink(
|
||||
String(claim._id),
|
||||
"v2",
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fanavaran expertise is now triggered on owner final sign, not here.
|
||||
// V2–V5 completion and FileMaker approval do not trigger a Fanavaran
|
||||
// submission; the expert submits manually when ready.
|
||||
|
||||
return {
|
||||
claimRequestId,
|
||||
status: nextCaseStatus,
|
||||
status: persistedCaseStatus,
|
||||
claimStatus: nextClaimStatus,
|
||||
currentStep,
|
||||
workflowNextStep: needsFactorUpload
|
||||
? nextWorkflowStep
|
||||
: ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
workflowNextStep: nextWorkflowStep,
|
||||
factorNeeded: needsFactorUpload,
|
||||
mixedPricingAndFactors: mixedFactorAndPrice,
|
||||
allPartsFactorNeeded: !!needsFactorUpload && !!allFactorLines,
|
||||
|
||||
@@ -270,7 +270,7 @@ export class ExpertClaimV2Controller {
|
||||
"**Frontend routing by `ClaimCaseStatus` (`status`):**\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):** `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:** **`INSURER_REVIEW_AWAITING_OWNER_SIGN`**, `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW`, `nextStep=CLAIM_COMPLETED` → owner final sign/reject only.\n\n" +
|
||||
"- **No factors (V2–V5):** **`COMPLETED`**, `claimStatus=APPROVED`, `workflow.currentStep=CLAIM_COMPLETED` → no final owner signature. The expert can submit to Fanavaran manually when ready. For V5, the case instead waits for FileMaker approval before becoming `COMPLETED`.\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.",
|
||||
})
|
||||
@@ -352,8 +352,8 @@ export class ExpertClaimV2Controller {
|
||||
"**Decisions:** each factor line gets `APPROVED` or `REJECTED`. **Every** decided line must include expert-entered `totalPayment` **or** both `price` and `salary` (factor photos are not read for amounts).\n\n" +
|
||||
"**Cap (when every factor line is decided):** sum of **all** reply lines (priced parts + factor lines) must be ≤ **53,000,000 Toman**; otherwise `PRICE_CAP_ERROR` with message that the maximum acceptable total was exceeded.\n\n" +
|
||||
"**Outcomes:**\n" +
|
||||
"- **All approved:** `caseStatus=COMPLETED`, `claimStatus=APPROVED`, workflow `CLAIM_COMPLETED` — no owner signature.\n" +
|
||||
"- **Any rejected (repriced):** same auto-complete for now (owner acceptance may be added later).\n" +
|
||||
"- **All approved:** `caseStatus=COMPLETED`, `claimStatus=APPROVED`, workflow `CLAIM_COMPLETED` — no owner signature. V5 instead waits for FileMaker approval.\n" +
|
||||
"- **Any rejected (repriced):** same completion behavior for now (V5 waits for FileMaker approval).\n" +
|
||||
"- **Partial batch:** returns pending until every factor line has a non-pending decision (no cap error until the batch is complete).",
|
||||
})
|
||||
@ApiParam({ name: "claimRequestId" })
|
||||
|
||||
37
src/helpers/claim-v2-expert-reply-workflow.spec.ts
Normal file
37
src/helpers/claim-v2-expert-reply-workflow.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { claimCaseStatusAfterExpertReplyV2 } from "./claim-v2-expert-reply-workflow";
|
||||
|
||||
describe("claimCaseStatusAfterExpertReplyV2", () => {
|
||||
it("keeps V1 priced-only claims in their final owner-signature state", () => {
|
||||
expect(
|
||||
claimCaseStatusAfterExpertReplyV2([
|
||||
{ factorNeeded: false },
|
||||
{ factorNeeded: false },
|
||||
]),
|
||||
).toBe(ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN);
|
||||
});
|
||||
|
||||
it("completes a V2–V5 priced-only claim without a final owner signature", () => {
|
||||
expect(
|
||||
claimCaseStatusAfterExpertReplyV2(
|
||||
[{ factorNeeded: false }, { factorNeeded: false }],
|
||||
true,
|
||||
),
|
||||
).toBe(ClaimCaseStatus.COMPLETED);
|
||||
});
|
||||
|
||||
it("keeps factor-required claims in their factor collection workflow", () => {
|
||||
expect(
|
||||
claimCaseStatusAfterExpertReplyV2([{ factorNeeded: true }]),
|
||||
).toBe(ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING);
|
||||
});
|
||||
|
||||
it("keeps the priced-line acceptance gate for mixed factor claims", () => {
|
||||
expect(
|
||||
claimCaseStatusAfterExpertReplyV2([
|
||||
{ factorNeeded: false },
|
||||
{ factorNeeded: true },
|
||||
]),
|
||||
).toBe(ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING);
|
||||
});
|
||||
});
|
||||
@@ -95,11 +95,16 @@ export function claimIsAwaitingExpertFactorValidationV2(claim: {
|
||||
);
|
||||
}
|
||||
|
||||
export function claimCaseStatusAfterExpertReplyV2(parts: ClaimPricingPartLite[]): ClaimCaseStatus {
|
||||
export function claimCaseStatusAfterExpertReplyV2(
|
||||
parts: ClaimPricingPartLite[],
|
||||
completeWithoutFinalOwnerSignature = false,
|
||||
): ClaimCaseStatus {
|
||||
const { mixedFactorAndPrice } = classifyV2ExpertPricingParts(parts);
|
||||
const needsFactorUpload = parts.some((p) => p.factorNeeded === true);
|
||||
if (!needsFactorUpload) {
|
||||
return ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN;
|
||||
return completeWithoutFinalOwnerSignature
|
||||
? ClaimCaseStatus.COMPLETED
|
||||
: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN;
|
||||
}
|
||||
if (mixedFactorAndPrice) {
|
||||
return ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING;
|
||||
|
||||
@@ -37,12 +37,13 @@ class FileMakerRejectDto {
|
||||
* V5 FileMaker approval panel.
|
||||
*
|
||||
* After the full claim flow completes (FileReviewer does damage assessment via
|
||||
* expert-claim APIs, user signs), the claim lands in WAITING_FOR_FILE_MAKER_APPROVAL.
|
||||
* expert-claim APIs and any required factors are validated), the claim lands in
|
||||
* WAITING_FOR_FILE_MAKER_APPROVAL.
|
||||
* The FileMaker who created the blame file can then:
|
||||
*
|
||||
* approve → triggers fanavaran submission (claim → COMPLETED)
|
||||
* approve → claim → COMPLETED; an expert submits to Fanavaran manually
|
||||
* reject → sends claim back to WAITING_FOR_DAMAGE_EXPERT so the FileReviewer
|
||||
* can re-lock, adjust pricing, and redo the user interaction
|
||||
* can re-lock, adjust pricing, and redo the claim work
|
||||
*
|
||||
* All endpoints operate on `claimRequestId` (the claim case ID, not the blame ID).
|
||||
* Use `GET v5/file-maker/blame-request-management/claim-id/:requestId` to obtain
|
||||
@@ -65,9 +66,8 @@ export class FileMakerClaimApprovalV5Controller {
|
||||
summary: "Approve the completed claim (FileMaker V5)",
|
||||
description:
|
||||
"Approves a claim that is in `WAITING_FOR_FILE_MAKER_APPROVAL` status. " +
|
||||
"Moves the claim to `INSURER_REVIEW_AWAITING_OWNER_SIGN` and sends the owner an SMS " +
|
||||
"with a signature link. Fanavaran submission is triggered automatically after the " +
|
||||
"owner signs.",
|
||||
"Moves the claim to `COMPLETED`. No final owner signature is requested and " +
|
||||
"Fanavaran submission remains a manual expert action.",
|
||||
})
|
||||
async approve(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
|
||||
56
src/request-management/file-maker-claim-approval-v5.spec.ts
Normal file
56
src/request-management/file-maker-claim-approval-v5.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Types } from "mongoose";
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { RequestManagementService } from "./request-management.service";
|
||||
|
||||
describe("RequestManagementService V5 FileMaker approval", () => {
|
||||
it("completes the claim without notifying the owner for a final signature", async () => {
|
||||
const fileMakerId = new Types.ObjectId();
|
||||
const claimId = new Types.ObjectId();
|
||||
const claimCaseDbService = {
|
||||
findById: jest.fn().mockResolvedValue({
|
||||
_id: claimId,
|
||||
publicId: "CLM-V5",
|
||||
status: ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL,
|
||||
requiresFileMakerApproval: true,
|
||||
fileMakerApprovalActorId: fileMakerId,
|
||||
}),
|
||||
findByIdAndUpdate: jest.fn().mockResolvedValue({}),
|
||||
};
|
||||
const service = new (RequestManagementService as any)(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
claimCaseDbService,
|
||||
) as RequestManagementService;
|
||||
|
||||
const result = await service.fileMakerApproveV5(
|
||||
{ sub: String(fileMakerId), role: RoleEnum.FILE_MAKER },
|
||||
String(claimId),
|
||||
);
|
||||
|
||||
expect(result.status).toBe(ClaimCaseStatus.COMPLETED);
|
||||
expect(result.message).toContain("Submit it to Fanavaran manually");
|
||||
expect(claimCaseDbService.findByIdAndUpdate).toHaveBeenCalledWith(
|
||||
String(claimId),
|
||||
expect.objectContaining({
|
||||
$set: expect.objectContaining({
|
||||
status: ClaimCaseStatus.COMPLETED,
|
||||
requiresFileMakerApproval: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -344,7 +344,7 @@ export class FileReviewerBlameV4Controller {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Owner signature on expert pricing ───────────────────────────────────────
|
||||
// ─── Mixed-factor priced-line signature ──────────────────────────────────────
|
||||
|
||||
@Put("claim-sign/:claimRequestId")
|
||||
@ApiParam({ name: "claimRequestId" })
|
||||
@@ -362,11 +362,12 @@ export class FileReviewerBlameV4Controller {
|
||||
},
|
||||
})
|
||||
@ApiOperation({
|
||||
summary: "Owner signature on expert pricing (V4 — FileReviewer acts on behalf of user)",
|
||||
summary: "Priced-line acceptance before factor uploads (V4 — FileReviewer acts for user)",
|
||||
description:
|
||||
"FileReviewer submits the damaged party's signature during the final approval stage. " +
|
||||
"Delegates to the same service method as the user sign endpoint; the FileReviewer's " +
|
||||
"identity is resolved to the claim owner via `resolveClaimEffectiveUserId`.",
|
||||
"For mixed priced/factor claims only, FileReviewer records the damaged party's " +
|
||||
"acceptance of priced lines before factor uploads. V2–V5 no longer require a final " +
|
||||
"owner signature: they complete after expert work. The FileReviewer's identity is " +
|
||||
"resolved to the claim owner via `resolveClaimEffectiveUserId`.",
|
||||
})
|
||||
@UseInterceptors(
|
||||
FileInterceptor("sign", {
|
||||
|
||||
@@ -342,7 +342,7 @@ export class FileReviewerBlameV5Controller {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Owner signature on expert pricing ───────────────────────────────────────
|
||||
// ─── Mixed-factor priced-line signature ──────────────────────────────────────
|
||||
|
||||
@Put("claim-sign/:claimRequestId")
|
||||
@ApiParam({ name: "claimRequestId" })
|
||||
@@ -360,11 +360,12 @@ export class FileReviewerBlameV5Controller {
|
||||
},
|
||||
})
|
||||
@ApiOperation({
|
||||
summary: "Owner signature on expert pricing (V5 — FileReviewer acts on behalf of user)",
|
||||
summary: "Priced-line acceptance before factor uploads (V5 — FileReviewer acts for user)",
|
||||
description:
|
||||
"FileReviewer submits the damaged party's signature during the final approval stage. " +
|
||||
"Delegates to the same service method as the user sign endpoint; the FileReviewer's " +
|
||||
"identity is resolved to the claim owner via `resolveClaimEffectiveUserId`.",
|
||||
"For mixed priced/factor claims only, FileReviewer records the damaged party's " +
|
||||
"acceptance of priced lines before factor uploads. V2–V5 no longer require a final " +
|
||||
"owner signature: they complete after expert work. The FileReviewer's identity is " +
|
||||
"resolved to the claim owner via `resolveClaimEffectiveUserId`.",
|
||||
})
|
||||
@UseInterceptors(
|
||||
FileInterceptor("sign", {
|
||||
|
||||
@@ -10462,9 +10462,9 @@ export class RequestManagementService {
|
||||
/**
|
||||
* V5 variant of expertUploadBlameVideoV3.
|
||||
* Same flow as V3/V4 but additionally marks the linked claim with
|
||||
* `requiresFileMakerApproval: true` so that after the owner signs,
|
||||
* the claim is held at WAITING_FOR_FILE_MAKER_APPROVAL rather than
|
||||
* being auto-submitted to fanavaran.
|
||||
* `requiresFileMakerApproval: true` so that after expert work completes,
|
||||
* the claim is held at WAITING_FOR_FILE_MAKER_APPROVAL. FileMaker approval
|
||||
* completes the claim; Fanavaran submission is manual.
|
||||
*/
|
||||
async expertUploadBlameVideoV5(
|
||||
expert: any,
|
||||
@@ -10612,7 +10612,7 @@ export class RequestManagementService {
|
||||
videoId,
|
||||
status: req.status,
|
||||
message: file
|
||||
? "Blame accident video uploaded. File is now in expert review queue. After the full claim flow completes and the owner signs, the FileMaker must approve before fanavaran submission."
|
||||
? "Blame accident video uploaded. File is now in expert review queue. After the full claim flow completes, the FileMaker must approve before manual Fanavaran submission."
|
||||
: "File completed. Claim is now ready for damage expert review.",
|
||||
};
|
||||
}
|
||||
@@ -10625,7 +10625,7 @@ export class RequestManagementService {
|
||||
* - claim.status === WAITING_FOR_FILE_MAKER_APPROVAL
|
||||
* - actor is FILE_MAKER and is the original creator of the linked blame file
|
||||
*
|
||||
* On approval: triggers fanavaran submission and moves claim to COMPLETED.
|
||||
* On approval: completes the claim. Fanavaran submission is manual.
|
||||
*/
|
||||
async fileMakerApproveV5(
|
||||
fileMaker: any,
|
||||
@@ -10669,16 +10669,14 @@ export class RequestManagementService {
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
// V5 approval: move claim to INSURER_REVIEW_AWAITING_OWNER_SIGN so the
|
||||
// owner can now sign. Clear requiresFileMakerApproval so that when the
|
||||
// owner later signs and autoSubmitToFanavaranV2OnClaimCompleted runs, it
|
||||
// does not re-intercept the claim as a pending V5 gate.
|
||||
// V5 approval is the final V5 gate. The damaged-party final signature is
|
||||
// no longer required, and Fanavaran submission is intentionally manual.
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
|
||||
status: ClaimCaseStatus.COMPLETED,
|
||||
claimStatus: ClaimStatus.APPROVED,
|
||||
requiresFileMakerApproval: false,
|
||||
"workflow.currentStep": ClaimWorkflowStep.INSURER_REVIEW,
|
||||
"workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
},
|
||||
$push: {
|
||||
@@ -10686,36 +10684,12 @@ export class RequestManagementService {
|
||||
},
|
||||
});
|
||||
|
||||
// Notify the owner that the claim is ready for their signature.
|
||||
const notifyUserId = (claim as any).damagedPartyUserId ?? (claim as any).owner?.userId;
|
||||
if (notifyUserId && claim.blameRequestId) {
|
||||
const blame = await this.blameRequestDbService.findById(
|
||||
String(claim.blameRequestId),
|
||||
);
|
||||
const ownerParty = (blame?.parties || []).find(
|
||||
(p: any) =>
|
||||
p?.person?.userId && String(p.person.userId) === String(notifyUserId),
|
||||
);
|
||||
const ownerPhone = ownerParty?.person?.phoneNumber
|
||||
?? (await this.userDbService.findOne({ _id: new Types.ObjectId(String(notifyUserId)) }))?.mobile;
|
||||
if (ownerPhone && typeof ownerPhone === "string") {
|
||||
await this.smsOrchestrationService.sendSignatureReviewNotice({
|
||||
receptor: ownerPhone,
|
||||
fileKind: "claim",
|
||||
publicId: claim.publicId,
|
||||
expertLastName: actorName.split(/\s+/).pop() || "کارشناس",
|
||||
link: this.smsOrchestrationService.buildClaimLink(claimRequestId, "v2"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
claimRequestId,
|
||||
publicId: claim.publicId,
|
||||
status: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
|
||||
status: ClaimCaseStatus.COMPLETED,
|
||||
message:
|
||||
"Claim approved by FileMaker. Owner has been notified to sign. " +
|
||||
"Once the owner signs, the claim will be completed and submitted to Fanavaran.",
|
||||
"Claim approved by FileMaker and completed. Submit it to Fanavaran manually when ready.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user