feat: complete in-person claims without final sign

This commit is contained in:
SepehrYahyaee
2026-09-01 15:03:08 +03:30
parent 6880de5960
commit ae49031c55
16 changed files with 249 additions and 162 deletions

View File

@@ -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",

View File

@@ -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;

View File

@@ -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;

View File

@@ -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", {

View File

@@ -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,

View File

@@ -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" })

View 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);
});
});

View File

@@ -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;

View File

@@ -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,

View 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,
}),
}),
);
});
});

View File

@@ -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", {

View File

@@ -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", {

View File

@@ -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.",
};
}