forked from Yara724/api
Merge pull request 'Added sign endpoint for claim' (#48) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#48
This commit is contained in:
@@ -5131,6 +5131,152 @@ export class ClaimRequestManagementService {
|
|||||||
return userRating;
|
return userRating;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Claim owner signs final priced expert reply (single-party), same intent as blame
|
||||||
|
* `PUT …/sign/:requestId` — only when case status is WAITING_FOR_INSURER_APPROVAL and
|
||||||
|
* workflow is at INSURER_REVIEW (not factor-validation queue at EXPERT_COST_EVALUATION).
|
||||||
|
*/
|
||||||
|
async submitOwnerInsurerApprovalSignV2(
|
||||||
|
claimRequestId: string,
|
||||||
|
agree: boolean,
|
||||||
|
signFile: Express.Multer.File,
|
||||||
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string; fullName?: string },
|
||||||
|
): Promise<{
|
||||||
|
message: string;
|
||||||
|
claimRequestId: string;
|
||||||
|
status: ClaimCaseStatus;
|
||||||
|
claimStatus: ClaimStatus;
|
||||||
|
currentStep?: ClaimWorkflowStep;
|
||||||
|
accepted: boolean;
|
||||||
|
}> {
|
||||||
|
if (!Types.ObjectId.isValid(claimRequestId)) {
|
||||||
|
throw new BadRequestException("Invalid claim request id");
|
||||||
|
}
|
||||||
|
if (!signFile) {
|
||||||
|
throw new BadRequestException("A signature file is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException("Claim request not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||||
|
claimCase,
|
||||||
|
currentUserId,
|
||||||
|
actor?.role,
|
||||||
|
);
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Only the claim owner (damaged party) can sign at this step.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claimCase.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Claim is not waiting for your approval signature. Current status: ${String(claimCase.status)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.INSURER_REVIEW) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Signing is only allowed at workflow step ${ClaimWorkflowStep.INSURER_REVIEW}. Current: ${String(claimCase.workflow?.currentStep ?? "")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (claimCase.claimStatus !== ClaimStatus.APPROVED) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Claim must be insurer-approved before owner signature. Current claimStatus: ${String(claimCase.claimStatus)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claimCase.evaluation?.ownerInsurerApproval?.signedAt) {
|
||||||
|
throw new ConflictException("You have already submitted a signature for this claim.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeReply =
|
||||||
|
claimCase.evaluation?.damageExpertReplyFinal ||
|
||||||
|
claimCase.evaluation?.damageExpertReply;
|
||||||
|
if (!activeReply?.submittedAt) {
|
||||||
|
throw new BadRequestException("No expert reply is available to sign off on yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const signDoc = await this.claimSignDbService.create({
|
||||||
|
fileName: signFile.filename,
|
||||||
|
userId: effectiveUserId,
|
||||||
|
path: signFile.path,
|
||||||
|
requestId: new Types.ObjectId(claimRequestId),
|
||||||
|
} as any);
|
||||||
|
const signId = new Types.ObjectId(String((signDoc as any)._id));
|
||||||
|
const signedAt = new Date();
|
||||||
|
const approvalPayload = {
|
||||||
|
agree,
|
||||||
|
signDetailId: signId,
|
||||||
|
signedAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const historyActor = {
|
||||||
|
actorId: new Types.ObjectId(effectiveUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || "User",
|
||||||
|
actorType: "user",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!agree) {
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
|
$set: {
|
||||||
|
status: ClaimCaseStatus.REJECTED,
|
||||||
|
claimStatus: ClaimStatus.REJECTED,
|
||||||
|
"evaluation.ownerInsurerApproval": approvalPayload,
|
||||||
|
"workflow.nextStep": null,
|
||||||
|
},
|
||||||
|
$push: {
|
||||||
|
history: {
|
||||||
|
type: "OWNER_REJECTED_INSURER_APPROVAL_PRICING",
|
||||||
|
actor: historyActor,
|
||||||
|
timestamp: signedAt,
|
||||||
|
metadata: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
"Your rejection has been recorded. This claim is marked rejected and will not proceed.",
|
||||||
|
claimRequestId,
|
||||||
|
status: ClaimCaseStatus.REJECTED,
|
||||||
|
claimStatus: ClaimStatus.REJECTED,
|
||||||
|
currentStep: claimCase.workflow?.currentStep,
|
||||||
|
accepted: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
|
$set: {
|
||||||
|
status: ClaimCaseStatus.COMPLETED,
|
||||||
|
claimStatus: ClaimStatus.APPROVED,
|
||||||
|
"evaluation.ownerInsurerApproval": approvalPayload,
|
||||||
|
"workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||||
|
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||||
|
},
|
||||||
|
$push: {
|
||||||
|
"workflow.completedSteps": ClaimWorkflowStep.INSURER_REVIEW,
|
||||||
|
history: {
|
||||||
|
type: "OWNER_SIGNED_INSURER_APPROVAL",
|
||||||
|
actor: historyActor,
|
||||||
|
timestamp: signedAt,
|
||||||
|
metadata: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Your signature has been recorded. The claim is completed.",
|
||||||
|
claimRequestId,
|
||||||
|
status: ClaimCaseStatus.COMPLETED,
|
||||||
|
claimStatus: ClaimStatus.APPROVED,
|
||||||
|
currentStep: ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||||
|
accepted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2 API: Get list of claims for current user (or for FIELD_EXPERT: claims from their expert-initiated IN_PERSON blame files).
|
* V2 API: Get list of claims for current user (or for FIELD_EXPERT: claims from their expert-initiated IN_PERSON blame files).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
HttpException,
|
HttpException,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
|
BadRequestException,
|
||||||
Param,
|
Param,
|
||||||
Query,
|
Query,
|
||||||
Post,
|
Post,
|
||||||
@@ -18,6 +19,7 @@ import { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, A
|
|||||||
import { FileInterceptor } from "@nestjs/platform-express";
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
import { diskStorage } from "multer";
|
import { diskStorage } from "multer";
|
||||||
import { extname } from "path";
|
import { extname } from "path";
|
||||||
|
import { Types } from "mongoose";
|
||||||
import { GlobalGuard } from "src/auth/guards/global.guard";
|
import { GlobalGuard } from "src/auth/guards/global.guard";
|
||||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
import { Roles } from "src/decorators/roles.decorator";
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
@@ -230,6 +232,83 @@ export class ClaimRequestManagementV2Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Owner signs acceptance of final expert pricing (WAITING_FOR_INSURER_APPROVAL @ INSURER_REVIEW).
|
||||||
|
*/
|
||||||
|
@Put("request/:claimRequestId/owner-insurer-approval/sign")
|
||||||
|
@ApiParam({
|
||||||
|
name: "claimRequestId",
|
||||||
|
description: "Claim case ID (MongoDB ObjectId)",
|
||||||
|
example: "507f1f77bcf86cd799439011",
|
||||||
|
})
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Sign final claim pricing (owner)",
|
||||||
|
description:
|
||||||
|
"Multipart: `sign` (signature image) and `agree` (boolean). Allowed only when `status` is WAITING_FOR_INSURER_APPROVAL, " +
|
||||||
|
"`workflow.currentStep` is INSURER_REVIEW (not EXPERT_COST_EVALUATION), and `claimStatus` is APPROVED. " +
|
||||||
|
"If `agree` is true, the case moves to COMPLETED; if false, to REJECTED.",
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
description: "Signature file and agreement",
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["sign", "agree"],
|
||||||
|
properties: {
|
||||||
|
sign: { type: "string", format: "binary", description: "Signature image" },
|
||||||
|
agree: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "true to accept expert pricing and complete the claim",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "Signature stored; claim completed or rejected" })
|
||||||
|
@ApiResponse({ status: 400, description: "Wrong step/status or missing file" })
|
||||||
|
@ApiResponse({ status: 403, description: "Not the claim owner" })
|
||||||
|
@ApiResponse({ status: 404, description: "Claim not found" })
|
||||||
|
@ApiResponse({ status: 409, description: "Already signed" })
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("sign", {
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/claim-sign",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
const base = file.originalname.split(/[.,\s-]/)[0] || "sign";
|
||||||
|
callback(null, `${base}-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
async submitOwnerInsurerApprovalSignV2(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body("agree") agree: string | boolean,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
@UploadedFile() sign: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
if (!Types.ObjectId.isValid(claimRequestId)) {
|
||||||
|
throw new BadRequestException("Invalid claim request id");
|
||||||
|
}
|
||||||
|
const agreed =
|
||||||
|
typeof agree === "string" ? agree === "true" || agree === "1" : Boolean(agree);
|
||||||
|
try {
|
||||||
|
return await this.claimRequestManagementService.submitOwnerInsurerApprovalSignV2(
|
||||||
|
claimRequestId,
|
||||||
|
agreed,
|
||||||
|
sign,
|
||||||
|
user.sub,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to submit signature",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Post("create-from-blame/:blameRequestId")
|
@Post("create-from-blame/:blameRequestId")
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "blameRequestId",
|
name: "blameRequestId",
|
||||||
|
|||||||
@@ -76,6 +76,21 @@ export class ClaimUserComment {
|
|||||||
export const ClaimUserCommentSchema =
|
export const ClaimUserCommentSchema =
|
||||||
SchemaFactory.createForClass(ClaimUserComment);
|
SchemaFactory.createForClass(ClaimUserComment);
|
||||||
|
|
||||||
|
/** Owner acceptance + signature after expert pricing (WAITING_FOR_INSURER_APPROVAL @ INSURER_REVIEW). */
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimOwnerInsurerApproval {
|
||||||
|
@Prop({ type: Boolean, required: true })
|
||||||
|
agree: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
signDetailId?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: Date, default: () => new Date() })
|
||||||
|
signedAt?: Date;
|
||||||
|
}
|
||||||
|
export const ClaimOwnerInsurerApprovalSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimOwnerInsurerApproval);
|
||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
export class ClaimExpertReply {
|
export class ClaimExpertReply {
|
||||||
@Prop({ type: String })
|
@Prop({ type: String })
|
||||||
@@ -254,6 +269,9 @@ export class ClaimEvaluation {
|
|||||||
@Prop({ type: ClaimFileRatingSchema })
|
@Prop({ type: ClaimFileRatingSchema })
|
||||||
rating?: ClaimFileRating;
|
rating?: ClaimFileRating;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimOwnerInsurerApprovalSchema })
|
||||||
|
ownerInsurerApproval?: ClaimOwnerInsurerApproval;
|
||||||
|
|
||||||
@Prop({ type: String })
|
@Prop({ type: String })
|
||||||
visitLocation?: string;
|
visitLocation?: string;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user