Files
yara724api/src/request-management/request-management.v2.controller.ts
SepehrYahyaee a31196774c FIX FILE SIZES
2026-05-23 16:23:11 +03:30

494 lines
15 KiB
TypeScript

import { extname } from "node:path";
import { mkdirSync } from "node:fs";
import {
Body,
Controller,
Get,
Param,
Post,
Put,
Query,
Req,
UploadedFile,
UploadedFiles,
UseGuards,
UseInterceptors,
} from "@nestjs/common";
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiOperation,
ApiParam,
ApiTags,
} from "@nestjs/swagger";
import { FileFieldsInterceptor, FileInterceptor } from "@nestjs/platform-express";
import { diskStorage } from "multer";
import { Request } from "express";
import { GlobalGuard } from "src/auth/guards/global.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,
CreateBlameRequestDtoV2,
DescriptionDto,
LocationDto,
CarBodyFormDto,
} from "./dto/create-request-management.dto";
import { RequestManagementService } from "./request-management.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { GetUserBlameListV2ResponseDto } from "./dto/blame-list-user-v2.dto";
/**
* V2 API surface for the redesigned `BlameRequest` model.
*
* Versioning strategy: prefix the controller route with `v2/` so
* all endpoints under this controller are automatically V2.
*/
@Controller("v2/blame-request-management")
@ApiTags("blame-request-management (v2)")
@ApiBearerAuth()
@UseGuards(GlobalGuard, RolesGuard)
@Roles(RoleEnum.USER)
export class RequestManagementV2Controller {
constructor(
private readonly requestManagementService: RequestManagementService,
private readonly mediaPolicyService: MediaPolicyService,
) { }
@Post()
@UseGuards(GlobalGuard)
async createRequestV2(
@CurrentUser() user,
@Body() createRequestDto: CreateBlameRequestDtoV2,
) {
return this.requestManagementService.createRequestV2(
user,
createRequestDto.type,
);
}
@Get()
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
@ApiOperation({
summary: "List my blame requests (V2)",
description:
"All blame files for the current user. Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`. Without `page`/`limit`, returns the full filtered list.",
})
async getAllBlameRequestsV2(
@CurrentUser() user: any,
@Query() query: ListQueryV2Dto,
): Promise<GetUserBlameListV2ResponseDto> {
return this.requestManagementService.getAllBlameRequestsV2(user, query);
}
/**
* Get one blame request by id. Allowed for the request owner (party) or the initiating field expert.
*/
@Get(":requestId")
@ApiOperation({
summary: "Get one blame request (v2)",
description:
"Returns a minimal, user-safe payload: requestNo, publicId, type, status, blameStatus, workflow, parties (PII stripped), expert (with **expertName**), carBodyInsuranceDetail, plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }). History and other internal fields are omitted.",
})
@ApiParam({ name: "requestId", description: "Blame request ID" })
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
async getBlameRequestV2(
@Param("requestId") requestId: string,
@CurrentUser() user: any,
) {
return this.requestManagementService.getBlameRequestV2(requestId, user);
}
@Post("/blame-confession/:requestId")
@ApiParam({ name: "requestId" })
@ApiBody({ type: BlameConfessionDtoV2 })
@UseGuards(GlobalGuard)
async blameConfessionV2(
@Param("requestId") requestId: string,
@Body() body: BlameConfessionDtoV2,
@CurrentUser() user,
) {
return this.requestManagementService.blameConfessionV2(requestId, body, user);
}
/** CAR_BODY only: submit accident type (car vs object). Call this when nextStep is CAR_BODY_ACCIDENT_TYPE. */
@Post("/car-body-form/:requestId")
@ApiParam({ name: "requestId" })
@ApiBody({ type: CarBodyFormDto })
@UseGuards(GlobalGuard)
async carBodyFormV2(
@Param("requestId") requestId: string,
@Body() body: CarBodyFormDto,
@CurrentUser() user,
) {
return this.requestManagementService.carBodyAccidentTypeFormV2(requestId, body, user);
}
@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);
const filename = `${file.originalname}-${unique}${ex}`;
callback(null, filename);
},
}),
}),
)
@ApiParam({ name: "requestId" })
@Post("upload-video/:requestId")
@UseGuards(GlobalGuard)
async uploadVideoV2(
@Param("requestId") requestId: string,
@CurrentUser() user,
@UploadedFile() file?: Express.Multer.File,
) {
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
return this.requestManagementService.uploadFirstPartyVideoV2(
requestId,
file,
user,
);
}
@Post("/initial-form/:requestId")
@ApiParam({ name: "requestId" })
@ApiBody({ type: AddPlateDto })
@UseGuards(GlobalGuard)
async initialFormV2(
@Param("requestId") requestId: string,
@Body() body: AddPlateDto,
@CurrentUser() user,
) {
return this.requestManagementService.initialFormV2(requestId, body, user);
}
@Post("/add-detail-location/:requestId")
@ApiParam({ name: "requestId" })
@ApiBody({ type: LocationDto })
@UseGuards(GlobalGuard)
async addLocationV2(
@Param("requestId") requestId: string,
@Body() body: LocationDto,
@CurrentUser() user,
) {
return this.requestManagementService.addDetailLocationV2(requestId, body, user);
}
@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/voice",
filename: (req, file, callback) => {
const unique = Date.now();
const ex = extname(file.originalname);
const flname = file.originalname.split(".")[0];
const filename = `${flname}-${unique}${ex}`;
callback(null, filename);
},
}),
}),
)
@ApiParam({ name: "requestId" })
@Post("upload-voice/:requestId")
@UseGuards(GlobalGuard)
async uploadVoiceV2(
@Param("requestId") requestId: string,
@CurrentUser() user,
@UploadedFile() voice?: Express.Multer.File,
) {
await this.mediaPolicyService.assertForBlame(voice, requestId, "voice");
return this.requestManagementService.uploadVoiceV2(requestId, voice, user);
}
@Post("/add-detail-description/:requestId")
@ApiParam({ name: "requestId" })
@ApiBody({
description:
"THIRD_PARTY: send only desc. CAR_BODY: send desc + accidentDate, accidentTime, weatherCondition, roadCondition, lightCondition (all required).",
type: DescriptionDto,
examples: {
thirdParty: {
summary: "THIRD_PARTY (only desc)",
value: { desc: "Accident description text" },
},
carBody: {
summary: "CAR_BODY (desc + conditions)",
value: {
desc: "Accident description",
accidentDate: "2025-12-08",
accidentTime: "14:30",
weatherCondition: "صاف",
roadCondition: "خشک",
lightCondition: "روز",
},
},
},
})
@UseGuards(GlobalGuard)
async addDescriptionV2(
@Param("requestId") requestId: string,
@Body() body: DescriptionDto,
@CurrentUser() user,
) {
return this.requestManagementService.addDescriptionV2(requestId, body, user);
}
@Post("add-second-party/:phoneNumber/:requestId/:frontendRoute")
@ApiParam({ name: "phoneNumber" })
@ApiParam({ name: "requestId" })
@ApiParam({ name: "frontendRoute" })
@UseGuards(GlobalGuard)
async addSecondPartyV2(
@Param("phoneNumber") phoneNumber: string,
@Param("requestId") requestId: string,
@Param("frontendRoute") frontendRoute: string,
@CurrentUser() user,
) {
return this.requestManagementService.addSecondPartyV2(
requestId,
phoneNumber,
frontendRoute,
user,
);
}
/**
* V2: Get list of documents/items the current user needs to resend
*/
@Get("resend-requirements/:requestId")
@ApiParam({ name: "requestId", description: "Blame request ID" })
async getResendRequirements(
@Param("requestId") requestId: string,
@CurrentUser() user: any,
) {
return this.requestManagementService.getResendRequirementsV2(
requestId,
user.sub,
);
}
/**
* V2: User signs/accepts or rejects expert decision
*/
@Put("sign/:requestId")
@ApiParam({ name: "requestId", description: "Blame request ID" })
@ApiConsumes("multipart/form-data")
@ApiBody({
description: "Upload signature file and acceptance decision",
schema: {
type: "object",
required: ["sign", "isAccept"],
properties: {
sign: {
type: "string",
format: "binary",
description: "Signature image file",
},
isAccept: {
type: "boolean",
description: "true to accept expert decision, false to reject",
},
},
},
})
@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);
const filename = `${file.originalname.split(/[.,\s-]/)[0]}-${unique}${ext}`;
callback(null, filename);
},
}),
}),
)
async userSignDecision(
@Param("requestId") requestId: string,
@Body("isAccept") isAccept: string | boolean,
@CurrentUser() user: any,
@UploadedFile() sign: Express.Multer.File,
) {
await this.mediaPolicyService.assertForBlame(sign, requestId, "image");
// Convert string "true"/"false" to boolean
const acceptDecision = typeof isAccept === "string"
? isAccept === "true"
: Boolean(isAccept);
return this.requestManagementService.userSignDecisionV2(
requestId,
user.sub,
acceptDecision,
sign,
);
}
/**
* V2: Upload requested documents/evidence for resend
* Field names must match ResendItemType enum values (e.g., drivingLicense, carCertificate, voice, video)
*/
@Put("resend/:requestId")
@ApiParam({ name: "requestId", description: "Blame request ID" })
@ApiConsumes("multipart/form-data")
@ApiBody({
description:
"Upload files with field names matching requested items (e.g., drivingLicense, carCertificate, voice, video)",
schema: {
type: "object",
properties: {
drivingLicense: {
type: "string",
format: "binary",
description: "Driving license file (if requested)",
},
carCertificate: {
type: "string",
format: "binary",
description: "Car certificate file (if requested)",
},
nationalCertificate: {
type: "string",
format: "binary",
description: "National certificate file (if requested)",
},
carGreenCard: {
type: "string",
format: "binary",
description: "Car green card file (if requested)",
},
voice: {
type: "string",
format: "binary",
description: "Voice recording file (if requested)",
},
video: {
type: "string",
format: "binary",
description: "Video file (if requested)",
},
description: {
type: "string",
description:
"Text description when expert requested ResendItemType.description",
},
textDescription: {
type: "string",
description:
"Alias for `description` (same semantics); either field may be used.",
},
},
},
})
@UseInterceptors(
FileFieldsInterceptor(
[
{ name: "drivingLicense", maxCount: 1 },
{ name: "carCertificate", maxCount: 1 },
{ name: "nationalCertificate", maxCount: 1 },
{ name: "carGreenCard", maxCount: 1 },
{ name: "voice", maxCount: 1 },
{ name: "video", maxCount: 1 },
],
{
storage: diskStorage({
destination: (req, file, cb) => {
const uploadDir = "./files/blame-resend";
mkdirSync(uploadDir, { recursive: true });
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix =
Date.now() + "-" + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname);
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
},
}),
},
),
)
async uploadResendDocuments(
@Param("requestId") requestId: string,
@CurrentUser() user: any,
@UploadedFiles()
files: {
drivingLicense?: Express.Multer.File[];
carCertificate?: Express.Multer.File[];
nationalCertificate?: Express.Multer.File[];
carGreenCard?: Express.Multer.File[];
voice?: Express.Multer.File[];
video?: Express.Multer.File[];
},
@Body("description") description?: string,
@Body("textDescription") textDescription?: string,
) {
// Each multipart field maps to a media kind so the policy gate can pick
// the right per-client bounds. Document images = `image`, audio = `voice`,
// video = `video`.
await this.mediaPolicyService.assertForBlameMany(files, requestId, {
drivingLicense: "image",
carCertificate: "image",
nationalCertificate: "image",
carGreenCard: "image",
voice: "voice",
video: "video",
});
const descCandidates = [description, textDescription]
.map((x) => (x != null ? String(x).trim() : ""))
.filter((s) => s.length > 0);
const mergedDescription =
descCandidates.length > 0 ? descCandidates[0] : undefined;
return this.requestManagementService.uploadResendDocumentsV2(
requestId,
user.sub,
files,
mergedDescription,
);
}
}