diff --git a/docs/blame-claim-flow-architecture.html b/docs/blame-claim-flow-architecture.html index 684dfb1..b9e7c14 100644 --- a/docs/blame-claim-flow-architecture.html +++ b/docs/blame-claim-flow-architecture.html @@ -1364,14 +1364,15 @@
10
- Owner sign on expert pricing (FileReviewer acts on - behalf of user) + Claim completes after expert work (no final owner + signature)
- PUT /claim-sign/:claimId + Expert submits to Fanavaran manually when ready
- Submits agree + branchId + signature image + Mixed priced/factor claims still require priced-line + acceptance before factor uploads
@@ -1395,11 +1396,13 @@ FileMaker steps are identical to V4 except requiresFileMakerApproval=true is set at creation. The FileReviewer steps are also identical to V4. The only difference is the - tail: after the damage expert completes their review and the owner signs, + tail: after the damage expert completes their review (and any required factors + are validated), the claim moves to WAITING_FOR_FILE_MAKER_APPROVAL instead of proceeding - directly to fanavaran submission. The FileMaker then approves (→ fanavaran) - or rejects (→ back to WAITING_FOR_DAMAGE_EXPERT, max 2 rejections). + directly to completion. The FileMaker then approves (→ COMPLETED; manual + Fanavaran submission) or rejects (→ back to WAITING_FOR_DAMAGE_EXPERT, + max 2 rejections).

@@ -1468,7 +1471,7 @@ Claim → WAITING_FOR_FILE_MAKER_APPROVAL
- (instead of direct fanavaran submit) + (before final completion; no owner signature)
@@ -1496,7 +1499,7 @@
✓
- Fanavaran submission (on approve) + Claim COMPLETED (on approve)
@@ -1845,8 +1848,8 @@ DAMAGE_EXPERT DAMAGE_EXPERT DAMAGE_EXPERT - DAMAGE_EXPERT → owner sign (FileReviewer) - DAMAGE_EXPERT → owner sign → FileMaker approval + DAMAGE_EXPERT → COMPLETED (manual Fanavaran submit) + DAMAGE_EXPERT → FileMaker approval → COMPLETED (manual Fanavaran submit) DAMAGE_EXPERT (standard V2) diff --git a/docs/panel-roles-reference.fa.html b/docs/panel-roles-reference.fa.html index a5f13ca..b23c284 100644 --- a/docs/panel-roles-reference.fa.html +++ b/docs/panel-roles-reference.fa.html @@ -220,7 +220,7 @@ file_maker - ارزیابی خسارت V4/V5 (فیلدهای تصادف، قطعات، عکس‌ها، امضای مالک) + ارزیابی خسارت V4/V5 (فیلدهای تصادف، قطعات و عکس‌ها؛ تأیید خطوط قیمت‌گذاری‌شده در جریان ترکیبیِ فاکتور در صورت نیاز) فایل‌های تخصیص‌یافته پنل بازبین فایل file_reviewer @@ -451,7 +451,8 @@ OTPها، استعلام‌ها، موقعیت/توضیحات/صدا و امضاها برای هر دو طرف. همچنین اسناد اولیه خسارت (گواهینامه‌ها، کارت‌های خودرو) را بارگذاری می‌کند. پس از امضای دوم، فایل برای تحویل به بازبین فایل «مهرومومه» می‌شود. در V5، فایل‌ساز - در انتها بازمی‌گردد تا خسارت تکمیل‌شده را قبل از ارسال به فناوران تأیید یا رد کند. + در انتها بازمی‌گردد تا خسارت تکمیل‌شده را تأیید یا رد کند. پس از تأیید، + کارشناس پرونده را به‌صورت دستی به فناوران ارسال می‌کند.

@@ -475,10 +476,10 @@

تأیید خسارت V5 — v5/file-maker/claim-approval/

-

فقط در V5 استفاده می‌شود. پس از بررسی کارشناس خسارت و امضای مالک، خسارت وارد WAITING_FOR_FILE_MAKER_APPROVAL می‌شود.

+

فقط در V5 استفاده می‌شود. پس از بررسی کارشناس خسارت و اعتبارسنجی فاکتورهای لازم، خسارت وارد WAITING_FOR_FILE_MAKER_APPROVAL می‌شود؛ امضای نهایی مالک لازم نیست.

