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,51 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model, Types } from "mongoose";
import { ClaimCase, ClaimCaseDocument } from "../schema/claim-cases.schema";
@Injectable()
export class ClaimCaseDbService {
constructor(
@InjectModel(ClaimCase.name)
private readonly claimCaseModel: Model<ClaimCaseDocument>,
) {}
async create(payload: Partial<ClaimCase>): Promise<ClaimCaseDocument> {
return this.claimCaseModel.create(payload);
}
async findById(id: string | Types.ObjectId): Promise<ClaimCaseDocument | null> {
return this.claimCaseModel.findById(id);
}
async findOne(filter: FilterQuery<ClaimCase>): Promise<ClaimCaseDocument | null> {
return this.claimCaseModel.findOne(filter);
}
async findByIdAndUpdate(
id: string | Types.ObjectId,
update: any,
): Promise<ClaimCaseDocument | null> {
return this.claimCaseModel.findByIdAndUpdate(id, update, { new: true });
}
async find(
filter: FilterQuery<ClaimCase>,
options?: { lean?: boolean; select?: string },
): Promise<ClaimCaseDocument[] | Record<string, unknown>[]> {
if (options?.lean) {
const query = options?.select
? this.claimCaseModel.find(filter).select(options.select).lean()
: this.claimCaseModel.find(filter).lean();
return query.exec() as Promise<Record<string, unknown>[]>;
}
const query = options?.select
? this.claimCaseModel.find(filter).select(options.select)
: this.claimCaseModel.find(filter);
return query.exec() as Promise<ClaimCaseDocument[]>;
}
async countByFilter(filter: FilterQuery<ClaimCase>): Promise<number> {
return this.claimCaseModel.countDocuments(filter);
}
}

View File

@@ -20,6 +20,6 @@ export class VideoCaptureDbService {
}
async findById(id: string): Promise<VideoCaptureModel | null> {
return this.videoCapture.findById(id).lean();
return this.videoCapture.findById(id);
}
}

View File

@@ -0,0 +1,31 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { CarDamagePartModel, CarDamagePartOtherModel } from "./car-parts.schema";
@Schema({ _id: false })
export class ClaimDamageSelection {
/**
* V2: Array of selected damaged outer car part names
* Examples: ['hood', 'front_right_door', 'rear_bumper']
*/
@Prop({ type: [String], default: [] })
selectedParts?: string[];
/**
* V2: Array of selected other (non-body) damaged parts
* Examples: ['engine', 'suspension', 'headlight']
*/
@Prop({ type: [String], default: [] })
otherParts?: string[];
/**
* Legacy fields - kept for backward compatibility
*/
@Prop({ type: [CarDamagePartModel] })
legacyParts?: CarDamagePartModel[];
@Prop({ type: CarDamagePartOtherModel })
legacyOtherParts?: CarDamagePartOtherModel;
}
export const ClaimDamageSelectionSchema =
SchemaFactory.createForClass(ClaimDamageSelection);

View File

