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

@@ -1,7 +1,7 @@
export enum ClaimRequiredDocumentType {
// Car green card
CAR_GREEN_CARD = "car_green_card",
CAR_CERTIFICATE = "car_certificate",
/** National ID card (or similar); may be requested on resend even if not in the initial upload set. */
NATIONAL_CARD = "national_card",

View File

@@ -77,14 +77,14 @@ dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
provide: APP_INTERCEPTOR,
useClass: UnicodeDigitsNormalizeInterceptor,
},
// {
// provide: APP_PIPE,
// useValue: new ValidationPipe({
// transform: true,
// whitelist: true,
// forbidNonWhitelisted: false,
// }),
// },
{
provide: APP_PIPE,
useValue: new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: false,
}),
},
],
})
export class AppModule {}

View File

@@ -8,6 +8,7 @@ import {
LightCondition,
} from "src/Types&Enums/blame-request-management/accident-conditions.enum";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { IsEnum } from "class-validator";
export class InitialFormDto {
@ApiProperty({ required: false, default: false })
@@ -161,16 +162,17 @@ export class LocationDto {
lon: number;
}
export class CreateRequestDto {
@ApiProperty({ type: String, required: false })
type?: "THIRD_PARTY" | "CAR_BODY";
}
// export class CreateRequestDto {
// @ApiProperty({ type: String, required: false })
// type?: "THIRD_PARTY" | "CAR_BODY";
// }
/**
* V2 create blame request (new model `BlameRequest`).
*/
export class CreateBlameRequestDtoV2 {
@ApiProperty({ enum: BlameRequestType })
@IsEnum(BlameRequestType)
@ApiProperty({ enum: BlameRequestType, description: "Type of blame request" })
type: BlameRequestType;
}

View File

@@ -36,7 +36,6 @@ import { BlameDocumentType } from "./entities/schema/blame-document.schema";
import {
CarBodyFormDto,
CarBodySecondForm,
CreateRequestDto,
DescriptionDto,
InitialFormDto,
LocationDto,
@@ -53,18 +52,18 @@ export class RequestManagementController {
private readonly requestManagementService: RequestManagementService,
) {}
@ApiOperation({ deprecated: true })
@Post()
@UseGuards(GlobalGuard)
async createRequest(
@CurrentUser() user,
@Body() createRequestDto: CreateRequestDto,
) {
return await this.requestManagementService.createRequest(
user,
createRequestDto.type,
);
}
// @ApiOperation({ deprecated: true })
// @Post()
// @UseGuards(GlobalGuard)
// async createRequest(
// @CurrentUser() user,
// @Body() createRequestDto: CreateRequestDto,
// ) {
// return await this.requestManagementService.createRequest(
// user,
// createRequestDto.type,
// );
// }
@ApiOperation({ deprecated: true })
@Post("/initial-form/:requestId")

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