1
0
forked from Yara724/api

Compare commits

...

3 Commits

5 changed files with 393 additions and 8 deletions

View File

@@ -98,6 +98,37 @@ export class ClaimDetailV2ResponseDto {
damageExpertReplyFinal?: unknown; 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() @ApiProperty()
createdAt: string; createdAt: string;

View File

@@ -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 { 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 { ClientDbService } from "src/client/entities/db-service/client.db.service";
import { buildFileLink } from "src/helpers/urlCreator"; 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 { 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 { 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"; 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 { ClaimSubmitResendV2Dto } from "./dto/expert-claim-v2.dto";
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service"; import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { FactorValidationV2Dto } from "./dto/factor-validation.dto"; import { FactorValidationV2Dto } from "./dto/factor-validation.dto";
import {
buildMutualAgreementExpertDecision,
enrichBlamePartiesForAgreementView,
} from "src/helpers/blame-party-agreement-decision";
@Injectable() @Injectable()
export class ExpertClaimService { export class ExpertClaimService {
@@ -2410,6 +2415,119 @@ export class ExpertClaimService {
return { list, total: list.length }; 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 * 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; let vehiclePayload = claim.vehicle as any;
const hasClaimVehicle = const hasClaimVehicle =
vehiclePayload && vehiclePayload &&
@@ -2508,23 +2629,73 @@ export class ExpertClaimService {
vehiclePayload.plate); vehiclePayload.plate);
let blameLean: any = null; let blameLean: any = null;
if (claim.blameRequestId) {
const rows = (await this.blameRequestDbService.find( // 2. Video capture and blame case db queries from "stashed"
{ _id: new Types.ObjectId(claim.blameRequestId.toString()) }, const videoCaptureIdRaw = (claim.media as any)?.videoCaptureId;
{ lean: true, select: "type parties expert.decision" }, const blameRequestIdStr = claim.blameRequestId?.toString();
)) as any[];
blameLean = rows[0] ?? null; 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) { if (!hasClaimVehicle && blameLean) {
const fromBlame = this.damagedPartyVehicleFromBlame(blameLean, claim); const fromBlame = this.damagedPartyVehicleFromBlame(blameLean, claim);
if (fromBlame) vehiclePayload = fromBlame; if (fromBlame) vehiclePayload = fromBlame;
} }
// 3. Blame file context logic from "upstream"
const blameFileContext = blameLean const blameFileContext = blameLean
? this.blameFileContextForExpert(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 { return {
claimRequestId: claim._id.toString(), claimRequestId: claim._id.toString(),
publicId: claim.publicId, publicId: claim.publicId,
@@ -2550,6 +2721,7 @@ export class ExpertClaimService {
...blameFileContext, ...blameFileContext,
blameRequestId: claim.blameRequestId?.toString(), blameRequestId: claim.blameRequestId?.toString(),
blameRequestNo: claim.blameRequestNo, blameRequestNo: claim.blameRequestNo,
money: moneyPayload,
selectedParts: claim.damage?.selectedParts, selectedParts: claim.damage?.selectedParts,
otherParts: claim.damage?.otherParts, otherParts: claim.damage?.otherParts,
requiredDocuments: requiredDocuments:
@@ -2565,6 +2737,9 @@ export class ExpertClaimService {
damageExpertReplyFinal: (claim as any).evaluation?.damageExpertReplyFinal, damageExpertReplyFinal: (claim as any).evaluation?.damageExpertReplyFinal,
} }
: undefined, : undefined,
videoCapture,
blameCase: blameCase ?? undefined,
blameExpertDecision,
createdAt: (claim as any).createdAt, createdAt: (claim as any).createdAt,
updatedAt: (claim as any).updatedAt, updatedAt: (claim as any).updatedAt,
}; };

View File

@@ -48,7 +48,7 @@ export class ExpertClaimV2Controller {
@ApiOperation({ @ApiOperation({
summary: "Get claim request detail for damage expert", summary: "Get claim request detail for damage expert",
description: 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" }) @ApiParam({ name: "claimRequestId" })
async getClaimDetailV2( async getClaimDetailV2(

View 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;
}

View File

@@ -75,6 +75,10 @@ import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.
import { UploadContext } from "./entities/schema/blame-voice.schema"; import { UploadContext } from "./entities/schema/blame-voice.schema";
import { HashService } from "src/utils/hash/hash.service"; import { HashService } from "src/utils/hash/hash.service";
import { UserAuthService } from "src/auth/auth-services/user.auth.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() @Injectable()
export class RequestManagementService { export class RequestManagementService {
@@ -1157,6 +1161,22 @@ export class RequestManagementService {
await this.advanceWorkflowToNext(req, stepKey); 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 = []; if (!Array.isArray(req.history)) req.history = [];
req.history.push({ req.history.push({
type: `${stepKey}_SUBMITTED`, type: `${stepKey}_SUBMITTED`,
@@ -4435,8 +4455,18 @@ export class RequestManagementService {
formData.guiltyPartyPhoneNumber === formData.firstPartyPhoneNumber formData.guiltyPartyPhoneNumber === formData.firstPartyPhoneNumber
? String(firstPartyUserId) ? String(firstPartyUserId)
: String(secondPartyUserId); : String(secondPartyUserId);
const damagedPhone =
formData.guiltyPartyPhoneNumber === formData.firstPartyPhoneNumber
? formData.secondParty.phoneNumber
: formData.firstPartyPhoneNumber;
if (!req.expert) req.expert = {} as any; 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 = { req.workflow = {
currentStep: WorkflowStep.FIRST_LOCATION as any, currentStep: WorkflowStep.FIRST_LOCATION as any,
nextStep: WorkflowStep.SECOND_LOCATION as any, nextStep: WorkflowStep.SECOND_LOCATION as any,