forked from Yara724/api
blame and claim refactored
This commit is contained in:
@@ -31,6 +31,7 @@ import { Roles } from "src/decorators/roles.decorator";
|
||||
import { CurrentUser } from "src/decorators/user.decorator";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
||||
import { CarDamagePartDto, OtherCarDamagePartDto } from "./dto/car-part.dto";
|
||||
import { UserCommentDto } from "./dto/user-comment.dto";
|
||||
import { UserObjectionDto } from "./dto/user-objection.dto";
|
||||
@@ -167,20 +168,7 @@ export class ClaimRequestManagementController {
|
||||
@ApiParam({ name: "claimRequestID" })
|
||||
@ApiQuery({
|
||||
name: "documentType",
|
||||
enum: [
|
||||
"damaged_driving_license_back",
|
||||
"damaged_driving_license_front",
|
||||
"damaged_chassis_number",
|
||||
"damaged_engine_photo",
|
||||
"damaged_car_card_front",
|
||||
"damaged_car_card_back",
|
||||
"damaged_metal_plate",
|
||||
"guilty_driving_license_front",
|
||||
"guilty_driving_license_back",
|
||||
"guilty_car_card_front",
|
||||
"guilty_car_card_back",
|
||||
"guilty_metal_plate",
|
||||
],
|
||||
enum: ClaimRequiredDocumentType,
|
||||
description: "Type of required document to upload",
|
||||
})
|
||||
@Patch("upload-required-document/:claimRequestID")
|
||||
|
||||
@@ -5,9 +5,12 @@ import { SandHubModule } from "src/sand-hub/sand-hub.module";
|
||||
import { RequestManagementModule } from "src/request-management/request-management.module";
|
||||
import { UsersModule } from "src/users/users.module";
|
||||
import { ClaimRequestManagementController } from "./claim-request-management.controller";
|
||||
import { ClaimRequestManagementV2Controller } from "./claim-request-management.v2.controller";
|
||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
||||
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
||||
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
|
||||
import { ClaimCase, ClaimCaseSchema } from "./entites/schema/claim-cases.schema";
|
||||
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
||||
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
||||
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
|
||||
@@ -38,12 +41,14 @@ import {
|
||||
VideoCaptureModel,
|
||||
VideoCaptureSchema,
|
||||
} from "./entites/schema/video-capture.schema";
|
||||
import { PublicIdModule } from "src/utils/public-id/public-id.module";
|
||||
import { ClientModule } from "src/client/client.module";
|
||||
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PublicIdModule,
|
||||
UsersModule,
|
||||
RequestManagementModule,
|
||||
AiModule,
|
||||
@@ -51,6 +56,7 @@ import { JwtModule } from "@nestjs/jwt";
|
||||
ClientModule,
|
||||
JwtModule.register({}),
|
||||
MongooseModule.forFeature([
|
||||
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
||||
{
|
||||
name: ClaimRequestManagementModel.name,
|
||||
schema: ClaimRequestManagementSchema,
|
||||
@@ -69,7 +75,9 @@ import { JwtModule } from "@nestjs/jwt";
|
||||
providers: [
|
||||
ClaimRequestManagementService,
|
||||
ClaimRequestManagementDbService,
|
||||
ClaimCaseDbService,
|
||||
CarGreenCardDbService,
|
||||
ClaimCaseDbService,
|
||||
DamageImageDbService,
|
||||
ClaimSignDbService,
|
||||
ClaimFactorsImageDbService,
|
||||
@@ -77,9 +85,10 @@ import { JwtModule } from "@nestjs/jwt";
|
||||
ClaimRequiredDocumentDbService,
|
||||
ClaimAccessGuard,
|
||||
],
|
||||
controllers: [ClaimRequestManagementController],
|
||||
controllers: [ClaimRequestManagementController, ClaimRequestManagementV2Controller],
|
||||
exports: [
|
||||
ClaimRequestManagementDbService,
|
||||
ClaimCaseDbService,
|
||||
DamageImageDbService,
|
||||
VideoCaptureDbService,
|
||||
ClaimRequiredDocumentDbService,
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
InternalServerErrorException,
|
||||
} from "@nestjs/common";
|
||||
import { Types } from "mongoose";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@@ -32,6 +34,21 @@ import { UserCommentDto } from "./dto/user-comment.dto";
|
||||
import { UserObjectionDto } from "./dto/user-objection.dto";
|
||||
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
||||
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
||||
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
|
||||
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||
import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto";
|
||||
import { SelectOuterPartsV2Dto, SelectOuterPartsV2ResponseDto } from "./dto/select-outer-parts-v2.dto";
|
||||
import { SelectOtherPartsV2Dto, SelectOtherPartsV2ResponseDto } from "./dto/select-other-parts-v2.dto";
|
||||
import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto";
|
||||
import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto } from "./dto/upload-document-v2.dto";
|
||||
import { CapturePartV2Dto, CapturePartV2ResponseDto } from "./dto/capture-part-v2.dto";
|
||||
import { GetMyClaimsV2ResponseDto, ClaimListItemV2Dto } from "./dto/my-claims-v2.dto";
|
||||
import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { ClaimRequiredDocumentType, CarAngle } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
||||
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||
import { CaseStatus as BlameCaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
||||
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
||||
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
|
||||
@@ -41,12 +58,11 @@ import {
|
||||
ClaimRequestManagementModel,
|
||||
UserClaimRating,
|
||||
} from "./entites/schema/claim-request-management.schema";
|
||||
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 { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { UserRatingDto } from "./dto/user-rating.dto";
|
||||
|
||||
@Injectable()
|
||||
@@ -75,6 +91,8 @@ export class ClaimRequestManagementService {
|
||||
constructor(
|
||||
private readonly blameDbService: RequestManagementDbService,
|
||||
private readonly claimDbService: ClaimRequestManagementDbService,
|
||||
private readonly claimCaseDbService: ClaimCaseDbService,
|
||||
private readonly blameRequestDbService: BlameRequestDbService,
|
||||
private readonly carGreenCardDbService: CarGreenCardDbService,
|
||||
private readonly damageImageDbService: DamageImageDbService,
|
||||
private readonly userDbService: UserDbService,
|
||||
@@ -86,6 +104,7 @@ export class ClaimRequestManagementService {
|
||||
private readonly damageExpertDbService: DamageExpertDbService,
|
||||
private readonly branchDbService: BranchDbService,
|
||||
private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService,
|
||||
private readonly publicIdService: PublicIdService,
|
||||
) {}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
@@ -152,8 +171,22 @@ export class ClaimRequestManagementService {
|
||||
if (!blameRequest) {
|
||||
throw new NotFoundException("Source blame request not found.");
|
||||
}
|
||||
|
||||
// Ensure the blame request has a shared human-friendly id.
|
||||
// For legacy records, generate it lazily exactly once.
|
||||
let publicId = (blameRequest as any).publicId as string | undefined;
|
||||
if (!publicId) {
|
||||
publicId = await this.publicIdService.generateRequestPublicId();
|
||||
await this.blameDbService.findAndUpdate(
|
||||
{ _id: new Types.ObjectId(requestId) },
|
||||
{ $set: { publicId } },
|
||||
);
|
||||
}
|
||||
|
||||
const existingClaimCount = await this.claimDbService.countByFilter({
|
||||
"blameFile._id": new Types.ObjectId(requestId),
|
||||
blameRequestId: new Types.ObjectId(requestId),
|
||||
publicId,
|
||||
});
|
||||
if (existingClaimCount > 0) {
|
||||
throw new ForbiddenException(
|
||||
@@ -2603,4 +2636,911 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Create claim request from completed blame case
|
||||
* Only damaged party can create claim after both parties accept expert decision
|
||||
*/
|
||||
async createClaimFromBlameV2(
|
||||
blameRequestId: string,
|
||||
currentUserId: string,
|
||||
): Promise<CreateClaimFromBlameResponseDto> {
|
||||
try {
|
||||
// 1. Fetch blame request from new blameCases collection
|
||||
const blameRequest = await this.blameRequestDbService.findById(blameRequestId);
|
||||
if (!blameRequest) {
|
||||
throw new NotFoundException("Blame request not found");
|
||||
}
|
||||
|
||||
// 2. Validate blame status is COMPLETED
|
||||
if (blameRequest.status !== BlameCaseStatus.COMPLETED) {
|
||||
throw new BadRequestException(
|
||||
`Blame request must be COMPLETED. Current status: ${blameRequest.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Validate expert decision exists
|
||||
if (!blameRequest.expert?.decision) {
|
||||
throw new BadRequestException(
|
||||
"Cannot create claim until expert has made a decision",
|
||||
);
|
||||
}
|
||||
|
||||
const guiltyPartyId = String(blameRequest.expert.decision.guiltyPartyId);
|
||||
|
||||
// 4. Validate both parties have signed and accepted
|
||||
const parties = blameRequest.parties || [];
|
||||
if (parties.length < 2) {
|
||||
throw new BadRequestException(
|
||||
"Both parties must be present in the blame request",
|
||||
);
|
||||
}
|
||||
|
||||
const allPartiesSigned = parties.every(
|
||||
(p) => p.confirmation !== undefined && p.confirmation !== null,
|
||||
);
|
||||
|
||||
if (!allPartiesSigned) {
|
||||
throw new BadRequestException(
|
||||
"Both parties must sign the expert decision before creating a claim",
|
||||
);
|
||||
}
|
||||
|
||||
const allPartiesAccepted = parties.every(
|
||||
(p) => p.confirmation?.accepted === true,
|
||||
);
|
||||
|
||||
if (!allPartiesAccepted) {
|
||||
throw new BadRequestException(
|
||||
"Both parties must accept the expert decision. If rejected, in-person resolution is required.",
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Validate current user is the DAMAGED party (NOT the guilty party)
|
||||
const currentUserParty = parties.find(
|
||||
(p) => String(p.person?.userId) === currentUserId,
|
||||
);
|
||||
|
||||
if (!currentUserParty) {
|
||||
throw new ForbiddenException(
|
||||
"You are not a party in this blame request",
|
||||
);
|
||||
}
|
||||
|
||||
const currentUserIsGuilty = parties.some(
|
||||
(p) =>
|
||||
String(p.person?.userId) === currentUserId &&
|
||||
String((p as any)._id || p.person?.userId) === guiltyPartyId,
|
||||
);
|
||||
|
||||
// Better check: compare guiltyPartyId with person.userId of each party
|
||||
const guiltyParty = parties.find((p) => {
|
||||
// guiltyPartyId could be userId or party identifier
|
||||
return String(p.person?.userId) === guiltyPartyId;
|
||||
});
|
||||
|
||||
if (guiltyParty && String(guiltyParty.person?.userId) === currentUserId) {
|
||||
throw new ForbiddenException(
|
||||
"You cannot create a claim as you are the guilty party",
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Validate no existing claim for this blame
|
||||
const existingClaim = await this.claimCaseDbService.findOne({
|
||||
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||
});
|
||||
|
||||
if (existingClaim) {
|
||||
throw new ConflictException(
|
||||
"A claim request for this blame case already exists",
|
||||
);
|
||||
}
|
||||
|
||||
// 7. Generate claim number
|
||||
const claimNo = await this.generateUniqueClaimNumber();
|
||||
|
||||
// 8. Create claim case
|
||||
const newClaim = await this.claimCaseDbService.create({
|
||||
requestNo: claimNo,
|
||||
publicId: blameRequest.publicId,
|
||||
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||
blameRequestNo: blameRequest.requestNo,
|
||||
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||
claimStatus: ClaimStatus.PENDING,
|
||||
workflow: {
|
||||
currentStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
nextStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||
locked: false,
|
||||
},
|
||||
owner: {
|
||||
userId: new Types.ObjectId(currentUserId),
|
||||
userRole: currentUserParty.role as any,
|
||||
},
|
||||
history: [
|
||||
{
|
||||
type: "CLAIM_CREATED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(currentUserId),
|
||||
actorName: currentUserParty.person?.fullName || "User",
|
||||
actorType: "user",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
blameRequestId,
|
||||
blamePublicId: blameRequest.publicId,
|
||||
},
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
|
||||
this.logger.log(
|
||||
`Claim created: ${newClaim._id} from blame: ${blameRequestId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
claimRequestId: String(newClaim._id),
|
||||
publicId: blameRequest.publicId,
|
||||
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||
message: "Claim request created successfully",
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error(
|
||||
"createClaimFromBlameV2 failed",
|
||||
blameRequestId,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to create claim request",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Select damaged outer car parts for claim (Step 2 of claim workflow)
|
||||
*
|
||||
* @param claimRequestId - The claim case ID
|
||||
* @param body - Selected outer parts
|
||||
* @param currentUserId - The current user's ID
|
||||
* @returns Response with updated claim information
|
||||
*
|
||||
* Validations:
|
||||
* - Claim case must exist
|
||||
* - User must be the claim owner (damaged party)
|
||||
* - Current workflow step must be CLAIM_CREATED (step 1)
|
||||
* - Selected parts array must not be empty
|
||||
* - Parts must not have been selected previously
|
||||
*/
|
||||
async selectOuterPartsV2(
|
||||
claimRequestId: string,
|
||||
body: SelectOuterPartsV2Dto,
|
||||
currentUserId: string,
|
||||
): Promise<SelectOuterPartsV2ResponseDto> {
|
||||
try {
|
||||
// 1. Validate claim exists
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claimCase) {
|
||||
throw new NotFoundException(
|
||||
`Claim case with ID ${claimRequestId} not found`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Validate user is the claim owner (damaged party)
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
throw new ForbiddenException(
|
||||
'Only the claim owner (damaged party) can select damaged parts'
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Validate workflow step is correct (should be at SELECT_OUTER_PARTS)
|
||||
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OUTER_PARTS) {
|
||||
throw new BadRequestException(
|
||||
`Invalid workflow step. Expected ${ClaimWorkflowStep.SELECT_OUTER_PARTS}, but current step is ${claimCase.workflow?.currentStep}`
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Validate outer parts haven't been selected yet
|
||||
if (claimCase.damage?.selectedParts && claimCase.damage.selectedParts.length > 0) {
|
||||
throw new ConflictException(
|
||||
'Outer parts have already been selected for this claim'
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Convert selected parts to storage format (simple array of strings)
|
||||
const selectedParts = body.selectedParts;
|
||||
|
||||
// 6. Update claim case with selected parts and move to next step
|
||||
const updatedClaim = 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(currentUserId),
|
||||
actorName: claimCase.owner?.fullName || 'User',
|
||||
actorType: 'user',
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
selectedParts: selectedParts,
|
||||
partsCount: selectedParts.length,
|
||||
description: `User selected ${selectedParts.length} damaged outer parts`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!updatedClaim) {
|
||||
throw new InternalServerErrorException('Failed to update claim case');
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Outer parts selected for claim ${claimRequestId}: ${selectedParts.length} parts`,
|
||||
);
|
||||
|
||||
// 7. Return response
|
||||
return {
|
||||
claimRequestId: updatedClaim._id.toString(),
|
||||
publicId: updatedClaim.publicId,
|
||||
selectedParts: selectedParts,
|
||||
currentStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
nextStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
message: 'Outer parts selected successfully. Please proceed to select other parts and provide bank information.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error(
|
||||
'selectOuterPartsV2 failed',
|
||||
claimRequestId,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to select outer parts',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Select other damaged parts and provide bank information (Step 3 of claim workflow)
|
||||
*
|
||||
* @param claimRequestId - The claim case ID
|
||||
* @param body - Other parts, Sheba number, and national code
|
||||
* @param currentUserId - The current user's ID
|
||||
* @returns Response with updated claim information
|
||||
*
|
||||
* Validations:
|
||||
* - Claim case must exist
|
||||
* - User must be the claim owner (damaged party)
|
||||
* - Current workflow step must be SELECT_OTHER_PARTS (step 3)
|
||||
* - Other parts and bank info must not have been submitted previously
|
||||
*/
|
||||
async selectOtherPartsV2(
|
||||
claimRequestId: string,
|
||||
body: SelectOtherPartsV2Dto,
|
||||
currentUserId: string,
|
||||
): Promise<SelectOtherPartsV2ResponseDto> {
|
||||
try {
|
||||
// 1. Validate claim exists
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claimCase) {
|
||||
throw new NotFoundException(
|
||||
`Claim case with ID ${claimRequestId} not found`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Validate user is the claim owner (damaged party)
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
throw new ForbiddenException(
|
||||
'Only the claim owner (damaged party) can submit other parts and bank information'
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Validate workflow step is correct (should be at SELECT_OTHER_PARTS)
|
||||
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OTHER_PARTS) {
|
||||
throw new BadRequestException(
|
||||
`Invalid workflow step. Expected ${ClaimWorkflowStep.SELECT_OTHER_PARTS}, but current step is ${claimCase.workflow?.currentStep}`
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Validate other parts and bank info haven't been submitted yet
|
||||
if (claimCase.money?.sheba || claimCase.money?.nationalCodeOfInsurer) {
|
||||
throw new ConflictException(
|
||||
'Bank information has already been submitted for this claim'
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Prepare data
|
||||
const otherParts = body.otherParts || [];
|
||||
const shebaNumber = body.shebaNumber;
|
||||
const nationalCode = body.nationalCodeOfOwner;
|
||||
|
||||
// 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`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!updatedClaim) {
|
||||
throw new InternalServerErrorException('Failed to update claim case');
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Other parts and bank info submitted for claim ${claimRequestId}: ${otherParts.length} parts`,
|
||||
);
|
||||
|
||||
// 7. Mask sensitive data for response
|
||||
const maskedSheba = shebaNumber.replace(/^(.{4})(.*)(.{4})$/, 'IR$1************$3');
|
||||
const maskedNationalCode = nationalCode.replace(/^(.{2})(.*)(.{2})$/, '$1******$3');
|
||||
|
||||
// 8. Return response
|
||||
return {
|
||||
claimRequestId: updatedClaim._id.toString(),
|
||||
publicId: updatedClaim.publicId,
|
||||
otherParts: otherParts,
|
||||
shebaNumber: maskedSheba,
|
||||
nationalCodeOfOwner: maskedNationalCode,
|
||||
currentStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
nextStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
message: 'Other parts and bank information saved successfully. Please proceed to upload required documents.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error(
|
||||
'selectOtherPartsV2 failed',
|
||||
claimRequestId,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to select other parts',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Get capture requirements for claim (documents, angles, parts)
|
||||
*/
|
||||
async getCaptureRequirementsV2(
|
||||
claimRequestId: string,
|
||||
currentUserId: string,
|
||||
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claimCase) {
|
||||
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||
}
|
||||
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
throw new ForbiddenException('Only the claim owner can view capture requirements');
|
||||
}
|
||||
|
||||
// Helper to get part labels
|
||||
const getPartLabel = (key: string): { fa: string; en: string } => {
|
||||
const labels = {
|
||||
hood: { fa: 'کاپوت', en: 'Hood' },
|
||||
trunk: { fa: 'صندوق عقب', en: 'Trunk' },
|
||||
roof: { fa: 'سقف', en: 'Roof' },
|
||||
front_right_door: { fa: 'درب جلو راست', en: 'Front Right Door' },
|
||||
front_left_door: { fa: 'درب جلو چپ', en: 'Front Left Door' },
|
||||
rear_right_door: { fa: 'درب عقب راست', en: 'Rear Right Door' },
|
||||
rear_left_door: { fa: 'درب عقب چپ', en: 'Rear Left Door' },
|
||||
front_bumper: { fa: 'سپر جلو', en: 'Front Bumper' },
|
||||
rear_bumper: { fa: 'سپر عقب', en: 'Rear Bumper' },
|
||||
front_right_fender: { fa: 'گلگیر جلو راست', en: 'Front Right Fender' },
|
||||
front_left_fender: { fa: 'گلگیر جلو چپ', en: 'Front Left Fender' },
|
||||
rear_right_fender: { fa: 'گلگیر عقب راست', en: 'Rear Right Fender' },
|
||||
rear_left_fender: { fa: 'گلگیر عقب چپ', en: 'Rear Left Fender' },
|
||||
};
|
||||
return labels[key] || { fa: key, en: key };
|
||||
};
|
||||
|
||||
// Required documents
|
||||
const requiredDocsDefinition = [
|
||||
{ key: 'car_green_card', label_fa: 'کارت سبز خودرو', label_en: 'Car Green Card', category: 'general' },
|
||||
{ key: 'damaged_driving_license_front', label_fa: 'گواهینامه طرف آسیبدیده - جلو', label_en: 'Damaged Party License - Front', category: 'damaged_party' },
|
||||
{ key: 'damaged_driving_license_back', label_fa: 'گواهینامه طرف آسیبدیده - پشت', label_en: 'Damaged Party License - Back', category: 'damaged_party' },
|
||||
{ key: 'damaged_chassis_number', label_fa: 'شماره شاسی', label_en: 'Chassis Number', category: 'damaged_party' },
|
||||
{ key: 'damaged_engine_photo', label_fa: 'عکس موتور', label_en: 'Engine Photo', category: 'damaged_party' },
|
||||
{ key: 'damaged_car_card_front', label_fa: 'کارت خودرو آسیبدیده - جلو', label_en: 'Damaged Car Card - Front', category: 'damaged_party' },
|
||||
{ key: 'damaged_car_card_back', label_fa: 'کارت خودرو آسیبدیده - پشت', label_en: 'Damaged Car Card - Back', category: 'damaged_party' },
|
||||
{ key: 'damaged_metal_plate', label_fa: 'پلاک فلزی آسیبدیده', label_en: 'Damaged Metal Plate', category: 'damaged_party' },
|
||||
{ key: 'guilty_driving_license_front', label_fa: 'گواهینامه طرف مقصر - جلو', label_en: 'Guilty Party License - Front', category: 'guilty_party' },
|
||||
{ key: 'guilty_driving_license_back', label_fa: 'گواهینامه طرف مقصر - پشت', label_en: 'Guilty Party License - Back', category: 'guilty_party' },
|
||||
{ key: 'guilty_car_card_front', label_fa: 'کارت خودرو مقصر - جلو', label_en: 'Guilty Car Card - Front', category: 'guilty_party' },
|
||||
{ key: 'guilty_car_card_back', label_fa: 'کارت خودرو مقصر - پشت', label_en: 'Guilty Car Card - Back', category: 'guilty_party' },
|
||||
{ key: 'guilty_metal_plate', label_fa: 'پلاک فلزی مقصر', label_en: 'Guilty Metal Plate', category: 'guilty_party' },
|
||||
];
|
||||
|
||||
const requiredDocuments = requiredDocsDefinition.map(doc => {
|
||||
const docData = (claimCase.requiredDocuments as any)?.get?.(doc.key);
|
||||
return {
|
||||
key: doc.key,
|
||||
label_fa: doc.label_fa,
|
||||
label_en: doc.label_en,
|
||||
category: doc.category,
|
||||
uploaded: docData?.uploaded || false,
|
||||
};
|
||||
});
|
||||
|
||||
const hasCapture = (data: any, key: string) =>
|
||||
data && (data instanceof Map ? data.get(key) : data[key]);
|
||||
|
||||
// Car angles
|
||||
const carAngles = [
|
||||
{ key: 'front', label_fa: 'جلو', label_en: 'Front' },
|
||||
{ key: 'back', label_fa: 'عقب', label_en: 'Back' },
|
||||
{ key: 'left', label_fa: 'چپ', label_en: 'Left' },
|
||||
{ key: 'right', label_fa: 'راست', label_en: 'Right' },
|
||||
].map(angle => ({
|
||||
key: angle.key,
|
||||
label_fa: angle.label_fa,
|
||||
label_en: angle.label_en,
|
||||
captured: !!hasCapture(claimCase.media?.carAngles as any, angle.key),
|
||||
}));
|
||||
|
||||
// Damaged parts from selected parts
|
||||
const selectedParts = claimCase.damage?.selectedParts || [];
|
||||
const damagedParts = selectedParts.map(partKey => {
|
||||
const label = getPartLabel(partKey);
|
||||
return {
|
||||
key: partKey,
|
||||
label_fa: label.fa,
|
||||
label_en: label.en,
|
||||
captured: !!hasCapture(claimCase.media?.damagedParts as any, partKey),
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate progress
|
||||
const documentsUploaded = requiredDocuments.filter(d => d.uploaded).length;
|
||||
const anglesCaptured = carAngles.filter(a => a.captured).length;
|
||||
const partsCaptured = damagedParts.filter(p => p.captured).length;
|
||||
|
||||
const totalRemaining =
|
||||
(requiredDocuments.length - documentsUploaded) +
|
||||
(carAngles.length - anglesCaptured) +
|
||||
(damagedParts.length - partsCaptured);
|
||||
|
||||
return {
|
||||
claimRequestId: claimCase._id.toString(),
|
||||
publicId: claimCase.publicId,
|
||||
currentStep: claimCase.workflow?.currentStep || '',
|
||||
requiredDocuments,
|
||||
carAngles,
|
||||
damagedParts,
|
||||
totalRemaining,
|
||||
progress: {
|
||||
documentsUploaded,
|
||||
documentsTotal: requiredDocuments.length,
|
||||
anglesCaptured,
|
||||
anglesTotal: carAngles.length,
|
||||
partsCaptured,
|
||||
partsTotal: damagedParts.length,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error('getCaptureRequirementsV2 failed', error);
|
||||
throw new InternalServerErrorException('Failed to get capture requirements');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Upload required document
|
||||
*/
|
||||
async uploadRequiredDocumentV2(
|
||||
claimRequestId: string,
|
||||
body: UploadRequiredDocumentV2Dto,
|
||||
file: Express.Multer.File,
|
||||
currentUserId: string,
|
||||
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claimCase) {
|
||||
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||
}
|
||||
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
throw new ForbiddenException('Only the claim owner can upload documents');
|
||||
}
|
||||
|
||||
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS) {
|
||||
throw new BadRequestException(
|
||||
`Invalid workflow step. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS}, but current step is ${claimCase.workflow?.currentStep}`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if document already uploaded
|
||||
const existingDoc = (claimCase.requiredDocuments as any)?.get?.(body.documentKey);
|
||||
if (existingDoc?.uploaded) {
|
||||
throw new ConflictException(`Document ${body.documentKey} has already been uploaded`);
|
||||
}
|
||||
|
||||
const fileUrl = buildFileLink(file.path);
|
||||
|
||||
// Create document reference in claim-required-documents collection
|
||||
const docRef = await this.claimRequiredDocumentDbService.create({
|
||||
path: file.path,
|
||||
fileName: file.filename,
|
||||
claimId: new Types.ObjectId(claimRequestId),
|
||||
documentType: body.documentKey,
|
||||
uploadedAt: new Date(),
|
||||
});
|
||||
|
||||
// Update claim case
|
||||
const updateData: any = {
|
||||
[`requiredDocuments.${body.documentKey}`]: {
|
||||
fileId: docRef._id,
|
||||
filePath: file.path,
|
||||
fileName: file.filename,
|
||||
uploaded: true,
|
||||
uploadedAt: new Date(),
|
||||
},
|
||||
$push: {
|
||||
history: {
|
||||
type: 'DOCUMENT_UPLOADED',
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(currentUserId),
|
||||
actorName: claimCase.owner?.fullName || 'User',
|
||||
actorType: 'user',
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
documentKey: body.documentKey,
|
||||
fileName: file.filename,
|
||||
fileId: docRef._id.toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Check if all documents are uploaded
|
||||
const totalDocs = 13;
|
||||
const currentDocs = claimCase.requiredDocuments || new Map();
|
||||
const uploadedCount = (currentDocs instanceof Map ? currentDocs.size : Object.keys(currentDocs).length) + 1;
|
||||
const allDocumentsUploaded = uploadedCount >= totalDocs;
|
||||
|
||||
// If all documents uploaded, move to next step
|
||||
if (allDocumentsUploaded) {
|
||||
updateData['status'] = ClaimCaseStatus.CAPTURING_PART_DAMAGES;
|
||||
updateData['workflow.currentStep'] = ClaimWorkflowStep.CAPTURE_PART_DAMAGES;
|
||||
updateData['workflow.nextStep'] = ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||
updateData.$push.history = [
|
||||
updateData.$push.history,
|
||||
{
|
||||
type: 'STEP_COMPLETED',
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(currentUserId),
|
||||
actorName: claimCase.owner?.fullName || 'User',
|
||||
actorType: 'user',
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
description: 'All required documents uploaded',
|
||||
},
|
||||
},
|
||||
];
|
||||
updateData.$push['workflow.completedSteps'] = ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
|
||||
}
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updateData);
|
||||
|
||||
const remaining = totalDocs - uploadedCount;
|
||||
const message = allDocumentsUploaded
|
||||
? 'All documents uploaded successfully. Please proceed to capture car angles and damaged parts.'
|
||||
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
||||
|
||||
return {
|
||||
claimRequestId: claimCase._id.toString(),
|
||||
documentKey: body.documentKey,
|
||||
fileUrl,
|
||||
allDocumentsUploaded,
|
||||
currentStep: allDocumentsUploaded ? ClaimWorkflowStep.CAPTURE_PART_DAMAGES : ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
message,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error('uploadRequiredDocumentV2 failed', error);
|
||||
throw new InternalServerErrorException('Failed to upload document');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Capture car angle or damaged part
|
||||
*/
|
||||
async capturePartV2(
|
||||
claimRequestId: string,
|
||||
body: CapturePartV2Dto,
|
||||
file: Express.Multer.File,
|
||||
currentUserId: string,
|
||||
): Promise<CapturePartV2ResponseDto> {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claimCase) {
|
||||
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||
}
|
||||
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
throw new ForbiddenException('Only the claim owner can capture parts');
|
||||
}
|
||||
|
||||
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.CAPTURE_PART_DAMAGES) {
|
||||
throw new BadRequestException(
|
||||
`Invalid workflow step. Expected ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES}, but current step is ${claimCase.workflow?.currentStep}`
|
||||
);
|
||||
}
|
||||
|
||||
const fileUrl = buildFileLink(file.path);
|
||||
const captureData = {
|
||||
path: file.path,
|
||||
fileName: file.filename,
|
||||
url: fileUrl,
|
||||
capturedAt: new Date(),
|
||||
};
|
||||
|
||||
const updateData: any = {
|
||||
$push: {
|
||||
history: {
|
||||
type: body.captureType === 'angle' ? 'ANGLE_CAPTURED' : 'PART_CAPTURED',
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(currentUserId),
|
||||
actorName: claimCase.owner?.fullName || 'User',
|
||||
actorType: 'user',
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
captureType: body.captureType,
|
||||
captureKey: body.captureKey,
|
||||
fileName: file.filename,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (body.captureType === 'angle') {
|
||||
updateData[`media.carAngles.${body.captureKey}`] = captureData;
|
||||
} else {
|
||||
updateData[`media.damagedParts.${body.captureKey}`] = captureData;
|
||||
}
|
||||
|
||||
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
|
||||
claimRequestId,
|
||||
updateData,
|
||||
);
|
||||
|
||||
const hasCapture = (data: any, key: string) =>
|
||||
data && (data instanceof Map ? data.get(key) : data[key]);
|
||||
|
||||
// Use same logic as getCaptureRequirementsV2 to determine completion
|
||||
const anglesKeys = ['front', 'back', 'left', 'right'];
|
||||
const carAnglesData = updatedClaim?.media?.carAngles as any;
|
||||
const damagedPartsData = updatedClaim?.media?.damagedParts as any;
|
||||
const selectedParts = updatedClaim?.damage?.selectedParts || [];
|
||||
|
||||
const anglesCaptured = anglesKeys.filter(k => hasCapture(carAnglesData, k)).length;
|
||||
const partsCaptured = selectedParts.filter(p => hasCapture(damagedPartsData, p)).length;
|
||||
|
||||
const allCapturesComplete =
|
||||
anglesCaptured >= 4 && partsCaptured >= selectedParts.length;
|
||||
|
||||
if (allCapturesComplete) {
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||
claimStatus: ClaimStatus.PENDING,
|
||||
'workflow.currentStep': ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
'workflow.nextStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
$push: {
|
||||
'workflow.completedSteps': ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
history: {
|
||||
type: 'STEP_COMPLETED',
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(currentUserId),
|
||||
actorName: claimCase.owner?.fullName || 'User',
|
||||
actorType: 'user',
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
description: 'User submission complete. Claim ready for damage expert review.',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const remaining = (4 - anglesCaptured) + (selectedParts.length - partsCaptured);
|
||||
const message = allCapturesComplete
|
||||
? 'All captures complete. Your claim is now ready for damage expert review.'
|
||||
: `${body.captureType === 'angle' ? 'Angle' : 'Part'} captured successfully. ${remaining} captures remaining.`;
|
||||
|
||||
return {
|
||||
claimRequestId: claimCase._id.toString(),
|
||||
captureType: body.captureType,
|
||||
captureKey: body.captureKey,
|
||||
fileUrl,
|
||||
allCapturesComplete,
|
||||
currentStep: allCapturesComplete
|
||||
? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
|
||||
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
message,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error('capturePartV2 failed', error);
|
||||
throw new InternalServerErrorException('Failed to capture part');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Get list of claims for current user
|
||||
*/
|
||||
async getMyClaimsV2(currentUserId: string): Promise<GetMyClaimsV2ResponseDto> {
|
||||
try {
|
||||
const claims = await this.claimCaseDbService.find(
|
||||
{ 'owner.userId': new Types.ObjectId(currentUserId) },
|
||||
{ lean: true },
|
||||
);
|
||||
const list = (claims as any[]).map((c) => ({
|
||||
claimRequestId: c._id.toString(),
|
||||
publicId: c.publicId,
|
||||
requestNo: c.requestNo,
|
||||
status: c.status,
|
||||
claimStatus: c.claimStatus || 'PENDING',
|
||||
currentStep: c.workflow?.currentStep || '',
|
||||
createdAt: c.createdAt,
|
||||
blameRequestId: c.blameRequestId?.toString(),
|
||||
})) as ClaimListItemV2Dto[];
|
||||
return { list, total: list.length };
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error('getMyClaimsV2 failed', error);
|
||||
throw new InternalServerErrorException('Failed to get claims list');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Get claim details by ID (owner only)
|
||||
*/
|
||||
async getClaimDetailsV2(
|
||||
claimRequestId: string,
|
||||
currentUserId: string,
|
||||
): Promise<ClaimDetailsV2ResponseDto> {
|
||||
try {
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim) {
|
||||
throw new NotFoundException('Claim request not found');
|
||||
}
|
||||
if (!claim.owner?.userId || claim.owner.userId.toString() !== currentUserId) {
|
||||
throw new ForbiddenException('You do not have access to this claim');
|
||||
}
|
||||
|
||||
const hasCapture = (data: any, key: string) =>
|
||||
data && (data instanceof Map ? data.get(key) : data[key]);
|
||||
|
||||
const requiredDocs = claim.requiredDocuments as any;
|
||||
const requiredDocumentsStatus: Record<string, { uploaded: boolean; fileId?: string }> = {};
|
||||
if (requiredDocs) {
|
||||
const keys = requiredDocs instanceof Map ? Array.from(requiredDocs.keys()) : Object.keys(requiredDocs);
|
||||
for (const k of keys) {
|
||||
const doc = requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
|
||||
requiredDocumentsStatus[k] = {
|
||||
uploaded: !!doc?.uploaded,
|
||||
fileId: doc?.fileId?.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const carAnglesData = claim.media?.carAngles as any;
|
||||
const carAngles: Record<string, { captured: boolean; url?: string }> = {};
|
||||
for (const k of ['front', 'back', 'left', 'right']) {
|
||||
const cap = hasCapture(carAnglesData, k);
|
||||
carAngles[k] = {
|
||||
captured: !!cap,
|
||||
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
const damagedPartsData = claim.media?.damagedParts as any;
|
||||
const damagedParts: Record<string, { captured: boolean; url?: string }> = {};
|
||||
for (const p of claim.damage?.selectedParts || []) {
|
||||
const cap = hasCapture(damagedPartsData, p);
|
||||
damagedParts[p] = {
|
||||
captured: !!cap,
|
||||
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
const maskSheba = (s?: string) =>
|
||||
s ? s.replace(/^(.{4})(.*)(.{4})$/, 'IR$1************$3') : undefined;
|
||||
const maskNationalCode = (s?: string) =>
|
||||
s ? s.replace(/^(.{2})(.*)(.{2})$/, '$1******$3') : undefined;
|
||||
|
||||
return {
|
||||
claimRequestId: claim._id.toString(),
|
||||
publicId: claim.publicId,
|
||||
requestNo: claim.requestNo,
|
||||
status: claim.status,
|
||||
claimStatus: claim.claimStatus || 'PENDING',
|
||||
currentStep: claim.workflow?.currentStep || '',
|
||||
nextStep: claim.workflow?.nextStep,
|
||||
blameRequestId: claim.blameRequestId?.toString(),
|
||||
blameRequestNo: claim.blameRequestNo,
|
||||
owner: claim.owner
|
||||
? {
|
||||
userId: claim.owner.userId?.toString(),
|
||||
fullName: claim.owner.fullName,
|
||||
}
|
||||
: undefined,
|
||||
vehicle: claim.vehicle,
|
||||
selectedParts: claim.damage?.selectedParts,
|
||||
otherParts: claim.damage?.otherParts,
|
||||
money: claim.money
|
||||
? {
|
||||
sheba: maskSheba(claim.money.sheba),
|
||||
nationalCodeOfOwner: maskNationalCode(claim.money.nationalCodeOfInsurer),
|
||||
}
|
||||
: undefined,
|
||||
requiredDocuments: Object.keys(requiredDocumentsStatus).length > 0 ? requiredDocumentsStatus : undefined,
|
||||
carAngles,
|
||||
damagedParts,
|
||||
createdAt: (claim as any).createdAt,
|
||||
updatedAt: (claim as any).updatedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error('getClaimDetailsV2 failed', error);
|
||||
throw new InternalServerErrorException('Failed to get claim details');
|
||||
}
|
||||
}
|
||||
|
||||
private async generateUniqueClaimNumber(): Promise<string> {
|
||||
const prefix = "CL";
|
||||
const randomPart = Math.floor(10000 + Math.random() * 90000); // 5 digits
|
||||
return `${prefix}${randomPart}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,609 @@
|
||||
import {
|
||||
Controller,
|
||||
HttpException,
|
||||
InternalServerErrorException,
|
||||
Param,
|
||||
Post,
|
||||
Patch,
|
||||
Body,
|
||||
UseGuards,
|
||||
Get,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
} from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, ApiConsumes } from "@nestjs/swagger";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import { diskStorage } from "multer";
|
||||
import { extname } from "path";
|
||||
import { GlobalGuard } from "src/auth/guards/global.guard";
|
||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||
import { Roles } from "src/decorators/roles.decorator";
|
||||
import { CurrentUser } from "src/decorators/user.decorator";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||
import { SelectOuterPartsV2Dto, SelectOuterPartsV2ResponseDto } from "./dto/select-outer-parts-v2.dto";
|
||||
import { SelectOtherPartsV2Dto, SelectOtherPartsV2ResponseDto } from "./dto/select-other-parts-v2.dto";
|
||||
import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto";
|
||||
import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto } from "./dto/upload-document-v2.dto";
|
||||
import { CapturePartV2Dto, CapturePartV2ResponseDto } from "./dto/capture-part-v2.dto";
|
||||
import { GetMyClaimsV2ResponseDto } from "./dto/my-claims-v2.dto";
|
||||
import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
|
||||
|
||||
@ApiTags("claim-request-management (v2)")
|
||||
@Controller("v2/claim-request-management")
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(GlobalGuard, RolesGuard)
|
||||
@Roles(RoleEnum.USER)
|
||||
export class ClaimRequestManagementV2Controller {
|
||||
constructor(
|
||||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||
) {}
|
||||
|
||||
@Get("requests")
|
||||
@ApiOperation({
|
||||
summary: "Get My Claims (V2)",
|
||||
description: "Get list of all claim requests for the current user.",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "List of user claims",
|
||||
type: GetMyClaimsV2ResponseDto,
|
||||
})
|
||||
async getMyClaims(@CurrentUser() user: any): Promise<GetMyClaimsV2ResponseDto> {
|
||||
try {
|
||||
return await this.claimRequestManagementService.getMyClaimsV2(user.sub);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error ? error.message : "Failed to get claims list",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get("request/:claimRequestId")
|
||||
@ApiParam({
|
||||
name: "claimRequestId",
|
||||
description: "The claim case ID (MongoDB ObjectId)",
|
||||
example: "507f1f77bcf86cd799439011",
|
||||
})
|
||||
@ApiOperation({
|
||||
summary: "Get Claim Details (V2)",
|
||||
description: "Get full details of a claim request. Only the claim owner can access.",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "Claim details",
|
||||
type: ClaimDetailsV2ResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 403,
|
||||
description: "User is not the claim owner",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: "Claim not found",
|
||||
})
|
||||
async getClaimDetails(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@CurrentUser() user: any,
|
||||
): Promise<ClaimDetailsV2ResponseDto> {
|
||||
try {
|
||||
return await this.claimRequestManagementService.getClaimDetailsV2(
|
||||
claimRequestId,
|
||||
user.sub,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error ? error.message : "Failed to get claim details",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Post("create-from-blame/:blameRequestId")
|
||||
@ApiParam({
|
||||
name: "blameRequestId",
|
||||
description: "ID of the completed blame case to create claim from",
|
||||
})
|
||||
async createClaimFromBlame(
|
||||
@Param("blameRequestId") blameRequestId: string,
|
||||
@CurrentUser() user: any,
|
||||
) {
|
||||
try {
|
||||
return await this.claimRequestManagementService.createClaimFromBlameV2(
|
||||
blameRequestId,
|
||||
user.sub,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to create claim request",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Select damaged outer car parts (Step 2 of claim workflow)
|
||||
*/
|
||||
@Patch("select-outer-parts/:claimRequestId")
|
||||
@ApiOperation({
|
||||
summary: "Select Damaged Outer Car Parts (V2 - Step 2)",
|
||||
description: `
|
||||
**Workflow Step:** SELECT_OUTER_PARTS (Step 2 of Claim)
|
||||
|
||||
**Purpose:** User selects which outer car parts (body parts) were damaged in the accident.
|
||||
|
||||
**Validations:**
|
||||
- Claim must exist
|
||||
- User must be the claim owner (damaged party from blame case)
|
||||
- Current workflow step must be CLAIM_CREATED
|
||||
- Parts array must contain at least 1 part
|
||||
- No duplicate parts allowed
|
||||
- Parts cannot be re-selected once submitted
|
||||
|
||||
**Valid Outer Parts:**
|
||||
- hood, trunk, roof
|
||||
- front_right_door, front_left_door, rear_right_door, rear_left_door
|
||||
- front_bumper, rear_bumper
|
||||
- front_right_fender, front_left_fender, rear_right_fender, rear_left_fender
|
||||
|
||||
**After Success:**
|
||||
- Workflow moves to: SELECT_OTHER_PARTS (Step 3)
|
||||
- User can proceed to select non-body parts and provide bank info
|
||||
`,
|
||||
})
|
||||
@ApiParam({
|
||||
name: "claimRequestId",
|
||||
description: "The claim case ID (MongoDB ObjectId)",
|
||||
example: "507f1f77bcf86cd799439011",
|
||||
})
|
||||
@ApiBody({
|
||||
type: SelectOuterPartsV2Dto,
|
||||
description: "Array of selected damaged outer parts",
|
||||
examples: {
|
||||
example1: {
|
||||
summary: "Minor front damage",
|
||||
value: {
|
||||
selectedParts: ["hood", "front_bumper", "front_right_fender"],
|
||||
},
|
||||
},
|
||||
example2: {
|
||||
summary: "Side impact damage",
|
||||
value: {
|
||||
selectedParts: [
|
||||
"front_left_door",
|
||||
"rear_left_door",
|
||||
"front_left_fender",
|
||||
"rear_left_fender",
|
||||
],
|
||||
},
|
||||
},
|
||||
example3: {
|
||||
summary: "Rear-end collision",
|
||||
value: {
|
||||
selectedParts: ["rear_bumper", "trunk", "rear_right_fender"],
|
||||
},
|
||||
},
|
||||
example4: {
|
||||
summary: "Multiple damage areas",
|
||||
value: {
|
||||
selectedParts: [
|
||||
"hood",
|
||||
"front_bumper",
|
||||
"front_right_door",
|
||||
"rear_bumper",
|
||||
"trunk",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "Outer parts selected successfully",
|
||||
type: SelectOuterPartsV2ResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: "Invalid workflow step or validation failed",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 403,
|
||||
description: "User is not the claim owner",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: "Claim case not found",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 409,
|
||||
description: "Outer parts already selected",
|
||||
})
|
||||
async selectOuterParts(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@Body() body: SelectOuterPartsV2Dto,
|
||||
@CurrentUser() user: any,
|
||||
): Promise<SelectOuterPartsV2ResponseDto> {
|
||||
try {
|
||||
return await this.claimRequestManagementService.selectOuterPartsV2(
|
||||
claimRequestId,
|
||||
body,
|
||||
user.sub,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to select outer parts",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Select other damaged parts and provide bank information (Step 3 of claim workflow)
|
||||
*/
|
||||
@Patch("select-other-parts/:claimRequestId")
|
||||
@ApiOperation({
|
||||
summary: "Select Other Parts & Bank Information (V2 - Step 3)",
|
||||
description: `
|
||||
**Workflow Step:** SELECT_OTHER_PARTS (Step 3 of Claim)
|
||||
|
||||
**Purpose:** User selects non-body damaged parts and provides bank information for payment.
|
||||
|
||||
**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
|
||||
- National code must be exactly 10 digits
|
||||
|
||||
**Valid Other Parts (Optional):**
|
||||
- engine, suspension, brake_system, electrical
|
||||
- radiator, transmission, exhaust
|
||||
- headlight, taillight, mirror, glass
|
||||
|
||||
**After Success:**
|
||||
- Workflow moves to: UPLOAD_REQUIRED_DOCUMENTS (Step 4)
|
||||
- User can proceed to upload required documents
|
||||
`,
|
||||
})
|
||||
@ApiParam({
|
||||
name: "claimRequestId",
|
||||
description: "The claim case ID (MongoDB ObjectId)",
|
||||
example: "507f1f77bcf86cd799439011",
|
||||
})
|
||||
@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",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "Other parts and bank information saved successfully",
|
||||
type: SelectOtherPartsV2ResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: "Invalid workflow step or validation failed",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 403,
|
||||
description: "User is not the claim owner",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: "Claim case not found",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 409,
|
||||
description: "Bank information already submitted",
|
||||
})
|
||||
async selectOtherParts(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@Body() body: SelectOtherPartsV2Dto,
|
||||
@CurrentUser() user: any,
|
||||
): Promise<SelectOtherPartsV2ResponseDto> {
|
||||
try {
|
||||
return await this.claimRequestManagementService.selectOtherPartsV2(
|
||||
claimRequestId,
|
||||
body,
|
||||
user.sub,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to select other parts",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Get capture requirements (documents, angles, parts)
|
||||
*/
|
||||
@Get("capture-requirements/:claimRequestId")
|
||||
@ApiOperation({
|
||||
summary: "Get Capture Requirements (V2)",
|
||||
description: `
|
||||
**Get list of what needs to be captured:**
|
||||
- Required documents (13 items)
|
||||
- Car angles (4 items: front, back, left, right)
|
||||
- Damaged parts (based on selected outer parts)
|
||||
|
||||
Returns status of each item (uploaded/captured or not).
|
||||
`,
|
||||
})
|
||||
@ApiParam({
|
||||
name: "claimRequestId",
|
||||
description: "The claim case ID (MongoDB ObjectId)",
|
||||
example: "507f1f77bcf86cd799439011",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "Capture requirements retrieved successfully",
|
||||
type: GetCaptureRequirementsV2ResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 403,
|
||||
description: "User is not the claim owner",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: "Claim case not found",
|
||||
})
|
||||
async getCaptureRequirements(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@CurrentUser() user: any,
|
||||
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
||||
try {
|
||||
return await this.claimRequestManagementService.getCaptureRequirementsV2(
|
||||
claimRequestId,
|
||||
user.sub,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to get capture requirements",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Upload required document
|
||||
*/
|
||||
@Post("upload-document/:claimRequestId")
|
||||
@UseInterceptors(
|
||||
FileInterceptor("file", {
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB
|
||||
},
|
||||
storage: diskStorage({
|
||||
destination: "./files/claim-documents",
|
||||
filename: (req, file, callback) => {
|
||||
const unique = Date.now();
|
||||
const ex = extname(file.originalname);
|
||||
const filename = `${file.originalname.split(".")[0]}-${unique}${ex}`;
|
||||
callback(null, filename);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
summary: "Upload Required Document (V2 - Step 4)",
|
||||
description: `
|
||||
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 4 of Claim)
|
||||
|
||||
**Upload one of the 13 required documents:**
|
||||
- car_green_card
|
||||
- damaged_driving_license_front/back
|
||||
- damaged_chassis_number, damaged_engine_photo
|
||||
- damaged_car_card_front/back, damaged_metal_plate
|
||||
- guilty_driving_license_front/back
|
||||
- guilty_car_card_front/back, guilty_metal_plate
|
||||
|
||||
**When all 13 documents are uploaded:**
|
||||
- Workflow automatically moves to: CAPTURE_PART_DAMAGES (Step 5)
|
||||
`,
|
||||
})
|
||||
@ApiParam({
|
||||
name: "claimRequestId",
|
||||
description: "The claim case ID",
|
||||
example: "507f1f77bcf86cd799439011",
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["documentKey", "file"],
|
||||
properties: {
|
||||
documentKey: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"car_green_card",
|
||||
"damaged_driving_license_front",
|
||||
"damaged_driving_license_back",
|
||||
"damaged_chassis_number",
|
||||
"damaged_engine_photo",
|
||||
"damaged_car_card_front",
|
||||
"damaged_car_card_back",
|
||||
"damaged_metal_plate",
|
||||
"guilty_driving_license_front",
|
||||
"guilty_driving_license_back",
|
||||
"guilty_car_card_front",
|
||||
"guilty_car_card_back",
|
||||
"guilty_metal_plate",
|
||||
],
|
||||
example: "car_green_card",
|
||||
},
|
||||
file: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "Document uploaded successfully",
|
||||
type: UploadRequiredDocumentV2ResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: "Invalid workflow step",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 409,
|
||||
description: "Document already uploaded",
|
||||
})
|
||||
async uploadDocument(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@Body() body: UploadRequiredDocumentV2Dto,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@CurrentUser() user: any,
|
||||
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||
try {
|
||||
return await this.claimRequestManagementService.uploadRequiredDocumentV2(
|
||||
claimRequestId,
|
||||
body,
|
||||
file,
|
||||
user.sub,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error ? error.message : "Failed to upload document",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Capture car angle or damaged part
|
||||
*/
|
||||
@Post("capture-part/:claimRequestId")
|
||||
@UseInterceptors(
|
||||
FileInterceptor("file", {
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB
|
||||
},
|
||||
storage: diskStorage({
|
||||
destination: "./files/claim-captures",
|
||||
filename: (req, file, callback) => {
|
||||
const unique = Date.now();
|
||||
const ex = extname(file.originalname);
|
||||
const filename = `${file.originalname.split(".")[0]}-${unique}${ex}`;
|
||||
callback(null, filename);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
summary: "Capture Car Angle or Damaged Part (V2 - Step 5)",
|
||||
description: `
|
||||
**Workflow Step:** CAPTURE_PART_DAMAGES (Step 5 of Claim)
|
||||
|
||||
**Capture types:**
|
||||
1. **angle**: Car angles (front, back, left, right) - 4 required
|
||||
2. **part**: Damaged parts based on selectedParts from Step 2
|
||||
|
||||
**All captures must be completed before user can submit claim.**
|
||||
`,
|
||||
})
|
||||
@ApiParam({
|
||||
name: "claimRequestId",
|
||||
description: "The claim case ID",
|
||||
example: "507f1f77bcf86cd799439011",
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["captureType", "captureKey", "file"],
|
||||
properties: {
|
||||
captureType: {
|
||||
type: "string",
|
||||
enum: ["angle", "part"],
|
||||
example: "angle",
|
||||
},
|
||||
captureKey: {
|
||||
type: "string",
|
||||
example: "front",
|
||||
description:
|
||||
'For angle: front/back/left/right. For part: hood/front_bumper/etc.',
|
||||
},
|
||||
file: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: "Capture saved successfully",
|
||||
type: CapturePartV2ResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: "Invalid workflow step",
|
||||
})
|
||||
async capturePart(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@Body() body: CapturePartV2Dto,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@CurrentUser() user: any,
|
||||
): Promise<CapturePartV2ResponseDto> {
|
||||
try {
|
||||
return await this.claimRequestManagementService.capturePartV2(
|
||||
claimRequestId,
|
||||
body,
|
||||
file,
|
||||
user.sub,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error ? error.message : "Failed to capture part",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/claim-request-management/dto/capture-part-v2.dto.ts
Normal file
81
src/claim-request-management/dto/capture-part-v2.dto.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { CarAngle } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
|
||||
|
||||
/**
|
||||
* V2 DTO for capturing car angle or damaged part
|
||||
*/
|
||||
export class CapturePartV2Dto {
|
||||
@ApiProperty({
|
||||
description: 'Type of capture: angle or part',
|
||||
example: 'angle',
|
||||
enum: ['angle', 'part'],
|
||||
})
|
||||
@IsNotEmpty({ message: 'Capture type is required' })
|
||||
@IsEnum(['angle', 'part'], {
|
||||
message: 'Capture type must be either "angle" or "part"',
|
||||
})
|
||||
captureType: 'angle' | 'part';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Key of the angle or part being captured',
|
||||
example: 'front',
|
||||
})
|
||||
@IsNotEmpty({ message: 'Capture key is required' })
|
||||
@IsString({ message: 'Capture key must be a string' })
|
||||
captureKey: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
description: 'Image file (JPG, PNG)',
|
||||
})
|
||||
file: Express.Multer.File;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response DTO for capture part V2
|
||||
*/
|
||||
export class CapturePartV2ResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Claim request ID',
|
||||
example: '507f1f77bcf86cd799439011',
|
||||
})
|
||||
claimRequestId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Type of capture',
|
||||
example: 'angle',
|
||||
})
|
||||
captureType: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Key of what was captured',
|
||||
example: 'front',
|
||||
})
|
||||
captureKey: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'File URL',
|
||||
example: 'http://localhost:3000/files/captures/front-1234567890.jpg',
|
||||
})
|
||||
fileUrl: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether all captures are now complete',
|
||||
example: false,
|
||||
})
|
||||
allCapturesComplete: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Current workflow step',
|
||||
example: 'CAPTURE_PART_DAMAGES',
|
||||
})
|
||||
currentStep: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Success message',
|
||||
example: 'Angle captured successfully. 6 captures remaining.',
|
||||
})
|
||||
message: string;
|
||||
}
|
||||
163
src/claim-request-management/dto/capture-requirements-v2.dto.ts
Normal file
163
src/claim-request-management/dto/capture-requirements-v2.dto.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* DTO for required document item
|
||||
*/
|
||||
export class RequiredDocumentItem {
|
||||
@ApiProperty({
|
||||
description: 'Document key/type',
|
||||
example: 'car_green_card',
|
||||
})
|
||||
key: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Display label in Farsi',
|
||||
example: 'کارت سبز خودرو',
|
||||
})
|
||||
label_fa: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Display label in English',
|
||||
example: 'Car Green Card',
|
||||
})
|
||||
label_en: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether this document has been uploaded',
|
||||
example: false,
|
||||
})
|
||||
uploaded: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Category of the document',
|
||||
example: 'general',
|
||||
enum: ['general', 'damaged_party', 'guilty_party'],
|
||||
})
|
||||
category: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for car angle item
|
||||
*/
|
||||
export class CarAngleItem {
|
||||
@ApiProperty({
|
||||
description: 'Angle key',
|
||||
example: 'front',
|
||||
enum: ['front', 'back', 'left', 'right'],
|
||||
})
|
||||
key: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Display label in Farsi',
|
||||
example: 'جلو',
|
||||
})
|
||||
label_fa: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Display label in English',
|
||||
example: 'Front',
|
||||
})
|
||||
label_en: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether this angle has been captured',
|
||||
example: false,
|
||||
})
|
||||
captured: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for damaged part item
|
||||
*/
|
||||
export class DamagedPartItem {
|
||||
@ApiProperty({
|
||||
description: 'Part key',
|
||||
example: 'hood',
|
||||
})
|
||||
key: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Display label in Farsi',
|
||||
example: 'کاپوت',
|
||||
})
|
||||
label_fa: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Display label in English',
|
||||
example: 'Hood',
|
||||
})
|
||||
label_en: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether this part has been captured',
|
||||
example: false,
|
||||
})
|
||||
captured: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response DTO for capture requirements V2
|
||||
*/
|
||||
export class GetCaptureRequirementsV2ResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Claim request ID',
|
||||
example: '507f1f77bcf86cd799439011',
|
||||
})
|
||||
claimRequestId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Public ID',
|
||||
example: 'A14235',
|
||||
})
|
||||
publicId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Current workflow step',
|
||||
example: 'UPLOAD_REQUIRED_DOCUMENTS',
|
||||
})
|
||||
currentStep: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'List of required documents to upload',
|
||||
type: [RequiredDocumentItem],
|
||||
})
|
||||
requiredDocuments: RequiredDocumentItem[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'List of car angles to capture',
|
||||
type: [CarAngleItem],
|
||||
})
|
||||
carAngles: CarAngleItem[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'List of damaged parts to capture',
|
||||
type: [DamagedPartItem],
|
||||
})
|
||||
damagedParts: DamagedPartItem[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Total number of items remaining',
|
||||
example: 18,
|
||||
})
|
||||
totalRemaining: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Summary of progress',
|
||||
example: {
|
||||
documentsUploaded: 2,
|
||||
documentsTotal: 13,
|
||||
anglesCapture: 0,
|
||||
anglesTotal: 4,
|
||||
partsCapture: 0,
|
||||
partsTotal: 3,
|
||||
},
|
||||
})
|
||||
progress: {
|
||||
documentsUploaded: number;
|
||||
documentsTotal: number;
|
||||
anglesCaptured: number;
|
||||
anglesTotal: number;
|
||||
partsCaptured: number;
|
||||
partsTotal: number;
|
||||
};
|
||||
}
|
||||
71
src/claim-request-management/dto/claim-details-v2.dto.ts
Normal file
71
src/claim-request-management/dto/claim-details-v2.dto.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class ClaimDetailsV2ResponseDto {
|
||||
@ApiProperty({ description: 'Claim case ID' })
|
||||
claimRequestId: string;
|
||||
|
||||
@ApiProperty({ description: 'Public ID' })
|
||||
publicId: string;
|
||||
|
||||
@ApiProperty({ description: 'Request number' })
|
||||
requestNo: string;
|
||||
|
||||
@ApiProperty({ description: 'Overall case status' })
|
||||
status: string;
|
||||
|
||||
@ApiProperty({ description: 'Claim damage status' })
|
||||
claimStatus: string;
|
||||
|
||||
@ApiProperty({ description: 'Current workflow step' })
|
||||
currentStep: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Next workflow step' })
|
||||
nextStep?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Blame request ID' })
|
||||
blameRequestId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Blame request number' })
|
||||
blameRequestNo?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Owner info' })
|
||||
owner?: {
|
||||
userId: string;
|
||||
fullName?: string;
|
||||
};
|
||||
|
||||
@ApiPropertyOptional({ description: 'Vehicle snapshot' })
|
||||
vehicle?: {
|
||||
carName?: string;
|
||||
carModel?: string;
|
||||
carType?: string;
|
||||
plate?: any;
|
||||
};
|
||||
|
||||
@ApiPropertyOptional({ description: 'Selected outer damaged parts' })
|
||||
selectedParts?: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Selected other damaged parts' })
|
||||
otherParts?: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Bank info (masked)' })
|
||||
money?: {
|
||||
sheba?: string;
|
||||
nationalCodeOfOwner?: string;
|
||||
};
|
||||
|
||||
@ApiPropertyOptional({ description: 'Required documents status' })
|
||||
requiredDocuments?: Record<string, { uploaded: boolean; fileId?: string }>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Car angles captured' })
|
||||
carAngles?: Record<string, { captured: boolean; url?: string }>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Damaged parts captured' })
|
||||
damagedParts?: Record<string, { captured: boolean; url?: string }>;
|
||||
|
||||
@ApiProperty({ description: 'Created at' })
|
||||
createdAt: string;
|
||||
|
||||
@ApiProperty({ description: 'Updated at' })
|
||||
updatedAt: string;
|
||||
}
|
||||
26
src/claim-request-management/dto/create-claim-v2.dto.ts
Normal file
26
src/claim-request-management/dto/create-claim-v2.dto.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class CreateClaimFromBlameResponseDto {
|
||||
@ApiProperty({
|
||||
example: "67fa10c259e15f231a2d1aae",
|
||||
description: "Created claim request ID",
|
||||
})
|
||||
claimRequestId: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: "A14235",
|
||||
description: "Shared public ID from blame case",
|
||||
})
|
||||
publicId: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: "SELECTING_OUTER_PARTS",
|
||||
description: "Current status after creation",
|
||||
})
|
||||
status: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: "Claim request created successfully",
|
||||
})
|
||||
message: string;
|
||||
}
|
||||
35
src/claim-request-management/dto/my-claims-v2.dto.ts
Normal file
35
src/claim-request-management/dto/my-claims-v2.dto.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ClaimListItemV2Dto {
|
||||
@ApiProperty({ description: 'Claim case ID', example: '507f1f77bcf86cd799439011' })
|
||||
claimRequestId: string;
|
||||
|
||||
@ApiProperty({ description: 'Public ID shared across blame and claim', example: 'A14235' })
|
||||
publicId: string;
|
||||
|
||||
@ApiProperty({ description: 'Claim request number', example: 'CL12345' })
|
||||
requestNo: string;
|
||||
|
||||
@ApiProperty({ description: 'Overall case status', example: 'WAITING_FOR_DAMAGE_EXPERT' })
|
||||
status: string;
|
||||
|
||||
@ApiProperty({ description: 'Claim damage determination status', example: 'PENDING' })
|
||||
claimStatus: string;
|
||||
|
||||
@ApiProperty({ description: 'Current workflow step', example: 'USER_SUBMISSION_COMPLETE' })
|
||||
currentStep: string;
|
||||
|
||||
@ApiProperty({ description: 'Creation date', example: '2026-02-22T10:00:00.000Z' })
|
||||
createdAt: string;
|
||||
|
||||
@ApiProperty({ description: 'Blame request ID this claim originated from' })
|
||||
blameRequestId?: string;
|
||||
}
|
||||
|
||||
export class GetMyClaimsV2ResponseDto {
|
||||
@ApiProperty({ description: 'List of user claims', type: [ClaimListItemV2Dto] })
|
||||
list: ClaimListItemV2Dto[];
|
||||
|
||||
@ApiProperty({ description: 'Total count', example: 5 })
|
||||
total: number;
|
||||
}
|
||||
126
src/claim-request-management/dto/select-other-parts-v2.dto.ts
Normal file
126
src/claim-request-management/dto/select-other-parts-v2.dto.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, Matches, Length, ArrayMaxSize } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Enum for valid other (non-body) car parts that can be damaged
|
||||
*/
|
||||
export enum OtherCarPart {
|
||||
ENGINE = 'engine',
|
||||
SUSPENSION = 'suspension',
|
||||
BRAKE_SYSTEM = 'brake_system',
|
||||
ELECTRICAL = 'electrical',
|
||||
RADIATOR = 'radiator',
|
||||
TRANSMISSION = 'transmission',
|
||||
EXHAUST = 'exhaust',
|
||||
HEADLIGHT = 'headlight',
|
||||
TAILLIGHT = 'taillight',
|
||||
MIRROR = 'mirror',
|
||||
GLASS = 'glass',
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 DTO for selecting other damaged parts and bank information
|
||||
* Step 3 of claim workflow
|
||||
*/
|
||||
export class SelectOtherPartsV2Dto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Array of selected other damaged parts (non-body parts)',
|
||||
example: ['engine', 'suspension', 'headlight'],
|
||||
enum: OtherCarPart,
|
||||
isArray: true,
|
||||
maxItems: 11,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray({ message: 'otherParts must be an array' })
|
||||
@ArrayMaxSize(11, { message: 'Maximum 11 other parts can be selected' })
|
||||
@IsEnum(OtherCarPart, {
|
||||
each: true,
|
||||
message: 'Invalid part name. Must be one of the valid other car parts',
|
||||
})
|
||||
otherParts?: OtherCarPart[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Sheba number (IBAN) for payment - 24 digits without IR prefix',
|
||||
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;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'National code of the owner - 10 digits',
|
||||
example: '1234567890',
|
||||
pattern: '^[0-9]{10}$',
|
||||
minLength: 10,
|
||||
maxLength: 10,
|
||||
})
|
||||
@IsNotEmpty({ message: 'National code of owner 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response DTO for select other parts V2
|
||||
*/
|
||||
export class SelectOtherPartsV2ResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Claim request ID',
|
||||
example: '507f1f77bcf86cd799439011',
|
||||
})
|
||||
claimRequestId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Public ID shared across blame and claim',
|
||||
example: 'A14235',
|
||||
})
|
||||
publicId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Array of selected other damaged parts',
|
||||
example: ['engine', 'suspension'],
|
||||
required: false,
|
||||
})
|
||||
otherParts?: string[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Sheba number (masked for security)',
|
||||
example: 'IR12************1234',
|
||||
})
|
||||
shebaNumber: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'National code of owner (masked)',
|
||||
example: '12******90',
|
||||
})
|
||||
nationalCodeOfOwner: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Current workflow step',
|
||||
example: 'UPLOAD_REQUIRED_DOCUMENTS',
|
||||
})
|
||||
currentStep: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Next possible workflow step',
|
||||
example: 'CAPTURE_PART_DAMAGES',
|
||||
})
|
||||
nextStep: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Success message',
|
||||
example: 'Other parts and bank information saved successfully. Please proceed to upload required documents.',
|
||||
})
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsEnum, IsNotEmpty, ArrayMinSize, ArrayUnique } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Enum for valid outer car parts that can be damaged
|
||||
* Follows the naming convention from workflow step definition
|
||||
*/
|
||||
export enum OuterCarPart {
|
||||
// Hood
|
||||
HOOD = 'hood',
|
||||
|
||||
// Doors
|
||||
FRONT_RIGHT_DOOR = 'front_right_door',
|
||||
FRONT_LEFT_DOOR = 'front_left_door',
|
||||
REAR_RIGHT_DOOR = 'rear_right_door',
|
||||
REAR_LEFT_DOOR = 'rear_left_door',
|
||||
|
||||
// Bumpers
|
||||
FRONT_BUMPER = 'front_bumper',
|
||||
REAR_BUMPER = 'rear_bumper',
|
||||
|
||||
// Fenders
|
||||
FRONT_RIGHT_FENDER = 'front_right_fender',
|
||||
FRONT_LEFT_FENDER = 'front_left_fender',
|
||||
REAR_RIGHT_FENDER = 'rear_right_fender',
|
||||
REAR_LEFT_FENDER = 'rear_left_fender',
|
||||
|
||||
// Trunk & Roof
|
||||
TRUNK = 'trunk',
|
||||
ROOF = 'roof',
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 DTO for selecting damaged outer car parts
|
||||
* Much cleaner than the nested boolean structure
|
||||
*/
|
||||
export class SelectOuterPartsV2Dto {
|
||||
@ApiProperty({
|
||||
description: 'Array of selected damaged outer car parts',
|
||||
example: ['hood', 'front_right_door', 'rear_bumper', 'roof'],
|
||||
enum: OuterCarPart,
|
||||
isArray: true,
|
||||
minItems: 1,
|
||||
maxItems: 13,
|
||||
})
|
||||
@IsNotEmpty({ message: 'Selected parts array cannot be empty' })
|
||||
@IsArray({ message: 'selectedParts must be an array' })
|
||||
@ArrayMinSize(1, { message: 'At least one damaged part must be selected' })
|
||||
@ArrayUnique({ message: 'Duplicate parts are not allowed' })
|
||||
@IsEnum(OuterCarPart, {
|
||||
each: true,
|
||||
message: 'Invalid part name. Must be one of the valid outer car parts',
|
||||
})
|
||||
selectedParts: OuterCarPart[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Response DTO for select outer parts V2
|
||||
*/
|
||||
export class SelectOuterPartsV2ResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Claim request ID',
|
||||
example: '507f1f77bcf86cd799439011',
|
||||
})
|
||||
claimRequestId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Public ID shared across blame and claim',
|
||||
example: 'A14235',
|
||||
})
|
||||
publicId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Array of selected damaged parts',
|
||||
example: ['hood', 'front_right_door', 'rear_bumper'],
|
||||
})
|
||||
selectedParts: string[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Current workflow step',
|
||||
example: 'SELECT_OUTER_PARTS',
|
||||
})
|
||||
currentStep: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Next possible workflow step',
|
||||
example: 'SELECT_OTHER_PARTS',
|
||||
})
|
||||
nextStep: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Success message',
|
||||
example: 'Outer parts selected successfully. Please proceed to select other parts.',
|
||||
})
|
||||
message: string;
|
||||
}
|
||||
67
src/claim-request-management/dto/upload-document-v2.dto.ts
Normal file
67
src/claim-request-management/dto/upload-document-v2.dto.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
|
||||
|
||||
/**
|
||||
* V2 DTO for uploading required document
|
||||
*/
|
||||
export class UploadRequiredDocumentV2Dto {
|
||||
@ApiProperty({
|
||||
description: 'Document type/key',
|
||||
example: 'car_green_card',
|
||||
enum: ClaimRequiredDocumentType,
|
||||
})
|
||||
@IsNotEmpty({ message: 'Document key is required' })
|
||||
@IsEnum(ClaimRequiredDocumentType, {
|
||||
message: 'Invalid document type',
|
||||
})
|
||||
documentKey: ClaimRequiredDocumentType;
|
||||
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
description: 'Image file (JPG, PNG, PDF)',
|
||||
})
|
||||
file: Express.Multer.File;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response DTO for upload required document V2
|
||||
*/
|
||||
export class UploadRequiredDocumentV2ResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Claim request ID',
|
||||
example: '507f1f77bcf86cd799439011',
|
||||
})
|
||||
claimRequestId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Document key that was uploaded',
|
||||
example: 'car_green_card',
|
||||
})
|
||||
documentKey: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'File URL',
|
||||
example: 'http://localhost:3000/files/documents/car-green-card-1234567890.jpg',
|
||||
})
|
||||
fileUrl: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether all required documents are now uploaded',
|
||||
example: false,
|
||||
})
|
||||
allDocumentsUploaded: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Current workflow step',
|
||||
example: 'UPLOAD_REQUIRED_DOCUMENTS',
|
||||
})
|
||||
currentStep: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Success message',
|
||||
example: 'Document uploaded successfully. 12 documents remaining.',
|
||||
})
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectModel } from "@nestjs/mongoose";
|
||||
import { FilterQuery, Model, Types } from "mongoose";
|
||||
import { ClaimCase, ClaimCaseDocument } from "../schema/claim-cases.schema";
|
||||
|
||||
@Injectable()
|
||||
export class ClaimCaseDbService {
|
||||
constructor(
|
||||
@InjectModel(ClaimCase.name)
|
||||
private readonly claimCaseModel: Model<ClaimCaseDocument>,
|
||||
) {}
|
||||
|
||||
async create(payload: Partial<ClaimCase>): Promise<ClaimCaseDocument> {
|
||||
return this.claimCaseModel.create(payload);
|
||||
}
|
||||
|
||||
async findById(id: string | Types.ObjectId): Promise<ClaimCaseDocument | null> {
|
||||
return this.claimCaseModel.findById(id);
|
||||
}
|
||||
|
||||
async findOne(filter: FilterQuery<ClaimCase>): Promise<ClaimCaseDocument | null> {
|
||||
return this.claimCaseModel.findOne(filter);
|
||||
}
|
||||
|
||||
async findByIdAndUpdate(
|
||||
id: string | Types.ObjectId,
|
||||
update: any,
|
||||
): Promise<ClaimCaseDocument | null> {
|
||||
return this.claimCaseModel.findByIdAndUpdate(id, update, { new: true });
|
||||
}
|
||||
|
||||
async find(
|
||||
filter: FilterQuery<ClaimCase>,
|
||||
options?: { lean?: boolean; select?: string },
|
||||
): Promise<ClaimCaseDocument[] | Record<string, unknown>[]> {
|
||||
if (options?.lean) {
|
||||
const query = options?.select
|
||||
? this.claimCaseModel.find(filter).select(options.select).lean()
|
||||
: this.claimCaseModel.find(filter).lean();
|
||||
return query.exec() as Promise<Record<string, unknown>[]>;
|
||||
}
|
||||
const query = options?.select
|
||||
? this.claimCaseModel.find(filter).select(options.select)
|
||||
: this.claimCaseModel.find(filter);
|
||||
return query.exec() as Promise<ClaimCaseDocument[]>;
|
||||
}
|
||||
|
||||
async countByFilter(filter: FilterQuery<ClaimCase>): Promise<number> {
|
||||
return this.claimCaseModel.countDocuments(filter);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,6 @@ export class VideoCaptureDbService {
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<VideoCaptureModel | null> {
|
||||
return this.videoCapture.findById(id).lean();
|
||||
return this.videoCapture.findById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { CarDamagePartModel, CarDamagePartOtherModel } from "./car-parts.schema";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimDamageSelection {
|
||||
/**
|
||||
* V2: Array of selected damaged outer car part names
|
||||
* Examples: ['hood', 'front_right_door', 'rear_bumper']
|
||||
*/
|
||||
@Prop({ type: [String], default: [] })
|
||||
selectedParts?: string[];
|
||||
|
||||
/**
|
||||
* V2: Array of selected other (non-body) damaged parts
|
||||
* Examples: ['engine', 'suspension', 'headlight']
|
||||
*/
|
||||
@Prop({ type: [String], default: [] })
|
||||
otherParts?: string[];
|
||||
|
||||
/**
|
||||
* Legacy fields - kept for backward compatibility
|
||||
*/
|
||||
@Prop({ type: [CarDamagePartModel] })
|
||||
legacyParts?: CarDamagePartModel[];
|
||||
|
||||
@Prop({ type: CarDamagePartOtherModel })
|
||||
legacyOtherParts?: CarDamagePartOtherModel;
|
||||
}
|
||||
export const ClaimDamageSelectionSchema =
|
||||
SchemaFactory.createForClass(ClaimDamageSelection);
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Schema as MongooseSchema, Types } from "mongoose";
|
||||
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
|
||||
import { UserReplyEnum } from "src/Types&Enums/claim-request-management/userReply.enum";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimPartPricing {
|
||||
@Prop({ type: String })
|
||||
partId: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
carPartDamage?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
typeOfDamage?: string;
|
||||
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
price?: string | number;
|
||||
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
salary?: string | number;
|
||||
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
totalPayment?: string | number;
|
||||
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
daghi?: string | number;
|
||||
|
||||
@Prop({ type: Boolean, default: false })
|
||||
factorNeeded?: boolean;
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
factorLink?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: String, enum: FactorStatus, default: null })
|
||||
factorStatus?: FactorStatus | null;
|
||||
|
||||
@Prop({ type: String, default: null })
|
||||
rejectionReason?: string | null;
|
||||
}
|
||||
export const ClaimPartPricingSchema =
|
||||
SchemaFactory.createForClass(ClaimPartPricing);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimExpertActor {
|
||||
@Prop({ type: String })
|
||||
actorName?: string;
|
||||
|
||||
/**
|
||||
* Kept as string to support existing queries like:
|
||||
* `"damageExpertReply.actorDetail.actorId": actor.sub`
|
||||
*/
|
||||
@Prop({ type: String })
|
||||
actorId?: string;
|
||||
}
|
||||
export const ClaimExpertActorSchema =
|
||||
SchemaFactory.createForClass(ClaimExpertActor);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimUserComment {
|
||||
@Prop({ type: Boolean, default: null })
|
||||
isAccept?: boolean | null;
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
signDetailId?: Types.ObjectId;
|
||||
}
|
||||
export const ClaimUserCommentSchema =
|
||||
SchemaFactory.createForClass(ClaimUserComment);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimExpertReply {
|
||||
@Prop({ type: String })
|
||||
description?: string;
|
||||
|
||||
@Prop({ type: ClaimExpertActorSchema })
|
||||
actorDetail?: ClaimExpertActor;
|
||||
|
||||
@Prop({ type: [ClaimPartPricingSchema], default: [] })
|
||||
parts?: ClaimPartPricing[];
|
||||
|
||||
@Prop({ type: Date, default: () => new Date() })
|
||||
submittedAt?: Date;
|
||||
|
||||
@Prop({ type: ClaimUserCommentSchema })
|
||||
userComment?: ClaimUserComment;
|
||||
}
|
||||
export const ClaimExpertReplySchema =
|
||||
SchemaFactory.createForClass(ClaimExpertReply);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimUserObjection {
|
||||
@Prop({ type: String, required: true })
|
||||
partId: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
reason?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
partPrice?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
partSalary?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
typeOfDamage?: string;
|
||||
}
|
||||
export const ClaimUserObjectionSchema =
|
||||
SchemaFactory.createForClass(ClaimUserObjection);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimResendRequest {
|
||||
@Prop({ type: String })
|
||||
resendDescription?: string;
|
||||
|
||||
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
|
||||
resendDocuments?: any[];
|
||||
|
||||
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
|
||||
resendCarParts?: any[];
|
||||
}
|
||||
export const ClaimResendRequestSchema =
|
||||
SchemaFactory.createForClass(ClaimResendRequest);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimUserResendPayload {
|
||||
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
|
||||
documents?: any[];
|
||||
|
||||
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
|
||||
carParts?: any[];
|
||||
}
|
||||
export const ClaimUserResendPayloadSchema =
|
||||
SchemaFactory.createForClass(ClaimUserResendPayload);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimFileRating {
|
||||
@Prop({ type: Number, min: 0, max: 5 })
|
||||
collisionMethodAccuracy?: number;
|
||||
|
||||
@Prop({ type: Number, min: 0, max: 5 })
|
||||
evaluationTimeliness?: number;
|
||||
|
||||
@Prop({ type: Number, min: 0, max: 5 })
|
||||
accidentCauseAccuracy?: number;
|
||||
|
||||
@Prop({ type: Number, min: 0, max: 5 })
|
||||
guiltyVehicleIdentification?: number;
|
||||
}
|
||||
export const ClaimFileRatingSchema =
|
||||
SchemaFactory.createForClass(ClaimFileRating);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimPriceDrop {
|
||||
@Prop({ type: Number })
|
||||
total?: number;
|
||||
|
||||
@Prop({ type: Number })
|
||||
carPrice?: number;
|
||||
|
||||
@Prop({ type: Number })
|
||||
carModel?: number;
|
||||
|
||||
@Prop({ type: [Number], default: [] })
|
||||
carValue?: number[];
|
||||
|
||||
@Prop({ type: Number })
|
||||
sumOfSeverity?: number;
|
||||
}
|
||||
export const ClaimPriceDropSchema = SchemaFactory.createForClass(ClaimPriceDrop);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimEvaluation {
|
||||
@Prop({ type: String, enum: UserReplyEnum, default: null })
|
||||
effectedUserReply?: UserReplyEnum | null;
|
||||
|
||||
@Prop({ type: ClaimExpertReplySchema })
|
||||
damageExpertReply?: ClaimExpertReply | null;
|
||||
|
||||
@Prop({ type: ClaimExpertReplySchema })
|
||||
damageExpertReplyFinal?: ClaimExpertReply | null;
|
||||
|
||||
@Prop({ type: ClaimResendRequestSchema })
|
||||
damageExpertResend?: ClaimResendRequest | null;
|
||||
|
||||
@Prop({ type: ClaimUserObjectionSchema })
|
||||
objection?: ClaimUserObjection;
|
||||
|
||||
@Prop({ type: ClaimUserResendPayloadSchema })
|
||||
userResendDocuments?: ClaimUserResendPayload;
|
||||
|
||||
@Prop({ type: ClaimFileRatingSchema })
|
||||
rating?: ClaimFileRating;
|
||||
|
||||
@Prop({ type: String })
|
||||
visitLocation?: string;
|
||||
|
||||
@Prop({ type: ClaimPriceDropSchema })
|
||||
priceDrop?: ClaimPriceDrop;
|
||||
}
|
||||
export const ClaimEvaluationSchema =
|
||||
SchemaFactory.createForClass(ClaimEvaluation);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimPlate {
|
||||
@Prop({ type: Number })
|
||||
leftDigits?: number;
|
||||
|
||||
@Prop({ type: String })
|
||||
centerAlphabet?: string;
|
||||
|
||||
@Prop({ type: Number })
|
||||
centerDigits?: number;
|
||||
|
||||
@Prop({ type: Number })
|
||||
ir?: number;
|
||||
|
||||
@Prop({ type: String })
|
||||
nationalCode?: string;
|
||||
}
|
||||
export const ClaimPlateSchema = SchemaFactory.createForClass(ClaimPlate);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimVehicleSnapshot {
|
||||
@Prop({ type: String })
|
||||
carName?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
carModel?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
carType?: string;
|
||||
|
||||
@Prop({ type: Boolean })
|
||||
isNewCar?: boolean;
|
||||
|
||||
@Prop({ type: ClaimPlateSchema })
|
||||
plate?: ClaimPlate;
|
||||
}
|
||||
export const ClaimVehicleSnapshotSchema =
|
||||
SchemaFactory.createForClass(ClaimVehicleSnapshot);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimOwner {
|
||||
@Prop({ type: Types.ObjectId })
|
||||
userId?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
userClientKey?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: String })
|
||||
fullName?: string;
|
||||
}
|
||||
export const ClaimOwnerSchema = SchemaFactory.createForClass(ClaimOwner);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimMoney {
|
||||
@Prop({ type: String, trim: true })
|
||||
sheba?: string;
|
||||
|
||||
@Prop({ type: String, trim: true })
|
||||
nationalCodeOfInsurer?: string;
|
||||
}
|
||||
export const ClaimMoneySchema = SchemaFactory.createForClass(ClaimMoney);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
import { AiImagesModel } from "./ai-image.schema";
|
||||
import { CarGreenCardModel } from "./car-green-card.schema";
|
||||
import { ImageRequiredModel } from "./image-required.schema";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class CapturedImage {
|
||||
@Prop({ type: String })
|
||||
path?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
fileName?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
url?: string;
|
||||
|
||||
@Prop({ type: Date })
|
||||
capturedAt?: Date;
|
||||
}
|
||||
export const CapturedImageSchema = SchemaFactory.createForClass(CapturedImage);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimMedia {
|
||||
@Prop({ type: CarGreenCardModel })
|
||||
carGreenCard?: CarGreenCardModel;
|
||||
|
||||
@Prop({ type: ImageRequiredModel })
|
||||
imageRequired?: ImageRequiredModel;
|
||||
|
||||
@Prop({ type: AiImagesModel })
|
||||
aiImages?: AiImagesModel;
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
videoCaptureId?: Types.ObjectId;
|
||||
|
||||
/**
|
||||
* V2: Car angles captures (front, back, left, right)
|
||||
* Map of angle key to captured image
|
||||
*/
|
||||
@Prop({
|
||||
type: Map,
|
||||
of: CapturedImageSchema,
|
||||
default: () => ({}),
|
||||
})
|
||||
carAngles?: Map<string, CapturedImage>;
|
||||
|
||||
/**
|
||||
* V2: Damaged parts captures
|
||||
* Map of part key to captured image
|
||||
*/
|
||||
@Prop({
|
||||
type: Map,
|
||||
of: CapturedImageSchema,
|
||||
default: () => ({}),
|
||||
})
|
||||
damagedParts?: Map<string, CapturedImage>;
|
||||
}
|
||||
export const ClaimMediaSchema = SchemaFactory.createForClass(ClaimMedia);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { AccidentInfo, AccidentInfoSchema } from "src/request-management/entities/schema/accidentInformation.type";
|
||||
import { Party, PartySchema } from "src/request-management/entities/schema/partyRole.enum";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimCaseSnapshot {
|
||||
@Prop({ type: AccidentInfoSchema })
|
||||
accident?: AccidentInfo;
|
||||
|
||||
@Prop({ type: [PartySchema], default: [] })
|
||||
parties?: Party[];
|
||||
}
|
||||
export const ClaimCaseSnapshotSchema =
|
||||
SchemaFactory.createForClass(ClaimCaseSnapshot);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimActorLock {
|
||||
@Prop({ type: Types.ObjectId })
|
||||
actorId: Types.ObjectId;
|
||||
|
||||
@Prop({ type: String })
|
||||
actorName: string;
|
||||
|
||||
@Prop({ type: String, default: "damage_expert" })
|
||||
actorRole: "damage_expert" | "expert" | "admin";
|
||||
}
|
||||
export const ClaimActorLockSchema = SchemaFactory.createForClass(ClaimActorLock);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimWorkflow {
|
||||
@Prop({ type: String, enum: ClaimWorkflowStep })
|
||||
currentStep: ClaimWorkflowStep;
|
||||
|
||||
@Prop({ type: String, enum: ClaimWorkflowStep })
|
||||
nextStep: ClaimWorkflowStep;
|
||||
|
||||
@Prop({ type: [String], enum: ClaimWorkflowStep, default: [] })
|
||||
completedSteps?: ClaimWorkflowStep[];
|
||||
|
||||
@Prop({ type: Boolean, default: false })
|
||||
locked?: boolean;
|
||||
|
||||
@Prop({ type: Date })
|
||||
lockedAt?: Date;
|
||||
|
||||
@Prop({ type: ClaimActorLockSchema })
|
||||
lockedBy?: ClaimActorLock;
|
||||
}
|
||||
export const ClaimWorkflowSchema = SchemaFactory.createForClass(ClaimWorkflow);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { HydratedDocument, Schema as MongooseSchema, Types } from "mongoose";
|
||||
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
||||
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { HistoryEvent, HistoryEventSchema } from "src/request-management/entities/schema/historyEvent.type";
|
||||
import {
|
||||
ClaimDamageSelection,
|
||||
ClaimDamageSelectionSchema,
|
||||
} from "./claim-case.damage.schema";
|
||||
import {
|
||||
ClaimMoney,
|
||||
ClaimMoneySchema,
|
||||
ClaimOwner,
|
||||
ClaimOwnerSchema,
|
||||
ClaimVehicleSnapshot,
|
||||
ClaimVehicleSnapshotSchema,
|
||||
} from "./claim-case.identity.schema";
|
||||
import { ClaimMedia, ClaimMediaSchema } from "./claim-case.media.schema";
|
||||
import {
|
||||
ClaimEvaluation,
|
||||
ClaimEvaluationSchema,
|
||||
} from "./claim-case.evaluation.schema";
|
||||
import {
|
||||
ClaimCaseSnapshot,
|
||||
ClaimCaseSnapshotSchema,
|
||||
} from "./claim-case.snapshot.schema";
|
||||
import { ClaimWorkflow, ClaimWorkflowSchema } from "./claim-case.workflow.schema";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class RequiredDocumentRef {
|
||||
@Prop({ type: Types.ObjectId })
|
||||
fileId?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: String })
|
||||
filePath?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
fileName?: string;
|
||||
|
||||
@Prop({ type: Boolean, default: true })
|
||||
uploaded?: boolean;
|
||||
|
||||
@Prop({ type: Date })
|
||||
uploadedAt?: Date;
|
||||
}
|
||||
export const RequiredDocumentRefSchema = SchemaFactory.createForClass(RequiredDocumentRef);
|
||||
|
||||
@Schema({
|
||||
collection: "claimCases",
|
||||
timestamps: true,
|
||||
id: true,
|
||||
versionKey: false,
|
||||
})
|
||||
export class ClaimCase {
|
||||
@Prop({ required: true, unique: true, index: true, trim: true })
|
||||
requestNo: string;
|
||||
|
||||
/**
|
||||
* Human-friendly shared id across blame+claim (e.g. A14235).
|
||||
* Generated once on blame creation and copied into claim.
|
||||
*/
|
||||
@Prop({ required: true, unique: true, index: true, trim: true })
|
||||
publicId: string;
|
||||
|
||||
/**
|
||||
* Overall case status (user flow + expert flow progression)
|
||||
*/
|
||||
@Prop({ required: true, type: String, enum: ClaimCaseStatus, default: ClaimCaseStatus.CREATED })
|
||||
status: ClaimCaseStatus;
|
||||
|
||||
/**
|
||||
* Claim damage determination status (expert assessment)
|
||||
*/
|
||||
@Prop({ type: String, enum: ClaimStatus, default: ClaimStatus.PENDING })
|
||||
claimStatus: ClaimStatus;
|
||||
|
||||
/**
|
||||
* Link to the blame case that originated this claim.
|
||||
* IMPORTANT: this is intentionally ONLY an id reference (no embedded blame file).
|
||||
*/
|
||||
@Prop({ type: Types.ObjectId, index: true })
|
||||
blameRequestId?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: String, index: true })
|
||||
blameRequestNo?: string;
|
||||
|
||||
@Prop({ type: ClaimWorkflowSchema, default: () => ({}) })
|
||||
workflow?: ClaimWorkflow;
|
||||
|
||||
@Prop({ type: ClaimOwnerSchema, default: () => ({}) })
|
||||
owner?: ClaimOwner;
|
||||
|
||||
@Prop({ type: ClaimVehicleSnapshotSchema, default: () => ({}) })
|
||||
vehicle?: ClaimVehicleSnapshot;
|
||||
|
||||
@Prop({ type: ClaimMoneySchema })
|
||||
money?: ClaimMoney;
|
||||
|
||||
@Prop({ type: Number })
|
||||
claimNo?: number;
|
||||
|
||||
@Prop({ type: Number })
|
||||
claimId?: number;
|
||||
|
||||
@Prop({ type: ClaimDamageSelectionSchema, default: () => ({}) })
|
||||
damage?: ClaimDamageSelection;
|
||||
|
||||
@Prop({ type: ClaimMediaSchema, default: () => ({}) })
|
||||
media?: ClaimMedia;
|
||||
|
||||
@Prop({ type: ClaimEvaluationSchema, default: () => ({}) })
|
||||
evaluation?: ClaimEvaluation;
|
||||
|
||||
/**
|
||||
* Optional “read-optimized” copy of fields from blame/request side.
|
||||
* Source of truth remains `blameRequestId`.
|
||||
*/
|
||||
@Prop({ type: ClaimCaseSnapshotSchema })
|
||||
snapshot?: ClaimCaseSnapshot;
|
||||
|
||||
/**
|
||||
* For quick “has user uploaded X?” checks. Values point to docs in the
|
||||
* `claim-required-documents` collection.
|
||||
*/
|
||||
@Prop({
|
||||
type: Map,
|
||||
of: RequiredDocumentRefSchema,
|
||||
default: () => ({}),
|
||||
})
|
||||
requiredDocuments?: Map<string, RequiredDocumentRef>;
|
||||
|
||||
@Prop({ type: [HistoryEventSchema], default: [] })
|
||||
history?: HistoryEvent[];
|
||||
|
||||
/**
|
||||
* Legacy fields kept optional to simplify progressive migration.
|
||||
* If you choose to migrate later, we can remove these.
|
||||
*/
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
legacy?: any;
|
||||
}
|
||||
|
||||
export type ClaimCaseDocument = HydratedDocument<ClaimCase>;
|
||||
export const ClaimCaseSchema = SchemaFactory.createForClass(ClaimCase);
|
||||
|
||||
|
||||
@@ -241,6 +241,22 @@ export class ClaimRequestManagementModel {
|
||||
@Prop({ type: RequestManagementModel, unique: true })
|
||||
blameFile: RequestManagementModel;
|
||||
|
||||
/**
|
||||
* Human-friendly shared id across blame+claim (e.g. A14235).
|
||||
* Copied from the source blame request.
|
||||
*
|
||||
* NOTE: `sparse` keeps existing documents without this field valid.
|
||||
*/
|
||||
@Prop({ type: String, unique: true, index: true, sparse: true, trim: true })
|
||||
publicId?: string;
|
||||
|
||||
/**
|
||||
* Explicit reference to the blame request id (in addition to embedded blameFile).
|
||||
* This will be the preferred reference going forward.
|
||||
*/
|
||||
@Prop({ type: Types.ObjectId, index: true })
|
||||
blameRequestId?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: Number, default: 100 })
|
||||
requestNumber?: number;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user