forked from Yara724/api
384 lines
17 KiB
TypeScript
384 lines
17 KiB
TypeScript
import {
|
||
Body,
|
||
Controller,
|
||
Get,
|
||
Header,
|
||
Param,
|
||
Headers,
|
||
Patch,
|
||
Post,
|
||
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 { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-result.dto";
|
||
import { ExpertClaimService } from "./expert-claim.service";
|
||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||
import {
|
||
UnifiedFileStatusReportDto,
|
||
UnifiedFileStatusReportQueryDto,
|
||
} from "src/common/dto/unified-file-status-report.dto";
|
||
import { GetClaimListV2ResponseDto } from "./dto/claim-list-v2.dto";
|
||
import {
|
||
ClaimSubmitResendV2Dto,
|
||
SubmitExpertReplyV2Dto,
|
||
} from "./dto/expert-claim-v2.dto";
|
||
import { UpdateClaimDamagedPartsV2Dto } from "./dto/update-claim-damaged-parts-v2.dto";
|
||
import {
|
||
ClaimPriceDropContextV2Dto,
|
||
ClaimPriceDropResultV2Dto,
|
||
UpsertClaimPriceDropV2Dto,
|
||
} from "./dto/claim-price-drop-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, RoleEnum.FIELD_EXPERT, RoleEnum.FILE_REVIEWER)
|
||
export class ExpertClaimV2Controller {
|
||
constructor(
|
||
private readonly expertClaimService: ExpertClaimService,
|
||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||
) {}
|
||
|
||
@Get("report/unified-file-statuses")
|
||
@ApiOperation({
|
||
summary: "Unified file status catalog + counts (claim + linked blame)",
|
||
description:
|
||
"Calculatable statuses for this expert's claim portfolio. Filter lists with `unifiedStatus` and `fileType` query params. Optional `from` / `to` ISO dates narrow the counted portfolio.",
|
||
})
|
||
@ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
|
||
async getUnifiedFileStatusReportV2(
|
||
@CurrentUser() actor: any,
|
||
@Query() query: UnifiedFileStatusReportQueryDto,
|
||
): Promise<UnifiedFileStatusReportDto> {
|
||
return await this.expertClaimService.getUnifiedFileStatusReportV2(
|
||
actor,
|
||
query,
|
||
);
|
||
}
|
||
|
||
@Get("report/status-counts")
|
||
@ApiOperation({
|
||
summary: "Count claim cases by grouped status bucket (this damage expert)",
|
||
deprecated: true,
|
||
description:
|
||
"Legacy claim-only buckets. Prefer GET report/unified-file-statuses for blame+claim calculatable statuses.",
|
||
})
|
||
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<OuterPartCatalogItemDto[]> {
|
||
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:
|
||
"Damage experts: tenant queue (`WAITING_FOR_DAMAGE_EXPERT`), locked/in-progress, factor validation. " +
|
||
"Field experts: all claims linked to blame files they initiated (`initiatedByFieldExpertId` or matching `blameRequestId`). " +
|
||
"Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
|
||
})
|
||
@ApiResponse({ status: 200, type: GetClaimListV2ResponseDto })
|
||
async getClaimListV2(
|
||
@CurrentUser() actor,
|
||
@Query() query: ListQueryV2Dto,
|
||
): Promise<GetClaimListV2ResponseDto> {
|
||
return await this.expertClaimService.getClaimListV2(actor, query);
|
||
}
|
||
|
||
@Get("request/:claimRequestId")
|
||
@ApiOperation({
|
||
summary: "Get claim request detail for damage expert",
|
||
description:
|
||
"Returns full claim details including captured images, required documents, damage selections, `evaluation.priceDrop` during damage review, `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,
|
||
);
|
||
}
|
||
|
||
@Get("request/:claimRequestId/price-drop")
|
||
@ApiOperation({
|
||
summary: "Price-drop context for manual expert entry (V2)",
|
||
description:
|
||
"While the claim is locked in EXPERT_REVIEWING: severity labels (جزیی / متوسط / شدید), coefficient catalog, damaged parts with price-drop mapping, suggested Jalali year from linked blame plate inquiry (`ModelField` / `ModelCii`), and saved `evaluation.priceDrop` if any.",
|
||
})
|
||
@ApiParam({ name: "claimRequestId" })
|
||
@ApiResponse({ status: 200, type: ClaimPriceDropContextV2Dto })
|
||
async getPriceDropContextV2(
|
||
@Param("claimRequestId") claimRequestId: string,
|
||
@CurrentUser() actor,
|
||
) {
|
||
return await this.expertClaimService.getPriceDropContextV2(
|
||
claimRequestId,
|
||
actor,
|
||
);
|
||
}
|
||
|
||
@Put("request/:claimRequestId/price-drop")
|
||
@ApiOperation({
|
||
summary: "Calculate and save price drop (V2)",
|
||
description:
|
||
"Expert supplies car market price, optional `vehicle.carName` / `vehicle.carModel`, per-damaged-part severity (`partId` from claim detail), and optional `carModelYear` (defaults from blame inquiry). Persists `evaluation.priceDrop` using `(carPrice × yearCoefficient × sumOfCoefficients) / 400`.",
|
||
})
|
||
@ApiParam({ name: "claimRequestId" })
|
||
@ApiBody({ type: UpsertClaimPriceDropV2Dto })
|
||
@ApiResponse({ status: 200, type: ClaimPriceDropResultV2Dto })
|
||
async upsertPriceDropV2(
|
||
@Param("claimRequestId") claimRequestId: string,
|
||
@Body() body: UpsertClaimPriceDropV2Dto,
|
||
@CurrentUser() actor,
|
||
) {
|
||
return await this.expertClaimService.upsertPriceDropV2(
|
||
claimRequestId,
|
||
body,
|
||
actor,
|
||
);
|
||
}
|
||
|
||
@Post("assign/:claimRequestId")
|
||
@ApiOperation({
|
||
summary: "Check availability and assign claim to this damage expert",
|
||
description:
|
||
"Call before opening a case from the list. Returns `assigned` when the claim was free and is now locked to you, `already_assigned_to_you` when you already hold the lock, or **409** with `status=locked` when another expert is reviewing. Works for both the damage-review queue (`WAITING_FOR_DAMAGE_EXPERT`) and factor-validation queue.",
|
||
})
|
||
@ApiParam({ name: "claimRequestId" })
|
||
@ApiResponse({ status: 200, type: ExpertFileAssignResultDto })
|
||
@ApiResponse({
|
||
status: 409,
|
||
description: "Another expert is processing this claim",
|
||
type: ExpertFileAssignResultDto,
|
||
})
|
||
async assignClaimForReviewV2(
|
||
@Param("claimRequestId") claimRequestId: string,
|
||
@CurrentUser() actor,
|
||
) {
|
||
return await this.expertClaimService.assignClaimForReviewV2(
|
||
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. Same assignment logic as POST `assign/:claimRequestId`, but returns the legacy lock shape and maps conflicts to 400 instead of 409.",
|
||
deprecated: true,
|
||
})
|
||
@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 **Toman** (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 Toman**; 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);
|
||
}
|
||
}
|