blame and claim refactored

This commit is contained in:
2026-02-24 12:19:25 +03:30
parent 0d8858f458
commit 35732dd70a
81 changed files with 11603 additions and 77 deletions

View File

@@ -7,6 +7,7 @@ import {
RoadCondition,
LightCondition,
} from "src/Types&Enums/blame-request-management/accident-conditions.enum";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
export class InitialFormDto {
@ApiProperty({ required: false, default: false })
@@ -135,6 +136,28 @@ export class CreateRequestDto {
type?: "THIRD_PARTY" | "CAR_BODY";
}
/**
* V2 create blame request (new model `BlameRequest`).
*/
export class CreateBlameRequestDtoV2 {
@ApiProperty({ enum: BlameRequestType })
type: BlameRequestType;
}
/**
* V2 step: FIRST_BLAME_CONFESSION
*/
export class BlameConfessionDtoV2 {
@ApiProperty({ required: false, default: false })
expertOpinion?: boolean;
@ApiProperty({ required: false, default: false })
imDamaged?: boolean;
@ApiProperty({ required: true, default: false })
imGuilty?: boolean;
}
// export class DocsOfThisFile {
// @ApiProperty()
// firstPartyFile?: FirstPartyFileDto;

View File

@@ -0,0 +1,46 @@
import { ApiProperty } from "@nestjs/swagger";
export class UserResendResponseDto {
@ApiProperty({
example: "67fa10c259e15f231a2d1aae",
description: "Request ID",
})
requestId: string;
@ApiProperty({
example: ["drivingLicense", "carCertificate"],
description: "List of items the current user needs to upload",
})
requestedItems: string[];
@ApiProperty({
example: "Please resend your driving license and car certificate with better quality",
description: "Expert's message explaining why resend is needed",
})
description: string;
@ApiProperty({
example: false,
description: "Whether the user has completed uploading all requested items",
})
completed: boolean;
}
export class UserResendUploadResponseDto {
@ApiProperty({
example: "Documents uploaded successfully",
})
message: string;
@ApiProperty({
example: ["drivingLicense", "carCertificate"],
description: "List of items that were uploaded",
})
uploadedItems: string[];
@ApiProperty({
example: true,
description: "Whether all requested items have been uploaded",
})
allItemsCompleted: boolean;
}

View File

@@ -0,0 +1,27 @@
import { ApiProperty } from "@nestjs/swagger";
export class UserSignatureResponseDto {
@ApiProperty({
example: "67fa10c259e15f231a2d1aae",
description: "Request ID",
})
requestId: string;
@ApiProperty({
example: true,
description: "Whether the user accepted the expert decision",
})
accepted: boolean;
@ApiProperty({
example: "COMPLETED",
description: "Updated request status",
})
status: string;
@ApiProperty({
example: "Both parties have signed. Case completed.",
description: "Result message",
})
message: string;
}

View File

@@ -0,0 +1,76 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model, Types } from "mongoose";
import {
BlameRequest,
BlameRequestDocument,
} from "src/request-management/entities/schema/blame-cases.schema";
@Injectable()
export class BlameRequestDbService {
constructor(
@InjectModel(BlameRequest.name)
private readonly blameRequestModel: Model<BlameRequestDocument>,
) {}
async create(payload: Partial<BlameRequest>): Promise<BlameRequestDocument> {
return this.blameRequestModel.create(payload);
}
async findOne(
filter: FilterQuery<BlameRequest>,
): Promise<BlameRequestDocument | null> {
return this.blameRequestModel.findOne(filter);
}
async findById(id: string | Types.ObjectId): Promise<BlameRequestDocument | null> {
return this.blameRequestModel.findById(id);
}
/** Find by id excluding history (e.g. for expert detail view). Returns lean plain object. */
async findByIdWithoutHistory(
id: string | Types.ObjectId,
): Promise<Record<string, unknown> | null> {
const doc = await this.blameRequestModel
.findById(id)
.select("-history")
.lean()
.exec();
return doc as Record<string, unknown> | null;
}
async findByIdAndUpdate(
id: string | Types.ObjectId,
update: any,
): Promise<BlameRequestDocument | null> {
return this.blameRequestModel.findByIdAndUpdate(id, update, { new: true });
}
/** Atomic find and update with filter conditions (e.g. for locking). */
async findOneAndUpdate(
filter: FilterQuery<BlameRequest>,
update: any,
options?: { new?: boolean },
): Promise<BlameRequestDocument | null> {
return this.blameRequestModel.findOneAndUpdate(filter, update, {
new: options?.new ?? true,
});
}
async find(
filter: FilterQuery<BlameRequest>,
options?: { lean?: boolean; select?: string },
): Promise<BlameRequestDocument[] | Record<string, unknown>[]> {
if (options?.lean) {
const query = options?.select
? this.blameRequestModel.find(filter).select(options.select).lean()
: this.blameRequestModel.find(filter).lean();
return query.exec() as Promise<Record<string, unknown>[]>;
}
const query = options?.select
? this.blameRequestModel.find(filter).select(options.select)
: this.blameRequestModel.find(filter);
return query.exec() as Promise<BlameRequestDocument[]>;
}
}