- +
متدمسیرتوضیح
POSTapprove/:claimIdتأیید خسارت تکمیل‌شده ← ارسال SMS امضای مالک را فعال می‌کند ← پس از امضای مالک به فناوران ارسال می‌شود.
POSTapprove/:claimIdتأیید خسارت تکمیل‌شده ← وضعیت خسارت COMPLETED می‌شود. کارشناس در زمان مناسب آن را به‌صورت دستی به فناوران ارسال می‌کند.
POSTreject/:claimIdرد به بازبین فایل ← خسارت به WAITING_FOR_DAMAGE_EXPERT برمی‌گردد. محدودیت: حداکثر ۲ رد در هر خسارت؛ تلاش سوم ۴۲۲ با کد FILE_MAKER_REJECTION_LIMIT_EXCEEDED برمی‌گرداند.
@@ -489,7 +490,7 @@ دومین اکتور در تقسیم V4/V5. بازبین فایل فایل‌های مهرومومه‌شده (پس از اتمام کار فایل‌ساز) را تحویل می‌گیرد و مرحله کامل ارزیابی خسارت را انجام می‌دهد: فیلدهای تصادف، دریافت الزامات عکس‌برداری، بارگذاری اسناد (شاسی/موتور)، انتخاب قطعات، - عکس‌های قطعات، ویدیوی دور زدن خودرو، امضای مالک. تقصیر با car-capture به + عکس‌های قطعات و ویدیوی دور زدن خودرو. تقصیر با car-capture به COMPLETED علامت‌گذاری می‌شود. بازبین فایل همچنین دسترسی خواندن به پنل expert-claim برای خسارت‌هایی که بررسی می‌کند دارد.

@@ -499,8 +500,8 @@

اندپوینت‌های V4 و V5 یکسان هستند — فقط پیشوند تغییر می‌کند.

- - + + @@ -509,7 +510,7 @@ - +
متدمسیرتوضیح
GETmy-filesفهرست تمام فایل‌های تخصیص‌یافته به این بازبین فایل.
GETmy-files/:requestIdجزئیات کامل یک فایل (طرفین، گردش کار، فیلدهای کارشناس، شناسه خسارت مرتبط).
GETmy-filesفهرست فایل‌های مهروموم‌شدهٔ فایل‌ساز که در شرکت بیمهٔ این بازبین برای دریافت آماده‌اند، به‌علاوهٔ فایل‌های تخصیص‌یافته به خود او.
GETmy-files/:requestIdجزئیات کامل یک فایل آماده برای دریافت یا تخصیص‌یافته، در شرکت بیمهٔ همین بازبین (طرفین، گردش کار، فیلدهای کارشناس، شناسه خسارت مرتبط).
GETclaim-id/:requestIdدریافت شناسه خسارت ایجادشده به‌صورت خودکار (از استعلام طرف مقصر فایل‌ساز).
POSTaccident-fields/:requestIdمرحله ۱ (بازبین): ذخیره فیلدهای تصادف (accidentWay، accidentReason، accidentType).
GETcapture-requirements/:claimIdالزامات عکس‌برداری آگاه از مرحله (فاز اسناد پیش از عکس‌برداری در مقابل فاز عکس‌برداری قطعات).
PATCHselect-other-parts/:claimIdانتخاب سایر قطعات آسیب‌دیده (غیر بدنه).
POSTcapture-part/:claimIdعکس‌برداری از قطعات + زوایا برای هر قطعه آسیب‌دیده انتخاب‌شده.
PATCHcar-capture/:claimIdویدیوی دور زدن خودرو (آخرین مرحله عکس‌برداری بازبین). خسارت ← WAITING_FOR_DAMAGE_EXPERT، تقصیر ← COMPLETED.
PUTclaim-sign/:claimIdثبت امضای مالک روی قیمت‌گذاری کارشناس به نمایندگی از طرف زیان‌دیده (موافقت + branchId + تصویر امضا).
PUTclaim-sign/:claimIdفقط برای خسارت‌های ترکیبیِ قیمت/فاکتور: ثبت موافقت با خطوط قیمت‌گذاری‌شده پیش از بارگذاری فاکتور. امضای نهایی مالک دیگر لازم نیست.
POSTupload-video/:requestIdدر V4/V5 بی‌اثر است — تقصیر از قبل توسط car-capture COMPLETED شده. موفقیت idempotent برمی‌گرداند.
diff --git a/docs/panel-roles-reference.html b/docs/panel-roles-reference.html index 84eab3e..145fc3d 100644 --- a/docs/panel-roles-reference.html +++ b/docs/panel-roles-reference.html @@ -217,7 +217,7 @@ file_reviewer FileReviewer panel Assigned files - V4/V5 damage assessment (accident fields, parts, captures, owner sign) + V4/V5 damage assessment (accident fields, parts, captures; mixed-factor priced-line acceptance when needed) registrar @@ -367,7 +367,7 @@ POSTv2/expert-claim/assign/:claimRequestIdLock claim to this expert. Returns assigned, already_assigned_to_you, or 409. GETv2/expert-claim/request/:claimRequestId/price-dropPrice-drop context: severity labels, coefficient catalog, damaged parts + mapping, suggested car year from blame inquiry. PUTv2/expert-claim/request/:claimRequestId/price-dropCalculate and persist price-drop: carPrice × yearCoeff × sumOfCoeffs ÷ 400. - PUTv2/expert-claim/reply/submit/:claimRequestIdSubmit damage assessment reply (priced parts list, daghi, branchId). Cap: total ≤ 53 000 000 Toman. Depending on factorNeeded flags moves claim to owner-sign, mixed-factors-pending, or factor-validation queue. + PUTv2/expert-claim/reply/submit/:claimRequestIdSubmit damage assessment reply (priced parts list, daghi, branchId). Cap: total ≤ 53 000 000 Toman. A priced-only claim completes immediately; factor claims continue through factor collection/validation. No final owner signature or automatic Fanavaran submission. PUTv2/expert-claim/reply/resend/:claimRequestIdRequest user to resend documents/photos. One resend per claim lifecycle; returns 422 if already fulfilled. PATCHv2/expert-claim/:claimRequestId/visitAsk user to come in person. Unlocks claim, sets claimStatus to NEEDS_REVISION. PATCHv2/expert-claim/validate-factors/:claimRequestIdValidate uploaded repair factor invoices. Approve or reject each factor line with totalPayment. Cap applies across all lines (≤ 53 000 000 Toman). Auto-completes when all lines are decided. @@ -444,7 +444,8 @@ OTPs, inquiries, location/description/voice, and signatures for both parties. They also upload the initial claim documents (licences, car cards). After the second signature the file is "sealed" for FileReviewer pickup. In V5, FileMaker comes - back at the end to approve or reject the completed claim before fanavaran submission. + back at the end to approve or reject the completed claim. After approval, an + expert submits the case to Fanavaran manually.

