From 8284aba8259a41cd119c1ac1f3b642ce2fe34577 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Wed, 22 Apr 2026 11:13:47 +0330 Subject: [PATCH] Added SMS for Link flow of FIELD-EXPERT: YARA-802 --- .../dto/send-expert-initiated-link.v2.dto.ts | 13 ++++ .../expert-initiated.v2.controller.ts | 10 ++- .../request-management.service.ts | 65 ++++++++++++++----- src/utils/sms-manager/sms-manager.service.ts | 2 + 4 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 src/request-management/dto/send-expert-initiated-link.v2.dto.ts diff --git a/src/request-management/dto/send-expert-initiated-link.v2.dto.ts b/src/request-management/dto/send-expert-initiated-link.v2.dto.ts new file mode 100644 index 0000000..508e1e3 --- /dev/null +++ b/src/request-management/dto/send-expert-initiated-link.v2.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; + +export class SendExpertInitiatedLinkV2Dto { + @ApiProperty({ + description: + "Frontend route path (same as add-second-party flow), e.g. requestManagement/firstParty", + example: "requestManagement/firstParty", + }) + @IsString() + @IsNotEmpty() + frontendRoute: string; +} diff --git a/src/request-management/expert-initiated.v2.controller.ts b/src/request-management/expert-initiated.v2.controller.ts index 5b6b169..fd4ae4a 100644 --- a/src/request-management/expert-initiated.v2.controller.ts +++ b/src/request-management/expert-initiated.v2.controller.ts @@ -35,6 +35,7 @@ import { ExpertUploadPartySignatureDto } from "./dto/expert-upload-party-signatu import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto"; import { SendPartyOtpsDto } from "./dto/send-party-otps.dto"; import { ExpertCompleteLocationV2Dto } from "./dto/expert-complete-location.v2.dto"; +import { SendExpertInitiatedLinkV2Dto } from "./dto/send-expert-initiated-link.v2.dto"; import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service"; import { PartyRole } from "./entities/schema/partyRole.enum"; @@ -156,12 +157,13 @@ export class ExpertInitiatedV2Controller { @ApiOperation({ summary: "[V2] Send blame link to party/parties (LINK)", description: - "For expert-initiated LINK files only. Sends the blame link to the first party (and second party for THIRD_PARTY). SMS delivery is mocked for now; first party opens the link and fills the form via the normal flow. Call after create when creationMethod is LINK.", + "For expert-initiated LINK files only. Sends SMS with template `yara-field-expert-link` to first party (and second party for THIRD_PARTY). Tokens: token=#1(بدنه/ثالث), token2=#2(expert name), token3=#3(invite link built exactly like add-second-party flow: `${URL}/{frontendRoute}?token={requestId}`).", }) @ApiParam({ name: "requestId", description: "Blame request ID" }) + @ApiBody({ type: SendExpertInitiatedLinkV2Dto }) @ApiResponse({ status: 200, - description: "Link sent (mocked); recipients can open the link to fill the form", + description: "Link sent; recipients can open and fill via normal user flow", schema: { type: "object", properties: { @@ -174,6 +176,7 @@ export class ExpertInitiatedV2Controller { properties: { role: { type: "string", enum: ["FIRST", "SECOND"] }, phoneNumber: { type: "string" }, + smsSent: { type: "boolean" }, }, }, }, @@ -183,8 +186,9 @@ export class ExpertInitiatedV2Controller { async sendLinkV2( @CurrentUser() expert: any, @Param("requestId") requestId: string, + @Body() dto: SendExpertInitiatedLinkV2Dto, ) { - return this.requestManagementService.sendLinkV2(expert, requestId); + return this.requestManagementService.sendLinkV2(expert, requestId, dto); } @Post("send-party-otps/:requestId") diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index c1caa70..9d7de28 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -3741,10 +3741,11 @@ export class RequestManagementService { async sendLinkV2( expert: any, requestId: string, + dto: { frontendRoute: string }, ): Promise<{ sent: boolean; linkUrl: string; - sentTo: { role: string; phoneNumber: string }[]; + sentTo: { role: string; phoneNumber: string; smsSent: boolean }[]; }> { const req = await this.blameRequestDbService.findById(requestId); if (!req) throw new NotFoundException("Request not found"); @@ -3755,29 +3756,55 @@ export class RequestManagementService { } this.verifyExpertAccessForBlameV2(req, expert); - const baseUrl = - process.env.FRONTEND_BASE_URL || process.env.APP_URL || ""; - const linkUrl = baseUrl - ? `${baseUrl.replace(/\/$/, "")}/blame/${requestId}` - : ""; + if (!dto?.frontendRoute) { + throw new BadRequestException("frontendRoute is required"); + } + if (!process.env.URL) { + throw new InternalServerErrorException( + "URL environment variable is not configured", + ); + } + const linkUrl = `${process.env.URL}/${dto.frontendRoute}?token=${requestId}`; + const typeToken = + req.type === BlameRequestType.CAR_BODY ? "بدنه" : "ثالث"; + const expertName = + `${expert?.lastName || ""}`.trim() || "کارشناس"; + + const sentTo: { role: string; phoneNumber: string; smsSent: boolean }[] = []; + const template = "yara-field-expert-link"; + const sendSms = async (phone: string, role: PartyRole): Promise => { + try { + await this.smsManagerService.verifyLookUp({ + template, + receptor: phone, + token: typeToken, + token2: expertName, + token3: linkUrl, + }); + this.logger.log( + `[SMS] Field-expert link sent role=${role} receptor=${phone} template=${template}`, + ); + return true; + } catch (er) { + this.logger.error( + `[SMS] Field-expert link failed role=${role} receptor=${phone}: ${describeSmsError(er)}`, + ); + return false; + } + }; - const sentTo: { role: string; phoneNumber: string }[] = []; const firstIdx = this.getPartyIndex(req, PartyRole.FIRST); if (firstIdx !== -1 && req.parties[firstIdx]?.person?.phoneNumber) { const phone = req.parties[firstIdx].person.phoneNumber; - this.logger.log( - `[MOCK SMS] Would send link to first party ${phone}: ${linkUrl}`, - ); - sentTo.push({ role: PartyRole.FIRST, phoneNumber: phone }); + const smsSent = await sendSms(phone, PartyRole.FIRST); + sentTo.push({ role: PartyRole.FIRST, phoneNumber: phone, smsSent }); } if (req.type === BlameRequestType.THIRD_PARTY) { const secondIdx = this.getPartyIndex(req, PartyRole.SECOND); if (secondIdx !== -1 && req.parties[secondIdx]?.person?.phoneNumber) { const phone = req.parties[secondIdx].person.phoneNumber; - this.logger.log( - `[MOCK SMS] Would send link to second party ${phone}: ${linkUrl}`, - ); - sentTo.push({ role: PartyRole.SECOND, phoneNumber: phone }); + const smsSent = await sendSms(phone, PartyRole.SECOND); + sentTo.push({ role: PartyRole.SECOND, phoneNumber: phone, smsSent }); } } @@ -3789,11 +3816,15 @@ export class RequestManagementService { actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), actorType: "field_expert", }, - metadata: { linkUrl, sentTo }, + metadata: { linkUrl, sentTo, template, typeToken, frontendRoute: dto.frontendRoute }, }); await (req as any).save(); - return { sent: true, linkUrl, sentTo }; + return { + sent: sentTo.length > 0 && sentTo.every((x) => x.smsSent), + linkUrl, + sentTo, + }; } /** diff --git a/src/utils/sms-manager/sms-manager.service.ts b/src/utils/sms-manager/sms-manager.service.ts index dbc96fe..fd31b73 100644 --- a/src/utils/sms-manager/sms-manager.service.ts +++ b/src/utils/sms-manager/sms-manager.service.ts @@ -21,6 +21,8 @@ export interface SendMessage { export interface VerifyLookUpMessage { template: string; token: string; + token2?: string; + token3?: string; receptor: string; }