forked from Yara724/api
833 lines
26 KiB
TypeScript
833 lines
26 KiB
TypeScript
import {
|
||
Controller,
|
||
HttpException,
|
||
InternalServerErrorException,
|
||
Param,
|
||
Post,
|
||
Patch,
|
||
Put,
|
||
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,
|
||
VideoCaptureV2ResponseDto,
|
||
} from "./dto/capture-part-v2.dto";
|
||
import { GetMyClaimsV2ResponseDto } from "./dto/my-claims-v2.dto";
|
||
import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
|
||
import { UserObjectionV2Dto } from "./dto/user-objection-v2.dto";
|
||
import { UserRatingDto } from "./dto/user-rating.dto";
|
||
|
||
@ApiTags("claim-request-management (v2)")
|
||
@Controller("v2/claim-request-management")
|
||
@ApiBearerAuth()
|
||
@UseGuards(GlobalGuard, RolesGuard)
|
||
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
|
||
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, user);
|
||
} 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,
|
||
user,
|
||
);
|
||
} catch (error) {
|
||
if (error instanceof HttpException) throw error;
|
||
throw new InternalServerErrorException(
|
||
error instanceof Error ? error.message : "Failed to get claim details",
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* V2: User objection after expert resend (same intent as v1 PUT …/request/resend/:id/objection).
|
||
*/
|
||
@Put("request/:claimRequestId/objection")
|
||
@ApiOperation({
|
||
summary: "Submit user objection (V2)",
|
||
description:
|
||
"After the damage expert submits a resend request (`damageExpertResend`), the owner may dispute priced parts " +
|
||
"and/or propose additional damaged parts. Stores a structured payload on `evaluation.objection`, merges " +
|
||
"`newParts` into `damage.selectedParts`, and moves the case back to `WAITING_FOR_DAMAGE_EXPERT` for re-review.",
|
||
})
|
||
@ApiParam({
|
||
name: "claimRequestId",
|
||
description: "The claim case ID (MongoDB ObjectId)",
|
||
example: "507f1f77bcf86cd799439011",
|
||
})
|
||
@ApiBody({ type: UserObjectionV2Dto })
|
||
@ApiResponse({ status: 200, description: "Objection stored" })
|
||
@ApiResponse({ status: 400, description: "No active resend or empty payload" })
|
||
@ApiResponse({ status: 403, description: "Not the claim owner" })
|
||
@ApiResponse({ status: 404, description: "Claim not found" })
|
||
@ApiResponse({ status: 409, description: "Objection already submitted" })
|
||
async submitUserObjectionV2(
|
||
@Param("claimRequestId") claimRequestId: string,
|
||
@Body() body: UserObjectionV2Dto,
|
||
@CurrentUser() user: any,
|
||
) {
|
||
try {
|
||
return await this.claimRequestManagementService.handleUserObjectionV2(
|
||
claimRequestId,
|
||
body,
|
||
user.sub,
|
||
user,
|
||
);
|
||
} catch (error) {
|
||
if (error instanceof HttpException) throw error;
|
||
throw new InternalServerErrorException(
|
||
error instanceof Error ? error.message : "Failed to submit objection",
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* V2: User satisfaction rating after the claim case is completed (same intent as v1 PUT …/request/:id/user-rating).
|
||
*/
|
||
@Put("request/:claimRequestId/user-rating")
|
||
@ApiOperation({
|
||
summary: "Submit user satisfaction rating (V2)",
|
||
description:
|
||
"Only the claim owner (damaged party) may submit. Allowed when `status` is `COMPLETED`. " +
|
||
"Stores scores on `ClaimCase.userRating` (0–5). One submission per case.",
|
||
})
|
||
@ApiParam({
|
||
name: "claimRequestId",
|
||
description: "The claim case ID (MongoDB ObjectId)",
|
||
example: "507f1f77bcf86cd799439011",
|
||
})
|
||
@ApiBody({ type: UserRatingDto })
|
||
@ApiResponse({ status: 200, description: "Rating saved" })
|
||
@ApiResponse({ status: 400, description: "Claim not completed or invalid scores" })
|
||
@ApiResponse({ status: 403, description: "Not the claim owner" })
|
||
@ApiResponse({ status: 404, description: "Claim not found" })
|
||
@ApiResponse({ status: 409, description: "Rating already submitted" })
|
||
async addUserRatingV2(
|
||
@Param("claimRequestId") claimRequestId: string,
|
||
@Body() ratingDto: UserRatingDto,
|
||
@CurrentUser() user: any,
|
||
) {
|
||
try {
|
||
return await this.claimRequestManagementService.addUserRatingV2(
|
||
claimRequestId,
|
||
ratingDto,
|
||
user.sub,
|
||
user,
|
||
);
|
||
} catch (error) {
|
||
if (error instanceof HttpException) throw error;
|
||
throw new InternalServerErrorException(
|
||
error instanceof Error ? error.message : "Failed to save rating",
|
||
);
|
||
}
|
||
}
|
||
|
||
@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,
|
||
user,
|
||
);
|
||
} 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.
|
||
Optional: upload car green card file in the same step.
|
||
|
||
**Validations:**
|
||
- Claim must exist
|
||
- User must be the claim owner (damaged party from blame case)
|
||
- Current workflow step must be SELECT_OTHER_PARTS
|
||
- Bank information must not have been submitted previously
|
||
- Sheba (sheba) accepted as IR + 24 digits or only 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: CAPTURE_PART_DAMAGES (Step 4)
|
||
- User captures car angles and damaged-part photos, then uploads required documents (Step 5)
|
||
`,
|
||
})
|
||
@ApiParam({
|
||
name: "claimRequestId",
|
||
description: "The claim case ID (MongoDB ObjectId)",
|
||
example: "507f1f77bcf86cd799439011",
|
||
})
|
||
@ApiConsumes("multipart/form-data")
|
||
@UseInterceptors(
|
||
FileInterceptor("file", {
|
||
limits: { fileSize: 10 * 1024 * 1024 },
|
||
storage: diskStorage({
|
||
destination: "./files/claim-required-document",
|
||
filename: (req, file, callback) => {
|
||
const unique = Date.now();
|
||
const ex = extname(file.originalname);
|
||
callback(null, `other-parts-${unique}${ex}`);
|
||
},
|
||
}),
|
||
}),
|
||
)
|
||
@ApiBody({
|
||
description:
|
||
"Other parts + bank information. Use `sheba` and `nationalCodeOfInsurer` like THIRD_PARTY flow. Optional file can be uploaded as car green card.",
|
||
schema: {
|
||
type: "object",
|
||
properties: {
|
||
otherParts: {
|
||
oneOf: [
|
||
{ type: "array", items: { type: "string" } },
|
||
{ type: "string", description: "JSON string array for multipart" },
|
||
],
|
||
example: ["engine", "suspension"],
|
||
},
|
||
sheba: { type: "string", example: "IR123456789012345678901234" },
|
||
nationalCodeOfInsurer: { type: "string", example: "1234567890" },
|
||
file: { type: "string", format: "binary" },
|
||
},
|
||
required: ["sheba", "nationalCodeOfInsurer"],
|
||
},
|
||
})
|
||
@ApiResponse({
|
||
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,
|
||
@UploadedFile() file?: Express.Multer.File,
|
||
): Promise<SelectOtherPartsV2ResponseDto> {
|
||
try {
|
||
return await this.claimRequestManagementService.selectOtherPartsV2(
|
||
claimRequestId,
|
||
body,
|
||
user.sub,
|
||
user,
|
||
file,
|
||
);
|
||
} 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).
|
||
|
||
**V2 order:** Complete angles and part photos first, then required documents.
|
||
`,
|
||
})
|
||
@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,
|
||
user,
|
||
);
|
||
} 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 5)",
|
||
description: `
|
||
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim)
|
||
|
||
**Upload one of the required documents** (13 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
|
||
- 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 (THIRD_PARTY)
|
||
|
||
**When all required documents are uploaded:** Workflow moves to USER_SUBMISSION_COMPLETE and the claim is ready for damage expert review.
|
||
|
||
**Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to upload documents on behalf of the damaged party.
|
||
`,
|
||
})
|
||
@ApiParam({
|
||
name: "claimRequestId",
|
||
description: "The claim case ID",
|
||
example: "507f1f77bcf86cd799439011",
|
||
})
|
||
@ApiBody({
|
||
schema: {
|
||
type: "object",
|
||
required: ["documentKey", "file"],
|
||
properties: {
|
||
documentKey: {
|
||
type: "string",
|
||
enum: [
|
||
"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: "damaged_driving_license_front",
|
||
},
|
||
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,
|
||
user,
|
||
);
|
||
} 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 4)",
|
||
description: `
|
||
**Workflow Step:** CAPTURE_PART_DAMAGES (Step 4 of Claim)
|
||
|
||
**Capture types:**
|
||
1. **angle**: Car angles (front, back, left, right) - 4 required
|
||
2. **part**: Damaged parts based on selectedParts from Step 2
|
||
|
||
**When all captures are complete:** Workflow moves to UPLOAD_REQUIRED_DOCUMENTS (Step 5).
|
||
|
||
**Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to capture photos on behalf of the damaged party.
|
||
`,
|
||
})
|
||
@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,
|
||
user,
|
||
);
|
||
} catch (error) {
|
||
if (error instanceof HttpException) throw error;
|
||
throw new InternalServerErrorException(
|
||
error instanceof Error ? error.message : "Failed to capture part",
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* V2: Upload repair factor for a part (same intent as v1 PATCH …/request/reply/:claimRequestId/:partId/upload-factor).
|
||
*/
|
||
@Patch("request/reply/:claimRequestId/:partId/upload-factor")
|
||
@ApiConsumes("multipart/form-data")
|
||
@ApiOperation({
|
||
summary: "Upload factor file for a priced part (V2)",
|
||
description:
|
||
"Use when the damage expert reply marks `factorNeeded: true` for this `partId`. " +
|
||
"Stores file in `claim-factors-image`, sets `factorLink` / `factorStatus` on the matching part in " +
|
||
"`evaluation.damageExpertReply` or `evaluation.damageExpertReplyFinal`. " +
|
||
"Requires claim `claimStatus` NEEDS_REVISION or UNDER_REVIEW.",
|
||
})
|
||
@ApiParam({ name: "claimRequestId", description: "Claim case ID" })
|
||
@ApiParam({ name: "partId", description: "Part id from expert reply" })
|
||
@ApiBody({
|
||
schema: {
|
||
type: "object",
|
||
properties: {
|
||
file: { type: "string", format: "binary" },
|
||
},
|
||
},
|
||
})
|
||
@UseInterceptors(
|
||
FileInterceptor("file", {
|
||
storage: diskStorage({
|
||
destination: "./files/claim-factors",
|
||
filename: (req, file, callback) => {
|
||
const unique = Date.now();
|
||
callback(null, `-${unique}-${file.originalname}`);
|
||
},
|
||
}),
|
||
limits: { fileSize: 10 * 1024 * 1024 },
|
||
}),
|
||
)
|
||
async uploadFactorForPartV2(
|
||
@Param("claimRequestId") claimRequestId: string,
|
||
@Param("partId") partId: string,
|
||
@UploadedFile() file: Express.Multer.File,
|
||
@CurrentUser() user: any,
|
||
) {
|
||
try {
|
||
return await this.claimRequestManagementService.uploadClaimFactorV2(
|
||
claimRequestId,
|
||
partId,
|
||
file,
|
||
user.sub,
|
||
user,
|
||
);
|
||
} catch (error) {
|
||
if (error instanceof HttpException) throw error;
|
||
throw new InternalServerErrorException(
|
||
error instanceof Error ? error.message : "Failed to upload factor",
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* V2 API: Upload car walk-around video (same behavior as v1 PATCH claim-request-management/car-capture/:id)
|
||
*/
|
||
@ApiBody({
|
||
schema: {
|
||
type: "object",
|
||
properties: {
|
||
file: { type: "string", format: "binary" },
|
||
},
|
||
},
|
||
})
|
||
@UseInterceptors(
|
||
FileInterceptor("file", {
|
||
limits: { fileSize: 50 * 1024 * 1024 },
|
||
storage: diskStorage({
|
||
destination: "./files/car-capture-videos/",
|
||
filename: (req, file, callback) => {
|
||
const unique = Date.now();
|
||
const ex = extname(file.originalname);
|
||
const filename = `claim-video-${unique}${ex}`;
|
||
callback(null, filename);
|
||
},
|
||
}),
|
||
}),
|
||
)
|
||
@ApiConsumes("multipart/form-data")
|
||
@Patch("car-capture/:claimRequestId")
|
||
@ApiOperation({
|
||
summary: "Upload car walk-around video (V2)",
|
||
description:
|
||
"Multipart upload of the car capture video during CAPTURE_PART_DAMAGES. " +
|
||
"Stores file metadata in `claim-video-capture` and sets `ClaimCase.media.videoCaptureId`. " +
|
||
"Only one video per claim; path is stored on the video document (not duplicated on the case).",
|
||
})
|
||
@ApiParam({
|
||
name: "claimRequestId",
|
||
description: "The claim case ID (MongoDB ObjectId)",
|
||
example: "507f1f77bcf86cd799439011",
|
||
})
|
||
@ApiResponse({
|
||
status: 200,
|
||
description: "Video uploaded successfully",
|
||
type: VideoCaptureV2ResponseDto,
|
||
})
|
||
@ApiResponse({ status: 400, description: "Wrong workflow step or missing file" })
|
||
@ApiResponse({ status: 403, description: "Not the claim owner" })
|
||
@ApiResponse({ status: 404, description: "Claim not found" })
|
||
@ApiResponse({ status: 409, description: "Video already uploaded" })
|
||
async captureVideoCaptureV2(
|
||
@Param("claimRequestId") claimRequestId: string,
|
||
@UploadedFile("file") file: Express.Multer.File,
|
||
@CurrentUser() user: any,
|
||
): Promise<VideoCaptureV2ResponseDto> {
|
||
try {
|
||
return await this.claimRequestManagementService.setVideoCaptureV2(
|
||
claimRequestId,
|
||
file,
|
||
user.sub,
|
||
user,
|
||
);
|
||
} catch (error) {
|
||
if (error instanceof HttpException) throw error;
|
||
throw new InternalServerErrorException(
|
||
error instanceof Error ? error.message : "Failed to upload car capture video",
|
||
);
|
||
}
|
||
}
|
||
}
|