forked from Yara724/api
Better error handling and validation and min/max prices added for expert submit
This commit is contained in:
@@ -913,6 +913,19 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
private expertReplySubmissionError(
|
||||
message: string,
|
||||
code: string,
|
||||
details: Record<string, unknown> = {},
|
||||
) {
|
||||
return {
|
||||
message,
|
||||
error: "EXPERT_REPLY_SUBMISSION_ERROR",
|
||||
code,
|
||||
...details,
|
||||
};
|
||||
}
|
||||
|
||||
/** Factor validation: expert must send a line amount (OCR is not used for factor photos). */
|
||||
private expertFactorValidationDecisionHasLinePricing(decision: {
|
||||
totalPayment?: string;
|
||||
@@ -948,28 +961,53 @@ export class ExpertClaimService {
|
||||
private validateAndNormalizeDaghiForExpertReplyV2(
|
||||
parts: import("./dto/expert-claim-v2.dto").PartPricingV2Dto[],
|
||||
) {
|
||||
for (const part of parts) {
|
||||
for (const [partIndex, part] of parts.entries()) {
|
||||
if (part.typeOfDamage === TypeOfDamage.Repair) continue;
|
||||
if (!part.daghi || !part.daghi.option) {
|
||||
throw new BadRequestException(
|
||||
`Daghi option is required for part ${part.partId}`,
|
||||
this.expertReplySubmissionError(
|
||||
`گزینه داغی برای قطعه ${part.partId} الزامی است.`,
|
||||
"DAGHI_OPTION_REQUIRED",
|
||||
{ field: `parts[${partIndex}].daghi.option`, partId: part.partId },
|
||||
),
|
||||
);
|
||||
}
|
||||
if (part.daghi.option === DaghiOption.RECYCLED_PARTS_VALUE) {
|
||||
if (!part.daghi.price) {
|
||||
throw new BadRequestException(
|
||||
`Price is required for part ${part.partId} when option is '${DaghiOption.RECYCLED_PARTS_VALUE}'`,
|
||||
this.expertReplySubmissionError(
|
||||
`قیمت داغی برای قطعه ${part.partId} الزامی است.`,
|
||||
"DAGHI_PRICE_REQUIRED",
|
||||
{
|
||||
field: `parts[${partIndex}].daghi.price`,
|
||||
partId: part.partId,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (part.daghi.option === DaghiOption.DELIVER_DAMAGED_PART) {
|
||||
if (!part.daghi.branchId) {
|
||||
throw new BadRequestException(
|
||||
`Branch ID is required for part ${part.partId} when option is '${DaghiOption.DELIVER_DAMAGED_PART}'`,
|
||||
this.expertReplySubmissionError(
|
||||
`شعبه تحویل داغی برای قطعه ${part.partId} الزامی است.`,
|
||||
"DAGHI_BRANCH_REQUIRED",
|
||||
{
|
||||
field: `parts[${partIndex}].daghi.branchId`,
|
||||
partId: part.partId,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!Types.ObjectId.isValid(part.daghi.branchId)) {
|
||||
throw new BadRequestException(
|
||||
`Invalid branch ID format for part ${part.partId}`,
|
||||
this.expertReplySubmissionError(
|
||||
`شناسه شعبه برای قطعه ${part.partId} معتبر نیست.`,
|
||||
"DAGHI_BRANCH_INVALID",
|
||||
{
|
||||
field: `parts[${partIndex}].daghi.branchId`,
|
||||
partId: part.partId,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2300,7 +2338,12 @@ export class ExpertClaimService {
|
||||
) {
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim) {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
throw new NotFoundException(
|
||||
this.expertReplySubmissionError(
|
||||
"پرونده خسارت یافت نشد.",
|
||||
"CLAIM_NOT_FOUND",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await this.assertExpertActorOnClaim(claim, actor);
|
||||
@@ -3265,29 +3308,47 @@ export class ExpertClaimService {
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claim) {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
throw new NotFoundException(
|
||||
this.expertReplySubmissionError(
|
||||
"پرونده خسارت یافت نشد.",
|
||||
"CLAIM_NOT_FOUND",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await this.assertExpertActorOnClaim(claim, actor);
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||
throw new BadRequestException(
|
||||
`Claim is not in a reviewable state. Current status: ${claim.status}`,
|
||||
this.expertReplySubmissionError(
|
||||
"پرونده در وضعیت قابل بررسی برای ثبت پاسخ کارشناسی نیست.",
|
||||
"CLAIM_NOT_REVIEWABLE",
|
||||
{ currentStatus: claim.status },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!claim.workflow?.locked) {
|
||||
throw new ForbiddenException(
|
||||
"You must lock the claim before submitting a reply",
|
||||
this.expertReplySubmissionError(
|
||||
"پیش از ثبت پاسخ کارشناسی باید پرونده را قفل کنید.",
|
||||
"CLAIM_NOT_LOCKED",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (claim.workflow.lockedBy?.actorId?.toString() !== actor.sub) {
|
||||
throw new ForbiddenException("This claim is locked by another expert");
|
||||
throw new ForbiddenException(
|
||||
this.expertReplySubmissionError(
|
||||
"این پرونده توسط کارشناس دیگری قفل شده است.",
|
||||
"CLAIM_LOCKED_BY_ANOTHER_EXPERT",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const pricingValidationError = getExpertReplyPricingValidationError(
|
||||
reply.parts,
|
||||
{ locale: "fa", enforceAmountBounds: true },
|
||||
);
|
||||
if (pricingValidationError) {
|
||||
throw new BadRequestException(pricingValidationError);
|
||||
@@ -3305,8 +3366,9 @@ export class ExpertClaimService {
|
||||
}
|
||||
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()}).`,
|
||||
message: `مجموع مبلغ قطعات (${totalPrice.toLocaleString("fa-IR")}) از سقف مجاز (${priceCap.toLocaleString("fa-IR")}) تومان بیشتر است.`,
|
||||
error: "PRICE_CAP_ERROR",
|
||||
code: "PRICE_CAP_ERROR",
|
||||
totalPrice,
|
||||
priceCap,
|
||||
});
|
||||
@@ -3337,7 +3399,11 @@ export class ExpertClaimService {
|
||||
);
|
||||
if (!selected) {
|
||||
throw new BadRequestException(
|
||||
`Unknown partId "${p.partId}". Use numeric catalog id from claim detail damagedParts.`,
|
||||
this.expertReplySubmissionError(
|
||||
`قطعه با شناسه ${p.partId} در فهرست قطعات آسیبدیده پرونده وجود ندارد.`,
|
||||
"PART_NOT_ON_CLAIM",
|
||||
{ field: "partId", partId: p.partId },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3355,22 +3421,32 @@ export class ExpertClaimService {
|
||||
);
|
||||
} catch (err) {
|
||||
throw new BadRequestException(
|
||||
`Invalid part ${p.partId}: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
this.expertReplySubmissionError(
|
||||
`اطلاعات قطعه ${p.partId} معتبر نیست.`,
|
||||
"PART_INVALID",
|
||||
{ field: "partId", partId: p.partId },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const catalogId = catalogPartIdFromCarPartDamage(carPartDamage);
|
||||
if (catalogId == null) {
|
||||
throw new BadRequestException(
|
||||
`Invalid part ${p.partId}: a numeric catalog id is required.`,
|
||||
this.expertReplySubmissionError(
|
||||
`شناسه قطعه ${p.partId} معتبر نیست.`,
|
||||
"PART_ID_INVALID",
|
||||
{ field: "partId", partId: p.partId },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (seenCatalogIds.has(catalogId)) {
|
||||
throw new BadRequestException(
|
||||
`Duplicate part submitted in expert reply: ${catalogId}.`,
|
||||
this.expertReplySubmissionError(
|
||||
`قطعه ${catalogId} بیش از یکبار ارسال شده است.`,
|
||||
"DUPLICATE_PART",
|
||||
{ field: "partId", partId: catalogId },
|
||||
),
|
||||
);
|
||||
}
|
||||
seenCatalogIds.add(catalogId);
|
||||
@@ -3399,7 +3475,10 @@ export class ExpertClaimService {
|
||||
|
||||
if (objectionSubmitted && hasFinalReply) {
|
||||
throw new ConflictException(
|
||||
"A final expert reply after objection already exists for this claim.",
|
||||
this.expertReplySubmissionError(
|
||||
"پاسخ نهایی کارشناسی پس از اعتراض، پیشتر برای این پرونده ثبت شده است.",
|
||||
"FINAL_REPLY_ALREADY_SUBMITTED",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user