From 7a3ddcc7beb420c3e1ecaff74eae8ff65ef6dc98 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sat, 11 Jul 2026 12:23:00 +0330 Subject: [PATCH] YARA-937 --- .../exceptions/business-rule.exception.ts | 25 +++++++++++++++++++ src/expert-claim/expert-claim.service.ts | 7 +++++- .../expert-claim.v2.controller.ts | 15 ++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/expert-claim/exceptions/business-rule.exception.ts diff --git a/src/expert-claim/exceptions/business-rule.exception.ts b/src/expert-claim/exceptions/business-rule.exception.ts new file mode 100644 index 0000000..97c7d7a --- /dev/null +++ b/src/expert-claim/exceptions/business-rule.exception.ts @@ -0,0 +1,25 @@ +import { HttpException, HttpStatus } from "@nestjs/common"; + +/** + * Thrown when a well-formed request violates a business rule that cannot be + * expressed as a generic validation error. Returns HTTP 422 with a structured + * body so the frontend can branch on `errorCode` without string-matching the + * human-readable `message`. + * + * Response body shape: + * ```json + * { "errorCode": "SOME_CODE", "message": "Human-readable explanation." } + * ``` + */ +export class BusinessRuleException extends HttpException { + constructor( + public readonly errorCode: string, + message: string, + ) { + super({ errorCode, message }, HttpStatus.UNPROCESSABLE_ENTITY); + } +} + +export const BusinessErrorCode = { + DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED: "DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED", +} as const; diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 7681b88..bb8b645 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -12,6 +12,10 @@ import { Logger, NotFoundException, } from "@nestjs/common"; +import { + BusinessErrorCode, + BusinessRuleException, +} from "./exceptions/business-rule.exception"; import { distance as stringDistance } from "fastest-levenshtein"; // حتما نصب بشه import { Types } from "mongoose"; import { lastValueFrom } from "rxjs"; @@ -3013,7 +3017,8 @@ export class ExpertClaimService { } if (existingResend?.fulfilledAt) { - throw new BadRequestException( + throw new BusinessRuleException( + BusinessErrorCode.DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED, "The owner has already fulfilled a damage-expert resend for this claim. You cannot request another resend.", ); } diff --git a/src/expert-claim/expert-claim.v2.controller.ts b/src/expert-claim/expert-claim.v2.controller.ts index 7ddc4a0..410bd8b 100644 --- a/src/expert-claim/expert-claim.v2.controller.ts +++ b/src/expert-claim/expert-claim.v2.controller.ts @@ -281,7 +281,20 @@ export class ExpertClaimV2Controller { 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`. " + "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.", + "**One resend per claim lifecycle:** if the owner has already fulfilled a resend (`damageExpertResend.fulfilledAt`), this endpoint returns **422** with `errorCode: DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED`—the expert may only submit a priced reply or request in-person visit afterward.", + }) + @ApiResponse({ + status: 422, + description: + "Business rule violation: resend limit exceeded. " + + "`errorCode: DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED` — the owner has already fulfilled a prior resend request for this claim.", + schema: { + example: { + errorCode: "DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED", + message: + "The owner has already fulfilled a damage-expert resend for this claim. You cannot request another resend.", + }, + }, }) @ApiParam({ name: "claimRequestId" }) @ApiBody({ type: ClaimSubmitResendV2Dto })