View File

@@ -0,0 +1,54 @@
//! NEW
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
@Schema({ _id: false })
export class ReferenceItem {
@Prop() id: string;
@Prop() label: string;
}
export const ReferenceItemSchema = SchemaFactory.createForClass(ReferenceItem);
@Schema({ _id: false })
export class AccidentReason extends ReferenceItem {
@Prop() fanavaran: number;
}
export const AccidentReasonSchema = SchemaFactory.createForClass(AccidentReason);
@Schema({ _id: false })
export class AccidentClassification {
@Prop({ type: ReferenceItemSchema })
accidentWay: ReferenceItem;
@Prop({ type: ReferenceItemSchema })
accidentType: ReferenceItem;
@Prop({ type: AccidentReasonSchema })
accidentReason: AccidentReason;
}
export const AccidentClassificationSchema =
SchemaFactory.createForClass(AccidentClassification);
@Schema({ _id: false })
export class Location {
@Prop() lat: number;
@Prop() lon: number;
}
export const LocationSchema = SchemaFactory.createForClass(Location);
@Schema({ _id: false })
export class AccidentInfo {
@Prop({ type: Date })
date?: Date;
@Prop()
time?: string;
@Prop({ type: LocationSchema })
location?: Location;
@Prop({ type: AccidentClassificationSchema })
classification?: AccidentClassification;
}
export const AccidentInfoSchema = SchemaFactory.createForClass(AccidentInfo);

View File

@@ -0,0 +1,69 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { Party, PartySchema } from "./partyRole.enum";
import { AccidentInfo, AccidentInfoSchema } from "./accidentInformation.type";
import { ExpertSection, ExpertSectionSchema } from "./expert-section.type";
import { Workflow, WorkflowSchema } from "./workflow.type";
import { HistoryEvent, HistoryEventSchema } from "./historyEvent.type";
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum";
@Schema({ _id: false })
export class BlameRequestSnapshot {
@Prop({ type: AccidentInfoSchema })
accident?: AccidentInfo;
@Prop({ type: [PartySchema], default: [] })
parties?: Party[];
}
export const BlameRequestSnapshotSchema =
SchemaFactory.createForClass(BlameRequestSnapshot);
@Schema({ collection: "blameCases", timestamps: true , id:true })
export class BlameRequest {
@Prop({ required: true, unique: true, index: true, trim: true })
requestNo: string;
/**
* Human-friendly shared id across blame+claim (e.g. A14235).
* Generated once at creation time and reused by claim.
*/
@Prop({ required: true, unique: true, index: true, trim: true })
publicId: string;
@Prop({ required: true, type: String, enum: BlameRequestType })
type: BlameRequestType;
@Prop({ required: true, type: String, enum: CaseStatus, default: CaseStatus.OPEN })
status: CaseStatus;
@Prop({ type: String, enum: BlameStatus, default: BlameStatus.UNKNOWN })
blameStatus: BlameStatus;
@Prop({ type: WorkflowSchema })
workflow?: Workflow;
@Prop({ type: AccidentInfoSchema })
accident?: AccidentInfo;
@Prop({ type: [PartySchema], default: [] })
parties: Party[];
@Prop({ type: ExpertSectionSchema })
expert?: ExpertSection;
@Prop({ type: [HistoryEventSchema], default: [] })
history: HistoryEvent[];
/**
* Optional snapshot structure (kept for future use / read-optimized copies).
* Source of truth remains the top-level fields of this document.
*/
@Prop({ type: BlameRequestSnapshotSchema, required: false })
snapshot?: BlameRequestSnapshot;
}
export type BlameRequestDocument = HydratedDocument<BlameRequest>;
export const BlameRequestSchema = SchemaFactory.createForClass(BlameRequest);

