1
0
forked from Yara724/api
This commit is contained in:
SepehrYahyaee
2026-05-23 14:05:11 +03:30
parent 39c4855b95
commit 94bde88cb6
11 changed files with 338 additions and 126 deletions

View File

@@ -0,0 +1,98 @@
import { KavenegarService } from "@fraybabak/kavenegar_nest";
import { Injectable, Logger } from "@nestjs/common";
import {
isKavenegarSuccess,
KavenegarNormalized,
normalizeKavenegarBody,
safeJsonStringify,
unwrapKavenegarTransportError,
} from "./kavenegar-response";
import {
SmsProviderException,
SmsTransportException,
} from "./sms-provider.exception";
import { SendMessage, VerifyLookUpMessage } from "./sms-gateway.types";
@Injectable()
export class KavenegarSmsGateway {
private readonly logger = new Logger(KavenegarSmsGateway.name);
constructor(private readonly sender: KavenegarService) {}
async sendMessage(data: SendMessage) {
try {
const body = await this.sender.Send({
sender: "10008663",
...data,
});
if (!isKavenegarSuccess(body)) {
const normalized = normalizeKavenegarBody(body);
this.logProviderRejection("send", data.receptor, undefined, normalized);
throw new SmsProviderException("send", {
receptor: data.receptor,
providerBody: body,
normalized,
});
}
this.logger.log(
`Kavenegar Send ok receptor=${data.receptor} status=200`,
);
return body;
} catch (e) {
if (e instanceof SmsProviderException) throw e;
const detail = safeJsonStringify(unwrapKavenegarTransportError(e));
this.logger.error(
`Kavenegar Send transport error receptor=${data.receptor} detail=${detail}`,
);
throw new SmsTransportException("send", { receptor: data.receptor }, e);
}
}
async verifyLookUp(data: VerifyLookUpMessage) {
try {
const body = await this.sender.verifyLookup(data);
if (!isKavenegarSuccess(body)) {
const normalized = normalizeKavenegarBody(body);
this.logProviderRejection(
"verifyLookUp",
data.receptor,
data.template,
normalized,
);
throw new SmsProviderException("verifyLookup", {
receptor: data.receptor,
template: data.template,
providerBody: body,
normalized,
});
}
this.logger.log(
`Kavenegar verifyLookUp ok receptor=${data.receptor} template=${data.template} status=200`,
);
return body;
} catch (e) {
if (e instanceof SmsProviderException) throw e;
const detail = safeJsonStringify(unwrapKavenegarTransportError(e));
this.logger.error(
`Kavenegar verifyLookUp transport error receptor=${data.receptor} template=${data.template} detail=${detail}`,
);
throw new SmsTransportException(
"verifyLookup",
{ receptor: data.receptor, template: data.template },
e,
);
}
}
private logProviderRejection(
op: "send" | "verifyLookUp",
receptor: string,
template: string | undefined,
normalized: KavenegarNormalized,
) {
const t = template != null && template !== "" ? ` template=${template}` : "";
this.logger.warn(
`Kavenegar ${op} rejected receptor=${receptor}${t} providerStatus=${normalized.httpLikeStatus ?? "unknown"} providerMessage=${normalized.message ?? "n/a"} body=${normalized.raw}`,
);
}
}

View File

@@ -0,0 +1,90 @@
import { Injectable, Logger } from "@nestjs/common";
import axios, { isAxiosError } from "axios";
import { buildParsianTemplateMessage } from "./parsian-template-messages";
import {
SmsProviderException,
SmsTransportException,
} from "./sms-provider.exception";
import { SendMessage, VerifyLookUpMessage } from "./sms-gateway.types";
@Injectable()
export class ParsianSmsGateway {
private readonly logger = new Logger(ParsianSmsGateway.name);
async sendMessage(data: SendMessage) {
try {
const body = await this.requestSend(data.receptor, data.message);
this.logger.log(`Parsian Send ok receptor=${data.receptor}`);
return body;
} catch (e) {
if (e instanceof SmsProviderException) throw e;
const detail = isAxiosError(e)
? { status: e.response?.status, data: e.response?.data, message: e.message }
: e;
this.logger.error(
`Parsian Send transport error receptor=${data.receptor} detail=${JSON.stringify(detail)}`,
);
throw new SmsTransportException("send", { receptor: data.receptor }, e);
}
}
async verifyLookUp(data: VerifyLookUpMessage) {
const message = buildParsianTemplateMessage(data);
try {
const body = await this.requestSend(data.receptor, message);
this.logger.log(
`Parsian verifyLookUp ok receptor=${data.receptor} template=${data.template}`,
);
return body;
} catch (e) {
if (e instanceof SmsProviderException) throw e;
const detail = isAxiosError(e)
? { status: e.response?.status, data: e.response?.data, message: e.message }
: e;
this.logger.error(
`Parsian verifyLookUp transport error receptor=${data.receptor} template=${data.template} detail=${JSON.stringify(detail)}`,
);
throw new SmsTransportException(
"verifyLookup",
{ receptor: data.receptor, template: data.template },
e,
);
}
}
private parsianHeaders() {
return {
"Content-Type": "application/json",
"X-PACKAGE-API-KEY": process.env.PARSIAN_API_KEY ?? "",
Authorization: `Basic ${process.env.PARSIAN_BASIC_TOKEN ?? ""}`,
};
}
private async requestSend(receptor: string, message: string): Promise<unknown> {
const baseUrl = process.env.PARSIAN_SMS_URL;
if (!baseUrl?.trim()) {
throw new SmsTransportException(
"send",
{ receptor },
new Error("PARSIAN_SMS_URL is not configured"),
);
}
const url = `${baseUrl}=${receptor}&Message=${encodeURIComponent(message)}`;
const response = await axios.get(url, { headers: this.parsianHeaders() });
if (response.status < 200 || response.status >= 300) {
throw new SmsProviderException("send", {
receptor,
providerBody: response.data,
normalized: {
httpLikeStatus: response.status,
message: String(response.statusText ?? ""),
raw: JSON.stringify(response.data),
},
});
}
return response.data;
}
}

