forked from Yara724/api
add blame in claim details plus auto expert decision bug fixed
This commit is contained in:
@@ -98,6 +98,37 @@ export class ClaimDetailV2ResponseDto {
|
||||
damageExpertReplyFinal?: unknown;
|
||||
};
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Car walk-around video from `claim-video-capture` (resolved from `media.videoCaptureId`).',
|
||||
})
|
||||
videoCapture?: {
|
||||
id: string;
|
||||
url?: string;
|
||||
path?: string;
|
||||
fileName?: string;
|
||||
};
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Linked blame case (`blameCases`), same shape as expert-blame detail: parties with video/voice URLs, workflow, expert, formatted dates.',
|
||||
})
|
||||
blameCase?: Record<string, unknown>;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Same as `blameCase.expert.decision` when present: guilty party id, description, accident fields, decidedAt.',
|
||||
})
|
||||
blameExpertDecision?: Record<string, unknown>;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Claim payout / banking metadata from the claim file (e.g. Sheba).',
|
||||
})
|
||||
money?: {
|
||||
sheba?: string;
|
||||
nationalCodeOfInsurer?: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
createdAt: string;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { DamageImageDbService } from "src/claim-request-management/entites/db-se
|
||||
import { VideoCaptureDbService } from "src/claim-request-management/entites/db-service/video-capture.db.service";
|
||||
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
|
||||
import { buildFileLink } from "src/helpers/urlCreator";
|
||||
import { toJalaliDateAndTime } from "src/helpers/date-jalali";
|
||||
import { BlameDocumentDbService } from "src/request-management/entities/db-service/blame-document.db.service";
|
||||
import { BlameVideoDbService } from "src/request-management/entities/db-service/blame-video.db.service";
|
||||
import { BlameVoiceDbService } from "src/request-management/entities/db-service/blame.voice.db.service";
|
||||
@@ -53,6 +54,10 @@ import { BranchDbService } from "src/client/entities/db-service/branch.db.servic
|
||||
import { ClaimSubmitResendV2Dto } from "./dto/expert-claim-v2.dto";
|
||||
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||
import { FactorValidationV2Dto } from "./dto/factor-validation.dto";
|
||||
import {
|
||||
buildMutualAgreementExpertDecision,
|
||||
enrichBlamePartiesForAgreementView,
|
||||
} from "src/helpers/blame-party-agreement-decision";
|
||||
|
||||
@Injectable()
|
||||
export class ExpertClaimService {
|
||||
@@ -2410,6 +2415,119 @@ export class ExpertClaimService {
|
||||
return { list, total: list.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load linked blame case for damage-expert claim detail (same enrichment as expert-blame `findOneV2`:
|
||||
* party evidence `videoUrl` / `voiceUrls`, Jalali date strings).
|
||||
*/
|
||||
private async buildBlameCaseSnapshotForClaimDetail(
|
||||
blameRequestId: string,
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
const doc = await this.blameRequestDbService.findByIdWithoutHistory(
|
||||
blameRequestId,
|
||||
);
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const docRec = doc as Record<string, unknown>;
|
||||
const built = buildMutualAgreementExpertDecision(docRec);
|
||||
const existingDecision = (docRec.expert as Record<string, unknown> | undefined)
|
||||
?.decision as Record<string, unknown> | undefined;
|
||||
if (built && !existingDecision?.guiltyPartyId) {
|
||||
const prevExpert =
|
||||
typeof docRec.expert === "object" && docRec.expert !== null
|
||||
? { ...(docRec.expert as Record<string, unknown>) }
|
||||
: {};
|
||||
const expert = { ...prevExpert, decision: built };
|
||||
await this.blameRequestDbService.findByIdAndUpdate(blameRequestId, {
|
||||
$set: { expert },
|
||||
});
|
||||
docRec.expert = expert;
|
||||
}
|
||||
|
||||
const parties = (doc.parties ?? []) as Array<{
|
||||
evidence?: {
|
||||
videoId?: string | number;
|
||||
voices?: (string | number)[];
|
||||
videoUrl?: string;
|
||||
voiceUrls?: string[];
|
||||
};
|
||||
}>;
|
||||
|
||||
for (const party of parties) {
|
||||
if (!party.evidence) continue;
|
||||
const evidence = party.evidence as Record<string, unknown>;
|
||||
|
||||
if (evidence.videoId) {
|
||||
const videoDoc = await this.blameVideoDbService.findById(
|
||||
String(evidence.videoId),
|
||||
);
|
||||
if (videoDoc?.path) {
|
||||
evidence.videoUrl = buildFileLink(videoDoc.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (evidence.voices && Array.isArray(evidence.voices)) {
|
||||
const voiceUrls: string[] = [];
|
||||
for (const voiceId of evidence.voices) {
|
||||
const voiceDoc = await this.blameVoiceDbService.findById(
|
||||
String(voiceId),
|
||||
);
|
||||
if (voiceDoc?.path) {
|
||||
voiceUrls.push(buildFileLink(voiceDoc.path));
|
||||
}
|
||||
}
|
||||
evidence.voiceUrls = voiceUrls;
|
||||
}
|
||||
}
|
||||
|
||||
const createdAt = doc.createdAt
|
||||
? new Date(doc.createdAt as string | number)
|
||||
: new Date();
|
||||
const updatedAt = doc.updatedAt
|
||||
? new Date(doc.updatedAt as string | number)
|
||||
: new Date();
|
||||
const [createdDate, createdTime] = toJalaliDateAndTime(createdAt);
|
||||
const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt);
|
||||
(doc as Record<string, unknown>).createdAtFormatted =
|
||||
`${createdDate} ${createdTime}`;
|
||||
(doc as Record<string, unknown>).updatedAtFormatted =
|
||||
`${updatedDate} ${updatedTime}`;
|
||||
|
||||
docRec.parties = enrichBlamePartiesForAgreementView(docRec);
|
||||
|
||||
return docRec;
|
||||
}
|
||||
|
||||
/** JSON-safe `expert.decision` for API responses (ObjectIds → string). */
|
||||
private serializeBlameExpertDecisionForClaimDetail(
|
||||
decision: unknown,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!decision || typeof decision !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const d = decision as Record<string, unknown>;
|
||||
const guilty = d.guiltyPartyId;
|
||||
const decidedBy = d.decidedByExpertId;
|
||||
const decidedAt = d.decidedAt;
|
||||
return {
|
||||
...d,
|
||||
guiltyPartyId:
|
||||
guilty != null && typeof (guilty as { toString?: () => string }).toString === "function"
|
||||
? String(guilty)
|
||||
: guilty,
|
||||
decidedByExpertId:
|
||||
decidedBy != null &&
|
||||
typeof (decidedBy as { toString?: () => string }).toString === "function"
|
||||
? String(decidedBy)
|
||||
: decidedBy,
|
||||
decidedAt:
|
||||
decidedAt instanceof Date
|
||||
? decidedAt.toISOString()
|
||||
: decidedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Get claim detail for damage expert
|
||||
*
|
||||
@@ -2499,6 +2617,9 @@ export class ExpertClaimService {
|
||||
};
|
||||
}
|
||||
|
||||
// --- Combine both branches of the conflict, preserving necessary logic from both ---
|
||||
|
||||
// 1. Vehicle payload logic from "upstream"
|
||||
let vehiclePayload = claim.vehicle as any;
|
||||
const hasClaimVehicle =
|
||||
vehiclePayload &&
|
||||
@@ -2508,23 +2629,73 @@ export class ExpertClaimService {
|
||||
vehiclePayload.plate);
|
||||
|
||||
let blameLean: any = null;
|
||||
if (claim.blameRequestId) {
|
||||
const rows = (await this.blameRequestDbService.find(
|
||||
{ _id: new Types.ObjectId(claim.blameRequestId.toString()) },
|
||||
{ lean: true, select: "type parties expert.decision" },
|
||||
)) as any[];
|
||||
blameLean = rows[0] ?? null;
|
||||
|
||||
// 2. Video capture and blame case db queries from "stashed"
|
||||
const videoCaptureIdRaw = (claim.media as any)?.videoCaptureId;
|
||||
const blameRequestIdStr = claim.blameRequestId?.toString();
|
||||
|
||||
const [
|
||||
videoCaptureRow,
|
||||
blameCase,
|
||||
blameLeanRows
|
||||
] = await Promise.all([
|
||||
videoCaptureIdRaw
|
||||
? this.claimVideoCaptureDbService.findById(String(videoCaptureIdRaw))
|
||||
: Promise.resolve(null),
|
||||
blameRequestIdStr
|
||||
? this.buildBlameCaseSnapshotForClaimDetail(blameRequestIdStr)
|
||||
: Promise.resolve(null),
|
||||
claim.blameRequestId
|
||||
? this.blameRequestDbService.find(
|
||||
{ _id: new Types.ObjectId(claim.blameRequestId.toString()) },
|
||||
{ lean: true, select: "type parties expert.decision" },
|
||||
)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
if (Array.isArray(blameLeanRows) && blameLeanRows.length) {
|
||||
blameLean = blameLeanRows[0] ?? null;
|
||||
}
|
||||
|
||||
// If claim vehicle not found and blameLean exists, replace vehiclePayload
|
||||
if (!hasClaimVehicle && blameLean) {
|
||||
const fromBlame = this.damagedPartyVehicleFromBlame(blameLean, claim);
|
||||
if (fromBlame) vehiclePayload = fromBlame;
|
||||
}
|
||||
|
||||
// 3. Blame file context logic from "upstream"
|
||||
const blameFileContext = blameLean
|
||||
? this.blameFileContextForExpert(blameLean)
|
||||
: {};
|
||||
|
||||
// 4. videoCapture logic from "stashed"
|
||||
let videoCapture: ClaimDetailV2ResponseDto['videoCapture'] = undefined;
|
||||
if (videoCaptureRow) {
|
||||
const vc = videoCaptureRow as any;
|
||||
videoCapture = {
|
||||
id: String(vc._id ?? videoCaptureIdRaw),
|
||||
fileName: vc.fileName,
|
||||
path: vc.path,
|
||||
url: vc.path ? buildFileLink(vc.path) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const blameExpertDecision = blameCase
|
||||
? this.serializeBlameExpertDecisionForClaimDetail(
|
||||
(blameCase.expert as Record<string, unknown> | undefined)?.decision,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const money = (claim as { money?: { sheba?: string; nationalCodeOfInsurer?: string } })
|
||||
.money;
|
||||
const moneyPayload =
|
||||
money && (money.sheba != null || money.nationalCodeOfInsurer != null)
|
||||
? {
|
||||
sheba: money.sheba,
|
||||
nationalCodeOfInsurer: money.nationalCodeOfInsurer,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
claimRequestId: claim._id.toString(),
|
||||
publicId: claim.publicId,
|
||||
@@ -2550,6 +2721,7 @@ export class ExpertClaimService {
|
||||
...blameFileContext,
|
||||
blameRequestId: claim.blameRequestId?.toString(),
|
||||
blameRequestNo: claim.blameRequestNo,
|
||||
money: moneyPayload,
|
||||
selectedParts: claim.damage?.selectedParts,
|
||||
otherParts: claim.damage?.otherParts,
|
||||
requiredDocuments:
|
||||
@@ -2565,6 +2737,9 @@ export class ExpertClaimService {
|
||||
damageExpertReplyFinal: (claim as any).evaluation?.damageExpertReplyFinal,
|
||||
}
|
||||
: undefined,
|
||||
videoCapture,
|
||||
blameCase: blameCase ?? undefined,
|
||||
blameExpertDecision,
|
||||
createdAt: (claim as any).createdAt,
|
||||
updatedAt: (claim as any).updatedAt,
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ export class ExpertClaimV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "Get claim request detail for damage expert",
|
||||
description:
|
||||
"Returns full claim details including captured images, required documents status, and damage selections. Allowed when status is WAITING_FOR_DAMAGE_EXPERT (if locked, only the locking expert) or when awaiting factor validation (WAITING_FOR_INSURER_APPROVAL + UNDER_REVIEW + EXPERT_COST_EVALUATION); in the latter case `evaluation` includes the active expert reply for factors.",
|
||||
"Returns full claim details including captured images, required documents, damage selections, `videoCapture` (from claim-video-capture via media.videoCaptureId), and `blameCase` (linked blameCases document with party video/voice URLs like expert-blame detail). Allowed when status is WAITING_FOR_DAMAGE_EXPERT (if locked, only the locking expert) or when awaiting factor validation.",
|
||||
})
|
||||
@ApiParam({ name: "claimRequestId" })
|
||||
async getClaimDetailV2(
|
||||
|
||||
149
src/helpers/blame-party-agreement-decision.ts
Normal file
149
src/helpers/blame-party-agreement-decision.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Types } from "mongoose";
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum";
|
||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||
|
||||
/** Default accident taxonomy when guilt is established by mutual party agreement (no field-expert decision). */
|
||||
export const MUTUAL_AGREEMENT_EXPERT_DECISION_FIELDS = {
|
||||
accidentWay: { id: 8, label: "جلو به عقب" },
|
||||
accidentReason: {
|
||||
id: 1,
|
||||
fanavaran: 1,
|
||||
label:
|
||||
"عدم رعايت فاصله مناسب با وسيله نقليه و يا عدم توجه به جلو",
|
||||
},
|
||||
accidentType: {
|
||||
id: 1,
|
||||
label: "برخورد یک وسیله نقلیه با یک وسیله نقلیه",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type MutualAgreementDecisionPayload = {
|
||||
guiltyPartyId: Types.ObjectId;
|
||||
description: string;
|
||||
decidedAt: Date;
|
||||
decidedByExpertId?: Types.ObjectId;
|
||||
fields: typeof MUTUAL_AGREEMENT_EXPERT_DECISION_FIELDS;
|
||||
};
|
||||
|
||||
function partyPhone(p: { person?: { phoneNumber?: string } } | undefined): string {
|
||||
return (p?.person?.phoneNumber || "").trim();
|
||||
}
|
||||
|
||||
function partyUserId(
|
||||
p: { person?: { userId?: Types.ObjectId } } | undefined,
|
||||
): Types.ObjectId | null {
|
||||
const id = p?.person?.userId;
|
||||
if (!id) return null;
|
||||
return id instanceof Types.ObjectId ? id : new Types.ObjectId(String(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* When the first party chose AGREED (guilty or damaged) and the second party exists,
|
||||
* derive the guilty party and build the synthetic expert.decision payload.
|
||||
* Does not set decidedByExpertId — this is not a field-expert adjudication.
|
||||
*/
|
||||
export function buildMutualAgreementExpertDecision(
|
||||
doc: Record<string, unknown>,
|
||||
): MutualAgreementDecisionPayload | null {
|
||||
if (doc.type !== BlameRequestType.THIRD_PARTY) return null;
|
||||
if (doc.blameStatus !== BlameStatus.AGREED) return null;
|
||||
|
||||
const parties = (doc.parties ?? []) as Array<{
|
||||
role?: string;
|
||||
person?: { userId?: Types.ObjectId; phoneNumber?: string };
|
||||
statement?: {
|
||||
admitsGuilt?: boolean;
|
||||
claimsDamage?: boolean;
|
||||
acceptsExpertOpinion?: boolean;
|
||||
};
|
||||
}>;
|
||||
|
||||
const first = parties.find((p) => p.role === PartyRole.FIRST);
|
||||
const second = parties.find((p) => p.role === PartyRole.SECOND);
|
||||
if (!first || !second) return null;
|
||||
|
||||
const st = first.statement;
|
||||
if (!st) return null;
|
||||
if (st.acceptsExpertOpinion) return null;
|
||||
|
||||
const admits = !!st.admitsGuilt;
|
||||
const claims = !!st.claimsDamage;
|
||||
if (admits === claims) return null;
|
||||
|
||||
let guiltyParty: typeof first;
|
||||
let damagedParty: typeof first;
|
||||
if (admits) {
|
||||
guiltyParty = first;
|
||||
damagedParty = second;
|
||||
} else {
|
||||
guiltyParty = second;
|
||||
damagedParty = first;
|
||||
}
|
||||
|
||||
const guiltyId = partyUserId(guiltyParty);
|
||||
if (!guiltyId) return null;
|
||||
|
||||
const gPhone = partyPhone(guiltyParty);
|
||||
const dPhone = partyPhone(damagedParty);
|
||||
if (!gPhone || !dPhone) return null;
|
||||
|
||||
const description = `با توافق طرفین مقصر و زیان دیده مشخص شد. کاربر ${gPhone} مقصر و کاربر ${dPhone} زیان دیده می باشد.`;
|
||||
|
||||
return {
|
||||
guiltyPartyId: guiltyId,
|
||||
description,
|
||||
decidedAt: new Date(),
|
||||
fields: MUTUAL_AGREEMENT_EXPERT_DECISION_FIELDS,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge complementary statement flags onto the second party for expert/claim UIs when
|
||||
* only the first party submitted a formal confession (V2 remote flow).
|
||||
*/
|
||||
export function enrichBlamePartiesForAgreementView(
|
||||
doc: Record<string, unknown>,
|
||||
): Array<Record<string, unknown>> {
|
||||
const parties = (doc.parties ?? []) as Array<Record<string, unknown>>;
|
||||
if (!Array.isArray(parties) || parties.length === 0) return parties;
|
||||
|
||||
const first = parties.find((p) => p.role === PartyRole.FIRST) as
|
||||
| {
|
||||
statement?: Record<string, unknown>;
|
||||
}
|
||||
| undefined;
|
||||
const secondIdx = parties.findIndex((p) => p.role === PartyRole.SECOND);
|
||||
if (!first?.statement || secondIdx === -1) return parties;
|
||||
|
||||
if (doc.blameStatus !== BlameStatus.AGREED) return parties;
|
||||
|
||||
const st = first.statement as {
|
||||
admitsGuilt?: boolean;
|
||||
claimsDamage?: boolean;
|
||||
acceptsExpertOpinion?: boolean;
|
||||
};
|
||||
if (st.acceptsExpertOpinion) return parties;
|
||||
|
||||
const admits = !!st.admitsGuilt;
|
||||
const claims = !!st.claimsDamage;
|
||||
if (admits === claims) return parties;
|
||||
|
||||
const second = parties[secondIdx] as {
|
||||
statement?: Record<string, unknown>;
|
||||
};
|
||||
const prev = (second.statement ?? {}) as Record<string, unknown>;
|
||||
const mergedStatement = {
|
||||
...prev,
|
||||
admitsGuilt: admits ? false : true,
|
||||
claimsDamage: admits ? true : false,
|
||||
acceptsExpertOpinion: false,
|
||||
};
|
||||
|
||||
const next = [...parties];
|
||||
next[secondIdx] = {
|
||||
...second,
|
||||
statement: mergedStatement,
|
||||
};
|
||||
return next;
|
||||
}
|
||||
@@ -75,6 +75,10 @@ import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.
|
||||
import { UploadContext } from "./entities/schema/blame-voice.schema";
|
||||
import { HashService } from "src/utils/hash/hash.service";
|
||||
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
|
||||
import {
|
||||
buildMutualAgreementExpertDecision,
|
||||
MUTUAL_AGREEMENT_EXPERT_DECISION_FIELDS,
|
||||
} from "src/helpers/blame-party-agreement-decision";
|
||||
|
||||
@Injectable()
|
||||
export class RequestManagementService {
|
||||
@@ -1157,6 +1161,22 @@ export class RequestManagementService {
|
||||
|
||||
await this.advanceWorkflowToNext(req, stepKey);
|
||||
|
||||
if (
|
||||
stepKey === WorkflowStep.SECOND_DESCRIPTION &&
|
||||
req.type === BlameRequestType.THIRD_PARTY &&
|
||||
!(req as any).expert?.decision?.guiltyPartyId
|
||||
) {
|
||||
const built = buildMutualAgreementExpertDecision(
|
||||
(req as any).toObject
|
||||
? (req as any).toObject()
|
||||
: { ...req },
|
||||
);
|
||||
if (built) {
|
||||
if (!(req as any).expert) (req as any).expert = {};
|
||||
(req as any).expert.decision = built as any;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: `${stepKey}_SUBMITTED`,
|
||||
@@ -4435,8 +4455,18 @@ export class RequestManagementService {
|
||||
formData.guiltyPartyPhoneNumber === formData.firstPartyPhoneNumber
|
||||
? String(firstPartyUserId)
|
||||
: String(secondPartyUserId);
|
||||
const damagedPhone =
|
||||
formData.guiltyPartyPhoneNumber === formData.firstPartyPhoneNumber
|
||||
? formData.secondParty.phoneNumber
|
||||
: formData.firstPartyPhoneNumber;
|
||||
if (!req.expert) req.expert = {} as any;
|
||||
req.expert.decision = { guiltyPartyId: new Types.ObjectId(guiltyPartyId) } as any;
|
||||
req.expert.decision = {
|
||||
guiltyPartyId: new Types.ObjectId(guiltyPartyId),
|
||||
description: `با توافق طرفین مقصر و زیان دیده مشخص شد. کاربر ${formData.guiltyPartyPhoneNumber} مقصر و کاربر ${damagedPhone} زیان دیده می باشد.`,
|
||||
decidedAt: new Date(),
|
||||
decidedByExpertId: new Types.ObjectId(expert.sub),
|
||||
fields: { ...MUTUAL_AGREEMENT_EXPERT_DECISION_FIELDS },
|
||||
} as any;
|
||||
req.workflow = {
|
||||
currentStep: WorkflowStep.FIRST_LOCATION as any,
|
||||
nextStep: WorkflowStep.SECOND_LOCATION as any,
|
||||
|
||||
Reference in New Issue
Block a user