Files
yara724api/src/expert-claim/expert-claim.v2.controller.ts
Soheil Hajizadeh 972b4b8719 status modfied
2026-05-04 21:19:11 +03:30

207 lines
9.9 KiB
TypeScript

import { Body, Controller, Get, Header, Param, Headers, Patch, Put, UseGuards, Query, Res } from "@nestjs/common";
import {
ApiBearerAuth,
ApiBody,
ApiOperation,
ApiParam,
ApiPropertyOptional,
ApiQuery,
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";
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) { }
@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);
}
@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:
"Claim must have status WAITING_FOR_DAMAGE_EXPERT. Locking sets status to EXPERT_REVIEWING and claimStatus to UNDER_REVIEW. Only one expert can lock a claim at a time.",
})
@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 part needs `daghi` (V1 rules) and may set `factorNeeded` (repair factor file required from owner) or priced lines only. **Cap:** sum of `totalPayment` ≤ 30,000,000. 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:
"**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`; rejected lines require expert `totalPayment` (or both `price` and `salary`).\n\n" +
"**Outcomes:**\n" +
"- **All approved:** `status=INSURER_REVIEW_AWAITING_OWNER_SIGN`, `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW` → owner must **sign or reject** via `owner-insurer-approval/sign`; agree completes the case.\n" +
"- **Any rejected (repriced):** same as above (`INSURER_REVIEW_AWAITING_OWNER_SIGN` + `INSURER_REVIEW`) → owner must **accept or reject** the repriced totals.\n" +
"- **Partial batch:** service may return pending until every factor line has a non-pending decision.",
})
@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);
}
}