@@ -0,0 +1,202 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Schema as MongooseSchema, Types } from "mongoose";
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
import { UserReplyEnum } from "src/Types&Enums/claim-request-management/userReply.enum";
@Schema({ _id: false })
export class ClaimPartPricing {
@Prop({ type: String })
partId: string;
@Prop({ type: String })
carPartDamage?: string;
@Prop({ type: String })
typeOfDamage?: string;
@Prop({ type: MongooseSchema.Types.Mixed })
price?: string | number;
@Prop({ type: MongooseSchema.Types.Mixed })
salary?: string | number;
@Prop({ type: MongooseSchema.Types.Mixed })
totalPayment?: string | number;
@Prop({ type: MongooseSchema.Types.Mixed })
daghi?: string | number;
@Prop({ type: Boolean, default: false })
factorNeeded?: boolean;
@Prop({ type: Types.ObjectId })
factorLink?: Types.ObjectId;
@Prop({ type: String, enum: FactorStatus, default: null })
factorStatus?: FactorStatus | null;
@Prop({ type: String, default: null })
rejectionReason?: string | null;
}
export const ClaimPartPricingSchema =
SchemaFactory.createForClass(ClaimPartPricing);
@Schema({ _id: false })
export class ClaimExpertActor {
@Prop({ type: String })
actorName?: string;
/**
* Kept as string to support existing queries like:
* `"damageExpertReply.actorDetail.actorId": actor.sub`
*/
@Prop({ type: String })
actorId?: string;
}
export const ClaimExpertActorSchema =
SchemaFactory.createForClass(ClaimExpertActor);
@Schema({ _id: false })
export class ClaimUserComment {
@Prop({ type: Boolean, default: null })
isAccept?: boolean | null;
@Prop({ type: Types.ObjectId })
signDetailId?: Types.ObjectId;
}
export const ClaimUserCommentSchema =
SchemaFactory.createForClass(ClaimUserComment);
@Schema({ _id: false })
export class ClaimExpertReply {
@Prop({ type: String })
description?: string;
@Prop({ type: ClaimExpertActorSchema })
actorDetail?: ClaimExpertActor;
@Prop({ type: [ClaimPartPricingSchema], default: [] })
parts?: ClaimPartPricing[];
@Prop({ type: Date, default: () => new Date() })
submittedAt?: Date;
@Prop({ type: ClaimUserCommentSchema })
userComment?: ClaimUserComment;
}
export const ClaimExpertReplySchema =
SchemaFactory.createForClass(ClaimExpertReply);
@Schema({ _id: false })
export class ClaimUserObjection {
@Prop({ type: String, required: true })
partId: string;
@Prop({ type: String })
reason?: string;
@Prop({ type: String })
partPrice?: string;
@Prop({ type: String })
partSalary?: string;
@Prop({ type: String })
typeOfDamage?: string;
}
export const ClaimUserObjectionSchema =
SchemaFactory.createForClass(ClaimUserObjection);
@Schema({ _id: false })
export class ClaimResendRequest {
@Prop({ type: String })
resendDescription?: string;
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
resendDocuments?: any[];
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
resendCarParts?: any[];
}
export const ClaimResendRequestSchema =
SchemaFactory.createForClass(ClaimResendRequest);
@Schema({ _id: false })
export class ClaimUserResendPayload {
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
documents?: any[];
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
carParts?: any[];
}
export const ClaimUserResendPayloadSchema =
SchemaFactory.createForClass(ClaimUserResendPayload);
@Schema({ _id: false })
export class ClaimFileRating {
@Prop({ type: Number, min: 0, max: 5 })
collisionMethodAccuracy?: number;
@Prop({ type: Number, min: 0, max: 5 })
evaluationTimeliness?: number;
@Prop({ type: Number, min: 0, max: 5 })
accidentCauseAccuracy?: number;
@Prop({ type: Number, min: 0, max: 5 })
guiltyVehicleIdentification?: number;
}
export const ClaimFileRatingSchema =
SchemaFactory.createForClass(ClaimFileRating);
@Schema({ _id: false })
export class ClaimPriceDrop {
@Prop({ type: Number })
total?: number;
@Prop({ type: Number })
carPrice?: number;
@Prop({ type: Number })
carModel?: number;
@Prop({ type: [Number], default: [] })
carValue?: number[];
@Prop({ type: Number })
sumOfSeverity?: number;
}
export const ClaimPriceDropSchema = SchemaFactory.createForClass(ClaimPriceDrop);
@Schema({ _id: false })
export class ClaimEvaluation {
@Prop({ type: String, enum: UserReplyEnum, default: null })
effectedUserReply?: UserReplyEnum | null;
@Prop({ type: ClaimExpertReplySchema })
damageExpertReply?: ClaimExpertReply | null;
@Prop({ type: ClaimExpertReplySchema })
damageExpertReplyFinal?: ClaimExpertReply | null;
@Prop({ type: ClaimResendRequestSchema })
damageExpertResend?: ClaimResendRequest | null;
@Prop({ type: ClaimUserObjectionSchema })
objection?: ClaimUserObjection;
@Prop({ type: ClaimUserResendPayloadSchema })
userResendDocuments?: ClaimUserResendPayload;
@Prop({ type: ClaimFileRatingSchema })
rating?: ClaimFileRating;
@Prop({ type: String })
visitLocation?: string;
@Prop({ type: ClaimPriceDropSchema })
priceDrop?: ClaimPriceDrop;
}
export const ClaimEvaluationSchema =
SchemaFactory.createForClass(ClaimEvaluation);

View File

@@ -0,0 +1,65 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
@Schema({ _id: false })
export class ClaimPlate {
@Prop({ type: Number })
leftDigits?: number;
@Prop({ type: String })
centerAlphabet?: string;
@Prop({ type: Number })
centerDigits?: number;
@Prop({ type: Number })
ir?: number;
@Prop({ type: String })
nationalCode?: string;
}
export const ClaimPlateSchema = SchemaFactory.createForClass(ClaimPlate);
@Schema({ _id: false })
export class ClaimVehicleSnapshot {
@Prop({ type: String })
carName?: string;
@Prop({ type: String })
carModel?: string;
@Prop({ type: String })
carType?: string;
@Prop({ type: Boolean })
isNewCar?: boolean;
@Prop({ type: ClaimPlateSchema })
plate?: ClaimPlate;
}
export const ClaimVehicleSnapshotSchema =
SchemaFactory.createForClass(ClaimVehicleSnapshot);
@Schema({ _id: false })
export class ClaimOwner {
@Prop({ type: Types.ObjectId })
userId?: Types.ObjectId;
@Prop({ type: Types.ObjectId })
userClientKey?: Types.ObjectId;
@Prop({ type: String })
fullName?: string;
}
export const ClaimOwnerSchema = SchemaFactory.createForClass(ClaimOwner);
@Schema({ _id: false })
export class ClaimMoney {
@Prop({ type: String, trim: true })
sheba?: string;
@Prop({ type: String, trim: true })
nationalCodeOfInsurer?: string;
}
export const ClaimMoneySchema = SchemaFactory.createForClass(ClaimMoney);

