diff --git a/src/expert-claim/dto/claim-detail-v2.dto.ts b/src/expert-claim/dto/claim-detail-v2.dto.ts index 29ad993..77f03e2 100644 --- a/src/expert-claim/dto/claim-detail-v2.dto.ts +++ b/src/expert-claim/dto/claim-detail-v2.dto.ts @@ -120,11 +120,32 @@ export class ClaimDetailV2ResponseDto { @ApiPropertyOptional({ 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?: { damageExpertReply?: 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({ diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index cca21af..cd75ca8 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -16,6 +16,7 @@ import { distance as stringDistance } from "fastest-levenshtein"; // حتما ن import { Types } from "mongoose"; import { lastValueFrom } from "rxjs"; 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 { VideoCaptureDbService } from "src/claim-request-management/entites/db-service/video-capture.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 userDbService: UserDbService, 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 { + 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 | undefined | null, + ): Promise | undefined> { + if (!evaluation || typeof evaluation !== "object") return undefined; + const ev = JSON.parse(JSON.stringify(evaluation)) as Record; + + const enrichReplyUserComment = async (key: string) => { + const reply = ev[key] as Record | undefined; + const uc = reply?.userComment as Record | undefined; + if (uc?.signDetailId != null) { + const signLink = await this.claimSignLinkFromId(uc.signDetailId); + (reply as Record).userComment = { ...uc, signLink }; + } + }; + await enrichReplyUserComment("damageExpertReply"); + await enrichReplyUserComment("damageExpertReplyFinal"); + + const enrichApproval = async (key: string) => { + const o = ev[key] as Record | 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. */ private async snapshotDamageExpert(actorId: string) { const doc = await this.damageExpertDbService.findById(actorId); @@ -3473,6 +3534,46 @@ export class ExpertClaimService { ) : 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 + | undefined; + const enrichedEvaluation = await this.enrichClaimEvaluationForExpert( + claimEvaluationRaw, + ); + + let evaluationForApi: Record | 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 { claimRequestId: claim._id.toString(), publicId: claim.publicId, @@ -3511,12 +3612,9 @@ export class ExpertClaimService { carAngles, damagedParts, awaitingFactorValidation: isFactorValidationPending, - evaluation: isFactorValidationPending - ? { - damageExpertReply: (claim as any).evaluation?.damageExpertReply, - damageExpertReplyFinal: (claim as any).evaluation?.damageExpertReplyFinal, - } - : undefined, + evaluation: evaluationForApi as + | ClaimDetailV2ResponseDto["evaluation"] + | undefined, objection, videoCapture, blameCase: blameCaseForApi,