forked from Yara724/api
Added registrar mirrored
This commit is contained in:
418
src/request-management/registrar-blame.mirror.controller.ts
Normal file
418
src/request-management/registrar-blame.mirror.controller.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import { extname } from "node:path";
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiConsumes,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import { diskStorage } from "multer";
|
||||
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 { MediaPolicyService } from "src/media-policy/media-policy.service";
|
||||
import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
||||
import {
|
||||
CarBodyFormDto,
|
||||
DescriptionDto,
|
||||
LocationDto,
|
||||
} from "./dto/create-request-management.dto";
|
||||
import { CreateRegistrarInitiatedFileDto } from "./dto/registrar-initiated.dto";
|
||||
import { SendPartyOtpDto, VerifyPartyOtpDto } from "./dto/party-otp.dto";
|
||||
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
||||
import { RequestManagementService } from "./request-management.service";
|
||||
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||
|
||||
/**
|
||||
* Registrar blame flow that mirrors the normal user blame API
|
||||
* (`v2/blame-request-management`) one-to-one, using only the IN_PERSON method.
|
||||
* The frontend can reuse the same pages by only swapping the route prefix:
|
||||
*
|
||||
* `v2/blame-request-management/*` -> `v2/registrar/blame-request-management/*`
|
||||
*
|
||||
* The registrar fills every step on behalf of both parties using the exact same
|
||||
* endpoints/bodies as a normal user. By contract the FIRST party registered is
|
||||
* always the guilty party. There is no LINK flow and no reviewing phase for the
|
||||
* registrar — the job is finished once the file is signed.
|
||||
*
|
||||
* Sequence: create -> send/verify OTP (first party) -> first party steps
|
||||
* (car-body-form or initial-form, upload-video, add-detail-location,
|
||||
* upload-voice, add-detail-description) -> add-second-party ->
|
||||
* send/verify OTP (second party) -> second party steps ->
|
||||
* add-accident-fields -> sign FIRST + sign SECOND -> COMPLETED.
|
||||
*/
|
||||
@ApiTags("registrar blame (mirror v2)")
|
||||
@Controller("v2/registrar/blame-request-management")
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||
@Roles(RoleEnum.REGISTRAR)
|
||||
export class RegistrarBlameMirrorController {
|
||||
constructor(
|
||||
private readonly requestManagementService: RequestManagementService,
|
||||
private readonly mediaPolicyService: MediaPolicyService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
summary: "[Registrar mirror] Create registrar-initiated blame file",
|
||||
description:
|
||||
"Always IN_PERSON. Creates a BlameRequest owned by the parties but filled by the registrar. FIRST party registered is always the guilty party.",
|
||||
})
|
||||
@ApiBody({ type: CreateRegistrarInitiatedFileDto })
|
||||
async create(
|
||||
@CurrentUser() registrar: any,
|
||||
@Body() dto: CreateRegistrarInitiatedFileDto,
|
||||
) {
|
||||
return this.requestManagementService.createRegistrarInitiatedBlame(
|
||||
registrar,
|
||||
dto,
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: "[Registrar mirror] List my registrar-initiated blame files",
|
||||
})
|
||||
async list(@CurrentUser() registrar: any) {
|
||||
return this.requestManagementService.getMyExpertInitiatedFilesV2(registrar);
|
||||
}
|
||||
|
||||
@Post("send-party-otp/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: SendPartyOtpDto })
|
||||
@ApiOperation({
|
||||
summary: "[Registrar mirror] Send OTP to one party (sequential IN_PERSON)",
|
||||
description:
|
||||
"Sends an OTP SMS to a single party's phone number. Flow: send to first party → verify → fill data → send to second party → verify → continue. No invite link is sent.",
|
||||
})
|
||||
async sendPartyOtp(
|
||||
@CurrentUser() registrar: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() dto: SendPartyOtpDto,
|
||||
) {
|
||||
return this.requestManagementService.sendPartyOtpV2(
|
||||
registrar,
|
||||
requestId,
|
||||
dto,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("verify-party-otp/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: VerifyPartyOtpDto })
|
||||
@ApiOperation({
|
||||
summary: "[Registrar mirror] Verify one party's OTP (sequential IN_PERSON)",
|
||||
description:
|
||||
"Verifies a single party's OTP and binds their account. `partyRole` is optional (inferred as FIRST until the first party is bound, then SECOND).",
|
||||
})
|
||||
async verifyPartyOtp(
|
||||
@CurrentUser() registrar: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() dto: VerifyPartyOtpDto,
|
||||
) {
|
||||
return this.requestManagementService.verifyPartyOtpV2(
|
||||
registrar,
|
||||
requestId,
|
||||
dto,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("/car-body-form/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: CarBodyFormDto })
|
||||
@ApiOperation({ summary: "[Registrar mirror] CAR_BODY accident type form" })
|
||||
async carBodyForm(
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() body: CarBodyFormDto,
|
||||
@CurrentUser() registrar: any,
|
||||
@Body("partyRole") partyRole?: string,
|
||||
) {
|
||||
return this.requestManagementService.carBodyAccidentTypeFormV2(
|
||||
requestId,
|
||||
body,
|
||||
registrar,
|
||||
partyRole,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("/initial-form/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: AddPlateDto })
|
||||
@ApiOperation({
|
||||
summary: "[Registrar mirror] Initial form (plate/insurance) for current party",
|
||||
description:
|
||||
"Fills the FIRST or SECOND party plate/insurance/vehicle data depending on the current workflow step.",
|
||||
})
|
||||
async initialForm(
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() body: AddPlateDto,
|
||||
@CurrentUser() registrar: any,
|
||||
@Body("partyRole") partyRole?: string,
|
||||
) {
|
||||
return this.requestManagementService.initialFormV2(
|
||||
requestId,
|
||||
body,
|
||||
registrar,
|
||||
partyRole,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: { file: { type: "string", format: "binary" } },
|
||||
},
|
||||
})
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@UseInterceptors(
|
||||
FileInterceptor("file", {
|
||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||
storage: diskStorage({
|
||||
destination: "./files/video",
|
||||
filename: (req, file, callback) => {
|
||||
const unique = Date.now();
|
||||
const ex = extname(file.originalname);
|
||||
callback(null, `registrar-${file.originalname}-${unique}${ex}`);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
@ApiParam({ name: "requestId" })
|
||||
@Post("upload-video/:requestId")
|
||||
@ApiOperation({ summary: "[Registrar mirror] Upload first-party video" })
|
||||
async uploadVideo(
|
||||
@Param("requestId") requestId: string,
|
||||
@CurrentUser() registrar: any,
|
||||
@UploadedFile() file?: Express.Multer.File,
|
||||
) {
|
||||
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
|
||||
return this.requestManagementService.uploadFirstPartyVideoV2(
|
||||
requestId,
|
||||
file,
|
||||
registrar,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("/add-detail-location/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: LocationDto })
|
||||
@ApiOperation({ summary: "[Registrar mirror] Add location for current party" })
|
||||
async addLocation(
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() body: LocationDto,
|
||||
@CurrentUser() registrar: any,
|
||||
@Body("partyRole") partyRole?: string,
|
||||
) {
|
||||
return this.requestManagementService.addDetailLocationV2(
|
||||
requestId,
|
||||
body,
|
||||
registrar,
|
||||
partyRole,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
file: { type: "string", format: "binary" },
|
||||
partyRole: {
|
||||
type: "string",
|
||||
enum: ["FIRST", "SECOND"],
|
||||
description:
|
||||
"Optional explicit party selector; must match the party currently collecting voice.",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@UseInterceptors(
|
||||
FileInterceptor("file", {
|
||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||
storage: diskStorage({
|
||||
destination: "./files/voice",
|
||||
filename: (req, file, callback) => {
|
||||
const unique = Date.now();
|
||||
const ex = extname(file.originalname);
|
||||
const flname = file.originalname.split(".")[0];
|
||||
callback(null, `registrar-${flname}-${unique}${ex}`);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
@ApiParam({ name: "requestId" })
|
||||
@Post("upload-voice/:requestId")
|
||||
@ApiOperation({ summary: "[Registrar mirror] Upload voice for current party" })
|
||||
async uploadVoice(
|
||||
@Param("requestId") requestId: string,
|
||||
@CurrentUser() registrar: any,
|
||||
@UploadedFile() voice?: Express.Multer.File,
|
||||
@Body("partyRole") partyRole?: string,
|
||||
) {
|
||||
await this.mediaPolicyService.assertForBlame(voice, requestId, "voice");
|
||||
return this.requestManagementService.uploadVoiceV2(
|
||||
requestId,
|
||||
voice,
|
||||
registrar,
|
||||
partyRole,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("/add-detail-description/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: DescriptionDto })
|
||||
@ApiOperation({
|
||||
summary: "[Registrar mirror] Add description for current party",
|
||||
description:
|
||||
"For THIRD_PARTY, submitting the SECOND party description finalizes guilt (FIRST party = guilty) and moves the file straight to WAITING_FOR_SIGNATURES.",
|
||||
})
|
||||
async addDescription(
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() body: DescriptionDto,
|
||||
@CurrentUser() registrar: any,
|
||||
@Body("partyRole") partyRole?: string,
|
||||
) {
|
||||
return this.requestManagementService.addDescriptionV2(
|
||||
requestId,
|
||||
body,
|
||||
registrar,
|
||||
partyRole,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("add-second-party/:phoneNumber/:requestId/")
|
||||
@ApiParam({ name: "phoneNumber" })
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiOperation({
|
||||
summary: "[Registrar mirror] Advance to second party (sends OTP, no link)",
|
||||
description:
|
||||
"Sends an OTP SMS to the second party's phone number and updates the workflow to await verification. No invite link is sent. Use verify-party-otp next.",
|
||||
})
|
||||
async addSecondParty(
|
||||
@Param("phoneNumber") phoneNumber: string,
|
||||
@Param("requestId") requestId: string,
|
||||
@CurrentUser() registrar: any,
|
||||
) {
|
||||
return this.requestManagementService.expertAdvanceToSecondPartyV2(
|
||||
requestId,
|
||||
registrar,
|
||||
phoneNumber,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("add-accident-fields/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: ExpertAccidentFieldsDto })
|
||||
@ApiOperation({
|
||||
summary: "[Registrar mirror] Add accident fields and advance to signatures",
|
||||
description:
|
||||
"Saves accidentWay, accidentReason, and accidentType. " +
|
||||
"The first party is always guilty, so this also advances the workflow " +
|
||||
"from WAITING_FOR_GUILT_DECISION straight to WAITING_FOR_SIGNATURES. " +
|
||||
"Must be called before sign.",
|
||||
})
|
||||
async addAccidentFields(
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() fields: ExpertAccidentFieldsDto,
|
||||
@CurrentUser() registrar: any,
|
||||
) {
|
||||
return this.requestManagementService.expertAddAccidentFieldsForBlameV2(
|
||||
registrar,
|
||||
requestId,
|
||||
fields,
|
||||
);
|
||||
}
|
||||
|
||||
@Put("sign/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
summary: "[Registrar mirror] Upload a party's on-site signature",
|
||||
description:
|
||||
"Collect each party's signature on site. CAR_BODY: partyRole=FIRST once. THIRD_PARTY: upload FIRST then SECOND. When all required parties have signed (accepted), the blame case completes.",
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["partyRole", "sign"],
|
||||
properties: {
|
||||
partyRole: { type: "string", enum: ["FIRST", "SECOND"] },
|
||||
isAccept: { type: "boolean", default: true },
|
||||
sign: { type: "string", format: "binary" },
|
||||
},
|
||||
},
|
||||
})
|
||||
@UseInterceptors(
|
||||
FileInterceptor("sign", {
|
||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||
storage: diskStorage({
|
||||
destination: "./files/signs",
|
||||
filename: (req, file, callback) => {
|
||||
const unique = Date.now();
|
||||
const ext = extname(file.originalname);
|
||||
callback(null, `registrar-party-${unique}${ext}`);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
async sign(
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() body: { partyRole?: string; isAccept?: string | boolean },
|
||||
@CurrentUser() registrar: any,
|
||||
@UploadedFile() sign: Express.Multer.File,
|
||||
) {
|
||||
// Guard: accident fields must be filled before signing to prevent the
|
||||
// double-sign bug where add-accident-fields re-enters WAITING_FOR_SIGNATURES.
|
||||
const requestData = await this.requestManagementService.getBlameRequestV2(
|
||||
requestId,
|
||||
registrar,
|
||||
);
|
||||
if (!(requestData?.expert?.decision as any)?.fields?.accidentWay) {
|
||||
throw new BadRequestException(
|
||||
"Accident fields (accidentWay, accidentReason, accidentType) must be submitted via add-accident-fields before signing.",
|
||||
);
|
||||
}
|
||||
|
||||
await this.mediaPolicyService.assertForBlame(sign, requestId, "image");
|
||||
const partyRole =
|
||||
body?.partyRole === "SECOND" ? PartyRole.SECOND : PartyRole.FIRST;
|
||||
const isAccept = !(body?.isAccept === false || body?.isAccept === "false");
|
||||
return this.requestManagementService.expertUploadPartySignatureV2(
|
||||
registrar,
|
||||
requestId,
|
||||
partyRole,
|
||||
isAccept,
|
||||
sign,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(":requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiOperation({ summary: "[Registrar mirror] Get one blame request" })
|
||||
async getOne(
|
||||
@Param("requestId") requestId: string,
|
||||
@CurrentUser() registrar: any,
|
||||
) {
|
||||
return this.requestManagementService.getBlameRequestV2(
|
||||
requestId,
|
||||
registrar,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user