From 5d2227b00b7d72fba27eea75fde2a66ab1bd8183 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sat, 25 Apr 2026 16:53:56 +0330 Subject: [PATCH 1/2] YARA-836 + 2 more sms --- src/ai/ai.service.ts | 2 +- src/expert-blame/expert-blame.service.ts | 47 ++++++++++++++- src/expert-claim/expert-claim.module.ts | 2 + src/expert-claim/expert-claim.service.ts | 59 ++++++++++++++++++- src/main.ts | 7 ++- .../provider/sms-gateway.service.ts | 1 + .../sms-orchestration.service.ts | 42 +++++++++++++ 7 files changed, 156 insertions(+), 4 deletions(-) diff --git a/src/ai/ai.service.ts b/src/ai/ai.service.ts index 1eff579..759ab79 100644 --- a/src/ai/ai.service.ts +++ b/src/ai/ai.service.ts @@ -25,7 +25,7 @@ export class AiService implements OnModuleInit { username: process.env.AI_USERNAME, password: process.env.AI_PASSWORD, }, - timeout: 30000, // 30 second timeout + timeout: 1000, // 30 second timeout }; private get profileOptions(): AxiosRequestConfig { diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index 3f16faa..ca6496a 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -1122,7 +1122,31 @@ export class ExpertBlameService { requestId, ); - // TODO: Send notifications to parties (SMS/Push) about required documents + const requestIdToken = String(request._id); + const partyPhoneByUserId = new Map(); + for (const p of request.parties || []) { + const uid = p?.person?.userId ? String(p.person.userId) : ""; + const phone = p?.person?.phoneNumber; + if (uid && typeof phone === "string" && phone.length > 0) { + partyPhoneByUserId.set(uid, phone); + } + } + for (const p of partyResendRequests) { + const uid = String((p as any).partyId); + const phone = partyPhoneByUserId.get(uid); + if (!phone) continue; + const role = + String(request.parties?.find((x: any) => String(x?.person?.userId) === uid)?.role) === + "SECOND" + ? "SECOND" + : "FIRST"; + await this.smsOrchestrationService.sendResendDocumentsNotice({ + receptor: phone, + fileKind: "blame", + publicId: request.publicId, + link: this.smsOrchestrationService.buildBlamePartyLink(requestIdToken, role), + }); + } return { requestId: String(request._id), @@ -1277,6 +1301,27 @@ export class ExpertBlameService { ); } + if (request.type === BlameRequestType.THIRD_PARTY) { + const requestIdToken = String(request._id); + const expertLastName = + actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس"; + for (const p of request.parties || []) { + const phone = p?.person?.phoneNumber; + if (!phone || typeof phone !== "string") continue; + const role = String(p?.role) === "SECOND" ? "SECOND" : "FIRST"; + await this.smsOrchestrationService.sendSignatureReviewNotice({ + receptor: phone, + fileKind: "blame", + publicId: request.publicId, + expertLastName, + link: this.smsOrchestrationService.buildBlamePartyLink( + requestIdToken, + role, + ), + }); + } + } + return { requestId: String(request._id), status: CaseStatus.WAITING_FOR_SIGNATURES, diff --git a/src/expert-claim/expert-claim.module.ts b/src/expert-claim/expert-claim.module.ts index 7aa2009..faa8631 100644 --- a/src/expert-claim/expert-claim.module.ts +++ b/src/expert-claim/expert-claim.module.ts @@ -13,6 +13,7 @@ import { ClientModule } from "src/client/client.module"; import { ExpertBlameModule } from "src/expert-blame/expert-blame.module"; import { RequestManagementModule } from "src/request-management/request-management.module"; import { SandHubModule } from "src/sand-hub/sand-hub.module"; +import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module"; import { UsersModule } from "src/users/users.module"; import { ExpertClaimController } from "./expert-claim.controller"; import { ExpertClaimV2Controller } from "./expert-claim.v2.controller"; @@ -31,6 +32,7 @@ import { ExpertClaimService } from "./expert-claim.service"; ExpertBlameModule, AiModule, RequestManagementModule, + SmsOrchestrationModule, UsersModule, ClientModule, ], diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 8029fe4..f05980d 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -65,6 +65,7 @@ import { import { resendRequestHasPayload } from "src/helpers/claim-expert-resend"; import { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot"; import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema"; +import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service"; @Injectable() export class ExpertClaimService { @@ -187,6 +188,7 @@ export class ExpertClaimService { private readonly claimFactorsImageDbService: ClaimFactorsImageDbService, private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService, private readonly branchDbService: BranchDbService, + private readonly smsOrchestrationService: SmsOrchestrationService, ) { } /** Load immutable profile fields from `damage-expert` for the acting expert. */ @@ -282,13 +284,16 @@ export class ExpertClaimService { private blameFileContextForExpert(blame: any): { blameRequestType?: BlameRequestType; carBodyFirstForm?: { car?: boolean; object?: boolean }; + blameStatus?: string } { if (!blame?.type) return {}; const blameRequestType = blame.type as BlameRequestType; const out: { blameRequestType?: BlameRequestType; carBodyFirstForm?: { car?: boolean; object?: boolean }; + blameStatus?: string; } = { blameRequestType }; + out.blameStatus = blame.blameStatus; if (blameRequestType !== BlameRequestType.CAR_BODY) return out; const parties = blame.parties; @@ -297,6 +302,7 @@ export class ExpertClaimService { const first = parties.find((p: any) => p?.role === PartyRole.FIRST) ?? parties[0]; const cbf = first?.carBodyFirstForm; + delete out.blameStatus if (cbf && typeof cbf === "object") { out.carBodyFirstForm = { ...(typeof cbf.car === "boolean" ? { car: cbf.car } : {}), @@ -2219,6 +2225,30 @@ export class ExpertClaimService { }, }); + if (claim.blameRequestId) { + const blame = await this.blameRequestDbService.findById( + claim.blameRequestId.toString(), + ); + if (blame?.type === BlameRequestType.THIRD_PARTY) { + const ownerUserId = claim.owner?.userId ? String(claim.owner.userId) : ""; + const ownerParty = (blame.parties || []).find( + (p: any) => + p?.person?.userId && String(p.person.userId) === ownerUserId, + ); + const ownerPhone = ownerParty?.person?.phoneNumber; + if (ownerPhone && typeof ownerPhone === "string") { + await this.smsOrchestrationService.sendResendDocumentsNotice({ + receptor: ownerPhone, + fileKind: "claim", + publicId: claim.publicId, + link: this.smsOrchestrationService.buildClaimLink( + String(claim._id), + ), + }); + } + } + } + return { claimRequestId, status: ClaimCaseStatus.WAITING_FOR_USER_RESEND, @@ -2371,6 +2401,33 @@ export class ExpertClaimService { await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updatePayload); + if (!needsFactorUpload && claim.blameRequestId) { + const blame = await this.blameRequestDbService.findById( + claim.blameRequestId.toString(), + ); + if (blame?.type === BlameRequestType.THIRD_PARTY) { + const ownerUserId = claim.owner?.userId ? String(claim.owner.userId) : ""; + const ownerParty = (blame.parties || []).find( + (p: any) => + p?.person?.userId && String(p.person.userId) === ownerUserId, + ); + const ownerPhone = ownerParty?.person?.phoneNumber; + if (ownerPhone && typeof ownerPhone === "string") { + const expertLastName = + actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس"; + await this.smsOrchestrationService.sendSignatureReviewNotice({ + receptor: ownerPhone, + fileKind: "claim", + publicId: claim.publicId, + expertLastName, + link: this.smsOrchestrationService.buildClaimLink( + String(claim._id), + ), + }); + } + } + } + return { claimRequestId, status: nextCaseStatus, @@ -2519,7 +2576,7 @@ export class ExpertClaimService { blameIds.length > 0 ? ((await this.blameRequestDbService.find( { _id: { $in: blameIds.map((id) => new Types.ObjectId(id)) } }, - { lean: true, select: "type parties expert.decision" }, + { lean: true, select: "type parties expert.decision blameStatus" }, )) as any[]) : []; const blameById = new Map( diff --git a/src/main.ts b/src/main.ts index d5379b8..3e66699 100644 --- a/src/main.ts +++ b/src/main.ts @@ -38,7 +38,12 @@ async function bootstrap() { .addBearerAuth() .build(); const docs = SwaggerModule.createDocument(app, config); - SwaggerModule.setup("/docs", app, docs); + SwaggerModule.setup("/docs", app, docs, { + swaggerOptions: { + persistAuthorization: true, + docExpansion: "none", + }, + }); await app.listen(process.env.PORT); } diff --git a/src/sms-orchestration/provider/sms-gateway.service.ts b/src/sms-orchestration/provider/sms-gateway.service.ts index 90cd7a8..df6e1d3 100644 --- a/src/sms-orchestration/provider/sms-gateway.service.ts +++ b/src/sms-orchestration/provider/sms-gateway.service.ts @@ -22,6 +22,7 @@ export interface VerifyLookUpMessage { token: string; token2?: string; token3?: string; + token10?: string; receptor: string; } diff --git a/src/sms-orchestration/sms-orchestration.service.ts b/src/sms-orchestration/sms-orchestration.service.ts index 4731fff..23a42e7 100644 --- a/src/sms-orchestration/sms-orchestration.service.ts +++ b/src/sms-orchestration/sms-orchestration.service.ts @@ -10,6 +10,7 @@ type TemplateArgs = { token: string; token2?: string; token3?: string; + token10?: string; }; @Injectable() @@ -40,6 +41,15 @@ export class SmsOrchestrationService implements OnModuleInit { return `${process.env.URL}/${frontendRoute}?token=${requestId}`; } + buildBlamePartyLink(requestId: string, partyRole: "FIRST" | "SECOND"): string { + const route = partyRole === "SECOND" ? "user2" : "user"; + return `${process.env.URL}/${route}?token=${requestId}`; + } + + buildClaimLink(claimRequestId: string): string { + return `${process.env.URL}/claim?token=${claimRequestId}`; + } + async sendInviteLink(phoneNumber: string, link: string): Promise { return this.sendTemplate({ template: "yara724-invite-link", @@ -124,6 +134,38 @@ export class SmsOrchestrationService implements OnModuleInit { }); } + async sendResendDocumentsNotice(params: { + receptor: string; + fileKind: "blame" | "claim"; + publicId: string; + link: string; + }): Promise { + return this.sendTemplate({ + template: "yara-resend-documents", + receptor: params.receptor, + token: params.fileKind === "blame" ? "تصادف" : "خسارت", + token2: params.publicId, + token3: params.link, + }); + } + + async sendSignatureReviewNotice(params: { + receptor: string; + fileKind: "blame" | "claim"; + publicId: string; + expertLastName: string; + link: string; + }): Promise { + return this.sendTemplate({ + template: "yara-signature", + receptor: params.receptor, + token: params.fileKind === "blame" ? "تصادف" : "خسارت", + token2: params.publicId, + token3: params.expertLastName || "کارشناس", + token10: params.link, + }); + } + private async sendTemplate(args: TemplateArgs): Promise { try { await this.smsGatewayService.verifyLookUp(args); From 05c5b70b4dd4c61426bff2e995b1493c017cf851 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sat, 25 Apr 2026 17:10:07 +0330 Subject: [PATCH 2/2] YARA-837 --- src/ai/ai.service.ts | 2 +- .../claim-request-management.service.ts | 31 ++++++++++++++++++- .../dto/claim-details-v2.dto.ts | 11 ++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/ai/ai.service.ts b/src/ai/ai.service.ts index 759ab79..1eff579 100644 --- a/src/ai/ai.service.ts +++ b/src/ai/ai.service.ts @@ -25,7 +25,7 @@ export class AiService implements OnModuleInit { username: process.env.AI_USERNAME, password: process.env.AI_PASSWORD, }, - timeout: 1000, // 30 second timeout + timeout: 30000, // 30 second timeout }; private get profileOptions(): AxiosRequestConfig { diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 720e997..02852c1 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -5009,8 +5009,30 @@ export class ClaimRequestManagementService { }; } + const getPartLabelFa = (key: string): string => { + const labels: Record = { + hood: 'کاپوت', + trunk: 'صندوق عقب', + roof: 'سقف', + front_right_door: 'درب جلو راست', + front_left_door: 'درب جلو چپ', + rear_right_door: 'درب عقب راست', + rear_left_door: 'درب عقب چپ', + front_bumper: 'سپر جلو', + rear_bumper: 'سپر عقب', + front_right_fender: 'گلگیر جلو راست', + front_left_fender: 'گلگیر جلو چپ', + rear_right_fender: 'گلگیر عقب راست', + rear_left_fender: 'گلگیر عقب چپ', + }; + return labels[key] || key; + }; + const damagedPartsData = claim.media?.damagedParts as any; - const damagedParts: Record = {}; + const damagedParts: Record< + string, + { label_fa: string; captured: boolean; url?: string } + > = {}; const resendPartKeys = normalizeResendPartKeys( claim.evaluation?.damageExpertResend?.resendCarParts, ); @@ -5021,6 +5043,7 @@ export class ClaimRequestManagementService { for (const p of partKeysForDetails) { const cap = hasCapture(damagedPartsData, p); damagedParts[p] = { + label_fa: getPartLabelFa(p), captured: !!cap, url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined), }; @@ -5080,6 +5103,12 @@ export class ClaimRequestManagementService { carAngles, damagedParts, expertResend, + evaluation: claim.evaluation + ? { + damageExpertReply: claim.evaluation.damageExpertReply, + damageExpertReplyFinal: claim.evaluation.damageExpertReplyFinal, + } + : undefined, ...(isExpertViewer ? { userRating: claim.userRating 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 4e942b0..2bbfe75 100644 --- a/src/claim-request-management/dto/claim-details-v2.dto.ts +++ b/src/claim-request-management/dto/claim-details-v2.dto.ts @@ -76,7 +76,7 @@ export class ClaimDetailsV2ResponseDto { carAngles?: Record; @ApiPropertyOptional({ description: 'Damaged parts captured' }) - damagedParts?: Record; + damagedParts?: Record; @ApiPropertyOptional({ description: @@ -84,6 +84,15 @@ export class ClaimDetailsV2ResponseDto { }) expertResend?: ExpertResendDetailsV2Dto; + @ApiPropertyOptional({ + description: "Damage expert opinion(s): initial and final (after objection).", + type: Object, + }) + evaluation?: { + damageExpertReply?: unknown; + damageExpertReplyFinal?: unknown; + }; + @ApiPropertyOptional({ description: 'User satisfaction rating (if submitted)' }) userRating?: { progressSpeed: number;