View File

@@ -0,0 +1,65 @@
import { VerifyLookUpMessage } from "./sms-gateway.types";
const TOKEN_KEYS = ["token", "token2", "token3", "token10"] as const;
/** Kavenegar template name → plain-text body for Parsian (placeholders: {token}, {token2}, …). */
const PARSIAN_TEMPLATE_BODIES: Record<string, string> = {
"yara724-invite-link":
"کاربر محترم، لینک ورود به پرونده یارا724:\nشناسه: {token}\n{token2}",
"yara-field-expert-link":
"کارشناس میدانی بیمه {token} ({token2}) پرونده شما را بررسی می‌کند.\nلینک: {token3}",
"yara-blame-agreement":
"پرونده تصادف شما ({token}) آماده امضاست.\nلینک: {token2}",
"yara-claim-link":
"پرونده خسارت شما ({token}) آماده ثبت است.\nلینک: {token2}",
"yara-expert-lock":
"کارشناس ({token3}) پرونده {token} ({token2}) را در حال بررسی دارد.",
"yara-resend-documents":
"لطفاً مدارک پرونده {token} ({token2}) را مجدداً ارسال کنید.\nلینک: {token3}",
"yara-signature":
"امضای پرونده {token} ({token2}) توسط کارشناس {token3} بررسی شد.\nلینک: {token10}",
};
function applyTokens(
pattern: string,
data: Pick<
VerifyLookUpMessage,
"token" | "token2" | "token3" | "token10"
>,
): string {
let out = pattern;
for (const key of TOKEN_KEYS) {
const value = data[key];
if (value != null && value !== "") {
out = out.split(`{${key}}`).join(value);
}
}
return out.replace(/\{token\d*\}/g, "").trim();
}
export function buildParsianTemplateMessage(
data: VerifyLookUpMessage,
): string {
const authTemplate = process.env.AUTH_SMS_TEMPLATE?.trim();
if (authTemplate && data.template === authTemplate) {
const authMessage =
process.env.AUTH_SMS_MESSAGE?.trim() ??
"کاربر محترم رمز یکبار مصرف شما:\nCode: {token}";
return applyTokens(authMessage, data);
}
const body = PARSIAN_TEMPLATE_BODIES[data.template];
if (body) {
return applyTokens(body, data);
}
const parts = [
`پیام سیستم (${data.template})`,
data.token ? `کد: ${data.token}` : "",
data.token2 ?? "",
data.token3 ?? "",
data.token10 ?? "",
].filter(Boolean);
return parts.join("\n");
}

View File

@@ -1,6 +1,8 @@
import { KavenegarModule } from "@fraybabak/kavenegar_nest";
import { Module } from "@nestjs/common";
import * as dotenv from "dotenv";
import { KavenegarSmsGateway } from "./kavenegar-sms.gateway";
import { ParsianSmsGateway } from "./parsian-sms.gateway";
import { SmsGatewayService } from "./sms-gateway.service";
dotenv.config();
@@ -12,7 +14,11 @@ dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
apikey: process.env.SMS_API_KEY || "",
}),
],
providers: [SmsGatewayService],
providers: [
KavenegarSmsGateway,
ParsianSmsGateway,
SmsGatewayService,
],
exports: [SmsGatewayService],
})
export class SmsGatewayModule {}

View File

