1
0
forked from Yara724/api

Validation for prices and objection

This commit is contained in:
SepehrYahyaee
2026-05-20 11:08:47 +03:30
parent 511d478064
commit e4dfe7c572
18 changed files with 467 additions and 60 deletions

View File

@@ -116,9 +116,11 @@ import {
type DamageSelectedPartV2,
migrateExpertReplyCarPartDamageToUnified,
normalizeDamageSelectedParts,
partIdentityKey,
parseCatalogPartIdInput,
partLookupKey,
resolvePartCaptureIndex,
} from "src/helpers/outer-damage-parts";
import { normalizeMoneyAmountString } from "src/utils/unicode-digits";
import {
CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS,
@@ -5544,19 +5546,20 @@ export class ClaimRequestManagementService {
);
}
const pricedPartIdSet =
const pricedCatalogIds =
activeExpert?.parts?.length ?
new Set(
activeExpert.parts
.filter((p) => p.factorNeeded !== true)
.map((p) => String((p as { partId?: string }).partId ?? ""))
.filter(Boolean),
.map((p) => parseCatalogPartIdInput(p.partId))
.filter((id): id is number => id != null),
)
: new Set<string>();
: new Set<number>();
if (partsIn.length && pricingObjectionEligible) {
for (const op of partsIn) {
if (!pricedPartIdSet.has(String(op.partId))) {
const catalogId = parseCatalogPartIdInput(op.partId);
if (catalogId == null || !pricedCatalogIds.has(catalogId)) {
throw new BadRequestException(
`Only priced repair lines can be disputed (${String(op.partId)} is factor-only or unknown).`,
);
@@ -5565,19 +5568,28 @@ export class ClaimRequestManagementService {
}
const objectionParts = partsIn.map((p) => ({
partId: p.partId,
reason: p.reason,
partPrice: p.partPrice,
partSalary: p.partSalary,
partId: parseCatalogPartIdInput(p.partId)!,
reason: p.reason?.trim() || undefined,
partPrice:
p.partPrice != null && String(p.partPrice).trim() !== ""
? normalizeMoneyAmountString(String(p.partPrice))
: undefined,
partSalary:
p.partSalary != null && String(p.partSalary).trim() !== ""
? normalizeMoneyAmountString(String(p.partSalary))
: undefined,
typeOfDamage: p.typeOfDamage != null ? String(p.typeOfDamage) : undefined,
carPartDamage: p.carPartDamage,
side: p.side,
carPartDamage: p.carPartDamage?.trim() || undefined,
side: p.side?.trim() || undefined,
}));
const newParts = newPartsIn.map((p) => ({
partId: p.partId ? String(p.partId) : new Types.ObjectId().toString(),
partName: p.partName,
side: p.side,
partId:
p.partId != null && parseCatalogPartIdInput(p.partId) != null
? parseCatalogPartIdInput(p.partId)!
: new Types.ObjectId().toString(),
partName: p.partName.trim(),
side: p.side?.trim() || undefined,
}));
const objectionCarType = claimCase.vehicle?.carType as
@@ -5588,7 +5600,7 @@ export class ClaimRequestManagementService {
objectionCarType,
(claimCase.damage as any)?.selectedOuterParts,
);
const mergedKeys = new Set(mergedNorm.map((p) => partIdentityKey(p)));
const mergedKeys = new Set(mergedNorm.map((p) => partLookupKey(p)));
for (const np of newParts) {
const name = np.partName?.trim();
if (!name) continue;
@@ -5599,7 +5611,7 @@ export class ClaimRequestManagementService {
side,
label_fa: name,
};
const k = partIdentityKey(row);
const k = partLookupKey(row);
if (!mergedKeys.has(k)) {
mergedNorm.push(row);
mergedKeys.add(k);
@@ -6112,7 +6124,7 @@ export class ClaimRequestManagementService {
);
const displayParts = [...selectedNormDetails];
const seenDetailKeys = new Set(
selectedNormDetails.map((p) => partIdentityKey(p)),
selectedNormDetails.map((p) => partLookupKey(p)),
);
for (const rk of resendPartKeys) {
const extra =
@@ -6124,7 +6136,7 @@ export class ClaimRequestManagementService {
label_fa: rk,
catalogKey: rk,
};
const k = partIdentityKey(extra);
const k = partLookupKey(extra);
if (!seenDetailKeys.has(k)) {
displayParts.push(extra);
seenDetailKeys.add(k);

View File

@@ -1,14 +1,21 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsOptional, ValidateNested } from "class-validator";
import {
IsArray,
IsOptional,
Validate,
ValidateNested,
} from "class-validator";
import { HasObjectionEntriesConstraint } from "src/common/validators/has-objection-entries.validator";
import { NewPartDto, UserObjectionPartDto } from "./user-objection.dto";
/**
* V2 user objection body — same shape as v1 {@link import("./user-objection.dto").UserObjectionDto}
* V2 user objection body — same shape as v1 {@link UserObjectionDto}
* with nested validation enabled for the v2 controller pipeline.
*/
export class UserObjectionV2Dto {
@ApiPropertyOptional({ type: [UserObjectionPartDto] })
@Validate(HasObjectionEntriesConstraint)
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@@ -16,6 +23,7 @@ export class UserObjectionV2Dto {
objectionParts?: UserObjectionPartDto[];
@ApiPropertyOptional({ type: [NewPartDto] })
@Validate(HasObjectionEntriesConstraint)
@IsOptional()
@IsArray()
@ValidateNested({ each: true })

View File

@@ -1,47 +1,113 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
IsEnum,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
MinLength,
Validate,
ValidateNested,
} from "class-validator";
import { HasObjectionEntriesConstraint } from "src/common/validators/has-objection-entries.validator";
import { IsRepairLineAmountToman } from "src/common/validators/repair-line-amount-toman.validator";
import { ObjectionPartHasContentConstraint } from "src/common/validators/objection-part-content.validator";
import { TypeOfDamage } from "src/Types&Enums/claim-request-management/type-of-damage.enum";
export class UserObjectionPartDto {
@ApiProperty()
partId: string;
@ApiProperty({
example: 201,
description: "Numeric catalog part id of the priced line being disputed.",
})
@Validate(ObjectionPartHasContentConstraint)
@IsInt()
partId: number;
@ApiProperty({ required: false })
@ApiPropertyOptional({
description:
"Why the owner disagrees (min 3 chars when provided). Required unless partPrice or partSalary is sent.",
})
@IsOptional()
@IsString()
@MaxLength(2000)
reason?: string;
@ApiProperty({ required: false })
@ApiPropertyOptional({
description: "Owner-proposed part price (Toman, integer string).",
})
@IsOptional()
@IsString()
@MaxLength(32)
@IsRepairLineAmountToman()
partPrice?: string;
@ApiProperty({ required: false })
@ApiPropertyOptional({
description: "Owner-proposed salary / labor (Toman, integer string).",
})
@IsOptional()
@IsString()
@MaxLength(32)
@IsRepairLineAmountToman()
partSalary?: string;
@ApiProperty({ required: false })
@ApiPropertyOptional({ enum: TypeOfDamage })
@IsOptional()
@IsEnum(TypeOfDamage)
typeOfDamage?: TypeOfDamage;
@ApiProperty({ required: false })
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(500)
carPartDamage?: string;
@ApiProperty({ required: false })
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(64)
side?: string;
}
export class NewPartDto {
@ApiProperty({ required: false, nullable: true })
partId: string | null;
@ApiPropertyOptional({
nullable: true,
description: "Optional catalog id when the new part exists in the outer catalog.",
})
@IsOptional()
@IsInt()
partId?: number | null;
@ApiProperty()
@ApiProperty({ example: "سپر جلو" })
@IsString()
@IsNotEmpty()
@MinLength(1)
@MaxLength(200)
partName: string;
@ApiProperty({ required: false })
@ApiPropertyOptional({ example: "front" })
@IsOptional()
@IsString()
@MaxLength(64)
side?: string;
}
export class UserObjectionDto {
@ApiProperty({ type: [UserObjectionPartDto], required: false })
@ApiPropertyOptional({ type: [UserObjectionPartDto] })
@Validate(HasObjectionEntriesConstraint)
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => UserObjectionPartDto)
objectionParts?: UserObjectionPartDto[];
@ApiProperty({ type: [NewPartDto], required: false })
@ApiPropertyOptional({ type: [NewPartDto] })
@Validate(HasObjectionEntriesConstraint)
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => NewPartDto)
newParts?: NewPartDto[];
}

View File

@@ -141,8 +141,8 @@ export const ClaimExpertReplySchema =
/** One line item the user disputes on an expert-priced part */
@Schema({ _id: false })
export class ClaimUserObjectionPart {
@Prop({ type: String, required: true })
partId: string;
@Prop({ type: Number, required: true })
partId: number;
@Prop({ type: String })
reason?: string;
@@ -167,8 +167,9 @@ export const ClaimUserObjectionPartSchema =
@Schema({ _id: false })
export class ClaimUserObjectionNewPart {
@Prop({ type: String })
partId?: string;
/** Catalog id when known; otherwise a generated string id for the new line. */
@Prop({ type: MongooseSchema.Types.Mixed })
partId?: number | string;
@Prop({ type: String, required: true })
partName: string;