forked from Yara724/api
598 lines
19 KiB
TypeScript
598 lines
19 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { extname } from "node:path";
|
|
import {
|
|
Body,
|
|
Controller,
|
|
Get,
|
|
Param,
|
|
Patch,
|
|
Post,
|
|
Query,
|
|
UploadedFile,
|
|
UseGuards,
|
|
UseInterceptors,
|
|
} from "@nestjs/common";
|
|
import {
|
|
ApiBearerAuth,
|
|
ApiBody,
|
|
ApiConsumes,
|
|
ApiOperation,
|
|
ApiParam,
|
|
ApiResponse,
|
|
ApiTags,
|
|
} from "@nestjs/swagger";
|
|
import { FileInterceptor } from "@nestjs/platform-express";
|
|
import { diskStorage } from "multer";
|
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
|
import { Roles } from "src/decorators/roles.decorator";
|
|
import { CurrentUser } from "src/decorators/user.decorator";
|
|
import { MediaPolicyService } from "src/media-policy/media-policy.service";
|
|
import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service";
|
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
|
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
|
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
|
import {
|
|
OuterPartCatalogItemDto,
|
|
SelectOuterPartsV2Dto,
|
|
SelectOuterPartsV2ResponseDto,
|
|
} from "./dto/select-outer-parts-v2.dto";
|
|
import {
|
|
SelectOtherPartsV2Dto,
|
|
SelectOtherPartsV2ResponseDto,
|
|
} from "./dto/select-other-parts-v2.dto";
|
|
import {
|
|
UploadRequiredDocumentV2Dto,
|
|
UploadRequiredDocumentV2ResponseDto,
|
|
} from "./dto/upload-document-v2.dto";
|
|
import {
|
|
CapturePartV2Dto,
|
|
CapturePartV2ResponseDto,
|
|
} from "./dto/capture-part-v2.dto";
|
|
import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto";
|
|
|
|
/**
|
|
* Expert-initiated claim flow that mirrors the normal user claim API
|
|
* (`v2/claim-request-management`) one-to-one. The frontend reuses the same
|
|
* claim pages by only swapping the route prefix:
|
|
*
|
|
* `v2/claim-request-management/*` -> `v2/expert-initiated/claim-request-management/*`
|
|
*
|
|
* The field expert fills the claim (parts, captures, documents) on behalf of the
|
|
* damaged party for a claim created from an expert-initiated IN_PERSON blame.
|
|
* After the expert submits the data, the file follows the exact normal review
|
|
* lifecycle: the damage expert prices it, and the owner can later object / resend
|
|
* from their own account through the normal user endpoints.
|
|
*/
|
|
@ApiTags("expert-initiated claim (mirror v2)")
|
|
@Controller("v2/expert-initiated/claim-request-management")
|
|
@ApiBearerAuth()
|
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
|
@Roles(RoleEnum.FIELD_EXPERT)
|
|
export class ExpertInitiatedClaimMirrorController {
|
|
constructor(
|
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
|
private readonly mediaPolicyService: MediaPolicyService,
|
|
) {}
|
|
|
|
@Post("create-from-blame/:blameRequestId")
|
|
@ApiParam({ name: "blameRequestId" })
|
|
@ApiOperation({
|
|
summary: "[Expert mirror] Create claim from expert-initiated IN_PERSON blame",
|
|
description:
|
|
"Blame must be COMPLETED. Creates a ClaimCase owned by the damaged (non-guilty) party.",
|
|
})
|
|
async createFromBlame(
|
|
@Param("blameRequestId") blameRequestId: string,
|
|
@CurrentUser() expert: any,
|
|
) {
|
|
return this.claimRequestManagementService.createClaimFromBlameForExpertV2(
|
|
blameRequestId,
|
|
expert,
|
|
);
|
|
}
|
|
|
|
@Get("requests")
|
|
@ApiOperation({ summary: "[Expert mirror] List my (in-person) claims" })
|
|
async getMyClaims(
|
|
@CurrentUser() expert: any,
|
|
@Query() query: ListQueryV2Dto,
|
|
) {
|
|
return this.claimRequestManagementService.getMyClaimsV2(
|
|
expert.sub,
|
|
expert,
|
|
query,
|
|
);
|
|
}
|
|
|
|
@Get("outer-parts-catalog")
|
|
@ApiOperation({
|
|
summary: "Get outer parts catalog (V2)",
|
|
description:
|
|
"Returns outer-damage parts with id/key/side. Optional `carType` filter returns only that type catalog.",
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: "Outer parts catalog",
|
|
type: [OuterPartCatalogItemDto],
|
|
})
|
|
async getOuterPartsCatalog(@Query("carType") carType?: ClaimVehicleTypeV2) {
|
|
return this.claimRequestManagementService.getOuterPartsCatalogV2(carType);
|
|
}
|
|
|
|
@Get("car-other-part")
|
|
@ApiOperation({
|
|
summary: "Get other (non-body) parts catalog",
|
|
description:
|
|
"Returns legacy other-parts catalog used by frontend. Response is parsed JSON.",
|
|
})
|
|
@ApiResponse({ status: 200, description: "Other parts catalog" })
|
|
async getCarOtherParts() {
|
|
const raw = await readFile(
|
|
`${process.cwd()}/src/static/car-part.json`,
|
|
"utf-8",
|
|
);
|
|
try {
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
return raw;
|
|
}
|
|
}
|
|
|
|
@Get("branches/:insuranceId")
|
|
@ApiParam({
|
|
name: "insuranceId",
|
|
description: "Insurer client id (MongoDB ObjectId)",
|
|
example: "60d5ec49e7b2f8001c8e4d2a",
|
|
})
|
|
@ApiOperation({
|
|
summary: "Get insurer branches (V2)",
|
|
description:
|
|
"Returns branch list for a given insurer/client id so frontend can render branch options (name/code/address/city/state) and submit selected branchId in daghi part options.",
|
|
})
|
|
@ApiResponse({ status: 200, description: "List of branches for insurer" })
|
|
async getInsuranceBranches(@Param("insuranceId") insuranceId: string) {
|
|
return this.claimRequestManagementService.retrieveInsuranceBranches(
|
|
insuranceId,
|
|
);
|
|
}
|
|
|
|
@Get("request/:claimRequestId")
|
|
@ApiParam({
|
|
name: "claimRequestId",
|
|
description: "The claim case ID (MongoDB ObjectId)",
|
|
example: "507f1f77bcf86cd799439011",
|
|
})
|
|
@ApiOperation({ summary: "[Expert mirror] Get claim details" })
|
|
async getClaimDetails(
|
|
@Param("claimRequestId") claimRequestId: string,
|
|
@CurrentUser() expert: any,
|
|
) {
|
|
return this.claimRequestManagementService.getClaimDetailsV2(
|
|
claimRequestId,
|
|
expert.sub,
|
|
expert,
|
|
);
|
|
}
|
|
|
|
@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:** Expert selects which outer car parts (body parts) were damaged on behalf of the damaged party.
|
|
|
|
**Validations:**
|
|
- Claim must exist
|
|
- 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
|
|
|
|
**After Success:**
|
|
- Workflow moves to: SELECT_OTHER_PARTS (Step 3)
|
|
`,
|
|
})
|
|
@ApiParam({
|
|
name: "claimRequestId",
|
|
description: "The claim case ID (MongoDB ObjectId)",
|
|
example: "507f1f77bcf86cd799439011",
|
|
})
|
|
@ApiBody({
|
|
type: SelectOuterPartsV2Dto,
|
|
description:
|
|
"Selected vehicle type + selected outer part IDs from catalog",
|
|
examples: {
|
|
example1: {
|
|
summary: "Sedan - minor front damage",
|
|
value: {
|
|
carType: "sedan",
|
|
selectedPartIds: [19, 21, 16],
|
|
},
|
|
},
|
|
example2: {
|
|
summary: "SUV - left side impact",
|
|
value: {
|
|
carType: "suv",
|
|
selectedPartIds: [102, 103, 104, 107],
|
|
},
|
|
},
|
|
example3: {
|
|
summary: "Hatchback - rear-end collision",
|
|
value: {
|
|
carType: "hatchback",
|
|
selectedPartIds: [225, 226, 210],
|
|
},
|
|
},
|
|
example4: {
|
|
summary: "Pickup - two sides + roof",
|
|
value: {
|
|
carType: "pickup",
|
|
selectedPartIds: [319, 312, 330],
|
|
},
|
|
},
|
|
},
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: "Outer parts selected successfully",
|
|
type: SelectOuterPartsV2ResponseDto,
|
|
})
|
|
@ApiResponse({ status: 400, description: "Invalid workflow step or validation failed" })
|
|
@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() expert: any,
|
|
): Promise<SelectOuterPartsV2ResponseDto> {
|
|
return this.claimRequestManagementService.selectOuterPartsV2(
|
|
claimRequestId,
|
|
body,
|
|
expert.sub,
|
|
expert,
|
|
);
|
|
}
|
|
|
|
@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:** Expert selects non-body damaged parts and provides bank information for payment on behalf of the damaged party.
|
|
Optional: upload car green card file in the same step.
|
|
|
|
**Validations:**
|
|
- Claim must exist
|
|
- 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)
|
|
`,
|
|
})
|
|
@ApiParam({
|
|
name: "claimRequestId",
|
|
description: "The claim case ID (MongoDB ObjectId)",
|
|
example: "507f1f77bcf86cd799439011",
|
|
})
|
|
@ApiConsumes("multipart/form-data")
|
|
@UseInterceptors(
|
|
FileInterceptor("file", {
|
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
|
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`. 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: 404, description: "Claim case not found" })
|
|
@ApiResponse({ status: 409, description: "Bank information already submitted" })
|
|
async selectOtherParts(
|
|
@Param("claimRequestId") claimRequestId: string,
|
|
@Body() body: SelectOtherPartsV2Dto,
|
|
@CurrentUser() expert: any,
|
|
@UploadedFile() file?: Express.Multer.File,
|
|
): Promise<SelectOtherPartsV2ResponseDto> {
|
|
// Green-card photo is optional here — the helper no-ops on missing file.
|
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
|
return this.claimRequestManagementService.selectOtherPartsV2(
|
|
claimRequestId,
|
|
body,
|
|
expert.sub,
|
|
expert,
|
|
file,
|
|
);
|
|
}
|
|
|
|
@Get("capture-requirements/:claimRequestId")
|
|
@ApiOperation({
|
|
summary: "Get Capture Requirements (V2)",
|
|
description: `
|
|
**Get list of what needs to be captured:**
|
|
- Required documents (10 remaining at the documents step for third-party; 3 damaged-party items should be uploaded during capture — see \`preferUploadDuringCapture\` on each item)
|
|
- 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 (enforced by API):** During \`CAPTURE_PART_DAMAGES\`, (1) all damaged-part photos, (2) four car angles, (3) chassis/engine/metal-plate via upload-document, then walk-around video. Remaining documents in \`UPLOAD_REQUIRED_DOCUMENTS\`. Use \`captureSequencePhase\` / \`captureSequenceHint\` in the response.
|
|
`,
|
|
})
|
|
@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() expert: any,
|
|
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
|
return this.claimRequestManagementService.getCaptureRequirementsV2(
|
|
claimRequestId,
|
|
expert.sub,
|
|
expert,
|
|
);
|
|
}
|
|
|
|
@Post("upload-document/:claimRequestId")
|
|
@UseInterceptors(
|
|
FileInterceptor("file", {
|
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
|
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** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
|
|
- 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.
|
|
`,
|
|
})
|
|
@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() expert: any,
|
|
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
|
return this.claimRequestManagementService.uploadRequiredDocumentV2(
|
|
claimRequestId,
|
|
body,
|
|
file,
|
|
expert.sub,
|
|
expert,
|
|
);
|
|
}
|
|
|
|
@Post("capture-part/:claimRequestId")
|
|
@UseInterceptors(
|
|
FileInterceptor("file", {
|
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
|
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 (parts, angles, capture-phase docs):** Workflow moves to UPLOAD_REQUIRED_DOCUMENTS (Step 5). Angles are blocked until all parts are captured; capture-phase documents are blocked until all angles are captured.
|
|
`,
|
|
})
|
|
@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() expert: any,
|
|
): Promise<CapturePartV2ResponseDto> {
|
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
|
return this.claimRequestManagementService.capturePartV2(
|
|
claimRequestId,
|
|
body,
|
|
file,
|
|
expert.sub,
|
|
expert,
|
|
);
|
|
}
|
|
|
|
@Patch("car-capture/:claimRequestId")
|
|
@ApiConsumes("multipart/form-data")
|
|
@UseInterceptors(
|
|
FileInterceptor("file", {
|
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
|
storage: diskStorage({
|
|
destination: "./files/car-capture-videos/",
|
|
filename: (req, file, callback) => {
|
|
const unique = Date.now();
|
|
const ex = extname(file.originalname);
|
|
callback(null, `claim-video-${unique}${ex}`);
|
|
},
|
|
}),
|
|
}),
|
|
)
|
|
@ApiParam({
|
|
name: "claimRequestId",
|
|
description: "The claim case ID",
|
|
example: "507f1f77bcf86cd799439011",
|
|
})
|
|
@ApiBody({
|
|
schema: {
|
|
type: "object",
|
|
required: ["file"],
|
|
properties: { file: { type: "string", format: "binary" } },
|
|
},
|
|
})
|
|
@ApiOperation({
|
|
summary: "Upload Car Walk-Around Video (V2 - Step 4b)",
|
|
description:
|
|
"Upload a walk-around video of the damaged car during the capture phase. Allowed at any point after outer parts are selected.",
|
|
})
|
|
@ApiResponse({ status: 200, description: "Video uploaded successfully" })
|
|
async carCapture(
|
|
@Param("claimRequestId") claimRequestId: string,
|
|
@UploadedFile("file") file: Express.Multer.File,
|
|
@CurrentUser() expert: any,
|
|
) {
|
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "video");
|
|
return this.claimRequestManagementService.setVideoCaptureV2(
|
|
claimRequestId,
|
|
file,
|
|
expert.sub,
|
|
expert,
|
|
);
|
|
}
|
|
}
|