forked from Yara724/api
blame and claim refactored
This commit is contained in:
@@ -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;
|
||||
|
||||
46
src/request-management/dto/user-resend.dto.ts
Normal file
46
src/request-management/dto/user-resend.dto.ts
Normal 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;
|
||||
}
|
||||
27
src/request-management/dto/user-signature.dto.ts
Normal file
27
src/request-management/dto/user-signature.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -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[]>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
69
src/request-management/entities/schema/blame-cases.schema.ts
Normal file
69
src/request-management/entities/schema/blame-cases.schema.ts
Normal 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);
|
||||
142
src/request-management/entities/schema/expert-section.type.ts
Normal file
142
src/request-management/entities/schema/expert-section.type.ts
Normal 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);
|
||||
32
src/request-management/entities/schema/historyEvent.type.ts
Normal file
32
src/request-management/entities/schema/historyEvent.type.ts
Normal 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);
|
||||
154
src/request-management/entities/schema/partyRole.enum.ts
Normal file
154
src/request-management/entities/schema/partyRole.enum.ts
Normal 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);
|
||||
@@ -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;
|
||||
|
||||
|
||||
40
src/request-management/entities/schema/workflow.type.ts
Normal file
40
src/request-management/entities/schema/workflow.type.ts
Normal 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);
|
||||
@@ -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
382
src/request-management/request-management.v2.controller.ts
Normal file
382
src/request-management/request-management.v2.controller.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user