View File

@@ -0,0 +1,60 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
import { AiImagesModel } from "./ai-image.schema";
import { CarGreenCardModel } from "./car-green-card.schema";
import { ImageRequiredModel } from "./image-required.schema";
@Schema({ _id: false })
export class CapturedImage {
@Prop({ type: String })
path?: string;
@Prop({ type: String })
fileName?: string;
@Prop({ type: String })
url?: string;
@Prop({ type: Date })
capturedAt?: Date;
}
export const CapturedImageSchema = SchemaFactory.createForClass(CapturedImage);
@Schema({ _id: false })
export class ClaimMedia {
@Prop({ type: CarGreenCardModel })
carGreenCard?: CarGreenCardModel;
@Prop({ type: ImageRequiredModel })
imageRequired?: ImageRequiredModel;
@Prop({ type: AiImagesModel })
aiImages?: AiImagesModel;
@Prop({ type: Types.ObjectId })
videoCaptureId?: Types.ObjectId;
/**
* V2: Car angles captures (front, back, left, right)
* Map of angle key to captured image
*/
@Prop({
type: Map,
of: CapturedImageSchema,
default: () => ({}),
})
carAngles?: Map<string, CapturedImage>;
/**
* V2: Damaged parts captures
* Map of part key to captured image
*/
@Prop({
type: Map,
of: CapturedImageSchema,
default: () => ({}),
})
damagedParts?: Map<string, CapturedImage>;
}
export const ClaimMediaSchema = SchemaFactory.createForClass(ClaimMedia);

View File

@@ -0,0 +1,15 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { AccidentInfo, AccidentInfoSchema } from "src/request-management/entities/schema/accidentInformation.type";
import { Party, PartySchema } from "src/request-management/entities/schema/partyRole.enum";
@Schema({ _id: false })
export class ClaimCaseSnapshot {
@Prop({ type: AccidentInfoSchema })
accident?: AccidentInfo;
@Prop({ type: [PartySchema], default: [] })
parties?: Party[];
}
export const ClaimCaseSnapshotSchema =
SchemaFactory.createForClass(ClaimCaseSnapshot);

View File

@@ -0,0 +1,39 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
@Schema({ _id: false })
export class ClaimActorLock {
@Prop({ type: Types.ObjectId })
actorId: Types.ObjectId;
@Prop({ type: String })
actorName: string;
@Prop({ type: String, default: "damage_expert" })
actorRole: "damage_expert" | "expert" | "admin";
}
export const ClaimActorLockSchema = SchemaFactory.createForClass(ClaimActorLock);
@Schema({ _id: false })
export class ClaimWorkflow {
@Prop({ type: String, enum: ClaimWorkflowStep })
currentStep: ClaimWorkflowStep;
@Prop({ type: String, enum: ClaimWorkflowStep })
nextStep: ClaimWorkflowStep;
@Prop({ type: [String], enum: ClaimWorkflowStep, default: [] })
completedSteps?: ClaimWorkflowStep[];
@Prop({ type: Boolean, default: false })
locked?: boolean;
@Prop({ type: Date })
lockedAt?: Date;
@Prop({ type: ClaimActorLockSchema })
lockedBy?: ClaimActorLock;
}
export const ClaimWorkflowSchema = SchemaFactory.createForClass(ClaimWorkflow);

View File