@@ -1,112 +1,33 @@
import { KavenegarService } from "@fraybabak/kavenegar_nest";
import { Injectable, Logger } from "@nestjs/common";
import {
isKavenegarSuccess,
KavenegarNormalized,
normalizeKavenegarBody,
safeJsonStringify,
unwrapKavenegarTransportError,
} from "./kavenegar-response";
import {
SmsProviderException,
SmsTransportException,
} from "./sms-provider.exception";
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { KavenegarSmsGateway } from "./kavenegar-sms.gateway";
import { ParsianSmsGateway } from "./parsian-sms.gateway";
import { getSmsProvider } from "./sms-provider.config";
import { SendMessage, VerifyLookUpMessage } from "./sms-gateway.types";
export interface SendMessage {
message: string;
receptor: string;
}
export interface VerifyLookUpMessage {
template: string;
token: string;
token2?: string;
token3?: string;
token10?: string;
receptor: string;
}
export type { SendMessage, VerifyLookUpMessage } from "./sms-gateway.types";
@Injectable()
export class SmsGatewayService {
export class SmsGatewayService implements OnModuleInit {
private readonly logger = new Logger(SmsGatewayService.name);
constructor(private readonly sender: KavenegarService) {}
constructor(
private readonly kavenegar: KavenegarSmsGateway,
private readonly parsian: ParsianSmsGateway,
) {}
onModuleInit() {
this.logger.log(`SMS provider active: ${getSmsProvider()}`);
}
async sendMessage(data: SendMessage) {
try {
const body = await this.sender.Send({
sender: "10008663",
...data,
});
if (!isKavenegarSuccess(body)) {
const normalized = normalizeKavenegarBody(body);
this.logProviderRejection("send", data.receptor, undefined, normalized);
throw new SmsProviderException("send", {
receptor: data.receptor,
providerBody: body,
normalized,
});
}
this.logger.log(
`Kavenegar Send ok receptor=${data.receptor} status=200`,
);
return body;
} catch (e) {
if (e instanceof SmsProviderException) throw e;
const detail = safeJsonStringify(unwrapKavenegarTransportError(e));
this.logger.error(
`Kavenegar Send transport error receptor=${data.receptor} detail=${detail}`,
);
throw new SmsTransportException("send", { receptor: data.receptor }, e);
}
return this.activeGateway().sendMessage(data);
}
async verifyLookUp(data: VerifyLookUpMessage) {
try {
console.log(data)
const body = await this.sender.verifyLookup(data);
if (!isKavenegarSuccess(body)) {
const normalized = normalizeKavenegarBody(body);
this.logProviderRejection(
"verifyLookUp",
data.receptor,
data.template,
normalized,
);
throw new SmsProviderException("verifyLookup", {
receptor: data.receptor,
template: data.template,
providerBody: body,
normalized,
});
}
this.logger.log(
`Kavenegar verifyLookUp ok receptor=${data.receptor} template=${data.template} status=200`,
);
return body;
} catch (e) {
if (e instanceof SmsProviderException) throw e;
const detail = safeJsonStringify(unwrapKavenegarTransportError(e));
this.logger.error(
`Kavenegar verifyLookUp transport error receptor=${data.receptor} template=${data.template} detail=${detail}`,
);
throw new SmsTransportException(
"verifyLookup",
{ receptor: data.receptor, template: data.template },
e,
);
}
return this.activeGateway().verifyLookUp(data);
}
private logProviderRejection(
op: "send" | "verifyLookUp",
receptor: string,
template: string | undefined,
normalized: KavenegarNormalized,
) {
const t = template != null && template !== "" ? ` template=${template}` : "";
this.logger.warn(
`Kavenegar ${op} rejected receptor=${receptor}${t} providerStatus=${normalized.httpLikeStatus ?? "unknown"} providerMessage=${normalized.message ?? "n/a"} body=${normalized.raw}`,
);
private activeGateway(): KavenegarSmsGateway | ParsianSmsGateway {
return getSmsProvider() === "parsian" ? this.parsian : this.kavenegar;
}
}

View File

@@ -0,0 +1,13 @@
export interface SendMessage {
message: string;
receptor: string;
}
export interface VerifyLookUpMessage {
template: string;
token: string;
token2?: string;
token3?: string;
token10?: string;
receptor: string;
}

View File

@@ -0,0 +1,18 @@
export type SmsProviderId = "kavenegar" | "parsian";
/** Reads `SMS` (e.g. `parsian`) or `SMS_PROVIDER`; defaults to kavenegar. */
export function getSmsProvider(): SmsProviderId {
const raw = (
process.env.SMS ??
process.env.SMS_PROVIDER ??
"kavenegar"
)
.trim()
.toLowerCase();
if (raw === "parsian") return "parsian";
return "kavenegar";
}
export function isParsianSmsProvider(): boolean {
return getSmsProvider() === "parsian";
}