1
0
forked from Yara724/api

user owner guidence added + status and steps fixed

This commit is contained in:
Soheil Hajizadeh
2026-05-02 01:50:45 +03:30
parent 908292b0c3
commit e1115b0632
10 changed files with 842 additions and 101 deletions

View File

@@ -80,7 +80,11 @@ export class PartPricingV2Dto {
@Type(() => DaghiDetailsV2Dto)
daghi: DaghiDetailsV2Dto;
@ApiProperty({ example: false })
@ApiProperty({
example: false,
description:
"If true, the owner must upload a repair factor image for this line; expert confirms cost later (validate-factors). Priced lines omit this or set false.",
})
@IsBoolean()
factorNeeded: boolean;
}
@@ -91,7 +95,11 @@ export class SubmitExpertReplyV2Dto {
@IsNotEmpty()
description: string;
@ApiProperty({ type: [PartPricingV2Dto] })
@ApiProperty({
type: [PartPricingV2Dto],
description:
"Each part: manual pricing and `daghi`; set `factorNeeded` only where a repair factor upload is required from the owner.",
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => PartPricingV2Dto)

View File

@@ -65,6 +65,9 @@ import {
enrichBlamePartiesForAgreementView,
} from "src/helpers/blame-party-agreement-decision";
import { resendRequestHasPayload } from "src/helpers/claim-expert-resend";
import {
classifyV2ExpertPricingParts,
} from "src/helpers/claim-v2-expert-reply-workflow";
import {
getClaimCarAngleCaptureBlob,
getDamagedPartCaptureBlob,
@@ -1907,8 +1910,8 @@ export class ExpertClaimService {
/**
* V2 (ClaimCase): validate user-uploaded repair factors on the active expert reply.
* Preconditions: all `factorNeeded` parts have `factorLink`; case is UNDER_REVIEW at EXPERT_COST_EVALUATION.
* — All approved → same as non-factor path: claimStatus APPROVED, step INSURER_REVIEW.
* — Any rejected (expert repriced) → case COMPLETED with expert amounts (damage-expert finalization).
* — All approved → claimStatus APPROVED, INSURER_REVIEW: owner accepts/rejects to close.
* — Any rejected (expert repriced) → WAITING_FOR_INSURER_APPROVAL, APPROVED, INSURER_REVIEW until owner accepts/rejects.
*/
async validateClaimFactorsV2(
claimRequestId: string,
@@ -2072,14 +2075,14 @@ export class ExpertClaimService {
if (anyRejected) {
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: {
status: ClaimCaseStatus.COMPLETED,
status: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
claimStatus: ClaimStatus.APPROVED,
"workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED,
"workflow.currentStep": ClaimWorkflowStep.INSURER_REVIEW,
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
},
$push: {
history: {
type: "FACTORS_VALIDATED_REJECTED_EXPERT_FINALIZED",
type: "FACTORS_VALIDATED_REJECTED_EXPERT_REPRICED",
actor: historyActor,
timestamp: new Date(),
metadata: { replyField },
@@ -2087,13 +2090,26 @@ export class ExpertClaimService {
},
});
const ownerPhoneRejected = await this.resolveClaimOwnerPhone(claim);
if (ownerPhoneRejected) {
const expertLastName =
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
await this.smsOrchestrationService.sendSignatureReviewNotice({
receptor: ownerPhoneRejected,
fileKind: "claim",
publicId: claim.publicId,
expertLastName,
link: this.smsOrchestrationService.buildClaimLink(String(claim._id)),
});
}
return {
message:
"Factors reviewed. Rejected parts were repriced by the expert; the claim is completed.",
"Factors reviewed with repricing. Owner must accept or reject via owner-insurer-approval/sign.",
claimRequestId,
claimStatus: ClaimStatus.APPROVED,
caseStatus: ClaimCaseStatus.COMPLETED,
outcome: "REJECTED_REPRICED_COMPLETED",
caseStatus: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
outcome: "REJECTED_REPRICED_OWNER_SIGN",
};
}
@@ -2350,6 +2366,12 @@ export class ExpertClaimService {
);
}
if (existingResend?.fulfilledAt) {
throw new BadRequestException(
"The owner has already fulfilled a damage-expert resend for this claim. You cannot request another resend.",
);
}
const allowedDocTypes = new Set<string>(
Object.values(ClaimRequiredDocumentType),
);
@@ -2458,10 +2480,11 @@ export class ExpertClaimService {
* - Each part must include `daghi` (option + conditional price/branchId) like V1
*
* On success:
* - Stores reply in evaluation.damageExpertReply
* - Stores reply; clears owner signature fields (`ownerInsurerApproval`, `ownerPricedPartsApproval`)
* - Unlocks the workflow
* - If any part has factorNeeded=true → status = WAITING_FOR_INSURER_APPROVAL, step = EXPERT_COST_EVALUATION
* - Otherwise → status = WAITING_FOR_INSURER_APPROVAL, step = INSURER_REVIEW, claimStatus = APPROVED
* - Pricing-only (`factorNeeded=false` everywhere): WAITING_FOR_INSURER_APPROVAL, APPROVED, INSURER_REVIEW → owner final sign/reject.
* - 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.
*/
async submitExpertReplyV2(
claimRequestId: string,
@@ -2514,7 +2537,11 @@ export class ExpertClaimService {
? this.validateAndNormalizeDaghiForExpertReplyV2(reply.parts)
: [];
const needsFactorUpload = reply.parts?.some((p) => p.factorNeeded === true) ?? false;
const { mixedFactorAndPrice, allFactorLines } = classifyV2ExpertPricingParts(
processedParts,
);
const needsFactorUpload =
reply.parts?.some((p) => p.factorNeeded === true) ?? false;
const objectionSubmitted = !!claim.evaluation?.objection?.submittedAt;
const hasFinalReply = !!claim.evaluation?.damageExpertReplyFinal;
@@ -2546,14 +2573,22 @@ export class ExpertClaimService {
...(expertProfileSnapshot && { expertProfileSnapshot }),
};
const nextStep = needsFactorUpload
? ClaimWorkflowStep.INSURER_REVIEW
: ClaimWorkflowStep.CLAIM_COMPLETED;
let currentStep = ClaimWorkflowStep.INSURER_REVIEW;
let nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED;
const nextCaseStatus = ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL;
const nextClaimStatus = needsFactorUpload
? ClaimStatus.NEEDS_REVISION
: ClaimStatus.APPROVED;
let nextClaimStatus = ClaimStatus.APPROVED;
if (needsFactorUpload) {
nextClaimStatus = ClaimStatus.NEEDS_REVISION;
if (mixedFactorAndPrice) {
currentStep = ClaimWorkflowStep.INSURER_REVIEW;
nextWorkflowStep = ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS;
} else {
currentStep = ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS;
nextWorkflowStep = ClaimWorkflowStep.EXPERT_COST_EVALUATION;
}
}
const updatePayload: Record<string, unknown> = {
status: nextCaseStatus,
@@ -2564,11 +2599,11 @@ export class ExpertClaimService {
'workflow.expiredAt': '',
'workflow.lockedBy': '',
'workflow.preLockQueueSnapshot': '',
'evaluation.ownerInsurerApproval': "",
'evaluation.ownerPricedPartsApproval': "",
},
'workflow.currentStep': nextStep,
'workflow.nextStep': needsFactorUpload
? ClaimWorkflowStep.EXPERT_COST_EVALUATION
: ClaimWorkflowStep.CLAIM_COMPLETED,
'workflow.currentStep': currentStep,
'workflow.nextStep': needsFactorUpload ? nextWorkflowStep : ClaimWorkflowStep.CLAIM_COMPLETED,
[`evaluation.${replyField}`]: replyPayload,
$push: {
'workflow.completedSteps': completedStep,
@@ -2584,6 +2619,8 @@ export class ExpertClaimService {
timestamp: new Date(),
metadata: {
factorNeeded: needsFactorUpload,
mixedFactorAndPrice,
allFactorLines,
partsCount: processedParts.length,
isFinalReplyAfterObjection,
replyField,
@@ -2602,30 +2639,28 @@ export class ExpertClaimService {
idempotencyKey: `claim:${claimRequestId}:handled:${actor.sub}`,
});
// Same effective step as WAITING_FOR_INSURER_APPROVAL @ INSURER_REVIEW: owner reviews/signs (yara-signature).
if (!needsFactorUpload) {
const ownerPhone = await this.resolveClaimOwnerPhone(claim);
if (ownerPhone) {
const expertLastName =
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
await this.smsOrchestrationService.sendSignatureReviewNotice({
receptor: ownerPhone,
fileKind: "claim",
publicId: claim.publicId,
expertLastName,
link: this.smsOrchestrationService.buildClaimLink(
String(claim._id),
),
});
}
const ownerPhoneNotify = await this.resolveClaimOwnerPhone(claim);
if (ownerPhoneNotify && !needsFactorUpload) {
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)),
});
}
return {
claimRequestId,
status: nextCaseStatus,
claimStatus: nextClaimStatus,
currentStep: nextStep,
currentStep,
workflowNextStep: needsFactorUpload ? nextWorkflowStep : ClaimWorkflowStep.CLAIM_COMPLETED,
factorNeeded: needsFactorUpload,
mixedPricingAndFactors: mixedFactorAndPrice,
allPartsFactorNeeded: !!needsFactorUpload && !!allFactorLines,
isFinalReplyAfterObjection,
};
}

View File

@@ -86,7 +86,12 @@ export class ExpertClaimV2Controller {
@ApiOperation({
summary: "Submit expert damage assessment reply",
description:
"Claim must be locked by this expert (status EXPERT_REVIEWING). Submitting unlocks the claim. Each part requires `daghi` with the same rules as V1 (DaghiOption, price for recycled value, branchId for deliver damaged part). If any part has factorNeeded=true the claim moves to EXPERT_COST_EVALUATION; otherwise moves to INSURER_REVIEW. Total payment cap is 30,000,000.",
"**Preconditions:** claim locked by this expert (`EXPERT_REVIEWING`). **Unlocks** the claim. Each part needs `daghi` (V1 rules) and may set `factorNeeded` (repair factor file required from owner) or priced lines only. **Cap:** sum of `totalPayment` ≤ 30,000,000. Clears any prior `evaluation.ownerInsurerApproval` / `ownerPricedPartsApproval`.\n\n" +
"**Frontend routing by outcome:**\n" +
"- **All parts `factorNeeded`:** `status=WAITING_FOR_INSURER_APPROVAL`, `claimStatus=NEEDS_REVISION`, `workflow.currentStep=OWNER_UPLOAD_FACTOR_DOCUMENTS`, `workflow.nextStep=EXPERT_COST_EVALUATION` → owner uploads all factors, then `claimStatus` becomes `UNDER_REVIEW` and `currentStep=EXPERT_COST_EVALUATION` for expert validate-factors.\n" +
"- **Mixed (some priced, some factorNeeded):** same `status`, `claimStatus=NEEDS_REVISION`, `currentStep=INSURER_REVIEW`, `nextStep=OWNER_UPLOAD_FACTOR_DOCUMENTS` → owner must call **owner-insurer-approval/sign** first (priced-line acceptance), then `currentStep` moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` for factor uploads, then expert validation.\n" +
"- **No factors:** `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW`, `nextStep=CLAIM_COMPLETED` → owner final sign/reject only.\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.",
})
@ApiParam({ name: "claimRequestId" })
@ApiBody({ type: SubmitExpertReplyV2Dto })
@@ -103,7 +108,8 @@ export class ExpertClaimV2Controller {
summary: "Submit expert damage resend request",
description:
"Claim must be locked by this expert (`EXPERT_REVIEWING`). Sets `WAITING_FOR_USER_RESEND`, `USER_EXPERT_RESEND`, `NEEDS_REVISION`, clears the lock, and stores `evaluation.damageExpertResend`. " +
"The owner uploads via the same document/capture endpoints or `POST .../expert-resend/acknowledge` when only instructions were given.",
"Owner completes via document/capture endpoints or `POST .../expert-resend/acknowledge` when only instructions were given.\n\n" +
"**One resend per claim lifecycle:** if the owner has already fulfilled a resend (`damageExpertResend.fulfilledAt`), this endpoint returns **400**—the expert may only submit a priced reply or request in-person visit afterward.",
})
@ApiParam({ name: "claimRequestId" })
@ApiBody({ type: ClaimSubmitResendV2Dto })
@@ -139,7 +145,12 @@ export class ExpertClaimV2Controller {
@ApiOperation({
summary: "Validate uploaded repair factors (V2 ClaimCase)",
description:
"After all required factors are uploaded, expert approves or rejects each part. Rejected parts must include expert totalPayment (or price and salary). All approved → insurer review. Any rejected → expert repricing and case COMPLETED.",
"**Preconditions:** `status=WAITING_FOR_INSURER_APPROVAL`, `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_COST_EVALUATION`, every `factorNeeded` line has `factorLink`.\n\n" +
"**Decisions:** each factor line gets `APPROVED` or `REJECTED`; rejected lines require expert `totalPayment` (or both `price` and `salary`).\n\n" +
"**Outcomes:**\n" +
"- **All approved:** `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW` → owner must **sign or reject** via `owner-insurer-approval/sign`; agree completes the case.\n" +
"- **Any rejected (repriced):** same waiter + `INSURER_REVIEW` → owner still must **accept or reject** the repriced totals (case is not completed until they sign).\n" +
"- **Partial batch:** service may return pending until every factor line has a non-pending decision.",
})
@ApiParam({ name: "claimRequestId" })
@ApiBody({ type: FactorValidationV2Dto })