diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 94962d1..ab7f247 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -4987,14 +4987,14 @@ export class ClaimRequestManagementService { data && (data instanceof Map ? data.get(key) : data[key]); const requiredDocs = claim.requiredDocuments as any; - const requiredDocumentsStatus: Record = {}; + const requiredDocumentsStatus: Record = {}; if (requiredDocs) { const keys = requiredDocs instanceof Map ? Array.from(requiredDocs.keys()) : Object.keys(requiredDocs); for (const k of keys) { const doc = requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k]; requiredDocumentsStatus[k] = { uploaded: !!doc?.uploaded, - fileId: doc?.fileId?.toString(), + fileUrl: doc?.filePath ? buildFileLink(doc.filePath) : undefined, }; } } @@ -5041,6 +5041,25 @@ export class ClaimRequestManagementService { s ? s.replace(/^(.{4})(.*)(.{4})$/, 'IR$1************$3') : undefined; const maskNationalCode = (s?: string) => s ? s.replace(/^(.{2})(.*)(.{2})$/, '$1******$3') : undefined; + const isExpertViewer = actor?.role === RoleEnum.FIELD_EXPERT; + const ownerData = claim.owner + ? { + userId: claim.owner.userId?.toString(), + fullName: claim.owner.fullName, + } + : undefined; + const moneyForUser = claim.money + ? { + sheba: maskSheba(claim.money.sheba), + nationalCodeOfOwner: maskNationalCode(claim.money.nationalCodeOfInsurer), + } + : undefined; + const moneyForExpert = claim.money + ? { + sheba: claim.money.sheba, + nationalCodeOfOwner: claim.money.nationalCodeOfInsurer, + } + : undefined; return { claimRequestId: claim._id.toString(), @@ -5052,34 +5071,28 @@ export class ClaimRequestManagementService { nextStep: claim.workflow?.nextStep, blameRequestId: claim.blameRequestId?.toString(), blameRequestNo: claim.blameRequestNo, - owner: claim.owner - ? { - userId: claim.owner.userId?.toString(), - fullName: claim.owner.fullName, - } - : undefined, + ...(isExpertViewer ? { owner: ownerData } : {}), vehicle: claim.vehicle, selectedParts: claim.damage?.selectedParts, otherParts: claim.damage?.otherParts, - money: claim.money - ? { - sheba: maskSheba(claim.money.sheba), - nationalCodeOfOwner: maskNationalCode(claim.money.nationalCodeOfInsurer), - } - : undefined, + money: isExpertViewer ? moneyForExpert : moneyForUser, requiredDocuments: Object.keys(requiredDocumentsStatus).length > 0 ? requiredDocumentsStatus : undefined, carAngles, damagedParts, expertResend, - userRating: claim.userRating + ...(isExpertViewer ? { - progressSpeed: claim.userRating.progressSpeed, - registrationEase: claim.userRating.registrationEase, - overallEvaluation: claim.userRating.overallEvaluation, - comment: claim.userRating.comment, - createdAt: claim.userRating.createdAt, + userRating: claim.userRating + ? { + progressSpeed: claim.userRating.progressSpeed, + registrationEase: claim.userRating.registrationEase, + overallEvaluation: claim.userRating.overallEvaluation, + comment: claim.userRating.comment, + createdAt: claim.userRating.createdAt, + } + : undefined, } - : undefined, + : {}), createdAt: (claim as any).createdAt, updatedAt: (claim as any).updatedAt, }; diff --git a/src/claim-request-management/claim-request-management.v2.controller.ts b/src/claim-request-management/claim-request-management.v2.controller.ts index 17afc98..d2b3874 100644 --- a/src/claim-request-management/claim-request-management.v2.controller.ts +++ b/src/claim-request-management/claim-request-management.v2.controller.ts @@ -75,7 +75,8 @@ export class ClaimRequestManagementV2Controller { }) @ApiOperation({ summary: "Get Claim Details (V2)", - description: "Get full details of a claim request. Only the claim owner can access.", + description: + "Get claim details for current actor. USER receives only minimal own-needed payload (no extra owner/security fields). FIELD_EXPERT keeps full view for expert workflows.", }) @ApiResponse({ status: 200, @@ -560,8 +561,7 @@ Returns status of each item (uploaded/captured or not). description: ` **Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim) -**Upload one of the required documents** (13 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements): -- car_green_card +**Upload one of the required documents** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements): - damaged_driving_license_front/back - damaged_chassis_number, damaged_engine_photo - damaged_car_card_front/back, damaged_metal_plate diff --git a/src/claim-request-management/dto/claim-details-v2.dto.ts b/src/claim-request-management/dto/claim-details-v2.dto.ts index 0ae0e91..4e942b0 100644 --- a/src/claim-request-management/dto/claim-details-v2.dto.ts +++ b/src/claim-request-management/dto/claim-details-v2.dto.ts @@ -69,8 +69,8 @@ export class ClaimDetailsV2ResponseDto { nationalCodeOfOwner?: string; }; - @ApiPropertyOptional({ description: 'Required documents status' }) - requiredDocuments?: Record; + @ApiPropertyOptional({ description: 'Required documents status (link instead of id)' }) + requiredDocuments?: Record; @ApiPropertyOptional({ description: 'Car angles captured' }) carAngles?: Record; diff --git a/src/helpers/mongoose-fa-timestamps.plugin.ts b/src/helpers/mongoose-fa-timestamps.plugin.ts index 47f56c5..4241cfc 100644 --- a/src/helpers/mongoose-fa-timestamps.plugin.ts +++ b/src/helpers/mongoose-fa-timestamps.plugin.ts @@ -46,15 +46,24 @@ function writeFaTimestampsOnUpdate(query: Query): void { update.$set = {}; } - if (update.$set.createdAtFa == null && update.createdAtFa == null) { - update.$set.createdAtFa = nowFa; - } update.$set.updatedAtFa = nowFa; + + // Keep createdAtFa immutable on regular updates; set only for upsert inserts. + const opts = (query as any).getOptions?.() ?? {}; + if (opts.upsert) { + if (!update.$setOnInsert || typeof update.$setOnInsert !== "object") { + update.$setOnInsert = {}; + } + if (update.$setOnInsert.createdAtFa == null) { + update.$setOnInsert.createdAtFa = nowFa; + } + } + query.setUpdate(update); } /** - * Adds `createdAtFa` / `updatedAtFa` (Iran timezone, UTC+03:30 offset string) + * Adds `createdAtFa` / `updatedAtFa` (Iran timezone tuple: [date, time]) * to all schemas that use mongoose timestamps. */ export function applyIranFaTimestampPlugin(connection: Connection): void { diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index e8a30a6..c1caa70 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -79,6 +79,7 @@ import { buildMutualAgreementExpertDecision, MUTUAL_AGREEMENT_EXPERT_DECISION_FIELDS, } from "src/helpers/blame-party-agreement-decision"; +import { buildFileLink } from "src/helpers/urlCreator"; @Injectable() export class RequestManagementService { @@ -4117,6 +4118,133 @@ export class RequestManagementService { return none; } + /** Party payload for user blame detail: no national codes, licenses, phone, birthdays, or vehicle inquiry blobs. */ + private async sanitizePartyForBlameUserView( + party: any, + ): Promise> { + if (!party || typeof party !== "object") { + return {}; + } + const person = party.person; + const safePerson = person + ? { + userId: + person.userId != null ? String(person.userId) : undefined, + fullName: person.fullName, + clientId: + person.clientId != null ? String(person.clientId) : undefined, + } + : undefined; + const vehicle = party.vehicle + ? { + plateId: party.vehicle.plateId, + name: party.vehicle.name, + model: party.vehicle.model, + type: party.vehicle.type, + isNew: party.vehicle.isNew, + } + : undefined; + const evidenceRaw = party.evidence as Record | undefined; + const evidence: Record | undefined = evidenceRaw + ? { ...evidenceRaw } + : undefined; + if (evidence) { + if (party.role === PartyRole.FIRST && evidence.videoId) { + const videoDoc = await this.blameVideoDbService.findById( + String(evidence.videoId), + ); + if (videoDoc?.path) { + evidence.videoUrl = buildFileLink(videoDoc.path); + } + } + delete evidence.videoId; + + const voiceIds = evidence.voices; + if (Array.isArray(voiceIds)) { + const voiceUrls: string[] = []; + for (const voiceId of voiceIds) { + const voiceDoc = await this.blameVoiceDbService.findById( + String(voiceId), + ); + if (voiceDoc?.path) { + voiceUrls.push(buildFileLink(voiceDoc.path)); + } + } + evidence.voiceUrls = voiceUrls; + } + delete evidence.voices; + } + + return { + role: party.role, + person: safePerson, + carBodyFirstForm: party.carBodyFirstForm, + location: party.location, + vehicle, + insurance: party.insurance, + statement: party.statement, + evidence, + confirmation: party.confirmation, + }; + } + + private async resolveBlameExpertDisplayNameFromPlain( + expert: Record, + ): Promise { + const decision = expert.decision as Record | undefined; + const decided = decision?.decidedByExpertId; + const assigned = expert.assignedExpertId; + const raw = decided ?? assigned; + if (raw == null || raw === "") return undefined; + const sid = String(raw); + if (!Types.ObjectId.isValid(sid)) return undefined; + const doc = await this.expertDbService.findOne({ + _id: new Types.ObjectId(sid), + }); + if (!doc) return undefined; + return `${doc.firstName || ""} ${doc.lastName || ""}`.trim() || undefined; + } + + /** Expert subdocument for user view: string ids, trimmed snapshot (name only), plus `expertName`. */ + private async buildExpertForBlameUserView( + expertRaw: any, + ): Promise | undefined> { + if (!expertRaw || typeof expertRaw !== "object") return undefined; + const out = JSON.parse(JSON.stringify(expertRaw)) as Record; + const decision = out.decision as Record | undefined; + let expertName: string | undefined; + + if (decision) { + if (decision.guiltyPartyId != null) { + decision.guiltyPartyId = String(decision.guiltyPartyId); + } + if (decision.decidedByExpertId != null) { + decision.decidedByExpertId = String(decision.decidedByExpertId); + } + const snap = decision.expertProfileSnapshot as + | Record + | undefined; + if (snap) { + expertName = + `${snap.firstName ?? ""} ${snap.lastName ?? ""}`.trim() || undefined; + decision.expertProfileSnapshot = { + firstName: snap.firstName, + lastName: snap.lastName, + }; + } + } + + if (out.assignedExpertId != null) { + out.assignedExpertId = String(out.assignedExpertId); + } + + if (!expertName) { + expertName = await this.resolveBlameExpertDisplayNameFromPlain(out); + } + + return { ...out, expertName }; + } + /** * V2: Get one blame request by id. Access allowed if current user is a party (by userId or phone) * or the initiating field expert. For expert-initiated LINK, party access by phone is sufficient. @@ -4165,7 +4293,36 @@ export class RequestManagementService { typeof (req as any).toObject === "function" ? (req as any).toObject({ versionKey: false }) : { ...(req as any) }; - return { ...plain, claimCreation }; + + const allParties = Array.isArray(plain.parties) ? plain.parties : []; + const visibleParties = + isFieldExpertOwner || isRegistrarOwner + ? allParties + : allParties.filter((p: any) => { + const pid = p?.person?.userId ? String(p.person.userId) : null; + const phone = p?.person?.phoneNumber; + return ( + (pid && pid === String(user?.sub)) || + (phone && phone === user?.username) + ); + }); + const parties = await Promise.all( + visibleParties.map((p: any) => this.sanitizePartyForBlameUserView(p)), + ); + const expert = await this.buildExpertForBlameUserView(plain.expert); + + return { + requestNo: plain.requestNo, + publicId: plain.publicId, + type: plain.type, + status: plain.status, + blameStatus: plain.blameStatus, + workflow: plain.workflow, + parties, + expert, + carBodyInsuranceDetail: plain.carBodyInsuranceDetail, + claimCreation, + }; } /** diff --git a/src/request-management/request-management.v2.controller.ts b/src/request-management/request-management.v2.controller.ts index f4b9bf3..b3f2bd3 100644 --- a/src/request-management/request-management.v2.controller.ts +++ b/src/request-management/request-management.v2.controller.ts @@ -81,7 +81,7 @@ export class RequestManagementV2Controller { @ApiOperation({ summary: "Get one blame request (v2)", description: - "Response is the blame document plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }) — computed for the current user so the app can route the damaged party to create-claim when the blame is COMPLETED and no v2 claim exists yet.", + "Returns a minimal, user-safe payload: requestNo, publicId, type, status, blameStatus, workflow, parties (PII stripped), expert (with **expertName**), carBodyInsuranceDetail, plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }). History and other internal fields are omitted.", }) @ApiParam({ name: "requestId", description: "Blame request ID" }) @Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)