@@ -0,0 +1,147 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument, Schema as MongooseSchema, Types } from "mongoose";
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
import { HistoryEvent, HistoryEventSchema } from "src/request-management/entities/schema/historyEvent.type";
import {
ClaimDamageSelection,
ClaimDamageSelectionSchema,
} from "./claim-case.damage.schema";
import {
ClaimMoney,
ClaimMoneySchema,
ClaimOwner,
ClaimOwnerSchema,
ClaimVehicleSnapshot,
ClaimVehicleSnapshotSchema,
} from "./claim-case.identity.schema";
import { ClaimMedia, ClaimMediaSchema } from "./claim-case.media.schema";
import {
ClaimEvaluation,
ClaimEvaluationSchema,
} from "./claim-case.evaluation.schema";
import {
ClaimCaseSnapshot,
ClaimCaseSnapshotSchema,
} from "./claim-case.snapshot.schema";
import { ClaimWorkflow, ClaimWorkflowSchema } from "./claim-case.workflow.schema";
@Schema({ _id: false })
export class RequiredDocumentRef {
@Prop({ type: Types.ObjectId })
fileId?: Types.ObjectId;
@Prop({ type: String })
filePath?: string;
@Prop({ type: String })
fileName?: string;
@Prop({ type: Boolean, default: true })
uploaded?: boolean;
@Prop({ type: Date })
uploadedAt?: Date;
}
export const RequiredDocumentRefSchema = SchemaFactory.createForClass(RequiredDocumentRef);
@Schema({
collection: "claimCases",
timestamps: true,
id: true,
versionKey: false,
})
export class ClaimCase {
@Prop({ required: true, unique: true, index: true, trim: true })
requestNo: string;
/**
* Human-friendly shared id across blame+claim (e.g. A14235).
* Generated once on blame creation and copied into claim.
*/
@Prop({ required: true, unique: true, index: true, trim: true })
publicId: string;
/**
* Overall case status (user flow + expert flow progression)
*/
@Prop({ required: true, type: String, enum: ClaimCaseStatus, default: ClaimCaseStatus.CREATED })
status: ClaimCaseStatus;
/**
* Claim damage determination status (expert assessment)
*/
@Prop({ type: String, enum: ClaimStatus, default: ClaimStatus.PENDING })
claimStatus: ClaimStatus;
/**
* Link to the blame case that originated this claim.
* IMPORTANT: this is intentionally ONLY an id reference (no embedded blame file).
*/
@Prop({ type: Types.ObjectId, index: true })
blameRequestId?: Types.ObjectId;
@Prop({ type: String, index: true })
blameRequestNo?: string;
@Prop({ type: ClaimWorkflowSchema, default: () => ({}) })
workflow?: ClaimWorkflow;
@Prop({ type: ClaimOwnerSchema, default: () => ({}) })
owner?: ClaimOwner;
@Prop({ type: ClaimVehicleSnapshotSchema, default: () => ({}) })
vehicle?: ClaimVehicleSnapshot;
@Prop({ type: ClaimMoneySchema })
money?: ClaimMoney;
@Prop({ type: Number })
claimNo?: number;
@Prop({ type: Number })
claimId?: number;
@Prop({ type: ClaimDamageSelectionSchema, default: () => ({}) })
damage?: ClaimDamageSelection;
@Prop({ type: ClaimMediaSchema, default: () => ({}) })
media?: ClaimMedia;
@Prop({ type: ClaimEvaluationSchema, default: () => ({}) })
evaluation?: ClaimEvaluation;
/**
* Optional “read-optimized” copy of fields from blame/request side.
* Source of truth remains `blameRequestId`.
*/
@Prop({ type: ClaimCaseSnapshotSchema })
snapshot?: ClaimCaseSnapshot;
/**
* For quick “has user uploaded X?” checks. Values point to docs in the
* `claim-required-documents` collection.
*/
@Prop({
type: Map,
of: RequiredDocumentRefSchema,
default: () => ({}),
})
requiredDocuments?: Map<string, RequiredDocumentRef>;
@Prop({ type: [HistoryEventSchema], default: [] })
history?: HistoryEvent[];
/**
* Legacy fields kept optional to simplify progressive migration.
* If you choose to migrate later, we can remove these.
*/
@Prop({ type: MongooseSchema.Types.Mixed })
legacy?: any;
}
export type ClaimCaseDocument = HydratedDocument<ClaimCase>;
export const ClaimCaseSchema = SchemaFactory.createForClass(ClaimCase);

View File

@@ -241,6 +241,22 @@ export class ClaimRequestManagementModel {
@Prop({ type: RequestManagementModel, unique: true })
blameFile: RequestManagementModel;
/**
* Human-friendly shared id across blame+claim (e.g. A14235).
* Copied from the source blame request.
*
* NOTE: `sparse` keeps existing documents without this field valid.
*/
@Prop({ type: String, unique: true, index: true, sparse: true, trim: true })
publicId?: string;
/**
* Explicit reference to the blame request id (in addition to embedded blameFile).
* This will be the preferred reference going forward.
*/
@Prop({ type: Types.ObjectId, index: true })
blameRequestId?: Types.ObjectId;
@Prop({ type: Number, default: 100 })
requestNumber?: number;