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);