1
0
forked from Yara724/api

Merge upstream/main into main

Resolve conflicts while keeping field-expert lock/view fixes and user
party-access query improvements from the fork.
This commit is contained in:
2026-06-18 18:27:01 +03:30
6 changed files with 839 additions and 97 deletions

View File

@@ -16,6 +16,10 @@ import { distance as stringDistance } from "fastest-levenshtein"; // حتما ن
import { Types } from "mongoose";
import { lastValueFrom } from "rxjs";
import { ClaimRequestManagementDbService } from "src/claim-request-management/entites/db-service/claim-request-management.db.service";
import {
ClaimRequestManagementService,
FanavaranAutoSubmitResult,
} from "src/claim-request-management/claim-request-management.service";
import { ClaimSignDbService } from "src/claim-request-management/entites/db-service/claim-sign.db.service";
import { DamageImageDbService } from "src/claim-request-management/entites/db-service/damage-image.db.service";
import { VideoCaptureDbService } from "src/claim-request-management/entites/db-service/video-capture.db.service";
@@ -135,14 +139,12 @@ import {
} from "src/users/entities/schema/expert-file-activity.schema";
/** Maximum sum of line `totalPayment` across the claim (Toman; priced parts + factor lines after validation). */
import { CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN } from "src/constants/repair-amount-limits";
import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { applyListQueryV2 } from "src/helpers/list-query-v2";
import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher";
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
const CLAIM_V2_TOTAL_PAYMENT_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN;
@Injectable()
export class ExpertClaimService {
private readonly logger = new Logger(ExpertClaimService.name);
@@ -268,8 +270,22 @@ export class ExpertClaimService {
private readonly userDbService: UserDbService,
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
private readonly claimSignDbService: ClaimSignDbService,
private readonly claimRequestManagementService: ClaimRequestManagementService,
) {}
private appendFanavaranAutoSubmitToMessage(
baseMessage: string,
fanavaran: FanavaranAutoSubmitResult,
): string {
if (fanavaran.submitted) {
return `${baseMessage} The claim was sent to Fanavaran successfully.`;
}
if (fanavaran.warning) {
return `${baseMessage} ${fanavaran.warning}`;
}
return baseMessage;
}
/**
* Resolve a `claim-sign` document id to a downloadable file URL.
* Mirrors the helper used in `ExpertInsurerService` so signature links are
@@ -1561,9 +1577,9 @@ export class ExpertClaimService {
);
}
// Validate total price cap (priced lines sum)
if (reply.parts && reply.parts.length > 0) {
const PRICE_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP;
// Validate total price cap (priced lines sum), when enabled.
const priceCap = getClaimV2TotalPaymentCapToman();
if (priceCap !== null && reply.parts && reply.parts.length > 0) {
let totalPrice = 0;
for (const part of reply.parts) {
@@ -1592,13 +1608,13 @@ export class ExpertClaimService {
}
}
if (totalPrice > PRICE_CAP) {
if (totalPrice > priceCap) {
throw new BadRequestException({
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${PRICE_CAP.toLocaleString()}).`,
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${priceCap.toLocaleString()}).`,
error: "PRICE_CAP_ERROR",
code: "PRICE_CAP_ERROR",
totalPrice: totalPrice,
priceCap: PRICE_CAP,
priceCap,
});
}
}
@@ -2160,7 +2176,7 @@ export class ExpertClaimService {
* Preconditions: all `factorNeeded` parts have `factorLink`; case is UNDER_REVIEW at EXPERT_COST_EVALUATION.
* — All approved → COMPLETED + APPROVED (expert-entered line totals; no extra owner signature).
* — 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` Toman (53_000_000; same as initial expert reply).
* When enabled by env, total of all repair lines must be ≤ the claim v2 total payment cap.
* Response: `claimStatus` = `ClaimStatus`; `caseStatus` = `ClaimCaseStatus`.
*/
async validateClaimFactorsV2(
@@ -2295,25 +2311,27 @@ export class ExpertClaimService {
};
}
const PRICE_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP;
let totalPrice = 0;
for (const part of updatedReply.parts || []) {
const line = this.claimReplyPartLineTotalForCap(part);
if (isNaN(line)) {
const priceCap = getClaimV2TotalPaymentCapToman();
if (priceCap !== null) {
let totalPrice = 0;
for (const part of updatedReply.parts || []) {
const line = this.claimReplyPartLineTotalForCap(part);
if (isNaN(line)) {
throw new BadRequestException({
message: `Part ${String(part.partId ?? "")}: invalid amount for price total — use totalPayment or price and salary.`,
error: "PRICE_CAP_ERROR",
});
}
totalPrice += line;
}
if (totalPrice > priceCap) {
throw new BadRequestException({
message: `Part ${String(part.partId ?? "")}: invalid amount for price total — use totalPayment or price and salary.`,
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${priceCap.toLocaleString()}).`,
error: "PRICE_CAP_ERROR",
totalPrice,
priceCap,
});
}
totalPrice += line;
}
if (totalPrice > PRICE_CAP) {
throw new BadRequestException({
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${PRICE_CAP.toLocaleString()}).`,
error: "PRICE_CAP_ERROR",
totalPrice,
priceCap: PRICE_CAP,
});
}
const historyActor = {
@@ -2331,13 +2349,20 @@ export class ExpertClaimService {
historyActor,
{ replyField },
);
const fanavaran =
await this.claimRequestManagementService.autoSubmitToFanavaranParsianV2OnClaimCompleted(
claimRequestId,
);
return {
message:
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,
),
claimRequestId,
claimStatus: ClaimStatus.APPROVED,
caseStatus: ClaimCaseStatus.COMPLETED,
outcome: "REJECTED_REPRICED_AUTO_COMPLETED",
fanavaran,
};
}
@@ -2350,13 +2375,21 @@ export class ExpertClaimService {
{ replyField },
);
const fanavaran =
await this.claimRequestManagementService.autoSubmitToFanavaranParsianV2OnClaimCompleted(
claimRequestId,
);
return {
message:
message: this.appendFanavaranAutoSubmitToMessage(
"All factors were approved by the expert. The claim is completed without an additional owner signature.",
fanavaran,
),
claimRequestId,
claimStatus: ClaimStatus.APPROVED,
caseStatus: ClaimCaseStatus.COMPLETED,
outcome: "ALL_APPROVED_AUTO_COMPLETED",
fanavaran,
};
}
@@ -2950,20 +2983,22 @@ export class ExpertClaimService {
throw new ForbiddenException("This claim is locked by another expert");
}
// Price cap validation
const PRICE_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP;
let totalPrice = 0;
for (const part of reply.parts || []) {
const parsed = this.parsePersianNumber(String(part.totalPayment ?? "0"));
if (!isNaN(parsed)) totalPrice += parsed;
}
if (totalPrice > PRICE_CAP) {
throw new BadRequestException({
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${PRICE_CAP.toLocaleString()}).`,
error: "PRICE_CAP_ERROR",
totalPrice,
priceCap: PRICE_CAP,
});
// Price cap validation, when enabled.
const priceCap = getClaimV2TotalPaymentCapToman();
if (priceCap !== null) {
let totalPrice = 0;
for (const part of reply.parts || []) {
const parsed = this.parsePersianNumber(String(part.totalPayment ?? "0"));
if (!isNaN(parsed)) totalPrice += parsed;
}
if (totalPrice > priceCap) {
throw new BadRequestException({
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${priceCap.toLocaleString()}).`,
error: "PRICE_CAP_ERROR",
totalPrice,
priceCap,
});
}
}
const carTypeSubmit = claim.vehicle?.carType as