1
0
forked from Yara724/api

Added registrar mirrored

This commit is contained in:
SepehrYahyaee
2026-06-15 15:58:31 +03:30
parent 41f81a2f76
commit a7849f915a
13 changed files with 1089 additions and 61 deletions

View File

@@ -150,60 +150,6 @@ export class ExpertInitiatedBlameMirrorController {
);
}
// @Post("send-party-otps/:requestId")
// @ApiParam({ name: "requestId" })
// @ApiBody({ type: SendPartyOtpsDto })
// @ApiOperation({
// summary: "[Expert mirror] Send OTP to party/parties (IN_PERSON)",
// description:
// "Bind the parties' user accounts before filling their data. Required so guilt and the claim owner can be resolved.",
// })
// async sendPartyOtps(
// @CurrentUser() expert: any,
// @Param("requestId") requestId: string,
// @Body() dto: SendPartyOtpsDto,
// ) {
// return this.requestManagementService.sendPartyOtpsV2(expert, requestId, dto);
// }
// @Post("verify-party-otps/:requestId")
// @ApiParam({ name: "requestId" })
// @ApiBody({ type: VerifyPartyOtpsDto })
// @ApiOperation({ summary: "[Expert mirror] Verify party OTPs (IN_PERSON)" })
// async verifyPartyOtps(
// @CurrentUser() expert: any,
// @Param("requestId") requestId: string,
// @Body() dto: VerifyPartyOtpsDto,
// ) {
// return this.requestManagementService.verifyPartyOtpsV2(
// expert,
// requestId,
// dto,
// );
// }
// @Post("/blame-confession/:requestId")
// @ApiParam({ name: "requestId" })
// @ApiBody({ type: BlameConfessionDtoV2 })
// @ApiOperation({
// summary: "[Expert mirror] First-party blame confession",
// description:
// "THIRD_PARTY step. The FIRST party is the guilty party, so send imGuilty=true.",
// })
// async blameConfession(
// @Param("requestId") requestId: string,
// @Body() body: BlameConfessionDtoV2,
// @CurrentUser() expert: any,
// @Body("partyRole") partyRole?: string,
// ) {
// return this.requestManagementService.blameConfessionV2(
// requestId,
// body,
// expert,
// partyRole,
// );
// }
@Post("/car-body-form/:requestId")
@ApiParam({ name: "requestId" })
@ApiBody({ type: CarBodyFormDto })

View 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,
);
}
}

View File

@@ -41,6 +41,7 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
import { ExpertInitiatedV2Controller } from "./expert-initiated.v2.controller";
import { ExpertInitiatedBlameMirrorController } from "./expert-initiated-blame.mirror.controller";
import { RegistrarInitiatedController } from "./registrar-initiated.controller";
import { RegistrarBlameMirrorController } from "./registrar-blame.mirror.controller";
@Module({
imports: [
@@ -75,6 +76,7 @@ import { RegistrarInitiatedController } from "./registrar-initiated.controller";
ExpertInitiatedV2Controller,
ExpertInitiatedBlameMirrorController,
RegistrarInitiatedController,
RegistrarBlameMirrorController,
],
providers: [
RequestManagementService,

View File

@@ -1424,7 +1424,7 @@ export class RequestManagementService {
status: req.status,
};
} catch (error) {
console.log(error);
throw error;
}
}