import { Body, Controller, Get, Header, Param, Headers, Patch, Put, UseGuards, Query, Res } from "@nestjs/common"; import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiPropertyOptional, ApiQuery, ApiResponse, ApiTags, } from "@nestjs/swagger"; import { IsOptional, IsString } from "class-validator"; 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 { RoleEnum } from "src/Types&Enums/role.enum"; import { ExpertClaimService } from "./expert-claim.service"; import { ClaimSubmitResendV2Dto, SubmitExpertReplyV2Dto } from "./dto/expert-claim-v2.dto"; import { UpdateClaimDamagedPartsV2Dto } from "./dto/update-claim-damaged-parts-v2.dto"; import { FactorValidationV2Dto } from "./dto/factor-validation.dto"; import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service"; import { OuterPartCatalogItemDto } from "src/claim-request-management/dto/select-outer-parts-v2.dto"; import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog"; class InPersonVisitV2Dto { @ApiPropertyOptional({ example: 'Paint damage requires physical inspection' }) @IsOptional() @IsString() note?: string; } @ApiTags("expert-claim-panel (v2)") @Controller("v2/expert-claim") @ApiBearerAuth() @UseGuards(LocalActorAuthGuard, RolesGuard) @Roles(RoleEnum.DAMAGE_EXPERT) export class ExpertClaimV2Controller { constructor( private readonly expertClaimService: ExpertClaimService, private readonly claimRequestManagementService: ClaimRequestManagementService, ) {} @Get("report/status-counts") @ApiOperation({ summary: "Count claim cases by grouped status bucket (tenant)", description: "IN_PROGRESS groups user-phase statuses before submission is complete (CREATED through CAPTURING_PART_DAMAGES). Other keys match ClaimCaseStatus. Scoped to the insurer in the JWT.", }) async getStatusReportBucketsV2(@CurrentUser() actor: any) { return await this.expertClaimService.getStatusReportBucketsV2(actor); } /** * Same catalog as `GET v2/claim-request-management/outer-parts-catalog` so factor/resend payloads use identical part ids. */ @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, ): Promise { return this.claimRequestManagementService.getOuterPartsCatalogV2(carType); } @Get("branches") @ApiOperation({ summary: "List insurer branches for this damage expert (V2)", description: "Returns branches for the insurance company bound to the expert JWT (`clientKey`). " + "Same payload as v1 `GET expert-claim/branches/:insuranceId` — no client id in the URL.", }) @ApiResponse({ status: 200, description: "Branch list (name, code, address, city, state, phoneNumber, isActive, …) for daghi / branch selection in expert replies", }) async getInsuranceBranchesV2(@CurrentUser() actor: { clientKey?: string }) { return await this.expertClaimService.retrieveInsuranceBranchesV2(actor); } @Get("requests") @ApiOperation({ summary: "List available claim requests for damage expert", description: "Returns claims that are WAITING_FOR_DAMAGE_EXPERT and not locked by another expert, this expert's locked/in-progress claims, and claims awaiting factor validation (`status=EXPERT_VALIDATING_REPAIR_FACTORS` or legacy `WAITING_FOR_INSURER_APPROVAL` with UNDER_REVIEW at EXPERT_COST_EVALUATION).", }) async getClaimListV2(@CurrentUser() actor) { return await this.expertClaimService.getClaimListV2(actor); } @Get("request/:claimRequestId") @ApiOperation({ summary: "Get claim request detail for damage expert", description: "Returns full claim details including captured images, required documents, damage selections, `videoCapture` (from claim-video-capture via media.videoCaptureId), and `blameCase` (linked blameCases document with party video/voice URLs like expert-blame detail). Allowed when status is WAITING_FOR_DAMAGE_EXPERT (if locked, only the locking expert) or when awaiting factor validation.", }) @ApiParam({ name: "claimRequestId" }) async getClaimDetailV2( @Param("claimRequestId") claimRequestId: string, @CurrentUser() actor, ) { return await this.expertClaimService.getClaimDetailV2(claimRequestId, actor); } @Put("lock/:claimRequestId") @ApiOperation({ summary: "Lock a claim request for review", description: "Lockable in two queues:\n" + "1. **Damage review queue** — claim status `WAITING_FOR_DAMAGE_EXPERT`. Locking advances the claim to `status=EXPERT_REVIEWING`, `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_DAMAGE_ASSESSMENT`.\n" + "2. **Factor validation queue** — claim status `EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`) with `claimStatus=UNDER_REVIEW` and `workflow.currentStep=EXPERT_COST_EVALUATION`. Locking only sets the workflow lock fields; status/claimStatus/currentStep are left untouched so the claim stays in the factor-validation queue when the lock expires.\n\n" + "Only one expert can hold the lock at a time. Re-locking by the same expert is idempotent.", }) @ApiParam({ name: "claimRequestId" }) async lockClaimRequestV2( @Param("claimRequestId") claimRequestId: string, @CurrentUser() actor, ) { return await this.expertClaimService.lockClaimRequestV2(claimRequestId, actor); } @Put("reply/submit/:claimRequestId") @ApiOperation({ summary: "Submit expert damage assessment reply", description: "**Preconditions:** claim locked by this expert (`EXPERT_REVIEWING`). **Unlocks** the claim. Each `parts[]` line needs `partId` (from GET claim detail `damagedParts[].partId`), plus pricing, `daghi`, and optional `factorNeeded`. **Cap:** sum of line `totalPayment` values ≤ 53,000,000 (same limit as factor-validation totals across priced + factor lines). Clears any prior `evaluation.ownerInsurerApproval` / `ownerPricedPartsApproval`.\n\n" + "**Frontend routing by `ClaimCaseStatus` (`status`):**\n" + "- **All parts `factorNeeded`:** `OWNER_REPAIR_FACTOR_UPLOAD_PENDING`, `claimStatus=NEEDS_REVISION`, `workflow.currentStep=OWNER_UPLOAD_FACTOR_DOCUMENTS`, `workflow.nextStep=EXPERT_COST_EVALUATION` → owner uploads all factors; then `status` becomes **`EXPERT_VALIDATING_REPAIR_FACTORS`**, `claimStatus=UNDER_REVIEW`, `currentStep=EXPERT_COST_EVALUATION` for expert **validate-factors**.\n" + "- **Mixed (some priced, some factorNeeded):** `INSURER_REVIEW_MIXED_FACTORS_PENDING`, `claimStatus=NEEDS_REVISION`, `currentStep=INSURER_REVIEW`, `nextStep=OWNER_UPLOAD_FACTOR_DOCUMENTS` → owner must call **owner-insurer-approval/sign** first (priced-line acceptance); `currentStep` then moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` (same case `status` until factors are done).\n" + "- **No factors:** **`INSURER_REVIEW_AWAITING_OWNER_SIGN`**, `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW`, `nextStep=CLAIM_COMPLETED` → owner final sign/reject only.\n\n" + "**Legacy rows** may still use `WAITING_FOR_INSURER_APPROVAL` instead of the specific values above.\n\n" + "**After the owner fulfilled an expert resend** (`damageExpertResend.fulfilledAt`), this expert **cannot** initiate **another resend**—use **reply/submit**, in-person visit, or **validate-factors** as appropriate.", }) @ApiParam({ name: "claimRequestId" }) @ApiBody({ type: SubmitExpertReplyV2Dto }) async submitExpertReplyV2( @Param("claimRequestId") claimRequestId: string, @Body() body: SubmitExpertReplyV2Dto, @CurrentUser() actor, ) { return await this.expertClaimService.submitExpertReplyV2(claimRequestId, body, actor); } @Put("reply/resend/:claimRequestId") @ApiOperation({ summary: "Submit expert damage resend request", description: "Claim must be locked by this expert (`EXPERT_REVIEWING`). Sets `WAITING_FOR_USER_RESEND`, `USER_EXPERT_RESEND`, `NEEDS_REVISION`, clears the lock, and stores `evaluation.damageExpertResend`. " + "Owner completes via document/capture endpoints or `POST .../expert-resend/acknowledge` when only instructions were given.\n\n" + "**One resend per claim lifecycle:** if the owner has already fulfilled a resend (`damageExpertResend.fulfilledAt`), this endpoint returns **400**—the expert may only submit a priced reply or request in-person visit afterward.", }) @ApiParam({ name: "claimRequestId" }) @ApiBody({ type: ClaimSubmitResendV2Dto }) async submitExpertResendV2( @Param("claimRequestId") claimRequestId: string, @Body() body: ClaimSubmitResendV2Dto, @CurrentUser() actor, ) { return await this.expertClaimService.submitResendDocsV2(claimRequestId, body, actor); } @Patch(":claimRequestId/visit") @ApiOperation({ summary: "Request in-person visit for claim", description: "Expert decides the user must come in person. Claim must be locked by this expert. Sets claimStatus to NEEDS_REVISION and unlocks the claim so the user is informed.", }) @ApiParam({ name: "claimRequestId" }) @ApiBody({ type: InPersonVisitV2Dto, required: false }) async requestInPersonVisitV2( @Param("claimRequestId") claimRequestId: string, @Body() body: InPersonVisitV2Dto, @CurrentUser() actor, ) { return await this.expertClaimService.requestInPersonVisitV2( claimRequestId, actor, body?.note, ); } @Patch("validate-factors/:claimRequestId") @ApiOperation({ summary: "Validate uploaded repair factors (V2 ClaimCase)", description: "**Response:** `claimStatus` = `ClaimStatus` (e.g. APPROVED). `caseStatus` = `ClaimCaseStatus` (e.g. COMPLETED vs insurer-review) — they are not interchangeable.\n\n" + "**Preconditions:** `status=EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`), `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_COST_EVALUATION`, every `factorNeeded` line has `factorLink`.\n\n" + "**Decisions:** each factor line gets `APPROVED` or `REJECTED`. **Every** decided line must include expert-entered `totalPayment` **or** both `price` and `salary` (factor photos are not read for amounts).\n\n" + "**Cap (when every factor line is decided):** sum of **all** reply lines (priced parts + factor lines) must be ≤ **53,000,000**; otherwise `PRICE_CAP_ERROR` with message that the maximum acceptable total was exceeded.\n\n" + "**Outcomes:**\n" + "- **All approved:** `caseStatus=COMPLETED`, `claimStatus=APPROVED`, workflow `CLAIM_COMPLETED` — no owner signature.\n" + "- **Any rejected (repriced):** same auto-complete for now (owner acceptance may be added later).\n" + "- **Partial batch:** returns pending until every factor line has a non-pending decision (no cap error until the batch is complete).", }) @ApiParam({ name: "claimRequestId" }) @ApiBody({ type: FactorValidationV2Dto }) async validateClaimFactorsV2( @Param("claimRequestId") claimRequestId: string, @Body() body: FactorValidationV2Dto, @CurrentUser() actor, ) { return await this.expertClaimService.validateClaimFactorsV2( claimRequestId, body, actor, ); } @Patch("request/:claimRequestId/damaged-parts") @ApiOperation({ summary: "Edit selected damaged parts (V2)", description: "Damage expert can modify selected damaged parts while claim is in EXPERT_REVIEWING and locked by the same expert.", }) @ApiParam({ name: "claimRequestId" }) @ApiBody({ type: UpdateClaimDamagedPartsV2Dto }) async updateClaimDamagedPartsV2( @Param("claimRequestId") claimRequestId: string, @Body() body: UpdateClaimDamagedPartsV2Dto, @CurrentUser() actor, ) { return await this.expertClaimService.updateClaimDamagedPartsV2( claimRequestId, body, actor, ); } @ApiQuery({ name: "query", enum: ["car-capture", "accident"], required: true, }) @Get("stream/:id/video") @Header("Accept-Ranges", "bytes") @Header("Content-Type", "video/mp4") async claimStream( @Headers() headers, @Param("id") id: string, @Query("query") query: "car-capture" | "accident", @Res() res, ) { return this.expertClaimService.streamServiceV2(id, query, res, headers); } }