View File

@@ -0,0 +1,142 @@
//! NEW
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
@Schema({ _id: false })
export class ExpertLink {
@Prop({ required: true })
token: string;
@Prop({ type: Date })
expiresAt: Date;
@Prop({ default: false })
isUsed: boolean;
@Prop({ default: 0 })
openCount: number;
@Prop()
sentTo?: string;
@Prop()
lastOpenedAt?: Date;
}
export const ExpertLinkSchema = SchemaFactory.createForClass(ExpertLink);
export enum FilledBy {
CUSTOMER = "CUSTOMER",
EXPERT = "EXPERT"
}
@Schema({ _id: false })
export class ExpertDecision {
@Prop({ type: Types.ObjectId })
guiltyPartyId: Types.ObjectId;
@Prop()
description: string;
@Prop({ type: Date, default: Date.now })
decidedAt: Date;
@Prop({ type: Types.ObjectId })
decidedByExpertId?: Types.ObjectId;
@Prop({ type: Object })
fields?: {
accidentWay: { id: string; label: string };
accidentReason: { id: string; label: string; fanavaran: number };
accidentType: { id: string; label: string };
};
}
export const ExpertDecisionSchema = SchemaFactory.createForClass(ExpertDecision);
@Schema({ _id: false })
export class PartyResendRequest {
@Prop({ type: Types.ObjectId, required: true })
partyId: Types.ObjectId;
@Prop({ type: [String], default: [] })
requestedItems: string[]; // Array of ResendItemType values
@Prop()
description?: string;
@Prop({ type: Date, default: Date.now })
requestedAt: Date;
@Prop({ type: Boolean, default: false })
completed?: boolean;
@Prop({ type: Date })
completedAt?: Date;
}
export const PartyResendRequestSchema = SchemaFactory.createForClass(PartyResendRequest);
@Schema({ _id: false })
export class ExpertResend {
@Prop({ type: [PartyResendRequestSchema], default: [] })
parties: PartyResendRequest[];
@Prop({ type: Date })
requestedAt?: Date;
@Prop({ type: Types.ObjectId })
requestedByExpertId?: Types.ObjectId;
}
export const ExpertResendSchema = SchemaFactory.createForClass(ExpertResend);
@Schema({ _id: false })
export class FileRating {
@Prop({ min: 0, max: 5 })
collisionMethodAccuracy: number;
@Prop({ min: 0, max: 5 })
evaluationTimeliness: number;
@Prop({ min: 0, max: 5 })
accidentCauseAccuracy: number;
@Prop({ min: 0, max: 5 })
guiltyVehicleIdentification: number;
}
export const FileRatingSchema = SchemaFactory.createForClass(FileRating);
export enum CreationMethod {
NORMAL = "NORMAL",
LINK = "LINK",
IN_PERSON = "IN_PERSON"
}
@Schema({ _id: false })
export class ExpertSection {
@Prop({ type: Types.ObjectId })
assignedExpertId?: Types.ObjectId;
@Prop({ enum: CreationMethod })
creationMethod: CreationMethod;
@Prop({ enum: FilledBy })
filledBy?: FilledBy;
@Prop({ enum: ExpertDecisionSchema })
decision?: ExpertDecision;
@Prop({ type: ExpertResendSchema })
resend?: ExpertResend;
@Prop({ type: FileRatingSchema })
rating?: FileRating;
@Prop({ type: ExpertLinkSchema })
link?: ExpertLink;
}
export const ExpertSectionSchema = SchemaFactory.createForClass(ExpertSection);