@@ -468,10 +469,10 @@

V5 claim approval — v5/file-maker/claim-approval/

-

Used only in V5. After damage expert review and owner sign, claim enters WAITING_FOR_FILE_MAKER_APPROVAL.

+

Used only in V5. After damage expert review and any required factor validation, claim enters WAITING_FOR_FILE_MAKER_APPROVAL; no final owner signature is needed.

- +
MethodRouteWhat it does
POSTapprove/:claimIdApprove the completed claim → triggers owner sign SMS → fanavaran submission after owner signs.
POSTapprove/:claimIdApprove the completed claim → claim becomes COMPLETED. An expert submits to Fanavaran manually when ready.
POSTreject/:claimIdReject back to FileReviewer → claim returns to WAITING_FOR_DAMAGE_EXPERT. Limit: max 2 rejections per claim; 3rd attempt returns 422 FILE_MAKER_REJECTION_LIMIT_EXCEEDED.
@@ -482,7 +483,7 @@ The second actor in the V4/V5 split. FileReviewer picks up sealed files (after FileMaker is done) and performs the full damage assessment pass: accident fields, capture requirements lookup, document upload (chassis/engine), part selection, - part photos, walk-around video, owner signature. The blame is marked COMPLETED + part photos and walk-around video. The blame is marked COMPLETED by car-capture. FileReviewer also has read access to the expert-claim panel for claims they are reviewing.

@@ -492,8 +493,8 @@

V4 and V5 endpoints are identical — only the prefix changes.

