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

@@ -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);