1
0
forked from Yara724/api

Added SMS for Link flow of FIELD-EXPERT: YARA-802

This commit is contained in:
SepehrYahyaee
2026-04-22 11:13:47 +03:30
parent 3c15901ecc
commit 8284aba825
4 changed files with 70 additions and 20 deletions

View File

@@ -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;
}

View File

@@ -35,6 +35,7 @@ import { ExpertUploadPartySignatureDto } from "./dto/expert-upload-party-signatu
import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto"; import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
import { SendPartyOtpsDto } from "./dto/send-party-otps.dto"; import { SendPartyOtpsDto } from "./dto/send-party-otps.dto";
import { ExpertCompleteLocationV2Dto } from "./dto/expert-complete-location.v2.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 { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
import { PartyRole } from "./entities/schema/partyRole.enum"; import { PartyRole } from "./entities/schema/partyRole.enum";
@@ -156,12 +157,13 @@ export class ExpertInitiatedV2Controller {
@ApiOperation({ @ApiOperation({
summary: "[V2] Send blame link to party/parties (LINK)", summary: "[V2] Send blame link to party/parties (LINK)",
description: 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" }) @ApiParam({ name: "requestId", description: "Blame request ID" })
@ApiBody({ type: SendExpertInitiatedLinkV2Dto })
@ApiResponse({ @ApiResponse({
status: 200, 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: { schema: {
type: "object", type: "object",
properties: { properties: {
@@ -174,6 +176,7 @@ export class ExpertInitiatedV2Controller {
properties: { properties: {
role: { type: "string", enum: ["FIRST", "SECOND"] }, role: { type: "string", enum: ["FIRST", "SECOND"] },
phoneNumber: { type: "string" }, phoneNumber: { type: "string" },
smsSent: { type: "boolean" },
}, },
}, },
}, },
@@ -183,8 +186,9 @@ export class ExpertInitiatedV2Controller {
async sendLinkV2( async sendLinkV2(
@CurrentUser() expert: any, @CurrentUser() expert: any,
@Param("requestId") requestId: string, @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") @Post("send-party-otps/:requestId")

View File

@@ -3741,10 +3741,11 @@ export class RequestManagementService {
async sendLinkV2( async sendLinkV2(
expert: any, expert: any,
requestId: string, requestId: string,
dto: { frontendRoute: string },
): Promise<{ ): Promise<{
sent: boolean; sent: boolean;
linkUrl: string; linkUrl: string;
sentTo: { role: string; phoneNumber: string }[]; sentTo: { role: string; phoneNumber: string; smsSent: boolean }[];
}> { }> {
const req = await this.blameRequestDbService.findById(requestId); const req = await this.blameRequestDbService.findById(requestId);
if (!req) throw new NotFoundException("Request not found"); if (!req) throw new NotFoundException("Request not found");
@@ -3755,29 +3756,55 @@ export class RequestManagementService {
} }
this.verifyExpertAccessForBlameV2(req, expert); this.verifyExpertAccessForBlameV2(req, expert);
const baseUrl = if (!dto?.frontendRoute) {
process.env.FRONTEND_BASE_URL || process.env.APP_URL || ""; throw new BadRequestException("frontendRoute is required");
const linkUrl = baseUrl }
? `${baseUrl.replace(/\/$/, "")}/blame/${requestId}` 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<boolean> => {
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); const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
if (firstIdx !== -1 && req.parties[firstIdx]?.person?.phoneNumber) { if (firstIdx !== -1 && req.parties[firstIdx]?.person?.phoneNumber) {
const phone = req.parties[firstIdx].person.phoneNumber; const phone = req.parties[firstIdx].person.phoneNumber;
this.logger.log( const smsSent = await sendSms(phone, PartyRole.FIRST);
`[MOCK SMS] Would send link to first party ${phone}: ${linkUrl}`, sentTo.push({ role: PartyRole.FIRST, phoneNumber: phone, smsSent });
);
sentTo.push({ role: PartyRole.FIRST, phoneNumber: phone });
} }
if (req.type === BlameRequestType.THIRD_PARTY) { if (req.type === BlameRequestType.THIRD_PARTY) {
const secondIdx = this.getPartyIndex(req, PartyRole.SECOND); const secondIdx = this.getPartyIndex(req, PartyRole.SECOND);
if (secondIdx !== -1 && req.parties[secondIdx]?.person?.phoneNumber) { if (secondIdx !== -1 && req.parties[secondIdx]?.person?.phoneNumber) {
const phone = req.parties[secondIdx].person.phoneNumber; const phone = req.parties[secondIdx].person.phoneNumber;
this.logger.log( const smsSent = await sendSms(phone, PartyRole.SECOND);
`[MOCK SMS] Would send link to second party ${phone}: ${linkUrl}`, sentTo.push({ role: PartyRole.SECOND, phoneNumber: phone, smsSent });
);
sentTo.push({ role: PartyRole.SECOND, phoneNumber: phone });
} }
} }
@@ -3789,11 +3816,15 @@ export class RequestManagementService {
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
actorType: "field_expert", actorType: "field_expert",
}, },
metadata: { linkUrl, sentTo }, metadata: { linkUrl, sentTo, template, typeToken, frontendRoute: dto.frontendRoute },
}); });
await (req as any).save(); await (req as any).save();
return { sent: true, linkUrl, sentTo }; return {
sent: sentTo.length > 0 && sentTo.every((x) => x.smsSent),
linkUrl,
sentTo,
};
} }
/** /**

View File

@@ -21,6 +21,8 @@ export interface SendMessage {
export interface VerifyLookUpMessage { export interface VerifyLookUpMessage {
template: string; template: string;
token: string; token: string;
token2?: string;
token3?: string;
receptor: string; receptor: string;
} }