View File

@@ -0,0 +1,32 @@
//! NEW
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Schema as MongooseSchema, Types } from "mongoose";
@Schema({ _id: false })
export class HistoryActor {
@Prop({ type: Types.ObjectId })
actorId?: Types.ObjectId;
@Prop({ type: String })
actorName?: string;
@Prop({ type: String })
actorType?: string;
}
export const HistoryActorSchema = SchemaFactory.createForClass(HistoryActor);
@Schema({ _id: false })
export class HistoryEvent {
@Prop({ type: String, required: true })
type: string;
@Prop({ type: HistoryActorSchema })
actor?: HistoryActor;
@Prop({ type: Date, default: () => new Date() })
timestamp: Date;
@Prop({ type: MongooseSchema.Types.Mixed })
metadata?: any;
}
export const HistoryEventSchema = SchemaFactory.createForClass(HistoryEvent);

View File

@@ -0,0 +1,154 @@
//! NEW
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Schema as MongooseSchema, Types } from "mongoose";
import { Location, LocationSchema } from "./accidentInformation.type";
export enum PartyRole {
FIRST = "FIRST",
SECOND = "SECOND",
}
@Schema({ _id: false })
export class Person {
@Prop({ type: Types.ObjectId })
userId?: Types.ObjectId;
@Prop() fullName?: string;
@Prop() phoneNumber?: string;
@Prop({ type: Types.ObjectId }) clientId?: Types.ObjectId;
@Prop() birthday?: string;
// ---- FIRST_INITIAL_FORM (step-manager driven) ----
@Prop() nationalCodeOfInsurer?: string;
@Prop() nationalCodeOfDriver?: string;
@Prop() insurerLicense?: string;
@Prop() driverLicense?: string;
@Prop({ type: Boolean })
driverIsInsurer?: boolean;
@Prop({ type: Boolean })
isNewCar?: boolean;
/**
* Mirrors existing DTO `userNoCertificate` (true means user has NO certificate).
*/
@Prop({ type: Boolean })
userNoCertificate?: boolean;
@Prop({ type: Number })
insurerBirthday?: number;
@Prop({ type: String })
driverBirthday?: string | null;
}
export const PersonSchema = SchemaFactory.createForClass(Person);
@Schema({ _id: false })
export class Vehicle {
@Prop() plateId?: string;
@Prop() name?: string;
@Prop() model?: string;
@Prop() type?: string;
@Prop() isNew?: boolean;
/**
* Full external inquiry payload (Tejarat/SandHub) stored as-is
* so we never lose fields that are not mapped yet.
*/
@Prop({ type: MongooseSchema.Types.Mixed })
inquiry?: any;
}
export const VehicleSchema = SchemaFactory.createForClass(Vehicle);
@Schema({ _id: false })
export class Insurance {
@Prop() policyNumber?: string;
@Prop() company?: string;
@Prop() startDate?: string;
@Prop() endDate?: string;
@Prop() financialCeiling?: string;
@Prop({ type: [String] })
coverages?: string[];
}
export const InsuranceSchema = SchemaFactory.createForClass(Insurance);
@Schema({ _id: false })
export class PartyStatement {
@Prop({ default: false })
acceptsExpertOpinion?: boolean;
@Prop({ default: false })
claimsDamage?: boolean;
@Prop({ default: false })
admitsGuilt?: boolean;
@Prop({ type: String })
description?: string;
}
export const PartyStatementSchema = SchemaFactory.createForClass(PartyStatement);
@Schema({ _id: false })
export class EvidenceBundle {
@Prop({ type: [String] })
images?: string[];
@Prop({ type: [String] })
voices?: string[];
@Prop() videoId?: string;
}
export const EvidenceBundleSchema = SchemaFactory.createForClass(EvidenceBundle);
@Schema({ _id: false })
export class Signature {
@Prop() fileId: string;
@Prop() fileName: string;
@Prop() fileUrl: string;
}
export const SignatureSchema = SchemaFactory.createForClass(Signature);
@Schema({ _id: false })
export class PartyConfirmation {
@Prop() partyRole: PartyRole;
@Prop() accepted: boolean;
@Prop({ type: SignatureSchema })
signature?: Signature;
}
export const PartyConfirmationSchema =
SchemaFactory.createForClass(PartyConfirmation);
@Schema({ _id: false })
export class Party {
@Prop({ enum: PartyRole })
role: PartyRole;
@Prop({ type: PersonSchema })
person: Person;
/**
* Party-submitted location (step-driven: FIRST_LOCATION / SECOND_LOCATION).
*/
@Prop({ type: LocationSchema })
location?: Location;
@Prop({ type: VehicleSchema })
vehicle?: Vehicle;
@Prop({ type: InsuranceSchema })
insurance?: Insurance;
@Prop({ type: PartyStatementSchema })
statement?: PartyStatement;
@Prop({ type: EvidenceBundleSchema })
evidence?: EvidenceBundle;
@Prop({ type: PartyConfirmationSchema })
confirmation?: PartyConfirmation;
}
export const PartySchema = SchemaFactory.createForClass(Party);

