forked from Yara724/api
Added registrar + fixed sign bug in car-body flow
This commit is contained in:
10
src/request-management/dto/registrar-initiated.dto.ts
Normal file
10
src/request-management/dto/registrar-initiated.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsIn, IsString } from "class-validator";
|
||||
|
||||
export class CreateRegistrarInitiatedFileDto {
|
||||
@ApiProperty({ enum: ["THIRD_PARTY", "CAR_BODY"], example: "THIRD_PARTY" })
|
||||
@IsString()
|
||||
@IsIn(["THIRD_PARTY", "CAR_BODY"])
|
||||
type: "THIRD_PARTY" | "CAR_BODY";
|
||||
}
|
||||
|
||||
@@ -81,6 +81,14 @@ export class BlameRequest {
|
||||
@Prop({ type: Types.ObjectId })
|
||||
initiatedByFieldExpertId?: Types.ObjectId;
|
||||
|
||||
/** True when this blame was created by a registrar. */
|
||||
@Prop({ default: false })
|
||||
registrarInitiated?: boolean;
|
||||
|
||||
/** Registrar who created this file (for registrar-initiated flows). */
|
||||
@Prop({ type: Types.ObjectId })
|
||||
initiatedByRegistrarId?: Types.ObjectId;
|
||||
|
||||
/** LINK = expert sends link to user(s); IN_PERSON = expert fills form on-site. */
|
||||
@Prop({ type: String, enum: CreationMethod })
|
||||
creationMethod?: CreationMethod;
|
||||
|
||||
@@ -282,6 +282,7 @@ export enum CreationMethod {
|
||||
export enum FilledBy {
|
||||
CUSTOMER = "customer",
|
||||
EXPERT = "expert",
|
||||
REGISTRAR = "registrar",
|
||||
}
|
||||
|
||||
export class ExpertLinkInfo {
|
||||
|
||||
155
src/request-management/registrar-initiated.controller.ts
Normal file
155
src/request-management/registrar-initiated.controller.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { extname } from "node:path";
|
||||
import { Body, Controller, Get, Param, Post, UploadedFile, UseGuards, UseInterceptors } from "@nestjs/common";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import { diskStorage } from "multer";
|
||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger";
|
||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||
import { CurrentUser } from "src/decorators/user.decorator";
|
||||
import { Roles } from "src/decorators/roles.decorator";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { RequestManagementService } from "./request-management.service";
|
||||
import { CreateRegistrarInitiatedFileDto } from "./dto/registrar-initiated.dto";
|
||||
import { SendPartyOtpsDto } from "./dto/send-party-otps.dto";
|
||||
import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
|
||||
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
||||
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||
|
||||
@ApiTags("registrar-initiated-blame (v1)")
|
||||
@Controller("registrar-initiated-blame")
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||
@Roles(RoleEnum.REGISTRAR)
|
||||
export class RegistrarInitiatedController {
|
||||
constructor(private readonly requestManagementService: RequestManagementService) {}
|
||||
|
||||
@Post("create")
|
||||
@ApiOperation({ summary: "Registrar creates IN_PERSON blame file" })
|
||||
@ApiBody({ type: CreateRegistrarInitiatedFileDto })
|
||||
create(@CurrentUser() registrar: any, @Body() dto: CreateRegistrarInitiatedFileDto) {
|
||||
return this.requestManagementService.createRegistrarInitiatedBlame(registrar, dto);
|
||||
}
|
||||
|
||||
@Get("my-files")
|
||||
myFiles(@CurrentUser() registrar: any) {
|
||||
return this.requestManagementService.getMyExpertInitiatedFilesV2(registrar);
|
||||
}
|
||||
|
||||
@Get("blame/:requestId")
|
||||
getBlame(@Param("requestId") requestId: string, @CurrentUser() registrar: any) {
|
||||
return this.requestManagementService.getBlameRequestV2(requestId, registrar);
|
||||
}
|
||||
|
||||
@Post("send-party-otps/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: SendPartyOtpsDto })
|
||||
sendPartyOtps(
|
||||
@CurrentUser() registrar: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() dto: SendPartyOtpsDto,
|
||||
) {
|
||||
return this.requestManagementService.sendPartyOtpsV2(registrar, requestId, dto);
|
||||
}
|
||||
|
||||
@Post("verify-party-otps/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: VerifyPartyOtpsDto })
|
||||
verifyPartyOtps(
|
||||
@CurrentUser() registrar: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() dto: VerifyPartyOtpsDto,
|
||||
) {
|
||||
return this.requestManagementService.verifyPartyOtpsV2(registrar, requestId, dto);
|
||||
}
|
||||
|
||||
@Post("complete-blame-data/:requestId")
|
||||
completeBlameData(
|
||||
@CurrentUser() registrar: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() body: any,
|
||||
) {
|
||||
return this.requestManagementService.expertCompleteBlameDataV2(registrar, requestId, body);
|
||||
}
|
||||
|
||||
@Post("upload-video/:requestId")
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@UseInterceptors(
|
||||
FileInterceptor("file", {
|
||||
limits: { fileSize: 20 * 1024 * 1024 },
|
||||
storage: diskStorage({
|
||||
destination: "./files/video",
|
||||
filename: (req, file, cb) => cb(null, `registrar-${Date.now()}${extname(file.originalname)}`),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
uploadVideo(
|
||||
@CurrentUser() registrar: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@UploadedFile() file?: Express.Multer.File,
|
||||
) {
|
||||
return this.requestManagementService.expertUploadVideoForBlameV2(registrar, requestId, file);
|
||||
}
|
||||
|
||||
@Post("upload-voice/:requestId")
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@UseInterceptors(
|
||||
FileInterceptor("voice", {
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
storage: diskStorage({
|
||||
destination: "./files/voice",
|
||||
filename: (req, file, cb) => cb(null, `registrar-${Date.now()}${extname(file.originalname)}`),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
uploadVoice(
|
||||
@CurrentUser() registrar: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@UploadedFile() voice?: Express.Multer.File,
|
||||
) {
|
||||
return this.requestManagementService.expertUploadVoiceForBlameV2(registrar, requestId, voice);
|
||||
}
|
||||
|
||||
@Post("add-accident-fields/:requestId")
|
||||
@ApiBody({ type: ExpertAccidentFieldsDto })
|
||||
addAccidentFields(
|
||||
@CurrentUser() registrar: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() fields: ExpertAccidentFieldsDto,
|
||||
) {
|
||||
return this.requestManagementService.expertAddAccidentFieldsForBlameV2(
|
||||
registrar,
|
||||
requestId,
|
||||
fields,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("upload-party-signature/:requestId")
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@UseInterceptors(
|
||||
FileInterceptor("sign", {
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
storage: diskStorage({
|
||||
destination: "./files/signs",
|
||||
filename: (req, file, cb) => cb(null, `registrar-party-${Date.now()}${extname(file.originalname)}`),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
uploadPartySignature(
|
||||
@CurrentUser() registrar: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() body: { partyRole?: string; isAccept?: boolean | string },
|
||||
@UploadedFile() sign?: Express.Multer.File,
|
||||
) {
|
||||
const role =
|
||||
body.partyRole === "SECOND" ? PartyRole.SECOND : PartyRole.FIRST;
|
||||
const isAccept = !(body.isAccept === false || body.isAccept === "false");
|
||||
return this.requestManagementService.expertUploadPartySignatureV2(
|
||||
registrar,
|
||||
requestId,
|
||||
role,
|
||||
isAccept,
|
||||
sign!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { RequestManagementController } from "./request-management.controller";
|
||||
import { RequestManagementV2Controller } from "./request-management.v2.controller";
|
||||
import { ExpertInitiatedController } from "./expert-initiated.controller";
|
||||
import { ExpertInitiatedV2Controller } from "./expert-initiated.v2.controller";
|
||||
import { RegistrarInitiatedController } from "./registrar-initiated.controller";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -69,6 +70,7 @@ import { ExpertInitiatedV2Controller } from "./expert-initiated.v2.controller";
|
||||
RequestManagementV2Controller,
|
||||
ExpertInitiatedController,
|
||||
ExpertInitiatedV2Controller,
|
||||
RegistrarInitiatedController,
|
||||
],
|
||||
providers: [
|
||||
RequestManagementService,
|
||||
|
||||
@@ -103,7 +103,7 @@ export class RequestManagementService {
|
||||
}
|
||||
|
||||
private async advanceWorkflowToNext(req: any, submittedStepKey: WorkflowStep) {
|
||||
// CAR_BODY: after first-party description there is no second party or expert; go to COMPLETED
|
||||
// CAR_BODY: after first-party description, move to signature step.
|
||||
if (
|
||||
req.type === BlameRequestType.CAR_BODY &&
|
||||
submittedStepKey === WorkflowStep.FIRST_DESCRIPTION
|
||||
@@ -113,11 +113,11 @@ export class RequestManagementService {
|
||||
: [];
|
||||
if (!completed.includes(submittedStepKey)) completed.push(submittedStepKey);
|
||||
req.workflow.completedSteps = completed;
|
||||
req.workflow.currentStep = WorkflowStep.COMPLETED;
|
||||
req.workflow.currentStep = WorkflowStep.WAITING_FOR_SIGNATURES;
|
||||
req.workflow.nextStep = undefined;
|
||||
req.status = CaseStatus.COMPLETED;
|
||||
req.status = CaseStatus.WAITING_FOR_SIGNATURES;
|
||||
this.logger.debug(
|
||||
"[WORKFLOW] CAR_BODY: advanced to COMPLETED after FIRST_DESCRIPTION",
|
||||
"[WORKFLOW] CAR_BODY: advanced to WAITING_FOR_SIGNATURES after FIRST_DESCRIPTION",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -3234,16 +3234,86 @@ export class RequestManagementService {
|
||||
* Verify the current user (field expert) can access this BlameRequest (v2).
|
||||
*/
|
||||
private verifyExpertAccessForBlameV2(req: any, expert: any): void {
|
||||
if (!req?.expertInitiated || !req?.initiatedByFieldExpertId) {
|
||||
throw new ForbiddenException(
|
||||
"This file is not expert-initiated or has no initiating expert",
|
||||
if (req?.expertInitiated && req?.initiatedByFieldExpertId) {
|
||||
if (String(req.initiatedByFieldExpertId) !== String(expert?.sub)) {
|
||||
throw new ForbiddenException(
|
||||
"You can only access files that you have initiated",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (req?.registrarInitiated && req?.initiatedByRegistrarId) {
|
||||
if (String(req.initiatedByRegistrarId) !== String(expert?.sub)) {
|
||||
throw new ForbiddenException(
|
||||
"You can only access files that you have initiated",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new ForbiddenException(
|
||||
"This file is not initiator-scoped or has no initiating actor",
|
||||
);
|
||||
}
|
||||
|
||||
async createRegistrarInitiatedBlame(
|
||||
registrar: any,
|
||||
dto: { type: "THIRD_PARTY" | "CAR_BODY" },
|
||||
): Promise<{ requestId: string; publicId: string }> {
|
||||
const registrarId = new Types.ObjectId(registrar.sub);
|
||||
const type =
|
||||
dto.type === "CAR_BODY"
|
||||
? BlameRequestType.CAR_BODY
|
||||
: BlameRequestType.THIRD_PARTY;
|
||||
const firstStep = await this.getWorkflowStep({ stepNumber: 1 });
|
||||
if (!firstStep?.stepKey) {
|
||||
throw new InternalServerErrorException(
|
||||
"Workflow stepNumber=1 not configured in step manager",
|
||||
);
|
||||
}
|
||||
if (String(req.initiatedByFieldExpertId) !== String(expert?.sub)) {
|
||||
throw new ForbiddenException(
|
||||
"You can only access files that you have initiated",
|
||||
const nextStepFromManager = firstStep.nextPossibleSteps?.[0];
|
||||
const nextStep =
|
||||
type === BlameRequestType.CAR_BODY
|
||||
? (WorkflowStep.CAR_BODY_ACCIDENT_TYPE as WorkflowStep)
|
||||
: (nextStepFromManager as WorkflowStep);
|
||||
if (!nextStep) {
|
||||
throw new InternalServerErrorException(
|
||||
"Workflow stepNumber=1 has no nextPossibleSteps configured",
|
||||
);
|
||||
}
|
||||
const publicId = await this.publicIdService.generateRequestPublicId();
|
||||
const parties: any[] = [{ role: PartyRole.FIRST, person: {} }];
|
||||
if (type === BlameRequestType.THIRD_PARTY) {
|
||||
parties.push({ role: PartyRole.SECOND, person: {} });
|
||||
}
|
||||
const created = await this.blameRequestDbService.create({
|
||||
publicId,
|
||||
requestNo: publicId,
|
||||
type,
|
||||
parties,
|
||||
workflow: {
|
||||
currentStep: firstStep.stepKey as WorkflowStep,
|
||||
nextStep,
|
||||
completedSteps: [firstStep.stepKey as WorkflowStep],
|
||||
locked: false,
|
||||
},
|
||||
history: [],
|
||||
registrarInitiated: true,
|
||||
initiatedByRegistrarId: registrarId,
|
||||
creationMethod: CreationMethod.IN_PERSON,
|
||||
filledBy: FilledBy.REGISTRAR,
|
||||
} as any);
|
||||
if (!Array.isArray((created as any).history)) (created as any).history = [];
|
||||
(created as any).history.push({
|
||||
type: "FILE_CREATED_BY_REGISTRAR",
|
||||
actor: {
|
||||
actorId: registrarId,
|
||||
actorName: `${registrar.firstName || ""} ${registrar.lastName || ""}`.trim(),
|
||||
actorType: "registrar",
|
||||
},
|
||||
metadata: { creationMethod: CreationMethod.IN_PERSON, type: dto.type },
|
||||
});
|
||||
await (created as any).save();
|
||||
return { requestId: String((created as any)._id), publicId: (created as any).publicId };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3596,7 +3666,10 @@ export class RequestManagementService {
|
||||
): Promise<{ sent: boolean; message: string }> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
if (
|
||||
(!req.expertInitiated && !req.registrarInitiated) ||
|
||||
req.creationMethod !== CreationMethod.IN_PERSON
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
@@ -3657,7 +3730,10 @@ export class RequestManagementService {
|
||||
): Promise<{ verified: boolean; message: string }> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
if (
|
||||
(!req.expertInitiated && !req.registrarInitiated) ||
|
||||
req.creationMethod !== CreationMethod.IN_PERSON
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
@@ -3712,7 +3788,7 @@ export class RequestManagementService {
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "field_expert",
|
||||
actorType: expert?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert",
|
||||
},
|
||||
metadata: {
|
||||
firstPartyVerified: true,
|
||||
@@ -3759,8 +3835,10 @@ export class RequestManagementService {
|
||||
async getMyExpertInitiatedFilesV2(expert: any): Promise<any[]> {
|
||||
const expertId = new Types.ObjectId(expert.sub);
|
||||
const files = await this.blameRequestDbService.find({
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: expertId,
|
||||
$or: [
|
||||
{ expertInitiated: true, initiatedByFieldExpertId: expertId },
|
||||
{ registrarInitiated: true, initiatedByRegistrarId: expertId },
|
||||
],
|
||||
});
|
||||
return (files || []).map((f: any) => ({
|
||||
_id: f._id,
|
||||
@@ -3790,16 +3868,20 @@ export class RequestManagementService {
|
||||
req.expertInitiated &&
|
||||
req.initiatedByFieldExpertId &&
|
||||
String(req.initiatedByFieldExpertId) === String(user?.sub);
|
||||
const isRegistrarOwner =
|
||||
req.registrarInitiated &&
|
||||
req.initiatedByRegistrarId &&
|
||||
String(req.initiatedByRegistrarId) === String(user?.sub);
|
||||
const isParty = Array.isArray(req.parties) && req.parties.some((p: any) => {
|
||||
const pid = p?.person?.userId ? String(p.person.userId) : null;
|
||||
const phone = p?.person?.phoneNumber;
|
||||
return (pid && pid === String(user?.sub)) || (phone && phone === user?.username);
|
||||
});
|
||||
if (!isFieldExpertOwner && !isParty) {
|
||||
if (!isFieldExpertOwner && !isRegistrarOwner && !isParty) {
|
||||
throw new ForbiddenException("You do not have access to this request");
|
||||
}
|
||||
// Optionally bind userId to party when user opens via LINK (phone match, no userId yet)
|
||||
if (!isFieldExpertOwner && user?.sub && Types.ObjectId.isValid(user.sub)) {
|
||||
if (!isFieldExpertOwner && !isRegistrarOwner && user?.sub && Types.ObjectId.isValid(user.sub)) {
|
||||
let updated = false;
|
||||
for (const p of req.parties || []) {
|
||||
if (
|
||||
@@ -3862,7 +3944,10 @@ export class RequestManagementService {
|
||||
if (!req) {
|
||||
throw new NotFoundException("Request not found");
|
||||
}
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
if (
|
||||
(!req.expertInitiated && !req.registrarInitiated) ||
|
||||
req.creationMethod !== CreationMethod.IN_PERSON
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated IN_PERSON BlameRequest files.",
|
||||
);
|
||||
@@ -3890,7 +3975,10 @@ export class RequestManagementService {
|
||||
): Promise<any> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
if (
|
||||
(!req.expertInitiated && !req.registrarInitiated) ||
|
||||
req.creationMethod !== CreationMethod.IN_PERSON
|
||||
) {
|
||||
throw new BadRequestException("Only expert-initiated IN_PERSON CAR_BODY files.");
|
||||
}
|
||||
if (req.type !== BlameRequestType.CAR_BODY) {
|
||||
@@ -4193,7 +4281,7 @@ export class RequestManagementService {
|
||||
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated) {
|
||||
if (!req.expertInitiated && !req.registrarInitiated) {
|
||||
throw new BadRequestException("This endpoint is only for expert-initiated files.");
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
Reference in New Issue
Block a user