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

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