View File

@@ -368,6 +368,15 @@ export class RequestManagementModel {
@Prop({ type: String, required: true, index: { unique: true } })
requestNumber: string;
/**
* Human-friendly shared id across blame+claim (e.g. A14235).
* Generated once at creation time and reused by claim.
*
* NOTE: `sparse` keeps existing documents without this field valid.
*/
@Prop({ type: String, unique: true, index: true, sparse: true, trim: true })
publicId?: string;
@Prop()
blameStatus: ReqBlameStatus | StepsEnum;

View File

@@ -0,0 +1,40 @@
//! NEW
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
import { WorkflowStep } from "src/Types&Enums/blame-request-management/blameWorkflow-steps.enum";
@Schema({ _id: false })
export class ActorLock {
@Prop({ type: Types.ObjectId, required: true })
actorId: Types.ObjectId;
@Prop({ type: String, required: true })
actorName: string;
@Prop({ type: String, default: "expert" })
actorRole: "expert" | "admin";
}
export const ActorLockSchema = SchemaFactory.createForClass(ActorLock);
@Schema({ _id: false })
export class Workflow {
@Prop({ type: String, enum: WorkflowStep })
currentStep: WorkflowStep;
@Prop({ type: String, enum: WorkflowStep })
nextStep: WorkflowStep;
@Prop({ type: [String], enum: WorkflowStep, default: [] })
completedSteps?: WorkflowStep[];
@Prop({ default: false })
locked?: boolean;
@Prop({ type: Date })
lockedAt?: Date;
@Prop({ type: ActorLockSchema })
lockedBy?: ActorLock;
}
export const WorkflowSchema = SchemaFactory.createForClass(Workflow);

View File

@@ -6,10 +6,13 @@ import { ClientModule } from "src/client/client.module";
import { PlatesModule } from "src/plates/plates.module";
import { BlameVideoDbService } from "src/request-management/entities/db-service/blame-video.db.service";
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { SandHubModule } from "src/sand-hub/sand-hub.module";
import { UsersModule } from "src/users/users.module";
import { CronModule } from "src/utils/cron/cron.module";
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
import { PublicIdModule } from "src/utils/public-id/public-id.module";
import { WorkflowStepManagementModule } from "src/workflow-step-management/workflow-step-management.module";
import { BlameDocumentDbService } from "./entities/db-service/blame-document.db.service";
import { BlameVoiceDbService } from "./entities/db-service/blame.voice.db.service";
import { UserSignDbService } from "./entities/db-service/sign.db.service";
@@ -23,12 +26,14 @@ import {
BlameVoicesModel,
} from "./entities/schema/blame-voice.schema";
import { BlameDocumentModel, BlameDocumentSchema } from "./entities/schema/blame-document.schema";
import { BlameRequest, BlameRequestSchema } from "./entities/schema/blame-cases.schema";
import {
RequestManagementModel,
RequestManagementSchema,
} from "./entities/schema/request-management.schema";
import { RequestManagementService } from "./request-management.service";
import { RequestManagementController } from "./request-management.controller";
import { RequestManagementV2Controller } from "./request-management.v2.controller";
import { ExpertInitiatedController } from "./expert-initiated.controller";
@Module({
@@ -37,12 +42,15 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
ClientModule,
SandHubModule,
SmsManagerModule,
PublicIdModule,
WorkflowStepManagementModule,
PlatesModule,
MulterModule.register({
dest: "./files/video",
}),
MongooseModule.forFeature([
{ name: RequestManagementModel.name, schema: RequestManagementSchema },
{ name: BlameRequest.name, schema: BlameRequestSchema },
{ name: BlameVideosModel.name, schema: BlameVideoSchema },
{ name: BlameVoicesModel.name, schema: BlameVoiceSchema },
{ name: UserSignModel.name, schema: UserSignSchema },
@@ -51,7 +59,11 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
forwardRef(() => ClaimRequestManagementModule),
CronModule,
],
controllers: [RequestManagementController, ExpertInitiatedController],
controllers: [
RequestManagementController,
RequestManagementV2Controller,
ExpertInitiatedController,
],
providers: [
RequestManagementService,
BlameVideoDbService,
@@ -59,6 +71,7 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
UserSignDbService,
RequestManagementDbService,
BlameDocumentDbService,
BlameRequestDbService,
],
exports: [
RequestManagementService,
@@ -67,6 +80,7 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
BlameVoiceDbService,
BlameDocumentDbService,
UserSignDbService,
BlameRequestDbService,
],
})
export class RequestManagementModule {}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,382 @@
import { extname } from "node:path";
import {
Body,
Controller,
Get,
Param,
Post,
Put,
Req,
UploadedFile,
UploadedFiles,
UseGuards,
UseInterceptors,
} from "@nestjs/common";
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
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 { RoleEnum } from "src/Types&Enums/role.enum";
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
import {
BlameConfessionDtoV2,
CreateBlameRequestDtoV2,
DescriptionDto,
LocationDto,
} from "./dto/create-request-management.dto";
import { RequestManagementService } from "./request-management.service";
/**
* 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,
) {}
@Post()
@UseGuards(GlobalGuard)
async createRequestV2(
@CurrentUser() user,
@Body() createRequestDto: CreateBlameRequestDtoV2,
) {
return this.requestManagementService.createRequestV2(
user,
createRequestDto.type,
);
}
@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);
}
@ApiBody({
schema: {
type: "object",
properties: {
file: {
type: "string",
format: "binary",
},
},
},
})
@ApiConsumes("multipart/form-data")
@UseInterceptors(
FileInterceptor("file", {
limits: {
fileSize: 20 * 1024 * 1024,
},
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,
) {
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: 10 * 1024 * 1024,
},
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,
) {
return this.requestManagementService.uploadVoiceV2(requestId, voice, user);
}
@Post("/add-detail-description/:requestId")
@ApiParam({ name: "requestId" })
@ApiBody({ type: DescriptionDto })
@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: 10 * 1024 * 1024, // 10MB
},
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,
) {
// 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)",
},
},
},
})
@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) => {
cb(null, "./uploads");
},
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[];
},
) {
return this.requestManagementService.uploadResendDocumentsV2(
requestId,
user.sub,
files,
);
}
}