Added sign links and data to expert-claim API

This commit is contained in:
SepehrYahyaee
2026-05-10 12:42:24 +03:30
parent 9c62dc4d3a
commit 82bb232d28
2 changed files with 126 additions and 7 deletions

View File

@@ -120,11 +120,32 @@ export class ClaimDetailV2ResponseDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
'Expert reply payloads (first and/or final) when awaiting factor validation.', 'Slice of `claim.evaluation` exposed to the damage expert. ' +
'`damageExpertReply` / `damageExpertReplyFinal` are returned only while ' +
'awaiting factor validation. `ownerInsurerApproval` and ' +
'`ownerPricedPartsApproval` are returned whenever they exist on the ' +
'claim — each carries `signLink` (resolved from `signDetailId`) so the ' +
'front-end can render the user signature directly. Likewise, ' +
'`damageExpertReply.userComment` / `damageExpertReplyFinal.userComment` ' +
'expose `signLink` when a user comment signature is present.',
}) })
evaluation?: { evaluation?: {
damageExpertReply?: unknown; damageExpertReply?: unknown;
damageExpertReplyFinal?: unknown; damageExpertReplyFinal?: unknown;
ownerInsurerApproval?: {
agree: boolean;
branchId?: string;
signDetailId?: string;
signLink?: string;
signedAt?: Date | string;
};
ownerPricedPartsApproval?: {
agree: boolean;
branchId?: string;
signDetailId?: string;
signLink?: string;
signedAt?: Date | string;
};
}; };
@ApiPropertyOptional({ @ApiPropertyOptional({

View File

@@ -16,6 +16,7 @@ import { distance as stringDistance } from "fastest-levenshtein"; // حتما ن
import { Types } from "mongoose"; import { Types } from "mongoose";
import { lastValueFrom } from "rxjs"; import { lastValueFrom } from "rxjs";
import { ClaimRequestManagementDbService } from "src/claim-request-management/entites/db-service/claim-request-management.db.service"; import { ClaimRequestManagementDbService } from "src/claim-request-management/entites/db-service/claim-request-management.db.service";
import { ClaimSignDbService } from "src/claim-request-management/entites/db-service/claim-sign.db.service";
import { DamageImageDbService } from "src/claim-request-management/entites/db-service/damage-image.db.service"; import { DamageImageDbService } from "src/claim-request-management/entites/db-service/damage-image.db.service";
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";
@@ -216,8 +217,68 @@ export class ExpertClaimService {
private readonly smsOrchestrationService: SmsOrchestrationService, private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly userDbService: UserDbService, private readonly userDbService: UserDbService,
private readonly expertFileActivityDbService: ExpertFileActivityDbService, private readonly expertFileActivityDbService: ExpertFileActivityDbService,
private readonly claimSignDbService: ClaimSignDbService,
) { } ) { }
/**
* Resolve a `claim-sign` document id to a downloadable file URL.
* Mirrors the helper used in `ExpertInsurerService` so signature links are
* exposed identically across the two expert-facing detail endpoints.
*/
private async claimSignLinkFromId(
signDetailId: unknown,
): Promise<string | undefined> {
if (signDetailId == null || signDetailId === "") return undefined;
const id = String(signDetailId);
if (!Types.ObjectId.isValid(id)) return undefined;
const doc = await this.claimSignDbService.findOne({
_id: new Types.ObjectId(id),
});
const path = (doc as any)?.path;
return path ? buildFileLink(String(path)) : undefined;
}
/**
* Build a deep-cloned `evaluation` payload with resolved `signLink`s for the
* fields that carry a user signature:
*
* - `damageExpertReply.userComment.signLink`
* - `damageExpertReplyFinal.userComment.signLink`
* - `ownerInsurerApproval.signLink`
* - `ownerPricedPartsApproval.signLink`
*
* Returns `undefined` when there's no evaluation to enrich.
*/
private async enrichClaimEvaluationForExpert(
evaluation: Record<string, unknown> | undefined | null,
): Promise<Record<string, unknown> | undefined> {
if (!evaluation || typeof evaluation !== "object") return undefined;
const ev = JSON.parse(JSON.stringify(evaluation)) as Record<string, unknown>;
const enrichReplyUserComment = async (key: string) => {
const reply = ev[key] as Record<string, unknown> | undefined;
const uc = reply?.userComment as Record<string, unknown> | undefined;
if (uc?.signDetailId != null) {
const signLink = await this.claimSignLinkFromId(uc.signDetailId);
(reply as Record<string, unknown>).userComment = { ...uc, signLink };
}
};
await enrichReplyUserComment("damageExpertReply");
await enrichReplyUserComment("damageExpertReplyFinal");
const enrichApproval = async (key: string) => {
const o = ev[key] as Record<string, unknown> | undefined;
if (o?.signDetailId != null) {
const signLink = await this.claimSignLinkFromId(o.signDetailId);
ev[key] = { ...o, signLink };
}
};
await enrichApproval("ownerInsurerApproval");
await enrichApproval("ownerPricedPartsApproval");
return ev;
}
/** Load immutable profile fields from `damage-expert` for the acting expert. */ /** Load immutable profile fields from `damage-expert` for the acting expert. */
private async snapshotDamageExpert(actorId: string) { private async snapshotDamageExpert(actorId: string) {
const doc = await this.damageExpertDbService.findById(actorId); const doc = await this.damageExpertDbService.findById(actorId);
@@ -3473,6 +3534,46 @@ export class ExpertClaimService {
) )
: undefined; : undefined;
// Enrich the full evaluation block with `signLink`s for every place a user
// signature is referenced, then assemble the response slice.
//
// We always expose owner approval blocks (when present in the DB) because
// the signature URL is needed across all phases of the claim. The expert
// reply payloads are still gated on `isFactorValidationPending` to match
// the historical contract of this endpoint.
const claimEvaluationRaw = (claim as any).evaluation as
| Record<string, unknown>
| undefined;
const enrichedEvaluation = await this.enrichClaimEvaluationForExpert(
claimEvaluationRaw,
);
let evaluationForApi: Record<string, unknown> | undefined;
if (enrichedEvaluation) {
evaluationForApi = {};
if (isFactorValidationPending) {
if (enrichedEvaluation.damageExpertReply !== undefined) {
evaluationForApi.damageExpertReply =
enrichedEvaluation.damageExpertReply;
}
if (enrichedEvaluation.damageExpertReplyFinal !== undefined) {
evaluationForApi.damageExpertReplyFinal =
enrichedEvaluation.damageExpertReplyFinal;
}
}
if (enrichedEvaluation.ownerInsurerApproval !== undefined) {
evaluationForApi.ownerInsurerApproval =
enrichedEvaluation.ownerInsurerApproval;
}
if (enrichedEvaluation.ownerPricedPartsApproval !== undefined) {
evaluationForApi.ownerPricedPartsApproval =
enrichedEvaluation.ownerPricedPartsApproval;
}
if (Object.keys(evaluationForApi).length === 0) {
evaluationForApi = undefined;
}
}
return { return {
claimRequestId: claim._id.toString(), claimRequestId: claim._id.toString(),
publicId: claim.publicId, publicId: claim.publicId,
@@ -3511,12 +3612,9 @@ export class ExpertClaimService {
carAngles, carAngles,
damagedParts, damagedParts,
awaitingFactorValidation: isFactorValidationPending, awaitingFactorValidation: isFactorValidationPending,
evaluation: isFactorValidationPending evaluation: evaluationForApi as
? { | ClaimDetailV2ResponseDto["evaluation"]
damageExpertReply: (claim as any).evaluation?.damageExpertReply, | undefined,
damageExpertReplyFinal: (claim as any).evaluation?.damageExpertReplyFinal,
}
: undefined,
objection, objection,
videoCapture, videoCapture,
blameCase: blameCaseForApi, blameCase: blameCaseForApi,