1
0
forked from Yara724/api

Merge remote-tracking branch 'upstream/main' | Merge upstream into main and reapply local changes

This commit is contained in:
2026-04-18 12:30:32 +03:30
19 changed files with 1317 additions and 1323 deletions

View File

@@ -68,7 +68,9 @@ import { PublicIdService } from "src/utils/public-id/public-id.service";
import { ImageRequiredModel } from "./entites/schema/image-required.schema";
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";import { RoleEnum } from "src/Types&Enums/role.enum";
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";
import { CreationMethod } from "src/request-management/entities/schema/request-management.schema";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { UserRatingDto } from "./dto/user-rating.dto";
@Injectable()
@@ -341,6 +343,166 @@ export class ClaimRequestManagementService {
return trueItems;
}
/** One section of CarDamagePartDto: either a single object or an array of objects (legacy). */
private carPartDamageSectionToRows(section: unknown): Record<string, unknown>[] {
if (section == null) return [];
if (Array.isArray(section)) {
return section.filter(
(row): row is Record<string, unknown> =>
!!row && typeof row === "object",
);
}
if (typeof section === "object") {
return [section as Record<string, unknown>];
}
return [];
}
private carPartDamageSectionHasTrue(section: unknown): boolean {
for (const row of this.carPartDamageSectionToRows(section)) {
for (const val of Object.values(row)) {
if (val === true) return true;
}
}
return false;
}
/**
* At most two of left, right, front, back may contain any damaged part (true).
* `top` and claim `otherParts` are not subject to this rule.
*/
private assertCarPartDamageAtMostTwoOfFourSides(dto: CarDamagePartDto): void {
const d = dto as unknown as Record<string, unknown>;
const quadrants = [
{ key: "left", section: d.left },
{ key: "right", section: d.right },
{ key: "front", section: d.front },
{ key: "back", section: d.back },
];
const used = quadrants.filter((q) =>
this.carPartDamageSectionHasTrue(q.section),
);
if (used.length > 2) {
throw new BadRequestException(
`At most two of the four sides (left, right, front, back) may include damaged parts. ` +
`Sides with selections: ${used.map((u) => u.key).join(", ")}.`,
);
}
}
/**
* Maps legacy expert CarDamagePartDto (nested booleans) to v2 outer part slugs (ClaimCase.damage.selectedParts).
*/
private carDamagePartDtoToOuterPartSlugs(dto: CarDamagePartDto): string[] {
const out = new Set<string>();
const add = (slug: string | undefined) => {
if (slug) out.add(slug);
};
const walkMainOnSide = (
side: "left" | "right",
items: unknown,
map: Record<string, string>,
) => {
for (const row of this.carPartDamageSectionToRows(items)) {
for (const [key, val] of Object.entries(row)) {
if (val !== true) continue;
const slug = map[`${side}_${key}`] ?? map[key];
add(slug);
}
}
};
// MainParts on left / right (side-specific door / fender)
walkMainOnSide("left", (dto as any).left, {
left_frontDoor: "front_left_door",
left_backDoor: "rear_left_door",
left_frontFender: "front_left_fender",
left_backFender: "rear_left_fender",
frontDoor: "front_left_door",
backDoor: "rear_left_door",
frontFender: "front_left_fender",
backFender: "rear_left_fender",
});
walkMainOnSide("right", (dto as any).right, {
right_frontDoor: "front_right_door",
right_backDoor: "rear_right_door",
right_frontFender: "front_right_fender",
right_backFender: "rear_right_fender",
frontDoor: "front_right_door",
backDoor: "rear_right_door",
frontFender: "front_right_fender",
backFender: "rear_right_fender",
});
const walkSimple = (items: unknown, keyToSlug: Record<string, string>) => {
for (const row of this.carPartDamageSectionToRows(items)) {
for (const [key, val] of Object.entries(row)) {
if (val === true) add(keyToSlug[key]);
}
}
};
walkSimple((dto as any).front, {
frontBumper: "front_bumper",
carHood: "hood",
});
walkSimple((dto as any).back, {
backBumper: "rear_bumper",
carTrunk: "trunk",
});
walkSimple((dto as any).top, {
roof: "roof",
});
return Array.from(out);
}
/** Normalize expert DTO otherParts (strings or legacy { label: true }[]) to string[]. */
private normalizeExpertOtherPartsInput(raw: any[] | undefined): string[] {
if (!raw || !Array.isArray(raw)) return [];
const out: string[] = [];
for (const item of raw) {
if (typeof item === "string" && item.trim()) {
out.push(item.trim());
continue;
}
if (item && typeof item === "object") {
for (const [k, v] of Object.entries(item)) {
if (v === true && k) out.push(k);
}
}
}
return out;
}
private async assertFieldExpertInPersonBlameForClaimCase(
claimCase: any,
expert: any,
): Promise<void> {
const blameRequestId = claimCase?.blameRequestId?.toString?.();
if (!blameRequestId) {
throw new BadRequestException("Claim case has no linked blame request.");
}
const blameRequest = await this.blameRequestDbService.findById(blameRequestId);
if (!blameRequest) {
throw new NotFoundException("Blame request not found for this claim.");
}
if (
!blameRequest.expertInitiated ||
blameRequest.creationMethod !== CreationMethod.IN_PERSON
) {
throw new BadRequestException(
"This endpoint is only for expert-initiated IN_PERSON claim files.",
);
}
if (!blameRequest.initiatedByFieldExpertId) {
throw new BadRequestException(
"Blame request is missing initiating field expert.",
);
}
if (String(blameRequest.initiatedByFieldExpertId) !== String(expert.sub)) {
throw new ForbiddenException(
"Only the initiating field expert can complete claim data for this file.",
);
}
}
async setCarPartDamageAndImage(cl, trueItems) {
cl.carPartDamage = trueItems;
const jsonTrueItems = JSON.parse(JSON.stringify(trueItems));
@@ -394,7 +556,9 @@ export class ClaimRequestManagementService {
/**
* Field expert completes claim-needed data for expert-initiated IN_PERSON files.
* Only the initiating expert can call this. Accepts car part damage and/or sheba, nationalCodeOfInsurer, otherParts.
* Only the initiating field expert can call this. Accepts car part damage and/or sheba, nationalCodeOfInsurer, otherParts.
*
* V2: Resolves `ClaimCase` in `claimCases` collection (workflow). Legacy v1 claim documents are still supported.
*/
async expertCompleteClaimData(
claimRequestId: string,
@@ -406,6 +570,16 @@ export class ClaimRequestManagementService {
otherParts?: any[];
},
): Promise<{ success: boolean; claimRequestId: string }> {
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
if (claimCase) {
return this.expertCompleteClaimDataForClaimCaseV2(
claimRequestId,
expert,
dto,
claimCase,
);
}
const claim = await this.claimDbService.findOne(claimRequestId);
if (!claim) {
throw new NotFoundException("Claim file not found");
@@ -427,6 +601,7 @@ export class ClaimRequestManagementService {
if (claim.carPartDamage && (claim.carPartDamage as any[]).length > 0) {
throw new BadRequestException("Car part damage is already set.");
}
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage);
const trueItems = this.findTrueItems(dto.carPartDamage);
const claimDoc = (await this.claimDbService.findOne(
claimRequestId,
@@ -455,6 +630,178 @@ export class ClaimRequestManagementService {
return { success: true, claimRequestId };
}
/**
* V2 ClaimCase: outer parts + optional bank/other parts (expert-initiated IN_PERSON only).
*/
private async expertCompleteClaimDataForClaimCaseV2(
claimRequestId: string,
expert: any,
dto: {
carPartDamage?: CarDamagePartDto;
sheba?: string;
nationalCodeOfInsurer?: string;
otherParts?: any[];
},
claimCase: any,
): Promise<{ success: boolean; claimRequestId: string }> {
await this.assertFieldExpertInPersonBlameForClaimCase(claimCase, expert);
let working = claimCase;
if (dto.carPartDamage) {
if (working.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OUTER_PARTS) {
throw new BadRequestException(
`Cannot set outer parts: workflow step must be ${ClaimWorkflowStep.SELECT_OUTER_PARTS}. Current: ${working.workflow?.currentStep}`,
);
}
if (
working.damage?.selectedParts &&
working.damage.selectedParts.length > 0
) {
throw new BadRequestException("Car part damage is already set.");
}
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage);
const selectedParts = this.carDamagePartDtoToOuterPartSlugs(dto.carPartDamage);
if (!selectedParts.length) {
throw new BadRequestException(
"No outer damaged parts could be derived from carPartDamage. Check part selections.",
);
}
const updated = await this.claimCaseDbService.findByIdAndUpdate(
claimRequestId,
{
"damage.selectedParts": selectedParts,
status: ClaimCaseStatus.SELECTING_OTHER_PARTS,
"workflow.currentStep": ClaimWorkflowStep.SELECT_OTHER_PARTS,
"workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
$push: {
"workflow.completedSteps": ClaimWorkflowStep.SELECT_OUTER_PARTS,
history: {
type: "STEP_COMPLETED",
actor: {
actorId: new Types.ObjectId(expert.sub),
actorName:
`${expert.firstName || ""} ${expert.lastName || ""}`.trim() ||
"field_expert",
actorType: "field_expert",
},
timestamp: new Date(),
metadata: {
stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS,
selectedParts,
description: "Field expert submitted outer damage parts",
},
},
},
},
);
if (!updated) {
throw new InternalServerErrorException("Failed to update claim case");
}
working = updated;
}
const hasBankOrOther =
dto.sheba != null ||
dto.nationalCodeOfInsurer != null ||
dto.otherParts != null;
if (hasBankOrOther) {
const fresh = await this.claimCaseDbService.findById(claimRequestId);
if (!fresh) throw new NotFoundException("Claim case not found");
if (fresh.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OTHER_PARTS) {
throw new BadRequestException(
`Bank/other parts can only be set at ${ClaimWorkflowStep.SELECT_OTHER_PARTS}. Current: ${fresh.workflow?.currentStep}`,
);
}
const mergedSheba =
dto.sheba != null ? dto.sheba : fresh.money?.sheba;
const mergedNational =
dto.nationalCodeOfInsurer != null
? dto.nationalCodeOfInsurer
: fresh.money?.nationalCodeOfInsurer;
if (fresh.money?.sheba && dto.sheba != null && dto.sheba !== fresh.money.sheba) {
throw new ConflictException("Sheba is already set for this claim.");
}
if (
fresh.money?.nationalCodeOfInsurer &&
dto.nationalCodeOfInsurer != null &&
dto.nationalCodeOfInsurer !== fresh.money.nationalCodeOfInsurer
) {
throw new ConflictException(
"National code of insurer is already set for this claim.",
);
}
const otherPartsArr = this.normalizeExpertOtherPartsInput(dto.otherParts);
const setPatch: Record<string, unknown> = {};
if (dto.sheba != null) setPatch["money.sheba"] = dto.sheba;
if (dto.nationalCodeOfInsurer != null) {
setPatch["money.nationalCodeOfInsurer"] = dto.nationalCodeOfInsurer;
}
if (dto.otherParts != null) {
setPatch["damage.otherParts"] =
otherPartsArr.length > 0 ? otherPartsArr : [];
}
const bankComplete =
mergedSheba &&
mergedNational &&
/^[0-9]{24}$/.test(String(mergedSheba).replace(/\s/g, "")) &&
/^[0-9]{10}$/.test(String(mergedNational).replace(/\s/g, ""));
if (bankComplete) {
const shebaNorm = String(mergedSheba).replace(/\s/g, "");
const nationalNorm = String(mergedNational).replace(/\s/g, "");
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
"money.sheba": shebaNorm,
"money.nationalCodeOfInsurer": nationalNorm,
...(dto.otherParts != null
? {
"damage.otherParts":
otherPartsArr.length > 0 ? otherPartsArr : [],
}
: {}),
status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
"workflow.currentStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
"workflow.nextStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
$push: {
"workflow.completedSteps": ClaimWorkflowStep.SELECT_OTHER_PARTS,
history: {
type: "STEP_COMPLETED",
actor: {
actorId: new Types.ObjectId(expert.sub),
actorName:
`${expert.firstName || ""} ${expert.lastName || ""}`.trim() ||
"field_expert",
actorType: "field_expert",
},
timestamp: new Date(),
metadata: {
stepKey: ClaimWorkflowStep.SELECT_OTHER_PARTS,
description:
"Field expert submitted bank info and optional other parts",
},
},
},
});
} else {
if (Object.keys(setPatch).length === 0) {
throw new BadRequestException(
"Provide sheba (24 digits) and nationalCodeOfInsurer (10 digits) together to finish this step, or supply partial fields incrementally.",
);
}
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: setPatch,
});
}
}
return { success: true, claimRequestId };
}
async selectCarOtherPartDamage(
requestId: string,
body: any, // Use your DTO for better type safety
@@ -3349,7 +3696,23 @@ export class ClaimRequestManagementService {
}
// 5. Convert selected parts to storage format (simple array of strings)
const selectedParts = body.selectedParts;
// Backward compatibility: some clients may still send legacy `carPartDamage` object.
let selectedParts = Array.isArray((body as any)?.selectedParts)
? ((body as any).selectedParts as string[])
: [];
if (selectedParts.length === 0 && (body as any)?.carPartDamage) {
this.assertCarPartDamageAtMostTwoOfFourSides(
(body as any).carPartDamage as CarDamagePartDto,
);
selectedParts = this.carDamagePartDtoToOuterPartSlugs(
(body as any).carPartDamage as CarDamagePartDto,
);
}
if (selectedParts.length === 0) {
throw new BadRequestException(
"selectedParts is required and must contain at least one item",
);
}
// 6. Update claim case with selected parts and move to next step
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
@@ -3372,8 +3735,8 @@ export class ClaimRequestManagementService {
metadata: {
stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS,
selectedParts: selectedParts,
partsCount: selectedParts.length,
description: `User selected ${selectedParts.length} damaged outer parts`,
partsCount: selectedParts?.length,
description: `User selected ${selectedParts?.length} damaged outer parts`,
},
},
},
@@ -3385,7 +3748,7 @@ export class ClaimRequestManagementService {
}
this.logger.log(
`Outer parts selected for claim ${claimRequestId}: ${selectedParts.length} parts`,
`Outer parts selected for claim ${claimRequestId}: ${selectedParts?.length} parts`,
);
// 7. Return response
@@ -3431,6 +3794,7 @@ export class ClaimRequestManagementService {
body: SelectOtherPartsV2Dto,
currentUserId: string,
actor?: { sub: string; role?: string },
file?: Express.Multer.File,
): Promise<SelectOtherPartsV2ResponseDto> {
try {
// 1. Validate claim exists
@@ -3469,40 +3833,108 @@ export class ClaimRequestManagementService {
}
// 5. Prepare data
const otherParts = body.otherParts || [];
const shebaNumber = body.shebaNumber;
const nationalCode = body.nationalCodeOfOwner;
// Backward compatibility aliases:
// - shebaNumber <- sheba
// - nationalCodeOfOwner <- nationalCodeOfInsurer
let otherParts = Array.isArray((body as any)?.otherParts)
? (body as any).otherParts
: [];
// Multipart clients may send otherParts as JSON string
if (!Array.isArray((body as any)?.otherParts) && typeof (body as any)?.otherParts === "string") {
try {
const parsed = JSON.parse((body as any).otherParts);
otherParts = Array.isArray(parsed) ? parsed : [];
} catch {
otherParts = [];
}
}
const shebaInput = String(
((body as any).shebaNumber ?? (body as any).sheba ?? "") as string,
)
.replace(/\s/g, "");
// 6. Update claim case with other parts, bank info and move to next step
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
claimRequestId,
{
'damage.otherParts': otherParts.length > 0 ? otherParts : undefined,
'money.sheba': shebaNumber,
'money.nationalCodeOfInsurer': nationalCode,
'status': ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
'workflow.currentStep': ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
'workflow.nextStep': ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
$push: {
'workflow.completedSteps': ClaimWorkflowStep.SELECT_OTHER_PARTS,
history: {
type: 'STEP_COMPLETED',
actor: {
actorId: new Types.ObjectId(currentUserId),
actorName: claimCase.owner?.fullName || 'User',
actorType: 'user',
},
timestamp: new Date(),
metadata: {
stepKey: ClaimWorkflowStep.SELECT_OTHER_PARTS,
otherParts: otherParts,
otherPartsCount: otherParts.length,
hasBankInfo: true,
description: `User selected ${otherParts.length} other damaged parts and provided bank information`,
},
const shebaDigits = shebaInput.replace(/^IR/i, "");
const shebaNumber = `IR${shebaDigits}`;
const nationalCode = String(
((body as any).nationalCodeOfOwner ??
(body as any).nationalCodeOfInsurer ??
"") as string,
).replace(/\s/g, "");
if (!/^[0-9]{24}$/.test(shebaDigits)) {
throw new BadRequestException(
"shebaNumber is required and must be valid (IR + 24 digits or only 24 digits)",
);
}
if (!/^[0-9]{10}$/.test(nationalCode)) {
throw new BadRequestException(
"nationalCodeOfOwner is required and must be exactly 10 digits",
);
}
const updatePayload: any = {
'damage.otherParts': otherParts.length > 0 ? otherParts : undefined,
'money.sheba': shebaNumber,
'money.nationalCodeOfInsurer': nationalCode,
'status': ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
'workflow.currentStep': ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
'workflow.nextStep': ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
$push: {
'workflow.completedSteps': ClaimWorkflowStep.SELECT_OTHER_PARTS,
history: {
type: 'STEP_COMPLETED',
actor: {
actorId: new Types.ObjectId(currentUserId),
actorName: claimCase.owner?.fullName || 'User',
actorType: 'user',
},
timestamp: new Date(),
metadata: {
stepKey: ClaimWorkflowStep.SELECT_OTHER_PARTS,
otherParts: otherParts,
otherPartsCount: otherParts.length,
hasBankInfo: true,
hasGreenCardUpload: !!file,
description: `User selected ${otherParts.length} other damaged parts and provided bank information`,
},
},
},
};
if (file) {
const existingGreen =
(claimCase.requiredDocuments as any)?.get?.(
ClaimRequiredDocumentType.CAR_GREEN_CARD,
) ||
(claimCase.requiredDocuments as any)?.[
ClaimRequiredDocumentType.CAR_GREEN_CARD
];
if (existingGreen?.uploaded) {
throw new ConflictException(
"car_green_card has already been uploaded for this claim",
);
}
const docRef = await this.claimRequiredDocumentDbService.create({
path: file.path,
fileName: file.filename,
claimId: new Types.ObjectId(claimRequestId),
documentType: ClaimRequiredDocumentType.CAR_GREEN_CARD,
uploadedAt: new Date(),
});
updatePayload[
`requiredDocuments.${ClaimRequiredDocumentType.CAR_GREEN_CARD}`
] = {
fileId: docRef._id,
filePath: file.path,
fileName: file.filename,
uploaded: true,
uploadedAt: new Date(),
};
}
// 6. Update claim case with other parts, bank info and optional green card file
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
claimRequestId,
updatePayload,
);
if (!updatedClaim) {
@@ -3514,8 +3946,8 @@ export class ClaimRequestManagementService {
);
// 7. Mask sensitive data for response
const maskedSheba = shebaNumber.replace(/^(.{4})(.*)(.{4})$/, 'IR$1************$3');
const maskedNationalCode = nationalCode.replace(/^(.{2})(.*)(.{2})$/, '$1******$3');
const maskedSheba = `IR${shebaDigits.slice(0, 4)}************${shebaDigits.slice(-4)}`;
const maskedNationalCode = `${nationalCode.slice(0, 2)}******${nationalCode.slice(-2)}`;
// 8. Return response
return {

View File

@@ -344,13 +344,14 @@ export class ClaimRequestManagementV2Controller {
**Workflow Step:** SELECT_OTHER_PARTS (Step 3 of Claim)
**Purpose:** User selects non-body damaged parts and provides bank information for payment.
Optional: upload car green card file in the same step.
**Validations:**
- Claim must exist
- User must be the claim owner (damaged party from blame case)
- Current workflow step must be SELECT_OTHER_PARTS
- Bank information must not have been submitted previously
- Sheba number must be exactly 24 digits
- Sheba (sheba) accepted as IR + 24 digits or only 24 digits
- National code must be exactly 10 digits
**Valid Other Parts (Optional):**
@@ -368,42 +369,38 @@ export class ClaimRequestManagementV2Controller {
description: "The claim case ID (MongoDB ObjectId)",
example: "507f1f77bcf86cd799439011",
})
@ApiConsumes("multipart/form-data")
@UseInterceptors(
FileInterceptor("file", {
limits: { fileSize: 10 * 1024 * 1024 },
storage: diskStorage({
destination: "./files/claim-required-document",
filename: (req, file, callback) => {
const unique = Date.now();
const ex = extname(file.originalname);
callback(null, `other-parts-${unique}${ex}`);
},
}),
}),
)
@ApiBody({
type: SelectOtherPartsV2Dto,
description: "Other parts selection and bank information",
examples: {
example1: {
summary: "Only bank info (no other parts damaged)",
value: {
otherParts: [],
shebaNumber: "123456789012345678901234",
nationalCodeOfOwner: "1234567890",
},
},
example2: {
summary: "Engine and suspension damage",
value: {
otherParts: ["engine", "suspension"],
shebaNumber: "123456789012345678901234",
nationalCodeOfOwner: "1234567890",
},
},
example3: {
summary: "Multiple systems damaged",
value: {
otherParts: ["engine", "brake_system", "electrical", "headlight"],
shebaNumber: "123456789012345678901234",
nationalCodeOfOwner: "1234567890",
},
},
example4: {
summary: "Lighting and glass damage",
value: {
otherParts: ["headlight", "taillight", "mirror", "glass"],
shebaNumber: "123456789012345678901234",
nationalCodeOfOwner: "1234567890",
description:
"Other parts + bank information. Use `sheba` and `nationalCodeOfInsurer` like THIRD_PARTY flow. Optional file can be uploaded as car green card.",
schema: {
type: "object",
properties: {
otherParts: {
oneOf: [
{ type: "array", items: { type: "string" } },
{ type: "string", description: "JSON string array for multipart" },
],
example: ["engine", "suspension"],
},
sheba: { type: "string", example: "IR123456789012345678901234" },
nationalCodeOfInsurer: { type: "string", example: "1234567890" },
file: { type: "string", format: "binary" },
},
required: ["sheba", "nationalCodeOfInsurer"],
},
})
@ApiResponse({
@@ -431,6 +428,7 @@ export class ClaimRequestManagementV2Controller {
@Param("claimRequestId") claimRequestId: string,
@Body() body: SelectOtherPartsV2Dto,
@CurrentUser() user: any,
@UploadedFile() file?: Express.Multer.File,
): Promise<SelectOtherPartsV2ResponseDto> {
try {
return await this.claimRequestManagementService.selectOtherPartsV2(
@@ -438,6 +436,7 @@ export class ClaimRequestManagementV2Controller {
body,
user.sub,
user,
file,
);
} catch (error) {
if (error instanceof HttpException) throw error;

View File

@@ -41,34 +41,43 @@ export class SelectOtherPartsV2Dto {
otherParts?: OtherCarPart[];
@ApiProperty({
description: 'Sheba number (IBAN) for payment - 24 digits without IR prefix',
description: 'Sheba number (IBAN). Accepted: IR + 24 digits OR only 24 digits',
example: 'IR123456789012345678901234',
})
@IsNotEmpty({ message: 'sheba is required' })
@IsString({ message: 'sheba must be a string' })
sheba: string;
@ApiPropertyOptional({
description: 'Legacy alias of sheba for backward compatibility',
example: '123456789012345678901234',
pattern: '^[0-9]{24}$',
minLength: 24,
maxLength: 24,
})
@IsNotEmpty({ message: 'Sheba number is required' })
@IsString({ message: 'Sheba number must be a string' })
@Length(24, 24, { message: 'Sheba number must be exactly 24 digits' })
@Matches(/^[0-9]{24}$/, {
message: 'Sheba number must contain exactly 24 digits (without IR prefix)',
})
shebaNumber: string;
@IsOptional()
@IsString()
shebaNumber?: string;
@ApiProperty({
description: 'National code of the owner - 10 digits',
description: 'National code of insurer/owner - 10 digits',
example: '1234567890',
pattern: '^[0-9]{10}$',
minLength: 10,
maxLength: 10,
})
@IsNotEmpty({ message: 'National code of owner is required' })
@IsNotEmpty({ message: 'nationalCodeOfInsurer is required' })
@IsString({ message: 'National code must be a string' })
@Length(10, 10, { message: 'National code must be exactly 10 digits' })
@Matches(/^[0-9]{10}$/, {
message: 'National code must contain exactly 10 digits',
})
nationalCodeOfOwner: string;
nationalCodeOfInsurer: string;
@ApiPropertyOptional({
description: 'Legacy alias for backward compatibility',
example: '1234567890',
})
@IsOptional()
@IsString()
nationalCodeOfOwner?: string;
}
/**