forked from Yara724/api
Merge pull request 'changed logs' (#65) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#65
This commit is contained in:
@@ -49,18 +49,24 @@ class FactorDecisionV2Dto {
|
|||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description:
|
description:
|
||||||
"Expert-adjusted values (Persian/ASCII digits ok). For REJECTED factors, provide at least totalPayment (or price and salary).",
|
"Part price (Persian/ASCII digits). For each factor line, send totalPayment **or** both price and salary. Required for **APPROVED** (accepted amount read from factor / expert) and **REJECTED** (expert’s replacement pricing).",
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
price?: string;
|
price?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Salary portion when not using a single totalPayment (must be sent together with price for APPROVED/REJECTED factor lines).",
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
salary?: string;
|
salary?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Line total payment; preferred when the expert enters one number. Required together with APPROVED/REJECTED unless both price and salary are sent.",
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
totalPayment?: string;
|
totalPayment?: string;
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ import {
|
|||||||
ExpertFileKind,
|
ExpertFileKind,
|
||||||
} from "src/users/entities/schema/expert-file-activity.schema";
|
} from "src/users/entities/schema/expert-file-activity.schema";
|
||||||
|
|
||||||
|
/** Maximum sum of line `totalPayment` across the claim (priced parts + factor lines after validation). */
|
||||||
|
const CLAIM_V2_TOTAL_PAYMENT_CAP = 53_000_000;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpertClaimService {
|
export class ExpertClaimService {
|
||||||
private readonly logger = new Logger(ExpertClaimService.name);
|
private readonly logger = new Logger(ExpertClaimService.name);
|
||||||
@@ -731,6 +734,39 @@ export class ExpertClaimService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Factor validation: expert must send a line amount (OCR is not used for factor photos). */
|
||||||
|
private expertFactorValidationDecisionHasLinePricing(decision: {
|
||||||
|
totalPayment?: string;
|
||||||
|
price?: string;
|
||||||
|
salary?: string;
|
||||||
|
}): boolean {
|
||||||
|
const tp =
|
||||||
|
decision.totalPayment != null &&
|
||||||
|
String(decision.totalPayment).trim() !== "";
|
||||||
|
const pr =
|
||||||
|
decision.price != null && String(decision.price).trim() !== "";
|
||||||
|
const sa =
|
||||||
|
decision.salary != null && String(decision.salary).trim() !== "";
|
||||||
|
return tp || (pr && sa);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Price-cap total for one reply line: `totalPayment` if set, otherwise `price` + `salary`. */
|
||||||
|
private claimReplyPartLineTotalForCap(part: {
|
||||||
|
partId?: string;
|
||||||
|
totalPayment?: unknown;
|
||||||
|
price?: unknown;
|
||||||
|
salary?: unknown;
|
||||||
|
}): number {
|
||||||
|
const tpRaw =
|
||||||
|
part.totalPayment != null ? String(part.totalPayment).trim() : "";
|
||||||
|
if (tpRaw !== "") {
|
||||||
|
return this.parsePersianNumber(tpRaw);
|
||||||
|
}
|
||||||
|
const pr = this.parsePersianNumber(String(part.price ?? "0"));
|
||||||
|
const sa = this.parsePersianNumber(String(part.salary ?? "0"));
|
||||||
|
return pr + sa;
|
||||||
|
}
|
||||||
|
|
||||||
/** Same rules as V1 `submitReplyRequest`: daghi option + conditional price / branchId; `branchId` stored as ObjectId. */
|
/** Same rules as V1 `submitReplyRequest`: daghi option + conditional price / branchId; `branchId` stored as ObjectId. */
|
||||||
private validateAndNormalizeDaghiForExpertReplyV2(
|
private validateAndNormalizeDaghiForExpertReplyV2(
|
||||||
parts: import("./dto/expert-claim-v2.dto").PartPricingV2Dto[],
|
parts: import("./dto/expert-claim-v2.dto").PartPricingV2Dto[],
|
||||||
@@ -1433,9 +1469,9 @@ export class ExpertClaimService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate total price cap (30,000,000)
|
// Validate total price cap (priced lines sum)
|
||||||
if (reply.parts && reply.parts.length > 0) {
|
if (reply.parts && reply.parts.length > 0) {
|
||||||
const PRICE_CAP = 30000000;
|
const PRICE_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP;
|
||||||
let totalPrice = 0;
|
let totalPrice = 0;
|
||||||
|
|
||||||
for (const part of reply.parts) {
|
for (const part of reply.parts) {
|
||||||
@@ -1466,7 +1502,7 @@ export class ExpertClaimService {
|
|||||||
|
|
||||||
if (totalPrice > PRICE_CAP) {
|
if (totalPrice > PRICE_CAP) {
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
message: `[PRICE_CAP_ERROR] Total price of damaged parts (${totalPrice.toLocaleString()}) exceeds the maximum allowed amount (${PRICE_CAP.toLocaleString()}). Please adjust the prices.`,
|
message: `You have reached the maximum acceptable total price. The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${PRICE_CAP.toLocaleString()}).`,
|
||||||
error: "PRICE_CAP_ERROR",
|
error: "PRICE_CAP_ERROR",
|
||||||
code: "PRICE_CAP_ERROR",
|
code: "PRICE_CAP_ERROR",
|
||||||
totalPrice: totalPrice,
|
totalPrice: totalPrice,
|
||||||
@@ -1985,11 +2021,55 @@ export class ExpertClaimService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the claim after factor validation without a second insurer-review signature.
|
||||||
|
* Owner already uploaded factor files; mixed replies already have priced-line acceptance on file.
|
||||||
|
*/
|
||||||
|
private async completeClaimCaseAfterFactorValidationV2(
|
||||||
|
claimRequestId: string,
|
||||||
|
claimForTenant: any,
|
||||||
|
actor: { sub: string; fullName?: string; clientKey?: string },
|
||||||
|
historyType: string,
|
||||||
|
historyActor: {
|
||||||
|
actorId: Types.ObjectId;
|
||||||
|
actorName?: string;
|
||||||
|
actorType: string;
|
||||||
|
},
|
||||||
|
metadata: Record<string, unknown>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
|
$set: {
|
||||||
|
status: ClaimCaseStatus.COMPLETED,
|
||||||
|
claimStatus: ClaimStatus.APPROVED,
|
||||||
|
"workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||||
|
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||||
|
},
|
||||||
|
$push: {
|
||||||
|
"workflow.completedSteps": ClaimWorkflowStep.INSURER_REVIEW,
|
||||||
|
history: {
|
||||||
|
type: historyType,
|
||||||
|
actor: historyActor,
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await this.recordClaimExpertActivity({
|
||||||
|
expertId: String(actor.sub),
|
||||||
|
tenantId: this.claimActivityTenantId(claimForTenant, actor),
|
||||||
|
claimId: String(claimRequestId),
|
||||||
|
eventType: ExpertFileActivityType.HANDLED,
|
||||||
|
idempotencyKey: `claim:${claimRequestId}:factor_validation:${historyType}:${actor.sub}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2 (ClaimCase): validate user-uploaded repair factors on the active expert reply.
|
* 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.
|
* Preconditions: all `factorNeeded` parts have `factorLink`; case is UNDER_REVIEW at EXPERT_COST_EVALUATION.
|
||||||
* — All approved → claimStatus APPROVED, INSURER_REVIEW: owner accepts/rejects to close.
|
* — All approved → COMPLETED + APPROVED (expert-entered line totals; no extra owner signature).
|
||||||
* — Any rejected (expert repriced) → INSURER_REVIEW_AWAITING_OWNER_SIGN, APPROVED, INSURER_REVIEW until owner accepts/rejects.
|
* — Any rejected (repriced) → COMPLETED + APPROVED (auto-close for now; owner sign may be added later).
|
||||||
|
* Total of all repair lines must be ≤ `CLAIM_V2_TOTAL_PAYMENT_CAP` (53_000_000; same as initial expert reply).
|
||||||
|
* Response: `claimStatus` = `ClaimStatus`; `caseStatus` = `ClaimCaseStatus`.
|
||||||
*/
|
*/
|
||||||
async validateClaimFactorsV2(
|
async validateClaimFactorsV2(
|
||||||
claimRequestId: string,
|
claimRequestId: string,
|
||||||
@@ -2065,13 +2145,13 @@ export class ExpertClaimService {
|
|||||||
$set[`${base}.totalPayment`] = decision.totalPayment;
|
$set[`${base}.totalPayment`] = decision.totalPayment;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decision.status === FactorStatus.REJECTED) {
|
if (
|
||||||
const hasOverride =
|
decision.status === FactorStatus.APPROVED ||
|
||||||
decision.totalPayment != null ||
|
decision.status === FactorStatus.REJECTED
|
||||||
(decision.price != null && decision.salary != null);
|
) {
|
||||||
if (!hasOverride) {
|
if (!this.expertFactorValidationDecisionHasLinePricing(decision)) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`Part ${decision.partId}: rejected factors require expert totalPayment (or both price and salary).`,
|
`Part ${decision.partId}: ${decision.status === FactorStatus.APPROVED ? "approved" : "rejected"} factor lines require expert totalPayment (or both price and salary).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2123,17 +2203,21 @@ export class ExpertClaimService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRICE_CAP = 30_000_000;
|
const PRICE_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP;
|
||||||
let totalPrice = 0;
|
let totalPrice = 0;
|
||||||
for (const part of updatedReply.parts || []) {
|
for (const part of updatedReply.parts || []) {
|
||||||
const parsed = this.parsePersianNumber(String(part.totalPayment ?? "0"));
|
const line = this.claimReplyPartLineTotalForCap(part);
|
||||||
if (!isNaN(parsed)) {
|
if (isNaN(line)) {
|
||||||
totalPrice += parsed;
|
throw new BadRequestException({
|
||||||
|
message: `Part ${String((part as { partId?: string }).partId ?? "")}: invalid amount for price total — use totalPayment or price and salary.`,
|
||||||
|
error: "PRICE_CAP_ERROR",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
totalPrice += line;
|
||||||
}
|
}
|
||||||
if (totalPrice > PRICE_CAP) {
|
if (totalPrice > PRICE_CAP) {
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
message: `Total price (${totalPrice.toLocaleString()}) exceeds the cap of ${PRICE_CAP.toLocaleString()}`,
|
message: `You have reached the maximum acceptable total price. The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${PRICE_CAP.toLocaleString()}).`,
|
||||||
error: "PRICE_CAP_ERROR",
|
error: "PRICE_CAP_ERROR",
|
||||||
totalPrice,
|
totalPrice,
|
||||||
priceCap: PRICE_CAP,
|
priceCap: PRICE_CAP,
|
||||||
@@ -2147,90 +2231,40 @@ export class ExpertClaimService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (anyRejected) {
|
if (anyRejected) {
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
await this.completeClaimCaseAfterFactorValidationV2(
|
||||||
$set: {
|
claimRequestId,
|
||||||
status: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
|
claim,
|
||||||
claimStatus: ClaimStatus.APPROVED,
|
actor,
|
||||||
"workflow.currentStep": ClaimWorkflowStep.INSURER_REVIEW,
|
"FACTORS_REJECTED_REPRICED_AUTO_COMPLETED",
|
||||||
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
historyActor,
|
||||||
},
|
{ replyField },
|
||||||
$push: {
|
);
|
||||||
history: {
|
|
||||||
type: "FACTORS_VALIDATED_REJECTED_EXPERT_REPRICED",
|
|
||||||
actor: historyActor,
|
|
||||||
timestamp: new Date(),
|
|
||||||
metadata: { replyField },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
message:
|
message:
|
||||||
"Factors reviewed with repricing. Owner must accept or reject via owner-insurer-approval/sign.",
|
"Factors were reviewed with expert repricing on rejected lines. The claim is completed without an owner signature (temporary policy; may require owner acceptance later).",
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
claimStatus: ClaimStatus.APPROVED,
|
claimStatus: ClaimStatus.APPROVED,
|
||||||
caseStatus: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
|
caseStatus: ClaimCaseStatus.COMPLETED,
|
||||||
outcome: "REJECTED_REPRICED_OWNER_SIGN",
|
outcome: "REJECTED_REPRICED_AUTO_COMPLETED",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
await this.completeClaimCaseAfterFactorValidationV2(
|
||||||
$set: {
|
claimRequestId,
|
||||||
claimStatus: ClaimStatus.APPROVED,
|
claim,
|
||||||
"workflow.currentStep": ClaimWorkflowStep.INSURER_REVIEW,
|
actor,
|
||||||
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
"FACTORS_VALIDATED_ALL_APPROVED_AUTO_COMPLETED",
|
||||||
},
|
historyActor,
|
||||||
$push: {
|
{ replyField },
|
||||||
history: {
|
);
|
||||||
type: "FACTORS_VALIDATED_ALL_APPROVED",
|
|
||||||
actor: historyActor,
|
|
||||||
timestamp: new Date(),
|
|
||||||
metadata: { replyField },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.recordClaimExpertActivity({
|
|
||||||
expertId: String(actor.sub),
|
|
||||||
tenantId: this.claimActivityTenantId(claim, actor),
|
|
||||||
claimId: String(claimRequestId),
|
|
||||||
eventType: ExpertFileActivityType.UNCHECKED,
|
|
||||||
idempotencyKey: `claim:${claimRequestId}:unchecked:resend:${actor.sub}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ownerPhoneFactors = await this.resolveClaimOwnerPhone(claim);
|
|
||||||
if (ownerPhoneFactors) {
|
|
||||||
const expertLastName =
|
|
||||||
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
|
|
||||||
await this.smsOrchestrationService.sendSignatureReviewNotice({
|
|
||||||
receptor: ownerPhoneFactors,
|
|
||||||
fileKind: "claim",
|
|
||||||
publicId: claim.publicId,
|
|
||||||
expertLastName,
|
|
||||||
link: this.smsOrchestrationService.buildClaimLink(String(claim._id)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message:
|
message:
|
||||||
"All factors approved. The claim proceeds to insurer review (same as non-factor expert reply).",
|
"All factors were approved by the expert. The claim is completed without an additional owner signature.",
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
claimStatus: ClaimStatus.APPROVED,
|
claimStatus: ClaimStatus.APPROVED,
|
||||||
caseStatus: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
|
caseStatus: ClaimCaseStatus.COMPLETED,
|
||||||
outcome: "ALL_APPROVED_INSURER_REVIEW",
|
outcome: "ALL_APPROVED_AUTO_COMPLETED",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2578,7 +2612,7 @@ export class ExpertClaimService {
|
|||||||
* - Claim must exist
|
* - Claim must exist
|
||||||
* - Must be locked by this expert (workflow.lockedBy.actorId === actor.sub)
|
* - Must be locked by this expert (workflow.lockedBy.actorId === actor.sub)
|
||||||
* - Must be in EXPERT_REVIEWING status
|
* - Must be in EXPERT_REVIEWING status
|
||||||
* - Total payment across all parts must not exceed 30,000,000
|
* - Total payment across all parts must not exceed 53,000,000 (same cap as factor validation totals)
|
||||||
* - Each part must include `daghi` (option + conditional price/branchId) like V1
|
* - Each part must include `daghi` (option + conditional price/branchId) like V1
|
||||||
*
|
*
|
||||||
* On success:
|
* On success:
|
||||||
@@ -2619,7 +2653,7 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Price cap validation
|
// Price cap validation
|
||||||
const PRICE_CAP = 53_000_000;
|
const PRICE_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP;
|
||||||
let totalPrice = 0;
|
let totalPrice = 0;
|
||||||
for (const part of reply.parts || []) {
|
for (const part of reply.parts || []) {
|
||||||
const parsed = this.parsePersianNumber(String(part.totalPayment ?? '0'));
|
const parsed = this.parsePersianNumber(String(part.totalPayment ?? '0'));
|
||||||
@@ -2627,7 +2661,7 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
if (totalPrice > PRICE_CAP) {
|
if (totalPrice > PRICE_CAP) {
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
message: `Total price (${totalPrice.toLocaleString()}) exceeds the cap of ${PRICE_CAP.toLocaleString()}`,
|
message: `You have reached the maximum acceptable total price. The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${PRICE_CAP.toLocaleString()}).`,
|
||||||
error: 'PRICE_CAP_ERROR',
|
error: 'PRICE_CAP_ERROR',
|
||||||
totalPrice,
|
totalPrice,
|
||||||
priceCap: PRICE_CAP,
|
priceCap: PRICE_CAP,
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export class ExpertClaimV2Controller {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Submit expert damage assessment reply",
|
summary: "Submit expert damage assessment reply",
|
||||||
description:
|
description:
|
||||||
"**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" +
|
"**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 line `totalPayment` values ≤ 53,000,000 (same limit as factor-validation totals across priced + factor lines). Clears any prior `evaluation.ownerInsurerApproval` / `ownerPricedPartsApproval`.\n\n" +
|
||||||
"**Frontend routing by `ClaimCaseStatus` (`status`):**\n" +
|
"**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" +
|
"- **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" +
|
"- **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" +
|
||||||
@@ -176,12 +176,14 @@ export class ExpertClaimV2Controller {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Validate uploaded repair factors (V2 ClaimCase)",
|
summary: "Validate uploaded repair factors (V2 ClaimCase)",
|
||||||
description:
|
description:
|
||||||
|
"**Response:** `claimStatus` = `ClaimStatus` (e.g. APPROVED). `caseStatus` = `ClaimCaseStatus` (e.g. COMPLETED vs insurer-review) — they are not interchangeable.\n\n" +
|
||||||
"**Preconditions:** `status=EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`), `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_COST_EVALUATION`, every `factorNeeded` line has `factorLink`.\n\n" +
|
"**Preconditions:** `status=EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `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" +
|
"**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**; otherwise `PRICE_CAP_ERROR` with message that the maximum acceptable total was exceeded.\n\n" +
|
||||||
"**Outcomes:**\n" +
|
"**Outcomes:**\n" +
|
||||||
"- **All approved:** `status=INSURER_REVIEW_AWAITING_OWNER_SIGN`, `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW` → owner must **sign or reject** via `owner-insurer-approval/sign`; agree completes the case.\n" +
|
"- **All approved:** `caseStatus=COMPLETED`, `claimStatus=APPROVED`, workflow `CLAIM_COMPLETED` — no owner signature.\n" +
|
||||||
"- **Any rejected (repriced):** same as above (`INSURER_REVIEW_AWAITING_OWNER_SIGN` + `INSURER_REVIEW`) → owner must **accept or reject** the repriced totals.\n" +
|
"- **Any rejected (repriced):** same auto-complete for now (owner acceptance may be added later).\n" +
|
||||||
"- **Partial batch:** service may return pending until every factor line has a non-pending decision.",
|
"- **Partial batch:** returns pending until every factor line has a non-pending decision (no cap error until the batch is complete).",
|
||||||
})
|
})
|
||||||
@ApiParam({ name: "claimRequestId" })
|
@ApiParam({ name: "claimRequestId" })
|
||||||
@ApiBody({ type: FactorValidationV2Dto })
|
@ApiBody({ type: FactorValidationV2Dto })
|
||||||
|
|||||||
Reference in New Issue
Block a user