forked from Yara724/api
401 lines
13 KiB
TypeScript
401 lines
13 KiB
TypeScript
import { extname } from "node:path";
|
|
import {
|
|
Body,
|
|
Controller,
|
|
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 {
|
|
BlameConfessionDtoV2,
|
|
CarBodyFormDto,
|
|
DescriptionDto,
|
|
LocationDto,
|
|
} from "./dto/create-request-management.dto";
|
|
import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto";
|
|
import { CreationMethod } from "./entities/schema/request-management.schema";
|
|
import { SendPartyOtpsDto } from "./dto/send-party-otps.dto";
|
|
import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
|
|
import { SendPartyOtpDto, VerifyPartyOtpDto } from "./dto/party-otp.dto";
|
|
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
|
import { SendExpertInitiatedLinkV2Dto } from "./dto/send-expert-initiated-link.v2.dto";
|
|
import { RequestManagementService } from "./request-management.service";
|
|
import { PartyRole } from "./entities/schema/partyRole.enum";
|
|
|
|
/**
|
|
* Expert-initiated IN_PERSON blame flow that mirrors the normal user blame API
|
|
* (`v2/blame-request-management`) one-to-one, so the frontend can reuse the same
|
|
* pages by only swapping the route prefix:
|
|
*
|
|
* `v2/blame-request-management/*` -> `v2/expert-initiated/blame-request-management/*`
|
|
*
|
|
* The field expert is on the accident scene and fills every step on behalf of
|
|
* both parties using the exact same endpoints/bodies as a normal user. By
|
|
* contract the FIRST party the expert registers is always the guilty party, and
|
|
* the blame is finalized (guilt decided + ready for signatures) without any
|
|
* waiting-for-expert phase or invite links.
|
|
*
|
|
* Sequence: create -> send/verify party OTPs -> first party steps
|
|
* (blame-confession, initial-form, upload-video, add-detail-location,
|
|
* upload-voice, add-detail-description) -> add-second-party (no link) ->
|
|
* second party steps -> sign FIRST + sign SECOND -> COMPLETED.
|
|
*/
|
|
@ApiTags("expert-initiated blame (mirror v2)")
|
|
@Controller("v2/expert-initiated/blame-request-management")
|
|
@ApiBearerAuth()
|
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
|
@Roles(RoleEnum.FIELD_EXPERT)
|
|
export class ExpertInitiatedBlameMirrorController {
|
|
constructor(
|
|
private readonly requestManagementService: RequestManagementService,
|
|
private readonly mediaPolicyService: MediaPolicyService,
|
|
) {}
|
|
|
|
@Post()
|
|
@ApiOperation({
|
|
summary: "[Expert mirror] Create expert-initiated blame file",
|
|
description:
|
|
"Creates an IN_PERSON blame file filled by the expert on behalf of both parties. creationMethod is always forced to IN_PERSON regardless of what is sent.",
|
|
})
|
|
@ApiBody({ type: CreateExpertInitiatedFileDto })
|
|
async create(
|
|
@CurrentUser() expert: any,
|
|
@Body() dto: CreateExpertInitiatedFileDto,
|
|
) {
|
|
return this.requestManagementService.createExpertInitiatedBlameV2(expert, {
|
|
...dto,
|
|
creationMethod: CreationMethod.IN_PERSON,
|
|
});
|
|
}
|
|
|
|
@Post("send-link/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiBody({ type: SendExpertInitiatedLinkV2Dto })
|
|
@ApiOperation({
|
|
summary: "[Expert mirror] Send blame link to first party (LINK method)",
|
|
description:
|
|
"For files created with creationMethod=LINK. Provide the first party phone number; the service stores it, registers the user if needed, and sends the invite link via SMS.",
|
|
})
|
|
async sendLink(
|
|
@CurrentUser() expert: any,
|
|
@Param("requestId") requestId: string,
|
|
@Body() dto: SendExpertInitiatedLinkV2Dto,
|
|
) {
|
|
return this.requestManagementService.sendLinkV2(expert, requestId, dto);
|
|
}
|
|
|
|
@Post("send-party-otp/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiBody({ type: SendPartyOtpDto })
|
|
@ApiOperation({
|
|
summary: "[Expert mirror] Send OTP to one party (one-at-a-time IN_PERSON)",
|
|
description:
|
|
"Sends an OTP to a single phone number. Use this for the sequential flow: send to first party -> verify -> fill data -> send to second party -> verify -> continue. No invite link is sent.",
|
|
})
|
|
async sendPartyOtp(
|
|
@CurrentUser() expert: any,
|
|
@Param("requestId") requestId: string,
|
|
@Body() dto: SendPartyOtpDto,
|
|
) {
|
|
return this.requestManagementService.sendPartyOtpV2(expert, requestId, dto);
|
|
}
|
|
|
|
@Post("verify-party-otp/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiBody({ type: VerifyPartyOtpDto })
|
|
@ApiOperation({
|
|
summary: "[Expert mirror] Verify one party's OTP (one-at-a-time 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() expert: any,
|
|
@Param("requestId") requestId: string,
|
|
@Body() dto: VerifyPartyOtpDto,
|
|
) {
|
|
return this.requestManagementService.verifyPartyOtpV2(
|
|
expert,
|
|
requestId,
|
|
dto,
|
|
);
|
|
}
|
|
|
|
@Post("/car-body-form/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiBody({ type: CarBodyFormDto })
|
|
@ApiOperation({ summary: "[Expert mirror] CAR_BODY accident type" })
|
|
async carBodyForm(
|
|
@Param("requestId") requestId: string,
|
|
@Body() body: CarBodyFormDto,
|
|
@CurrentUser() expert: any,
|
|
@Body("partyRole") partyRole?: string,
|
|
) {
|
|
return this.requestManagementService.carBodyAccidentTypeFormV2(
|
|
requestId,
|
|
body,
|
|
expert,
|
|
partyRole,
|
|
);
|
|
}
|
|
|
|
@Post("/run-inquiries/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiBody({ type: AddPlateDto })
|
|
@ApiOperation({
|
|
summary: "[Expert 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() expert: any,
|
|
@Body("partyRole") partyRole?: string,
|
|
) {
|
|
return this.requestManagementService.initialFormV2(
|
|
requestId,
|
|
body,
|
|
expert,
|
|
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, `expert-${file.originalname}-${unique}${ex}`);
|
|
},
|
|
}),
|
|
}),
|
|
)
|
|
@ApiParam({ name: "requestId" })
|
|
@Post("upload-video/:requestId")
|
|
@ApiOperation({ summary: "[Expert mirror] Upload first-party video" })
|
|
async uploadVideo(
|
|
@Param("requestId") requestId: string,
|
|
@CurrentUser() expert: any,
|
|
@UploadedFile() file?: Express.Multer.File,
|
|
) {
|
|
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
|
|
return this.requestManagementService.uploadFirstPartyVideoV2(
|
|
requestId,
|
|
file,
|
|
expert,
|
|
);
|
|
}
|
|
|
|
@Post("/add-detail-location/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiBody({ type: LocationDto })
|
|
@ApiOperation({ summary: "[Expert mirror] Add location for current party" })
|
|
async addLocation(
|
|
@Param("requestId") requestId: string,
|
|
@Body() body: LocationDto,
|
|
@CurrentUser() expert: any,
|
|
@Body("partyRole") partyRole?: string,
|
|
) {
|
|
return this.requestManagementService.addDetailLocationV2(
|
|
requestId,
|
|
body,
|
|
expert,
|
|
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 the file is currently collecting (FIRST for these steps).",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
@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, `expert-${flname}-${unique}${ex}`);
|
|
},
|
|
}),
|
|
}),
|
|
)
|
|
@ApiParam({ name: "requestId" })
|
|
@Post("upload-voice/:requestId")
|
|
@ApiOperation({ summary: "[Expert mirror] Upload voice for current party" })
|
|
async uploadVoice(
|
|
@Param("requestId") requestId: string,
|
|
@CurrentUser() expert: any,
|
|
@UploadedFile() voice?: Express.Multer.File,
|
|
@Body("partyRole") partyRole?: string,
|
|
) {
|
|
await this.mediaPolicyService.assertForBlame(voice, requestId, "voice");
|
|
return this.requestManagementService.uploadVoiceV2(
|
|
requestId,
|
|
voice,
|
|
expert,
|
|
partyRole,
|
|
);
|
|
}
|
|
|
|
@Post("/add-detail-description/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiBody({ type: DescriptionDto })
|
|
@ApiOperation({
|
|
summary: "[Expert 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() expert: any,
|
|
@Body("partyRole") partyRole?: string,
|
|
) {
|
|
return this.requestManagementService.addDescriptionV2(
|
|
requestId,
|
|
body,
|
|
expert,
|
|
partyRole,
|
|
);
|
|
}
|
|
|
|
@Post("add-second-party/:phoneNumber/:requestId/")
|
|
@ApiParam({ name: "phoneNumber" })
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiOperation({
|
|
summary: "[Expert mirror] Advance to second party (no SMS link)",
|
|
description:
|
|
"IN_PERSON variant of add-second-party: there is no invite link. Advances the workflow so the expert can fill the second party's steps. `frontendRoute` is accepted for route compatibility but ignored.",
|
|
})
|
|
async addSecondParty(
|
|
@Param("phoneNumber") phoneNumber: string,
|
|
@Param("requestId") requestId: string,
|
|
@CurrentUser() expert: any,
|
|
) {
|
|
return this.requestManagementService.expertAdvanceToSecondPartyV2(
|
|
requestId,
|
|
expert,
|
|
phoneNumber,
|
|
);
|
|
}
|
|
|
|
@Put("sign/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiConsumes("multipart/form-data")
|
|
@ApiOperation({
|
|
summary: "[Expert 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, `expert-party-${unique}${ext}`);
|
|
},
|
|
}),
|
|
}),
|
|
)
|
|
async sign(
|
|
@Param("requestId") requestId: string,
|
|
@Body() body: { partyRole?: string; isAccept?: string | boolean },
|
|
@CurrentUser() expert: any,
|
|
@UploadedFile() sign: Express.Multer.File,
|
|
) {
|
|
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(
|
|
expert,
|
|
requestId,
|
|
partyRole,
|
|
isAccept,
|
|
sign,
|
|
);
|
|
}
|
|
|
|
@Post("accident-fields/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiBody({ type: ExpertAccidentFieldsDto })
|
|
@ApiOperation({
|
|
summary: "[Expert mirror] Add accident fields and advance to signatures",
|
|
description:
|
|
"Saves accidentWay, accidentReason, and accidentType for the blame file. " +
|
|
"In an IN_PERSON expert-initiated flow the first party is always guilty, so " +
|
|
"calling this endpoint both records the accident details and advances the workflow " +
|
|
"from WAITING_FOR_GUILT_DECISION straight to WAITING_FOR_SIGNATURES.",
|
|
})
|
|
async addAccidentFields(
|
|
@Param("requestId") requestId: string,
|
|
@Body() fields: ExpertAccidentFieldsDto,
|
|
@CurrentUser() expert: any,
|
|
) {
|
|
return this.requestManagementService.expertAddAccidentFieldsForBlameV2(
|
|
expert,
|
|
requestId,
|
|
fields,
|
|
);
|
|
}
|
|
}
|