From 94bde88cb6382c73145ae2b1f4fb799a36c66003 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sat, 23 May 2026 14:05:11 +0330 Subject: [PATCH] YARA-941 --- .../required-document-type.enum.ts | 2 +- src/app.module.ts | 16 +-- .../dto/create-request-management.dto.ts | 12 +- .../request-management.controller.ts | 25 ++-- .../provider/kavenegar-sms.gateway.ts | 98 +++++++++++++++ .../provider/parsian-sms.gateway.ts | 90 ++++++++++++++ .../provider/parsian-template-messages.ts | 65 ++++++++++ .../provider/sms-gateway.module.ts | 8 +- .../provider/sms-gateway.service.ts | 117 +++--------------- .../provider/sms-gateway.types.ts | 13 ++ .../provider/sms-provider.config.ts | 18 +++ 11 files changed, 338 insertions(+), 126 deletions(-) create mode 100644 src/sms-orchestration/provider/kavenegar-sms.gateway.ts create mode 100644 src/sms-orchestration/provider/parsian-sms.gateway.ts create mode 100644 src/sms-orchestration/provider/parsian-template-messages.ts create mode 100644 src/sms-orchestration/provider/sms-gateway.types.ts create mode 100644 src/sms-orchestration/provider/sms-provider.config.ts diff --git a/src/Types&Enums/claim-request-management/required-document-type.enum.ts b/src/Types&Enums/claim-request-management/required-document-type.enum.ts index 2596530..5f6312e 100644 --- a/src/Types&Enums/claim-request-management/required-document-type.enum.ts +++ b/src/Types&Enums/claim-request-management/required-document-type.enum.ts @@ -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", diff --git a/src/app.module.ts b/src/app.module.ts index c99acc1..6389d4f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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 {} diff --git a/src/request-management/dto/create-request-management.dto.ts b/src/request-management/dto/create-request-management.dto.ts index b48b3ef..9e28ec3 100644 --- a/src/request-management/dto/create-request-management.dto.ts +++ b/src/request-management/dto/create-request-management.dto.ts @@ -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; } diff --git a/src/request-management/request-management.controller.ts b/src/request-management/request-management.controller.ts index ac656bb..9255083 100644 --- a/src/request-management/request-management.controller.ts +++ b/src/request-management/request-management.controller.ts @@ -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") diff --git a/src/sms-orchestration/provider/kavenegar-sms.gateway.ts b/src/sms-orchestration/provider/kavenegar-sms.gateway.ts new file mode 100644 index 0000000..20e4273 --- /dev/null +++ b/src/sms-orchestration/provider/kavenegar-sms.gateway.ts @@ -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}`, + ); + } +} diff --git a/src/sms-orchestration/provider/parsian-sms.gateway.ts b/src/sms-orchestration/provider/parsian-sms.gateway.ts new file mode 100644 index 0000000..084db57 --- /dev/null +++ b/src/sms-orchestration/provider/parsian-sms.gateway.ts @@ -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 { + 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; + } +} diff --git a/src/sms-orchestration/provider/parsian-template-messages.ts b/src/sms-orchestration/provider/parsian-template-messages.ts new file mode 100644 index 0000000..167d166 --- /dev/null +++ b/src/sms-orchestration/provider/parsian-template-messages.ts @@ -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 = { + "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"); +} diff --git a/src/sms-orchestration/provider/sms-gateway.module.ts b/src/sms-orchestration/provider/sms-gateway.module.ts index 5c104a6..6d942d7 100644 --- a/src/sms-orchestration/provider/sms-gateway.module.ts +++ b/src/sms-orchestration/provider/sms-gateway.module.ts @@ -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 {} diff --git a/src/sms-orchestration/provider/sms-gateway.service.ts b/src/sms-orchestration/provider/sms-gateway.service.ts index e7eb66c..d4520ca 100644 --- a/src/sms-orchestration/provider/sms-gateway.service.ts +++ b/src/sms-orchestration/provider/sms-gateway.service.ts @@ -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; } } diff --git a/src/sms-orchestration/provider/sms-gateway.types.ts b/src/sms-orchestration/provider/sms-gateway.types.ts new file mode 100644 index 0000000..95d8713 --- /dev/null +++ b/src/sms-orchestration/provider/sms-gateway.types.ts @@ -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; +} diff --git a/src/sms-orchestration/provider/sms-provider.config.ts b/src/sms-orchestration/provider/sms-provider.config.ts new file mode 100644 index 0000000..07e4c4a --- /dev/null +++ b/src/sms-orchestration/provider/sms-provider.config.ts @@ -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"; +}