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

View File

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

View File

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

View File

@@ -127,9 +127,7 @@ export class ClaimRequestManagementController {
@ApiOperation({ deprecated: true }) @ApiOperation({ deprecated: true })
@Get("required-documents-status/:claimRequestID") @Get("required-documents-status/:claimRequestID")
@ApiParam({ name: "claimRequestID" }) @ApiParam({ name: "claimRequestID" })
async getRequiredDocumentsStatus( async getRequiredDocumentsStatus(@Param("claimRequestID") requestId: string) {
@Param("claimRequestID") requestId: string,
) {
return await this.claimRequestManagementService.getRequiredDocumentsStatus( return await this.claimRequestManagementService.getRequiredDocumentsStatus(
requestId, requestId,
); );
@@ -293,10 +291,7 @@ export class ClaimRequestManagementController {
@ApiOperation({ deprecated: true }) @ApiOperation({ deprecated: true })
@Get("request/:claimRequestId") @Get("request/:claimRequestId")
@ApiParam({ name: "claimRequestId" }) @ApiParam({ name: "claimRequestId" })
myRequests( myRequests(@Param("claimRequestId") requestId: string, @CurrentUser() user) {
@Param("claimRequestId") requestId: string,
@CurrentUser() user,
) {
return this.claimRequestManagementService.requestDetails(requestId, user); return this.claimRequestManagementService.requestDetails(requestId, user);
} }
@@ -331,12 +326,12 @@ export class ClaimRequestManagementController {
@UploadedFile() file: Express.Multer.File, @UploadedFile() file: Express.Multer.File,
@CurrentUser() user, @CurrentUser() user,
) { ) {
return await this.claimRequestManagementService.submitUserReply( // return await this.claimRequestManagementService.submitUserReply(
requestId, // requestId,
body, // body,
file, // file,
user, // user,
); // );
} }
@ApiOperation({ deprecated: true }) @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 { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service"; import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
import { ClaimCaseDbService } from "./entites/db-service/claim-case.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 { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service"; import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-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 { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
import { JwtModule } from "@nestjs/jwt"; import { JwtModule } from "@nestjs/jwt";
import { MediaPolicyModule } from "src/media-policy/media-policy.module"; import { MediaPolicyModule } from "src/media-policy/media-policy.module";
import { HttpModule } from "@nestjs/axios";
@Module({ @Module({
imports: [ imports: [
HttpModule,
PublicIdModule, PublicIdModule,
UsersModule, UsersModule,
RequestManagementModule, 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 { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, ApiConsumes } from "@nestjs/swagger";
import { FileInterceptor } from "@nestjs/platform-express"; import { FileInterceptor } from "@nestjs/platform-express";
import { diskStorage } from "multer"; import { diskStorage } from "multer";
import { extname } from "path"; import { extname } from "node:path";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { GlobalGuard } from "src/auth/guards/global.guard"; import { GlobalGuard } from "src/auth/guards/global.guard";
import { RolesGuard } from "src/auth/guards/role.guard"; import { RolesGuard } from "src/auth/guards/role.guard";

View File

@@ -1,5 +1,5 @@
import { Prop, Schema } from "@nestjs/mongoose"; import { Prop, Schema } from "@nestjs/mongoose";
import { v4 as uuidv4 } from "uuid"; import { randomUUID } from "node:crypto";
@Schema({ versionKey: false, _id: false }) @Schema({ versionKey: false, _id: false })
export class ImageRequiredModel { export class ImageRequiredModel {
@@ -20,7 +20,7 @@ export class ImageRequiredModel {
constructor(claimFile: any[]) { constructor(claimFile: any[]) {
this.aroundTheCar.forEach((a) => { this.aroundTheCar.forEach((a) => {
Object.assign(a, { Object.assign(a, {
partId: uuidv4(), partId: randomUUID(),
imageId: null, imageId: null,
aiReport: {}, aiReport: {},
upload: false, upload: false,
@@ -28,7 +28,7 @@ export class ImageRequiredModel {
}); });
this.selectPartOfCar = claimFile.map((c, idx) => this.selectPartOfCar = claimFile.map((c, idx) =>
Object.assign(c, { Object.assign(c, {
partId: uuidv4(), partId: randomUUID(),
aiReport: {}, aiReport: {},
imageId: null, imageId: null,
upload: false, 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. * 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 * @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'] * @returns Tuple [dateStr, timeStr] e.g. ['1404/11/24', '13:37']
*/ */
export function toJalaliDateAndTime( export function toJalaliDateAndTime(input: string | Date): [string, string] {
input: string | Date,
): [string, string] {
const d = new Date(input); const d = new Date(input);
const dateStr = toJalaliDate(d); const dateStr = toJalaliDate(d);
const timeStr = toJalaliTime(d); const timeStr = toJalaliTime(d);
@@ -32,8 +28,7 @@ export function jalaliToGregorianDate(
): string | null { ): string | null {
if (input === null || input === undefined) return null; if (input === null || input === undefined) return null;
const raw = const raw = typeof input === "number" ? String(input) : String(input).trim();
typeof input === "number" ? String(input) : String(input).trim();
if (!raw) return null; if (!raw) return null;
let year = 0; let year = 0;
@@ -58,20 +53,16 @@ export function jalaliToGregorianDate(
if (!year || !month || !day) return null; if (!year || !month || !day) return null;
// If the year already looks Gregorian (>= 1900), assume the caller already // If the year already looks Gregorian (>= 1900), just normalise formatting.
// sent a Gregorian date and just normalise the formatting.
if (year >= 1900) { if (year >= 1900) {
const mm = String(month).padStart(2, "0"); const mm = String(month).padStart(2, "0");
const dd = String(day).padStart(2, "0"); const dd = String(day).padStart(2, "0");
const m = jMoment(`${year}-${mm}-${dd}`, "YYYY-MM-DD"); const d = new Date(`${year}-${mm}-${dd}`);
return m.isValid() ? m.format("YYYY-MM-DD") : null; if (isNaN(d.getTime())) return null;
return `${year}-${mm}-${dd}`;
} }
const mm = String(month).padStart(2, "0"); return jalaliPartsToGregorian(year, month, day);
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");
} }
/** /**
@@ -95,6 +86,92 @@ export function toJalaliTime(d: Date): string {
return `${hours}:${minutes}`; 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> = { const PERSIAN_TO_ENGLISH: Record<string, string> = {
"۰": "0", "۰": "0",
"۱": "1", "۱": "1",

View File

@@ -1,8 +1,8 @@
import { NestFactory } from "@nestjs/core"; import { NestFactory } from "@nestjs/core";
import { NestExpressApplication } from "@nestjs/platform-express"; import { NestExpressApplication } from "@nestjs/platform-express";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import * as basicAuth from "express-basic-auth";
import { AppModule } from "./app.module"; import { AppModule } from "./app.module";
import { ConfigService } from "@nestjs/config";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, { const app = await NestFactory.create<NestExpressApplication>(AppModule, {
@@ -15,16 +15,47 @@ async function bootstrap() {
allowedHeaders: "Content-Type, Authorization", allowedHeaders: "Content-Type, Authorization",
}); });
app.use( const configService = app.get(ConfigService);
["/docs"],
basicAuth({ const isDevelopment = configService.get<string>("NODE_ENV") === "development";
challenge: true,
users: { if (isDevelopment) {
[process.env.SWAGGER_USER]: process.env.SWAGGER_PASSWORD, 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() const config = new DocumentBuilder()
.setTitle("yara724-backend") .setTitle("yara724-backend")
.setVersion("1.0.0") .setVersion("1.0.0")
@@ -33,11 +64,15 @@ async function bootstrap() {
.addServer("http://localhost:9001") .addServer("http://localhost:9001")
.addBearerAuth() .addBearerAuth()
.build(); .build();
const docs = SwaggerModule.createDocument(app, config); const docs = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("/docs", app, docs, {
SwaggerModule.setup("docs", app, docs, {
swaggerOptions: { swaggerOptions: {
persistAuthorization: true, persistAuthorization: true,
docExpansion: "none", docExpansion: "none",
ui: isDevelopment ? true : false,
raw: isDevelopment ? true : false,
tagsSorter: (a: string, b: string) => { tagsSorter: (a: string, b: string) => {
const priority: Record<string, number> = { const priority: Record<string, number> = {
user: 0, user: 0,
@@ -50,6 +85,7 @@ async function bootstrap() {
}, },
}, },
}); });
await app.listen(process.env.PORT); await app.listen(process.env.PORT);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,12 @@ import {
ServiceUnavailableException, ServiceUnavailableException,
UnauthorizedException, UnauthorizedException,
} from "@nestjs/common"; } from "@nestjs/common";
import axios from "axios";
import { SandHubDbService } from "src/sand-hub/entity/db-service/sand-hub.db.service"; 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 { SandHubModel } from "src/sand-hub/entity/schema/sand-hub.schema";
import { SystemSettingsService } from "src/system-settings/system-settings.service"; import { SystemSettingsService } from "src/system-settings/system-settings.service";
import { SandHubDetailDto } from "./dto/sand-hub.dto"; import { SandHubDetailDto } from "./dto/sand-hub.dto";
import { jalaliToGregorianDate } from "src/helpers/date-jalali"; import { jalaliToGregorianDate } from "src/helpers/date-jalali";
import { firstValueFrom } from "rxjs";
@Injectable() @Injectable()
export class SandHubService { export class SandHubService {
@@ -123,8 +123,7 @@ export class SandHubService {
const u = (url || "").toLowerCase(); const u = (url || "").toLowerCase();
if (u.includes("personal-inquiry")) { if (u.includes("personal-inquiry")) {
const p = payload as { nationalCode?: string } | undefined; const p = payload as { nationalCode?: string } | undefined;
const nin = const nin = typeof p?.nationalCode === "string" ? p.nationalCode : "-";
typeof p?.nationalCode === "string" ? p.nationalCode : "-";
return { return {
message: "success", message: "success",
data: { data: {
@@ -182,12 +181,11 @@ export class SandHubService {
); );
try { try {
const response = await this.httpService.axiosRef.post( const response = await firstValueFrom(
process.env.SANDHUB_URL_LOGIN, this.httpService.post(process.env.SANDHUB_URL_LOGIN, {
{
email: process.env.SANDHUB_USERNAME, email: process.env.SANDHUB_USERNAME,
password: process.env.SANDHUB_PASSWORD, password: process.env.SANDHUB_PASSWORD,
}, }),
); );
const token = response.data.accessToken; const token = response.data.accessToken;
@@ -237,7 +235,8 @@ export class SandHubService {
); );
try { try {
const response = await this.httpService.axiosRef.post( const response = await firstValueFrom(
this.httpService.post(
`${baseUrl}/user/login`, `${baseUrl}/user/login`,
{ email, password }, { email, password },
{ {
@@ -247,6 +246,7 @@ export class SandHubService {
}, },
timeout: 30000, timeout: 30000,
}, },
),
); );
const token = response.data?.accessToken; const token = response.data?.accessToken;
@@ -260,7 +260,10 @@ export class SandHubService {
return this.tejaratAccessToken; return this.tejaratAccessToken;
} catch (er) { } 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.tejaratAccessToken = null;
this.tejaratTokenExpiry = null; this.tejaratTokenExpiry = null;
throw new UnauthorizedException("Tejarat inquiry authentication failed"); throw new UnauthorizedException("Tejarat inquiry authentication failed");
@@ -278,14 +281,16 @@ export class SandHubService {
for (let attempt = 0; attempt < maxRetries; attempt++) { for (let attempt = 0; attempt < maxRetries; attempt++) {
const token = await this.getTejaratAccessToken(); const token = await this.getTejaratAccessToken();
try { try {
const response = await axios.post(url, payload, { const response = await firstValueFrom(
this.httpService.post(url, payload, {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
}, },
timeout: 30000, timeout: 30000,
}); }),
);
if (!response?.data) throw new Error("EMPTY_RESPONSE"); if (!response?.data) throw new Error("EMPTY_RESPONSE");
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
@@ -361,14 +366,16 @@ export class SandHubService {
for (let attempt = 0; attempt < maxRetries; attempt++) { for (let attempt = 0; attempt < maxRetries; attempt++) {
try { try {
const response = await axios.post(url, payload, { const response = await firstValueFrom(
this.httpService.post(url, payload, {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
}, },
timeout: 50000, timeout: 50000,
}); }),
);
if (!response?.data) throw new Error("EMPTY_RESPONSE"); if (!response?.data) throw new Error("EMPTY_RESPONSE");
return response.data; return response.data;
} catch (err) { } catch (err) {
@@ -397,22 +404,32 @@ export class SandHubService {
// Vehicle information // Vehicle information
MapTypNam: newResponse.vehiclePersianName || newResponse.MapTypNam, 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, MapUsageName: newResponse.MapUsageName || newResponse.MapUsageName,
// Financial coverage // Financial coverage
FinancialCvrCptl: newResponse.financeCoverage || newResponse.FinancialCvrCptl || "0", FinancialCvrCptl:
newResponse.financeCoverage || newResponse.FinancialCvrCptl || "0",
// Dates // Dates
IssueDate: newResponse.persianStartDate || newResponse.IssueDate, IssueDate: newResponse.persianStartDate || newResponse.IssueDate,
EndDate: newResponse.persianEndDate || newResponse.EndDate, EndDate: newResponse.persianEndDate || newResponse.EndDate,
// Insurance details // Insurance details
LastCompanyDocumentNumber: newResponse.lastCompanyInsuranceNumber || newResponse.LastCompanyDocumentNumber || newResponse.insuranceNumber, LastCompanyDocumentNumber:
newResponse.lastCompanyInsuranceNumber ||
newResponse.LastCompanyDocumentNumber ||
newResponse.insuranceNumber,
// Technical details // Technical details
MtrNum: newResponse.MtrNum || newResponse.mtrnum, 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, vin: newResponse.vin || newResponse.VinNumberField || newResponse.vin,
VinNumberField: newResponse.vin || newResponse.VinNumberField, VinNumberField: newResponse.vin || newResponse.VinNumberField,
ChassisNumberField: newResponse.ChassisNumberField || newResponse.shsNam, ChassisNumberField: newResponse.ChassisNumberField || newResponse.shsNam,
@@ -422,7 +439,8 @@ export class SandHubService {
// Discount information // Discount information
DisFnYrPrcnt: newResponse.DisFnYrPrcnt || newResponse.disFnYrPrcnt || "0", DisFnYrPrcnt: newResponse.DisFnYrPrcnt || newResponse.disFnYrPrcnt || "0",
DisLfYrPrcnt: newResponse.DisLfYrPrcnt || newResponse.disLfYrPrcnt || "0", DisLfYrPrcnt: newResponse.DisLfYrPrcnt || newResponse.disLfYrPrcnt || "0",
DisPrsnYrPrcnt: newResponse.DisPrsnYrPrcnt || newResponse.disPrsnYrPrcnt || "0", DisPrsnYrPrcnt:
newResponse.DisPrsnYrPrcnt || newResponse.disPrsnYrPrcnt || "0",
DisFnYrNum: newResponse.DisFnYrNum || newResponse.disFnYrNum, DisFnYrNum: newResponse.DisFnYrNum || newResponse.disFnYrNum,
DisLfYrNum: newResponse.DisLfYrNum || newResponse.disLfYrNum, DisLfYrNum: newResponse.DisLfYrNum || newResponse.disLfYrNum,
DisPrsnYrNum: newResponse.DisPrsnYrNum || newResponse.disPrsnYrNum, DisPrsnYrNum: newResponse.DisPrsnYrNum || newResponse.disPrsnYrNum,
@@ -433,10 +451,13 @@ export class SandHubService {
CarGroupCode: newResponse.CarGroupCode || newResponse.carGrpCod, CarGroupCode: newResponse.CarGroupCode || newResponse.carGrpCod,
// Policy information // Policy information
ThirdPolicyCode: newResponse.ThirdPolicyCode || newResponse.ThirdPolicyCode, ThirdPolicyCode:
newResponse.ThirdPolicyCode || newResponse.ThirdPolicyCode,
// Endorsement data // Endorsement data
EdrsJson: newResponse.EdrsJson || (newResponse.edrSes ? JSON.stringify(newResponse.edrSes) : undefined), EdrsJson:
newResponse.EdrsJson ||
(newResponse.edrSes ? JSON.stringify(newResponse.edrSes) : undefined),
// Insurance fullname // Insurance fullname
InsuranceFullName: newResponse.InsuranceFullName || newResponse.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 * 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. * `"1377-06-24"`/`"1377/06/24"`). We convert here so callers don't have to.
*/ */
async getPersonalInquiry( async getPersonalInquiry(nationalCode: string, birthDate: number | string) {
nationalCode: string,
birthDate: number | string,
) {
try { try {
const requestUrl = `${process.env.SANDHUB_BASE_URL}/personal-inquiry/tejarat-no`; 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 { Injectable, Logger } from "@nestjs/common";
import { KavenegarService } from "./kavenegar.service";
import { import {
isKavenegarSuccess, isKavenegarSuccess,
KavenegarNormalized, KavenegarNormalized,
@@ -21,29 +21,27 @@ export class KavenegarSmsGateway {
async sendMessage(data: SendMessage) { async sendMessage(data: SendMessage) {
try { try {
const body = await this.sender.Send({ const body = await this.sender.send({
sender: "10008663", sender: "10008663",
...data, ...data,
}); });
if (!isKavenegarSuccess(body)) { if (!isKavenegarSuccess(body)) {
const normalized = normalizeKavenegarBody(body); const normalized = normalizeKavenegarBody(body);
this.logProviderRejection("send", data.receptor, undefined, normalized); this.logProviderRejection("send", data.receptor, undefined, normalized);
throw new SmsProviderException("send", { throw new SmsProviderException("send", {
receptor: data.receptor, receptor: data.receptor,
providerBody: body, providerBody: body,
normalized, normalized,
}); });
} }
this.logger.log(
`Kavenegar Send ok receptor=${data.receptor} status=200`,
);
return body; return body;
} catch (e) { } catch (e) {
if (e instanceof SmsProviderException) throw 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); throw new SmsTransportException("send", { receptor: data.receptor }, e);
} }
} }
@@ -51,14 +49,17 @@ export class KavenegarSmsGateway {
async verifyLookUp(data: VerifyLookUpMessage) { async verifyLookUp(data: VerifyLookUpMessage) {
try { try {
const body = await this.sender.verifyLookup(data); const body = await this.sender.verifyLookup(data);
if (!isKavenegarSuccess(body)) { if (!isKavenegarSuccess(body)) {
const normalized = normalizeKavenegarBody(body); const normalized = normalizeKavenegarBody(body);
this.logProviderRejection( this.logProviderRejection(
"verifyLookUp", "verifyLookUp",
data.receptor, data.receptor,
data.template, data.template,
normalized, normalized,
); );
throw new SmsProviderException("verifyLookup", { throw new SmsProviderException("verifyLookup", {
receptor: data.receptor, receptor: data.receptor,
template: data.template, template: data.template,
@@ -66,19 +67,21 @@ export class KavenegarSmsGateway {
normalized, normalized,
}); });
} }
this.logger.log(
`Kavenegar verifyLookUp ok receptor=${data.receptor} template=${data.template} status=200`,
);
return body; return body;
} catch (e) { } catch (e) {
if (e instanceof SmsProviderException) throw e; if (e instanceof SmsProviderException) throw e;
const detail = safeJsonStringify(unwrapKavenegarTransportError(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( throw new SmsTransportException(
"verifyLookup", "verifyLookup",
{ receptor: data.receptor, template: data.template }, {
receptor: data.receptor,
template: data.template,
},
e, e,
); );
} }
@@ -90,9 +93,10 @@ export class KavenegarSmsGateway {
template: string | undefined, template: string | undefined,
normalized: KavenegarNormalized, normalized: KavenegarNormalized,
) { ) {
const t = template != null && template !== "" ? ` template=${template}` : ""; const t = template != null ? ` template=${template}` : "";
this.logger.warn( 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 { Injectable, Logger } from "@nestjs/common";
import axios, { isAxiosError } from "axios"; import { isAxiosError } from "axios";
import { buildParsianTemplateMessage } from "./parsian-template-messages"; import { buildParsianTemplateMessage } from "./parsian-template-messages";
import { import {
SmsProviderException, SmsProviderException,
SmsTransportException, SmsTransportException,
} from "./sms-provider.exception"; } from "./sms-provider.exception";
import { SendMessage, VerifyLookUpMessage } from "./sms-gateway.types"; import { SendMessage, VerifyLookUpMessage } from "./sms-gateway.types";
import { firstValueFrom } from "rxjs";
import { HttpService } from "@nestjs/axios";
@Injectable() @Injectable()
export class ParsianSmsGateway { export class ParsianSmsGateway {
private readonly logger = new Logger(ParsianSmsGateway.name); private readonly logger = new Logger(ParsianSmsGateway.name);
constructor(private readonly httpService: HttpService) {}
async sendMessage(data: SendMessage) { async sendMessage(data: SendMessage) {
try { try {
const body = await this.requestSend(data.receptor, data.message); const body = await this.requestSend(data.receptor, data.message);
@@ -19,7 +23,11 @@ export class ParsianSmsGateway {
} catch (e) { } catch (e) {
if (e instanceof SmsProviderException) throw e; if (e instanceof SmsProviderException) throw e;
const detail = isAxiosError(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; : e;
this.logger.error( this.logger.error(
`Parsian Send transport error receptor=${data.receptor} detail=${JSON.stringify(detail)}`, `Parsian Send transport error receptor=${data.receptor} detail=${JSON.stringify(detail)}`,
@@ -39,7 +47,11 @@ export class ParsianSmsGateway {
} catch (e) { } catch (e) {
if (e instanceof SmsProviderException) throw e; if (e instanceof SmsProviderException) throw e;
const detail = isAxiosError(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; : e;
this.logger.error( this.logger.error(
`Parsian verifyLookUp transport error receptor=${data.receptor} template=${data.template} detail=${JSON.stringify(detail)}`, `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; const baseUrl = process.env.PARSIAN_SMS_URL;
if (!baseUrl?.trim()) { if (!baseUrl?.trim()) {
throw new SmsTransportException( throw new SmsTransportException(
@@ -71,7 +86,9 @@ export class ParsianSmsGateway {
} }
const url = `${baseUrl}=${receptor}&Message=${encodeURIComponent(message)}`; 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) { if (response.status < 200 || response.status >= 300) {
throw new SmsProviderException("send", { 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 { 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 { KavenegarSmsGateway } from "./kavenegar-sms.gateway";
import { ParsianSmsGateway } from "./parsian-sms.gateway"; import { ParsianSmsGateway } from "./parsian-sms.gateway";
import { SmsGatewayService } from "./sms-gateway.service"; import { SmsGatewayService } from "./sms-gateway.service";
dotenv.config();
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
@Module({ @Module({
imports: [ imports: [HttpModule, ConfigModule],
KavenegarModule.forRoot({
apikey: process.env.SMS_API_KEY || "",
}),
],
providers: [ providers: [
KavenegarService,
KavenegarSmsGateway, KavenegarSmsGateway,
ParsianSmsGateway, ParsianSmsGateway,
SmsGatewayService, SmsGatewayService,

View File

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

View File

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