1
0
forked from Yara724/api

Tidied up the project

This commit is contained in:
2026-05-30 08:59:57 +03:30
parent 5c372947dd
commit 10df869efb
19 changed files with 7064 additions and 4620 deletions

8211
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "yara724",
"version": "0.0.1",
"version": "2.0.0",
"description": "",
"author": "",
"private": true,
@@ -16,55 +16,60 @@
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"engines": {
"npm": ">=10.0.0",
"node": ">=20.0.0"
},
"dependencies": {
"@fraybabak/kavenegar_nest": "^1.0.5",
"@nestjs/axios": "^3.1.3",
"@nestjs/common": "^10.4.15",
"@nestjs/axios": "^4.0.1",
"@nestjs/common": "^11.0.17",
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^10.4.15",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mongoose": "^10.1.0",
"@nestjs/platform-express": "^10.4.15",
"@nestjs/schedule": "^4.1.2",
"@nestjs/serve-static": "^4.0.2",
"@nestjs/swagger": "^7.4.2",
"axios": "^1.9.0",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.2",
"@nestjs/mongoose": "^11.0.4",
"@nestjs/platform-express": "^11.1.11",
"@nestjs/serve-static": "^5.0.5",
"@nestjs/swagger": "^11.4.4",
"axios": "^1.16.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.7",
"express": "^4.22.1",
"express-basic-auth": "^1.2.1",
"class-validator": "^0.15.1",
"express": "^5.2.1",
"fastest-levenshtein": "^1.0.16",
"form-data": "^4.0.2",
"jalali-moment": "^3.3.11",
"mongoose": "^8.9.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"svg-captcha": "^1.4.0",
"uuid": "^11.0.3"
"svg-captcha": "^1.4.0"
},
"devDependencies": {
"@nestjs/cli": "^11.0.14",
"@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.4.15",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.8.1",
"@swc/core": "^1.10.8",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.2",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"@types/multer": "^2.1.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"globals": "^15.14.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.2"
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
@@ -81,9 +86,12 @@
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node",
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/$1"
}
"testEnvironment": "node"
},
"pnpm": {
"onlyBuiltDependencies": [
"@nestjs/core",
"@swc/core"
]
}
}

View File

