diff --git a/src/expert-claim/dto/expert-claim-v2.dto.ts b/src/expert-claim/dto/expert-claim-v2.dto.ts index 2b7d2c7..fdb4ddd 100644 --- a/src/expert-claim/dto/expert-claim-v2.dto.ts +++ b/src/expert-claim/dto/expert-claim-v2.dto.ts @@ -8,6 +8,8 @@ import { ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; +import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum'; +import { DamagedPartItem } from 'src/claim-request-management/dto/capture-requirements-v2.dto'; export class CarPartDamageV2Dto { @ApiProperty({ example: 'left' }) @@ -68,3 +70,14 @@ export class SubmitExpertReplyV2Dto { @Type(() => PartPricingV2Dto) parts: PartPricingV2Dto[]; } + +export class ClaimSubmitResendV2Dto { + @ApiProperty({ required: true }) + resendDescription: string; + + @ApiProperty({ required: true, examples: ClaimRequiredDocumentType }) + resendDocuments: ClaimRequiredDocumentType[]; + + @ApiProperty({ required: true, type: [DamagedPartItem] }) + resendCarParts: DamagedPartItem[]; +} diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 210954b..94d7862 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -42,6 +42,8 @@ import { ClaimRequiredDocumentDbService } from "src/claim-request-management/ent import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum"; import { DaghiOption } from "src/Types&Enums/claim-request-management/daghi-option.enum"; import { BranchDbService } from "src/client/entities/db-service/branch.db.service"; +import { ClaimSubmitResendV2Dto } from "./dto/expert-claim-v2.dto"; +import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service"; @Injectable() export class ExpertClaimService { @@ -154,13 +156,14 @@ export class ExpertClaimService { private readonly claimVideoCaptureDbService: VideoCaptureDbService, private readonly httpService: HttpService, private readonly claimCaseDbService: ClaimCaseDbService, + private readonly blameRequestDbService: BlameRequestDbService, private readonly sandHubService: SandHubService, private readonly damageExpertDbService: DamageExpertDbService, private readonly clientDbService: ClientDbService, private readonly claimFactorsImageDbService: ClaimFactorsImageDbService, private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService, private readonly branchDbService: BranchDbService, - ) {} + ) { } private isVisibleToClientType(client: any, actor: any): boolean { if (actor.userType === UserType.GENUINE) { @@ -474,7 +477,7 @@ export class ExpertClaimService { async calculatePriceDrop(request: any, carPartsAi: any, query?: any) { const requestId = request?._id?.toString() || 'unknown'; - + try { // Step 1: Check SandHub data if (!request?.blameFile?.sanHubId) { @@ -532,7 +535,7 @@ export class ExpertClaimService { } else if (severityValues.length > 0) { // Only fetch car prices if we have severity values to calculate const carPrices = await this.fetchCarPrices(); - + if (!carPrices || carPrices.length === 0) { this.logger.error( `[PriceDrop] Request ${requestId}: Failed to fetch car prices or car prices list is empty. Cannot find matching car.`, @@ -542,9 +545,9 @@ export class ExpertClaimService { `[PriceDrop] Request ${requestId}: Fetched ${carPrices.length} car prices. Searching for: "${request.carDetail.carName}"`, ); - const closestCar = this.findClosestCar( - request.carDetail.carName, - carPrices, + const closestCar = this.findClosestCar( + request.carDetail.carName, + carPrices, ); if (!closestCar) { @@ -558,11 +561,11 @@ export class ExpertClaimService { } else { this.logger.debug( `[PriceDrop] Request ${requestId}: Found matching car "${closestCar.carName}" with marketPrice: ${closestCar.marketPrice}`, - ); - autoCalculatedData = { + ); + autoCalculatedData = { carPrice: closestCar.marketPrice, - carValue: severityValues, - }; + carValue: severityValues, + }; } } } @@ -600,10 +603,10 @@ export class ExpertClaimService { } // Step 6: Save to database - await this.claimRequestManagementDbService.findOneAndUpdate( - { _id: new Types.ObjectId(request._id) }, - { $set: { priceDrop: finalPriceDropData } }, - ); + await this.claimRequestManagementDbService.findOneAndUpdate( + { _id: new Types.ObjectId(request._id) }, + { $set: { priceDrop: finalPriceDropData } }, + ); this.logger.log( `[PriceDrop] Request ${requestId}: Price drop calculation completed and saved.`, @@ -671,7 +674,7 @@ export class ExpertClaimService { const severityValue = this.priceDropPart[originalPriceDropKey]?.[ - damagedPartInfo.severity + damagedPartInfo.severity ]; if (severityValue !== undefined) { @@ -968,13 +971,13 @@ export class ExpertClaimService { private isCurrentUserAllowed(request: any, currentUser: any): boolean { // Check if locked by current user and lock is still active - const isLockedByCurrentUser = + const isLockedByCurrentUser = String(request?.actorLocked?.actorId) === currentUser.sub && request.lockFile; - + if (!isLockedByCurrentUser) { return false; - } + } // Also check if lock has expired (unlockTime has passed) if (request.unlockTime) { @@ -986,7 +989,7 @@ export class ExpertClaimService { return true; } } - + return true; } @@ -994,7 +997,7 @@ export class ExpertClaimService { if (!request.lockFile || request.claimStatus !== ReqClaimStatus.ReviewRequest) { return false; } - + // Check if lock has expired if (request.unlockTime) { const unlockTime = new Date(request.unlockTime).getTime(); @@ -1004,15 +1007,15 @@ export class ExpertClaimService { return false; } } - + // If currentUser is provided, allow access if they are the one who locked it if (currentUser) { - const isLockedByCurrentUser = + const isLockedByCurrentUser = String(request?.actorLocked?.actorId) === currentUser.sub; // Return false (not locked) if locked by current user, true if locked by someone else return !isLockedByCurrentUser; } - + // If no currentUser provided, treat as locked return true; } @@ -1035,18 +1038,18 @@ export class ExpertClaimService { ) { throw new ForbiddenException("Access denied to this request"); } - + // Check if lock has expired (unlockTime has passed) if (request?.unlockTime && !request?.objection) { const unlockTime = new Date(request.unlockTime).getTime(); const now = Date.now(); if (now >= unlockTime) { - throw new ForbiddenException("Your time has expired"); - } + throw new ForbiddenException("Your time has expired"); + } } else if (request?.unlockTime == null && !request?.objection) { throw new ForbiddenException("Your time has expired"); } - + if (!request.lockFile && !request?.objection) { throw new ForbiddenException( "For submit reply you must lock the request", @@ -1255,6 +1258,66 @@ export class ExpertClaimService { return request; } + async streamServiceV2(requestId, query, res, header) { + const request = await this.claimCaseDbService.findById(requestId); + console.log(request) + // const blameCase = await this.blameCaseDbService + return request; + // let videoPath: string = ""; + // switch (query) { + // case "accident": + // videoPath = await this.getAccidentVideoPath( + // String( + // request?.blameFile?.firstPartyDetails?.firstPartyFile + // ?.firstPartyVideoId, + // ), + // ); + // break; + // case "car-capture": + // videoPath = await this.getCarCapture(requestId); + // break; + // default: + // throw new NotFoundException("Invalid query type"); + // } + // if (!videoPath) throw new NotFoundException("video_not_found"); + + // const absolutePath = join(process.cwd(), videoPath.replace(/^\/+/, "")); + + // if (!existsSync(absolutePath)) { + // throw new NotFoundException( + // `Video file not found at path: ${absolutePath}`, + // ); + // } + + // const { size } = statSync(absolutePath); + // const range = header.range; + + // if (range) { + // const [startStr, endStr] = range.replace(/bytes=/, "").split("-"); + // const start = parseInt(startStr, 10); + // const end = endStr ? parseInt(endStr, 10) : size - 1; + // const chunkSize = end - start + 1; + + // const file = createReadStream(absolutePath, { start, end }); + + // res.writeHead(206, { + // "Content-Range": `bytes ${start}-${end}/${size}`, + // "Accept-Ranges": "bytes", + // "Content-Length": chunkSize, + // "Content-Type": "video/mp4", + // }); + + // file.pipe(res); + // } else { + // res.writeHead(200, { + // "Content-Length": size, + // "Content-Type": "video/mp4", + // }); + + // createReadStream(absolutePath).pipe(res); + // } + } + async streamService(requestId, query, res, header) { const request = await this.claimRequestManagementDbService.findOne(requestId); @@ -1635,6 +1698,18 @@ export class ExpertClaimService { }; } + async submitResendDocsV2( + claimRequestId: string, + reply: ClaimSubmitResendV2Dto, + actor: any, + ) { + try { + + } catch (err) { + console.log(err); + } + } + /** * V2: Submit damage expert reply for a claim. * @@ -1839,16 +1914,16 @@ export class ExpertClaimService { locked: c.workflow?.locked || false, lockedBy: c.workflow?.lockedBy ? { - actorId: c.workflow.lockedBy.actorId?.toString(), - actorName: c.workflow.lockedBy.actorName, - } + actorId: c.workflow.lockedBy.actorId?.toString(), + actorName: c.workflow.lockedBy.actorName, + } : undefined, vehicle: c.vehicle ? { - carName: c.vehicle.carName, - carModel: c.vehicle.carModel, - carType: c.vehicle.carType, - } + carName: c.vehicle.carName, + carModel: c.vehicle.carModel, + carType: c.vehicle.carType, + } : undefined, createdAt: c.createdAt, })) as ClaimListItemV2Dto[]; @@ -1944,16 +2019,16 @@ export class ExpertClaimService { locked: claim.workflow?.locked || false, lockedBy: claim.workflow?.lockedBy ? { - actorId: claim.workflow.lockedBy.actorId?.toString(), - actorName: claim.workflow.lockedBy.actorName, - lockedAt: (claim.workflow as any).lockedAt?.toISOString(), - } + actorId: claim.workflow.lockedBy.actorId?.toString(), + actorName: claim.workflow.lockedBy.actorName, + lockedAt: (claim.workflow as any).lockedAt?.toISOString(), + } : undefined, owner: claim.owner ? { - userId: claim.owner.userId?.toString(), - fullName: claim.owner.fullName, - } + userId: claim.owner.userId?.toString(), + fullName: claim.owner.fullName, + } : undefined, vehicle: claim.vehicle, blameRequestId: claim.blameRequestId?.toString(), diff --git a/src/expert-claim/expert-claim.v2.controller.ts b/src/expert-claim/expert-claim.v2.controller.ts index cbc6c57..2dad04c 100644 --- a/src/expert-claim/expert-claim.v2.controller.ts +++ b/src/expert-claim/expert-claim.v2.controller.ts @@ -1,10 +1,11 @@ -import { Body, Controller, Get, Param, Patch, Put, UseGuards } from "@nestjs/common"; +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"; @@ -14,7 +15,7 @@ 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 { SubmitExpertReplyV2Dto } from "./dto/expert-claim-v2.dto"; +import { ClaimSubmitResendV2Dto, SubmitExpertReplyV2Dto } from "./dto/expert-claim-v2.dto"; import { UpdateClaimDamagedPartsV2Dto } from "./dto/update-claim-damaged-parts-v2.dto"; class InPersonVisitV2Dto { @@ -30,7 +31,7 @@ class InPersonVisitV2Dto { @UseGuards(LocalActorAuthGuard, RolesGuard) @Roles(RoleEnum.DAMAGE_EXPERT) export class ExpertClaimV2Controller { - constructor(private readonly expertClaimService: ExpertClaimService) {} + constructor(private readonly expertClaimService: ExpertClaimService) { } @Get("requests") @ApiOperation({ @@ -89,6 +90,22 @@ export class ExpertClaimV2Controller { 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 (status EXPERT_REVIEWING). Submitting unlocks the claim. If any part has factorNeeded=true the claim moves to EXPERT_COST_EVALUATION; otherwise moves to INSURER_REVIEW. Total payment cap is 30,000,000.", + }) + @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", @@ -128,4 +145,21 @@ export class ExpertClaimV2Controller { 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); + } }