diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 7f1225f..23e5399 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -3525,7 +3525,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( @@ -3548,8 +3564,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`, }, }, }, @@ -3561,7 +3577,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 @@ -3607,6 +3623,7 @@ export class ClaimRequestManagementService { body: SelectOtherPartsV2Dto, currentUserId: string, actor?: { sub: string; role?: string }, + file?: Express.Multer.File, ): Promise { try { // 1. Validate claim exists @@ -3645,40 +3662,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) { @@ -3690,8 +3775,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 { diff --git a/src/claim-request-management/claim-request-management.v2.controller.ts b/src/claim-request-management/claim-request-management.v2.controller.ts index 765751d..d91deac 100644 --- a/src/claim-request-management/claim-request-management.v2.controller.ts +++ b/src/claim-request-management/claim-request-management.v2.controller.ts @@ -254,13 +254,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):** @@ -278,42 +279,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({ @@ -341,6 +338,7 @@ export class ClaimRequestManagementV2Controller { @Param("claimRequestId") claimRequestId: string, @Body() body: SelectOtherPartsV2Dto, @CurrentUser() user: any, + @UploadedFile() file?: Express.Multer.File, ): Promise { try { return await this.claimRequestManagementService.selectOtherPartsV2( @@ -348,6 +346,7 @@ export class ClaimRequestManagementV2Controller { body, user.sub, user, + file, ); } catch (error) { if (error instanceof HttpException) throw error; diff --git a/src/claim-request-management/dto/select-other-parts-v2.dto.ts b/src/claim-request-management/dto/select-other-parts-v2.dto.ts index eadfe3a..8b3fe3e 100644 --- a/src/claim-request-management/dto/select-other-parts-v2.dto.ts +++ b/src/claim-request-management/dto/select-other-parts-v2.dto.ts @@ -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; } /**