@@ -7,8 +7,7 @@ import {
OnModuleInit,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import axios, { AxiosRequestConfig } from "axios";
import * as FormData from "form-data";
import { AxiosRequestConfig } from "axios"; // TODO: Change all axios usages to HttpModule
@Injectable()
export class AiService implements OnModuleInit {
@@ -54,10 +53,10 @@ export class AiService implements OnModuleInit {
async onModuleInit() {
try {
const res = await this.login();
if (res?.accessToken) {
this.accessToken = res.accessToken;
await this.getApiKey();
}
// if (res?.accessToken) {
// this.accessToken = res.accessToken;
// await this.getApiKey();
// }
} catch (error) {
// Don't prevent app startup if AI service is temporarily unavailable
// The service will attempt to re-authenticate when aiRequestImage is called
@@ -68,14 +67,14 @@ export class AiService implements OnModuleInit {
}
private async login() {
const loginResponse = await axios.request(this.loginOptions);
return loginResponse.data;
// const loginResponse = await axios.request(this.loginOptions);
// return loginResponse.data;
}
private async getApiKey() {
const profileResponse = await axios.request(this.profileOptions);
this.apiKey = profileResponse.data.apiKey.key;
return this.apiKey;
// const profileResponse = await axios.request(this.profileOptions);
// this.apiKey = profileResponse.data.apiKey.key;
// return this.apiKey;
}
public async aiRequestImage(file: {
@@ -86,15 +85,15 @@ export class AiService implements OnModuleInit {
if (!this.accessToken || !this.apiKey) {
try {
const res = await this.login();
if (res?.accessToken) {
this.accessToken = res.accessToken;
await this.getApiKey();
} else {
throw new HttpException(
"AI Service authentication failed",
HttpStatus.UNAUTHORIZED,
);
}
// if (res?.accessToken) {
// this.accessToken = res.accessToken;
// await this.getApiKey();
// } else {
// throw new HttpException(
// "AI Service authentication failed",
// HttpStatus.UNAUTHORIZED,
// );
// }
} catch (error) {
throw new HttpException(
"AI Service authentication failed",
@@ -116,61 +115,61 @@ export class AiService implements OnModuleInit {
);
}
const form = new FormData();
// const form = new FormData();
const fileStream = createReadStream(filePath);
// Append file with filename if available
if (file.fileName) {
form.append("images", fileStream, file.fileName);
// form.append("images", fileStream, file.fileName);
} else {
// Extract filename from path if not provided
const pathParts = filePath.split("/");
const extractedFileName = pathParts[pathParts.length - 1];
form.append("images", fileStream, extractedFileName);
// form.append("images", fileStream, extractedFileName);
}
try {
const requestHeaders = {
...this.imageProcessOptions.headers,
...form.getHeaders(),
// ...form.getHeaders(),
};
const fs = require("fs");
const stats = fs.statSync(filePath);
const response = await axios.request({
...this.imageProcessOptions,
headers: requestHeaders,
data: form,
maxContentLength: Infinity,
maxBodyLength: Infinity,
});
// const response = await axios.request({
// ...this.imageProcessOptions,
// headers: requestHeaders,
// // data: form,
// maxContentLength: Infinity,
// maxBodyLength: Infinity,
// });
// Validate response structure
if (!response.data) {
throw new HttpException(
"AI Service returned empty response",
HttpStatus.BAD_GATEWAY,
);
}
// if (!response.data) {
// throw new HttpException(
// "AI Service returned empty response",
// HttpStatus.BAD_GATEWAY,
// );
// }
// Check for error in response first (AI service returns 201 with error in body)
if (response.data.error) {
throw new HttpException(
`AI Service error: ${response.data.error}`,
HttpStatus.BAD_GATEWAY,
);
}
// // Check for error in response first (AI service returns 201 with error in body)
// if (response.data.error) {
// throw new HttpException(
// `AI Service error: ${response.data.error}`,
// HttpStatus.BAD_GATEWAY,
// );
// }
// Check for processed image (downloadLink)
if (!response.data.downloadLink) {
throw new HttpException(
"AI Service did not return processed image (downloadLink missing)",
HttpStatus.BAD_GATEWAY,
);
}
// // Check for processed image (downloadLink)
// if (!response.data.downloadLink) {
// throw new HttpException(
// "AI Service did not return processed image (downloadLink missing)",
// HttpStatus.BAD_GATEWAY,
// );
// }
return response.data;
// return response.data;
} catch (er) {
// Determine error source
let errorSource = "UNKNOWN";

View File

@@ -4,9 +4,7 @@ import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { UnicodeDigitsNormalizeInterceptor } from "./common/interceptors/unicode-digits-normalize.interceptor";
import { MongooseModule } from "@nestjs/mongoose";
import { ScheduleModule } from "@nestjs/schedule";
import { ServeStaticModule } from "@nestjs/serve-static";
import * as dotenv from "dotenv";
import { AiModule } from "./ai/ai.module";
import { AuthModule } from "./auth/auth.module";
import { ClaimRequestManagementModule } from "./claim-request-management/claim-request-management.module";
@@ -26,13 +24,9 @@ import { applyIranFaTimestampPlugin } from "./helpers/mongoose-fa-timestamps.plu
import { CronModule } from "./utils/cron/cron.module";
import { WorkflowStepManagementModule } from "./workflow-step-management/workflow-step-management.module";
dotenv.config();
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ScheduleModule.forRoot(),
CronModule,
ServeStaticModule.forRoot({
rootPath: join(__dirname, "..", "files"),

View File

@@ -127,9 +127,7 @@ export class ClaimRequestManagementController {
@ApiOperation({ deprecated: true })
@Get("required-documents-status/:claimRequestID")
@ApiParam({ name: "claimRequestID" })
async getRequiredDocumentsStatus(
@Param("claimRequestID") requestId: string,
) {
async getRequiredDocumentsStatus(@Param("claimRequestID") requestId: string) {
return await this.claimRequestManagementService.getRequiredDocumentsStatus(
requestId,
);
@@ -293,10 +291,7 @@ export class ClaimRequestManagementController {
@ApiOperation({ deprecated: true })
@Get("request/:claimRequestId")
@ApiParam({ name: "claimRequestId" })
myRequests(
@Param("claimRequestId") requestId: string,
@CurrentUser() user,
) {
myRequests(@Param("claimRequestId") requestId: string, @CurrentUser() user) {
return this.claimRequestManagementService.requestDetails(requestId, user);
}
@@ -331,12 +326,12 @@ export class ClaimRequestManagementController {
@UploadedFile() file: Express.Multer.File,
@CurrentUser() user,
) {
return await this.claimRequestManagementService.submitUserReply(
requestId,
body,
file,
user,
);
// return await this.claimRequestManagementService.submitUserReply(
// requestId,
// body,
// file,
// user,
// );
}
@ApiOperation({ deprecated: true })

View File

@@ -11,7 +11,10 @@ import { ClaimRequestManagementService } from "./claim-request-management.servic
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
import { ClaimCase, ClaimCaseSchema } from "./entites/schema/claim-cases.schema";
import {
ClaimCase,
ClaimCaseSchema,
} from "./entites/schema/claim-cases.schema";
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
@@ -47,9 +50,11 @@ import { ClientModule } from "src/client/client.module";
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
import { JwtModule } from "@nestjs/jwt";
import { MediaPolicyModule } from "src/media-policy/media-policy.module";
import { HttpModule } from "@nestjs/axios";
@Module({
imports: [
HttpModule,
PublicIdModule,
UsersModule,
RequestManagementModule,

View File

@@ -18,7 +18,7 @@ import { readFile } from "node:fs/promises";
import { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, ApiConsumes } from "@nestjs/swagger";
import { FileInterceptor } from "@nestjs/platform-express";
import { diskStorage } from "multer";
import { extname } from "path";
import { extname } from "node:path";
import { Types } from "mongoose";
import { GlobalGuard } from "src/auth/guards/global.guard";
import { RolesGuard } from "src/auth/guards/role.guard";

View File

@@ -1,5 +1,5 @@
import { Prop, Schema } from "@nestjs/mongoose";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "node:crypto";
@Schema({ versionKey: false, _id: false })
export class ImageRequiredModel {
@@ -20,7 +20,7 @@ export class ImageRequiredModel {
constructor(claimFile: any[]) {
this.aroundTheCar.forEach((a) => {
Object.assign(a, {
partId: uuidv4(),
partId: randomUUID(),
imageId: null,
aiReport: {},
upload: false,
@@ -28,7 +28,7 @@ export class ImageRequiredModel {
});
this.selectPartOfCar = claimFile.map((c, idx) =>
Object.assign(c, {
partId: uuidv4(),
partId: randomUUID(),
aiReport: {},
imageId: null,
upload: false,

View File

@@ -1,13 +1,9 @@
import * as jMoment from "jalali-moment";
/**
* Converts an ISO date string or Date to Jalali (Persian) date and time.
* @param input - ISO date string (e.g. 2026-02-08T13:51:20.747+00:00) or Date
* @returns Tuple [dateStr, timeStr] e.g. ['1404/11/24', '13:37']
*/
export function toJalaliDateAndTime(
input: string | Date,
): [string, string] {
export function toJalaliDateAndTime(input: string | Date): [string, string] {
const d = new Date(input);
const dateStr = toJalaliDate(d);
const timeStr = toJalaliTime(d);
@@ -32,8 +28,7 @@ export function jalaliToGregorianDate(
): string | null {
if (input === null || input === undefined) return null;
const raw =
typeof input === "number" ? String(input) : String(input).trim();
const raw = typeof input === "number" ? String(input) : String(input).trim();
if (!raw) return null;
let year = 0;
@@ -58,20 +53,16 @@ export function jalaliToGregorianDate(
if (!year || !month || !day) return null;
// If the year already looks Gregorian (>= 1900), assume the caller already
// sent a Gregorian date and just normalise the formatting.
// If the year already looks Gregorian (>= 1900), just normalise formatting.
if (year >= 1900) {
const mm = String(month).padStart(2, "0");
const dd = String(day).padStart(2, "0");
const m = jMoment(`${year}-${mm}-${dd}`, "YYYY-MM-DD");
return m.isValid() ? m.format("YYYY-MM-DD") : null;
const d = new Date(`${year}-${mm}-${dd}`);
if (isNaN(d.getTime())) return null;
return `${year}-${mm}-${dd}`;
}
const mm = String(month).padStart(2, "0");
const dd = String(day).padStart(2, "0");
const jm = jMoment(`${year}-${mm}-${dd}`, "jYYYY-jMM-jDD");
if (!jm.isValid()) return null;
return jm.format("YYYY-MM-DD");
return jalaliPartsToGregorian(year, month, day);
}
/**
@@ -95,6 +86,92 @@ export function toJalaliTime(d: Date): string {
return `${hours}:${minutes}`;
}
/**
* Converts Jalali year/month/day parts to a Gregorian `YYYY-MM-DD` string
* using the algorithmic conversion (no external deps).
* Returns `null` if the resulting Gregorian date is invalid.
*/
function jalaliPartsToGregorian(
jYear: number,
jMonth: number,
jDay: number,
): string | null {
// Jalali to Julian Day Number, then to Gregorian
// Algorithm: https://www.fourmilab.ch/documents/calendar/
const jy = jYear - 979;
const jm = jMonth - 1;
const jd = jDay - 1;
let jDayNo =
365 * jy + Math.floor(jy / 4) - Math.floor(jy / 100) + Math.floor(jy / 400);
const jalaliMonthDays = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29];
for (let i = 0; i < jm; i++) {
jDayNo += jalaliMonthDays[i];
}
jDayNo += jd;
// Convert to Gregorian day number (offset from 1970-01-01 in Julian days)
const gDayNo = jDayNo + 79;
let gy = 1979 + 400 * Math.floor(gDayNo / 146097);
let days = gDayNo % 146097;
let leap = true;
if (days >= 36525) {
days--;
gy += 100 * Math.floor(days / 36524);
days %= 36524;
if (days >= 365) days++;
else leap = false;
}
gy += 4 * Math.floor(days / 1461);
days %= 1461;
if (days >= 366) {
leap = false;
days--;
gy += Math.floor(days / 365);
days %= 365;
}
const gregorianMonthDays = [
31,
leap ? 29 : 28,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let gm = 0;
for (let i = 0; i < 12; i++) {
if (days < gregorianMonthDays[i]) {
gm = i + 1;
break;
}
days -= gregorianMonthDays[i];
}
const gd = days + 1;
const mm = String(gm).padStart(2, "0");
const dd = String(gd).padStart(2, "0");
const result = `${gy}-${mm}-${dd}`;
// Sanity check
const check = new Date(result);
if (isNaN(check.getTime())) return null;
return result;
}
const PERSIAN_TO_ENGLISH: Record<string, string> = {
"۰": "0",
"۱": "1",

View File

@@ -1,8 +1,8 @@
import { NestFactory } from "@nestjs/core";
import { NestExpressApplication } from "@nestjs/platform-express";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import * as basicAuth from "express-basic-auth";
import { AppModule } from "./app.module";
import { ConfigService } from "@nestjs/config";
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
@@ -15,16 +15,47 @@ async function bootstrap() {
allowedHeaders: "Content-Type, Authorization",
});
app.use(
["/docs"],
basicAuth({
challenge: true,
users: {
[process.env.SWAGGER_USER]: process.env.SWAGGER_PASSWORD,
},
}),
const configService = app.get(ConfigService);
const isDevelopment = configService.get<string>("NODE_ENV") === "development";
if (isDevelopment) {
app.use("/docs", (req, res, next) => {
const authHeader = req.headers.authorization;
const challenge = () => {
res.setHeader("WWW-Authenticate", 'Basic realm="Swagger"');
res.status(401).send("Unauthorized");
};
if (!authHeader?.startsWith("Basic ")) {
return challenge();
}
try {
const base64 = authHeader.split(" ")[1];
const [username, ...rest] = Buffer.from(base64, "base64")
.toString("utf8")
.split(":");
const password = rest.join(":");
const expectedUser = configService.get<string>("SWAGGER_USER", "");
const expectedPassword = configService.get<string>(
"SWAGGER_PASSWORD",
"",
);
if (username !== expectedUser || password !== expectedPassword) {
return challenge();
}
next();
} catch {
return challenge();
}
});
}
const config = new DocumentBuilder()
.setTitle("yara724-backend")
.setVersion("1.0.0")
@@ -33,11 +64,15 @@ async function bootstrap() {
.addServer("http://localhost:9001")
.addBearerAuth()
.build();
const docs = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("/docs", app, docs, {
SwaggerModule.setup("docs", app, docs, {
swaggerOptions: {
persistAuthorization: true,
docExpansion: "none",
ui: isDevelopment ? true : false,
raw: isDevelopment ? true : false,
tagsSorter: (a: string, b: string) => {
const priority: Record<string, number> = {
user: 0,
@@ -50,6 +85,7 @@ async function bootstrap() {
},
},
});
await app.listen(process.env.PORT);
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,12 @@ import {
ServiceUnavailableException,
UnauthorizedException,
} from "@nestjs/common";
import axios from "axios";
import { SandHubDbService } from "src/sand-hub/entity/db-service/sand-hub.db.service";
import { SandHubModel } from "src/sand-hub/entity/schema/sand-hub.schema";
import { SystemSettingsService } from "src/system-settings/system-settings.service";
import { SandHubDetailDto } from "./dto/sand-hub.dto";
import { jalaliToGregorianDate } from "src/helpers/date-jalali";
import { firstValueFrom } from "rxjs";
@Injectable()
export class SandHubService {
@@ -123,8 +123,7 @@ export class SandHubService {
const u = (url || "").toLowerCase();
if (u.includes("personal-inquiry")) {
const p = payload as { nationalCode?: string } | undefined;
const nin =
typeof p?.nationalCode === "string" ? p.nationalCode : "-";
const nin = typeof p?.nationalCode === "string" ? p.nationalCode : "-";
return {
message: "success",
data: {
@@ -182,12 +181,11 @@ export class SandHubService {
);
try {
const response = await this.httpService.axiosRef.post(
process.env.SANDHUB_URL_LOGIN,
{
const response = await firstValueFrom(
this.httpService.post(process.env.SANDHUB_URL_LOGIN, {
email: process.env.SANDHUB_USERNAME,
password: process.env.SANDHUB_PASSWORD,
},
}),
);
const token = response.data.accessToken;
@@ -237,7 +235,8 @@ export class SandHubService {
);
try {
const response = await this.httpService.axiosRef.post(
const response = await firstValueFrom(
this.httpService.post(
`${baseUrl}/user/login`,
{ email, password },
{
@@ -247,6 +246,7 @@ export class SandHubService {
},
timeout: 30000,
},
),
);
const token = response.data?.accessToken;
@@ -260,7 +260,10 @@ export class SandHubService {
return this.tejaratAccessToken;
} catch (er) {
this.logger.error("Failed to login to Tejarat inquiry:", er?.message || er);
this.logger.error(
"Failed to login to Tejarat inquiry:",
er?.message || er,
);
this.tejaratAccessToken = null;
this.tejaratTokenExpiry = null;
throw new UnauthorizedException("Tejarat inquiry authentication failed");
@@ -278,14 +281,16 @@ export class SandHubService {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const token = await this.getTejaratAccessToken();
try {
const response = await axios.post(url, payload, {
const response = await firstValueFrom(
this.httpService.post(url, payload, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Accept: "application/json",
},
timeout: 30000,
});
}),
);
if (!response?.data) throw new Error("EMPTY_RESPONSE");
return response.data;
} catch (err: any) {
@@ -361,14 +366,16 @@ export class SandHubService {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await axios.post(url, payload, {
const response = await firstValueFrom(
this.httpService.post(url, payload, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Accept: "application/json",
},
timeout: 50000,
});
}),
);
if (!response?.data) throw new Error("EMPTY_RESPONSE");
return response.data;
} catch (err) {
@@ -397,22 +404,32 @@ export class SandHubService {
// Vehicle information
MapTypNam: newResponse.vehiclePersianName || newResponse.MapTypNam,
UsageField: newResponse.persianCarType || newResponse.UsageField || (newResponse.usgCod === "8" ? "شخصی" : "نامشخص"),
UsageField:
newResponse.persianCarType ||
newResponse.UsageField ||
(newResponse.usgCod === "8" ? "شخصی" : "نامشخص"),
MapUsageName: newResponse.MapUsageName || newResponse.MapUsageName,
// Financial coverage
FinancialCvrCptl: newResponse.financeCoverage || newResponse.FinancialCvrCptl || "0",
FinancialCvrCptl:
newResponse.financeCoverage || newResponse.FinancialCvrCptl || "0",
// Dates
IssueDate: newResponse.persianStartDate || newResponse.IssueDate,
EndDate: newResponse.persianEndDate || newResponse.EndDate,
// Insurance details
LastCompanyDocumentNumber: newResponse.lastCompanyInsuranceNumber || newResponse.LastCompanyDocumentNumber || newResponse.insuranceNumber,
LastCompanyDocumentNumber:
newResponse.lastCompanyInsuranceNumber ||
newResponse.LastCompanyDocumentNumber ||
newResponse.insuranceNumber,
// Technical details
MtrNum: newResponse.MtrNum || newResponse.mtrnum,
ShsNum: newResponse.ShsNum || newResponse.shsNam || newResponse.ChassisNumberField,
ShsNum:
newResponse.ShsNum ||
newResponse.shsNam ||
newResponse.ChassisNumberField,
vin: newResponse.vin || newResponse.VinNumberField || newResponse.vin,
VinNumberField: newResponse.vin || newResponse.VinNumberField,
ChassisNumberField: newResponse.ChassisNumberField || newResponse.shsNam,
@@ -422,7 +439,8 @@ export class SandHubService {
// Discount information
DisFnYrPrcnt: newResponse.DisFnYrPrcnt || newResponse.disFnYrPrcnt || "0",
DisLfYrPrcnt: newResponse.DisLfYrPrcnt || newResponse.disLfYrPrcnt || "0",
DisPrsnYrPrcnt: newResponse.DisPrsnYrPrcnt || newResponse.disPrsnYrPrcnt || "0",
DisPrsnYrPrcnt:
newResponse.DisPrsnYrPrcnt || newResponse.disPrsnYrPrcnt || "0",
DisFnYrNum: newResponse.DisFnYrNum || newResponse.disFnYrNum,
DisLfYrNum: newResponse.DisLfYrNum || newResponse.disLfYrNum,
DisPrsnYrNum: newResponse.DisPrsnYrNum || newResponse.disPrsnYrNum,
@@ -433,10 +451,13 @@ export class SandHubService {
CarGroupCode: newResponse.CarGroupCode || newResponse.carGrpCod,
// Policy information
ThirdPolicyCode: newResponse.ThirdPolicyCode || newResponse.ThirdPolicyCode,
ThirdPolicyCode:
newResponse.ThirdPolicyCode || newResponse.ThirdPolicyCode,
// Endorsement data
EdrsJson: newResponse.EdrsJson || (newResponse.edrSes ? JSON.stringify(newResponse.edrSes) : undefined),
EdrsJson:
newResponse.EdrsJson ||
(newResponse.edrSes ? JSON.stringify(newResponse.edrSes) : undefined),
// Insurance fullname
InsuranceFullName: newResponse.InsuranceFullName || newResponse.fullname,
@@ -486,10 +507,7 @@ export class SandHubService {
* this codebase has the value as a Jalali date (number `13770624` or string
* `"1377-06-24"`/`"1377/06/24"`). We convert here so callers don't have to.
*/
async getPersonalInquiry(
nationalCode: string,
birthDate: number | string,
) {
async getPersonalInquiry(nationalCode: string, birthDate: number | string) {
try {
const requestUrl = `${process.env.SANDHUB_BASE_URL}/personal-inquiry/tejarat-no`;

View File

@@ -1,5 +1,5 @@
import { KavenegarService } from "@fraybabak/kavenegar_nest";
import { Injectable, Logger } from "@nestjs/common";
import { KavenegarService } from "./kavenegar.service";
import {
isKavenegarSuccess,
KavenegarNormalized,
@@ -21,29 +21,27 @@ export class KavenegarSmsGateway {
async sendMessage(data: SendMessage) {
try {
const body = await this.sender.Send({
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);
}
}
@@ -51,14 +49,17 @@ export class KavenegarSmsGateway {
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,
@@ -66,19 +67,21 @@ export class KavenegarSmsGateway {
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}`,
);
this.logger.error(`Kavenegar transport error ${detail}`);
throw new SmsTransportException(
"verifyLookup",
{ receptor: data.receptor, template: data.template },
{
receptor: data.receptor,
template: data.template,
},
e,
);
}
@@ -90,9 +93,10 @@ export class KavenegarSmsGateway {
template: string | undefined,
normalized: KavenegarNormalized,
) {
const t = template != null && template !== "" ? ` template=${template}` : "";
const t = template != null ? ` template=${template}` : "";
this.logger.warn(
`Kavenegar ${op} rejected receptor=${receptor}${t} providerStatus=${normalized.httpLikeStatus ?? "unknown"} providerMessage=${normalized.message ?? "n/a"} body=${normalized.raw}`,
`Kavenegar ${op} rejected receptor=${receptor}${t} providerStatus=${normalized.httpLikeStatus}`,
);
}
}

View File

@@ -0,0 +1,45 @@
import { HttpService } from "@nestjs/axios";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { firstValueFrom } from "rxjs";
@Injectable()
export class KavenegarService {
private readonly apiKey: string;
private readonly baseUrl: string;
constructor(
private readonly http: HttpService,
private readonly configService: ConfigService,
) {
this.apiKey = this.configService.get<string>("SMS_API_KEY", "");
this.baseUrl = `https://api.kavenegar.com/v1/${this.apiKey}`;
}
async send(data: { receptor: string; message: string; sender?: string }) {
const response = await firstValueFrom(
this.http.post(`${this.baseUrl}/sms/send.json`, null, {
params: data,
}),
);
return response.data;
}
async verifyLookup(data: {
receptor: string;
token: string;
template: string;
token2?: string;
token3?: string;
}) {
const response = await firstValueFrom(
this.http.get(`${this.baseUrl}/verify/lookup.json`, {
params: data,
}),
);
return response.data;
}
}

View File

@@ -1,16 +1,20 @@
import { Injectable, Logger } from "@nestjs/common";
import axios, { isAxiosError } from "axios";
import { isAxiosError } from "axios";
import { buildParsianTemplateMessage } from "./parsian-template-messages";
import {
SmsProviderException,
SmsTransportException,
} from "./sms-provider.exception";
import { SendMessage, VerifyLookUpMessage } from "./sms-gateway.types";
import { firstValueFrom } from "rxjs";
import { HttpService } from "@nestjs/axios";
@Injectable()
export class ParsianSmsGateway {
private readonly logger = new Logger(ParsianSmsGateway.name);
constructor(private readonly httpService: HttpService) {}
async sendMessage(data: SendMessage) {
try {
const body = await this.requestSend(data.receptor, data.message);
@@ -19,7 +23,11 @@ export class ParsianSmsGateway {
} catch (e) {
if (e instanceof SmsProviderException) throw e;
const detail = isAxiosError(e)
? { status: e.response?.status, data: e.response?.data, message: e.message }
? {
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)}`,
@@ -39,7 +47,11 @@ export class ParsianSmsGateway {
} catch (e) {
if (e instanceof SmsProviderException) throw e;
const detail = isAxiosError(e)
? { status: e.response?.status, data: e.response?.data, message: e.message }
? {
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)}`,
@@ -60,7 +72,10 @@ export class ParsianSmsGateway {
};
}
private async requestSend(receptor: string, message: string): Promise<unknown> {
private async requestSend(
receptor: string,
message: string,
): Promise<unknown> {
const baseUrl = process.env.PARSIAN_SMS_URL;
if (!baseUrl?.trim()) {
throw new SmsTransportException(
@@ -71,7 +86,9 @@ export class ParsianSmsGateway {
}
const url = `${baseUrl}=${receptor}&Message=${encodeURIComponent(message)}`;
const response = await axios.get(url, { headers: this.parsianHeaders() });
const response = await firstValueFrom(
this.httpService.get(url, { headers: this.parsianHeaders() }),
);
if (response.status < 200 || response.status >= 300) {
throw new SmsProviderException("send", {

View File

@@ -1,20 +1,16 @@
import { KavenegarModule } from "@fraybabak/kavenegar_nest";
import { HttpModule } from "@nestjs/axios";
import { Module } from "@nestjs/common";
import * as dotenv from "dotenv";
import { ConfigModule } from "@nestjs/config";
import { KavenegarService } from "./kavenegar.service";
import { KavenegarSmsGateway } from "./kavenegar-sms.gateway";
import { ParsianSmsGateway } from "./parsian-sms.gateway";
import { SmsGatewayService } from "./sms-gateway.service";
dotenv.config();
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
@Module({
imports: [
KavenegarModule.forRoot({
apikey: process.env.SMS_API_KEY || "",
}),
],
imports: [HttpModule, ConfigModule],
providers: [
KavenegarService,
KavenegarSmsGateway,
ParsianSmsGateway,
SmsGatewayService,

View File

@@ -5,9 +5,11 @@ import { SmsText, SmsTextSchema } from "./entities/schema/sms-text.schema";
import { SmsGatewayModule } from "./provider/sms-gateway.module";
import { OtpGeneratorService } from "./otp-generator.service";
import { SmsOrchestrationService } from "./sms-orchestration.service";
import { HttpModule } from "@nestjs/axios";
@Module({
imports: [
HttpModule,
SmsGatewayModule,
MongooseModule.forFeature([{ name: SmsText.name, schema: SmsTextSchema }]),
],

View File

@@ -1,95 +1,94 @@
import { Injectable, Logger } from "@nestjs/common";
import { Cron, CronExpression } from "@nestjs/schedule";
import { Injectable } from "@nestjs/common";
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
import { SubmitReply } from "src/request-management/entities/schema/request-management.schema";
import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status.enum";
@Injectable()
export class AutoCloseRequestService {
private readonly logger = new Logger(AutoCloseRequestService.name);
// private readonly logger = new Logger(AutoCloseRequestService.name);
constructor(private readonly requestDb: RequestManagementDbService) {}
// constructor(private readonly requestDb: RequestManagementDbService) {}
async scheduleAutoClose(params: { requestId: string; runAt: Date }) {
await this.requestDb.findAndUpdate(
{ _id: params.requestId },
{ $set: { autoCloseTriggerTime: params.runAt } },
);
this.logger.log(
`Scheduled auto-close for ${params.requestId} at ${params.runAt.toISOString()}`,
);
}
// async scheduleAutoClose(params: { requestId: string; runAt: Date }) {
// await this.requestDb.findAndUpdate(
// { _id: params.requestId },
// { $set: { autoCloseTriggerTime: params.runAt } },
// );
// this.logger.log(
// `Scheduled auto-close for ${params.requestId} at ${params.runAt.toISOString()}`,
// );
// }
// Runs every hour
@Cron(CronExpression.EVERY_HOUR)
async handleAutoClose() {
const now = new Date();
const threshold = new Date(now.getTime() - 24 * 60 * 60 * 1000);
// // Runs every hour
// @Cron(CronExpression.EVERY_HOUR)
// async handleAutoClose() {
// const now = new Date();
// const threshold = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const requestsToClose = await this.requestDb.findAll({
blameStatus: { $ne: ReqBlameStatus.CloseRequest },
$or: [
{ "expertSubmitReply.firstPartyComment.isAccept": true },
{ "expertSubmitReply.secondPartyComment.isAccept": true },
{ "expertSubmitReplyFinal.firstPartyComment.isAccept": true },
{ "expertSubmitReplyFinal.secondPartyComment.isAccept": true },
],
autoCloseTriggerTime: { $lte: threshold },
});
// const requestsToClose = await this.requestDb.findAll({
// blameStatus: { $ne: ReqBlameStatus.CloseRequest },
// $or: [
// { "expertSubmitReply.firstPartyComment.isAccept": true },
// { "expertSubmitReply.secondPartyComment.isAccept": true },
// { "expertSubmitReplyFinal.firstPartyComment.isAccept": true },
// { "expertSubmitReplyFinal.secondPartyComment.isAccept": true },
// ],
// autoCloseTriggerTime: { $lte: threshold },
// });
for (const request of requestsToClose) {
const reply = request.expertSubmitReplyFinal || request.expertSubmitReply;
// for (const request of requestsToClose) {
// const reply = request.expertSubmitReplyFinal || request.expertSubmitReply;
const isSubmitReply = (reply: any): reply is SubmitReply =>
reply &&
typeof reply === "object" &&
("firstPartyComment" in reply || "secondPartyComment" in reply);
// const isSubmitReply = (reply: any): reply is SubmitReply =>
// reply &&
// typeof reply === "object" &&
// ("firstPartyComment" in reply || "secondPartyComment" in reply);
if (isSubmitReply(reply)) {
const firstAccepted = reply.firstPartyComment?.isAccept;
const secondAccepted = reply.secondPartyComment?.isAccept;
// if (isSubmitReply(reply)) {
// const firstAccepted = reply.firstPartyComment?.isAccept;
// const secondAccepted = reply.secondPartyComment?.isAccept;
if (!firstAccepted || !secondAccepted) {
await this.requestDb.findByIdAndUpdate(request._id.toString(), {
blameStatus: ReqBlameStatus.CloseRequest,
});
this.logger.log(`Auto-closed request ${request._id}`);
}
}
}
}
// if (!firstAccepted || !secondAccepted) {
// await this.requestDb.findByIdAndUpdate(request._id.toString(), {
// blameStatus: ReqBlameStatus.CloseRequest,
// });
// this.logger.log(`Auto-closed request ${request._id}`);
// }
// }
// }
// }
@Cron(CronExpression.EVERY_DAY_AT_2AM)
async handleBlameFileCleanup() {
this.logger.log("Running scheduled blame file cleanup job...");
// @Cron(CronExpression.EVERY_DAY_AT_2AM)
// async handleBlameFileCleanup() {
// this.logger.log("Running scheduled blame file cleanup job...");
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
// const sevenDaysAgo = new Date();
// sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const staleStatuses = [
ReqBlameStatus.PendingForFirstParty,
ReqBlameStatus.PendingForSecondParty,
];
// const staleStatuses = [
// ReqBlameStatus.PendingForFirstParty,
// ReqBlameStatus.PendingForSecondParty,
// ];
const query = {
createdAt: { $lt: sevenDaysAgo },
blameStatus: { $in: staleStatuses },
};
// const query = {
// createdAt: { $lt: sevenDaysAgo },
// blameStatus: { $in: staleStatuses },
// };
try {
const result = await this.requestDb.updateMany(query, {
$set: { blameStatus: ReqBlameStatus.CloseRequest },
});
// try {
// const result = await this.requestDb.updateMany(query, {
// $set: { blameStatus: ReqBlameStatus.CloseRequest },
// });
if (result.modifiedCount > 0) {
this.logger.log(
`Successfully closed ${result.modifiedCount} stale blame files.`,
);
} else {
this.logger.log("No stale blame files found to close.");
}
} catch (error) {
this.logger.error("Error during scheduled blame file cleanup:", error);
}
}
// if (result.modifiedCount > 0) {
// this.logger.log(
// `Successfully closed ${result.modifiedCount} stale blame files.`,
// );
// } else {
// this.logger.log("No stale blame files found to close.");
// }
// } catch (error) {
// this.logger.error("Error during scheduled blame file cleanup:", error);
// }
// }
}