forked from Yara724/api
494 lines
15 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
}
|
|
|