forked from Yara724/api
Merge pull request 'Added SMS for Link flow of FIELD-EXPERT: YARA-802' (#35) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#35
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user