- - + + @@ -502,7 +503,7 @@ - +
MethodRouteWhat it does
GETmy-filesList all files assigned to this FileReviewer.
GETmy-files/:requestIdFull detail for one file (parties, workflow, expert fields, linked claim ID).
GETmy-filesList FileMaker-sealed files available to claim in this reviewer’s insurer, plus files already assigned to this FileReviewer.
GETmy-files/:requestIdFull detail for one available or assigned file in this reviewer’s insurer (parties, workflow, expert fields, linked claim ID).
GETclaim-id/:requestIdGet the auto-created claim ID (from FileMaker's guilty-party inquiry).
POSTaccident-fields/:requestIdStep 1 (FileReviewer): save accident fields (accidentWay, accidentReason, accidentType).
GETcapture-requirements/:claimIdStep-aware capture requirements (pre-capture docs phase vs capture-parts phase).
PATCHselect-other-parts/:claimIdSelect other (non-body) damaged parts.
POSTcapture-part/:claimIdCapture part photos + angles for each selected damaged part.
PATCHcar-capture/:claimIdWalk-around video (final FileReviewer capture step). Claim → WAITING_FOR_DAMAGE_EXPERT, blame → COMPLETED.
PUTclaim-sign/:claimIdSubmit owner signature on expert pricing on behalf of the damaged party (agree + branchId + signature image).
PUTclaim-sign/:claimIdFor mixed priced/factor claims only: record acceptance of priced lines before factor uploads. A final owner signature is no longer required.
POSTupload-video/:requestIdNo-op in V4/V5 — blame already COMPLETED by car-capture. Returns idempotent success.
diff --git a/src/claim-request-management/claim-request-management.v2.controller.ts b/src/claim-request-management/claim-request-management.v2.controller.ts index b3628bb..823cc37 100644 --- a/src/claim-request-management/claim-request-management.v2.controller.ts +++ b/src/claim-request-management/claim-request-management.v2.controller.ts @@ -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", diff --git a/src/claim-request-management/dto/claim-details-v2.dto.ts b/src/claim-request-management/dto/claim-details-v2.dto.ts index a801922..26f84f6 100644 --- a/src/claim-request-management/dto/claim-details-v2.dto.ts +++ b/src/claim-request-management/dto/claim-details-v2.dto.ts @@ -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; diff --git a/src/claim-request-management/dto/my-claims-v2.dto.ts b/src/claim-request-management/dto/my-claims-v2.dto.ts index 2941146..3c98b71 100644 --- a/src/claim-request-management/dto/my-claims-v2.dto.ts +++ b/src/claim-request-management/dto/my-claims-v2.dto.ts @@ -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; diff --git a/src/claim-request-management/expert-initiated-claim.mirror.controller.ts b/src/claim-request-management/expert-initiated-claim.mirror.controller.ts index 6670bbc..a3474f8 100644 --- a/src/claim-request-management/expert-initiated-claim.mirror.controller.ts +++ b/src/claim-request-management/expert-initiated-claim.mirror.controller.ts @@ -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", { diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 0d34d82..bfe9f60 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -2223,10 +2223,16 @@ export class ExpertClaimService { actorType: string; }, metadata: Record, - ): Promise { + ): Promise { + // 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 = { - status: isV5Claim && !needsFactorUpload + const persistedCaseStatus = isV5Claim && !needsFactorUpload ? ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL - : nextCaseStatus, + : nextCaseStatus; + const updatePayload: Record = { + 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, diff --git a/src/expert-claim/expert-claim.v2.controller.ts b/src/expert-claim/expert-claim.v2.controller.ts index c506e11..85f6a59 100644 --- a/src/expert-claim/expert-claim.v2.controller.ts +++ b/src/expert-claim/expert-claim.v2.controller.ts @@ -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" }) diff --git a/src/helpers/claim-v2-expert-reply-workflow.spec.ts b/src/helpers/claim-v2-expert-reply-workflow.spec.ts new file mode 100644 index 0000000..0cafb66 --- /dev/null +++ b/src/helpers/claim-v2-expert-reply-workflow.spec.ts @@ -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); + }); +}); diff --git a/src/helpers/claim-v2-expert-reply-workflow.ts b/src/helpers/claim-v2-expert-reply-workflow.ts index 894b23f..02f329a 100644 --- a/src/helpers/claim-v2-expert-reply-workflow.ts +++ b/src/helpers/claim-v2-expert-reply-workflow.ts @@ -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; diff --git a/src/request-management/file-maker-claim-approval-v5.controller.ts b/src/request-management/file-maker-claim-approval-v5.controller.ts index b186981..e14aea3 100644 --- a/src/request-management/file-maker-claim-approval-v5.controller.ts +++ b/src/request-management/file-maker-claim-approval-v5.controller.ts @@ -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, diff --git a/src/request-management/file-maker-claim-approval-v5.spec.ts b/src/request-management/file-maker-claim-approval-v5.spec.ts new file mode 100644 index 0000000..e8089a4 --- /dev/null +++ b/src/request-management/file-maker-claim-approval-v5.spec.ts @@ -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, + }), + }), + ); + }); +}); diff --git a/src/request-management/file-reviewer-blame-v4.controller.ts b/src/request-management/file-reviewer-blame-v4.controller.ts index fa66740..6710f09 100644 --- a/src/request-management/file-reviewer-blame-v4.controller.ts +++ b/src/request-management/file-reviewer-blame-v4.controller.ts @@ -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", { diff --git a/src/request-management/file-reviewer-blame-v5.controller.ts b/src/request-management/file-reviewer-blame-v5.controller.ts index 7be306c..f67f35e 100644 --- a/src/request-management/file-reviewer-blame-v5.controller.ts +++ b/src/request-management/file-reviewer-blame-v5.controller.ts @@ -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", { diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 0f795f9..0afce0b 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -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.", }; }