forked from Yara724/api
Merge pull request 'ENV edit, Joi validation' (#88) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#88
This commit is contained in:
8272
package-lock.json
generated
8272
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
81
package.json
81
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yara724",
|
||||
"version": "0.0.1",
|
||||
"version": "2.0.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -16,55 +16,61 @@
|
||||
"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",
|
||||
"joi": "^18.2.1",
|
||||
"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 +87,12 @@
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node",
|
||||
"moduleNameMapper": {
|
||||
"^src/(.*)$": "<rootDir>/$1"
|
||||
}
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@nestjs/core",
|
||||
"@swc/core"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { join } from "node:path";
|
||||
import { APP_INTERCEPTOR } from "@nestjs/core";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { APP_INTERCEPTOR, APP_PIPE } from "@nestjs/core";
|
||||
import { Module, ValidationPipe } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } 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";
|
||||
@@ -25,35 +23,77 @@ import { UsersModule } from "./users/users.module";
|
||||
import { applyIranFaTimestampPlugin } from "./helpers/mongoose-fa-timestamps.plugin";
|
||||
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` });
|
||||
import * as Joi from "joi";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
ScheduleModule.forRoot(),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
validationSchema: Joi.object({
|
||||
NODE_ENV: Joi.string()
|
||||
.valid("development", "production")
|
||||
.default("development"),
|
||||
PORT: Joi.number().port().default(9001),
|
||||
URL: Joi.string().uri().required(),
|
||||
BASE_URL_DEV: Joi.string()
|
||||
.uri()
|
||||
.when("NODE_ENV", {
|
||||
is: Joi.string().valid("development"),
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
SWAGGER_USER_DEV: Joi.string().when("NODE_ENV", {
|
||||
is: Joi.string().valid("development"),
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
SWAGGER_PASSWORD_DEV: Joi.string().when("NODE_ENV", {
|
||||
is: Joi.string().valid("development"),
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
MONGO_HOST: Joi.string().required(),
|
||||
MONGO_PORT: Joi.number().port().required(),
|
||||
MONGO_USER: Joi.string().required(),
|
||||
MONGO_PASS: Joi.string().required(),
|
||||
MONGO_DB_NAME: Joi.string().required(),
|
||||
JWT_SECRET: Joi.string().required(),
|
||||
SANHUB_BASE_URL: Joi.string().uri(),
|
||||
SANHUB_URL_LOGIN: Joi.string().uri(),
|
||||
SANHUB_USERNAME: Joi.string(),
|
||||
SANHUB_PASSWORD: Joi.string(),
|
||||
AI_URL: Joi.string().uri(),
|
||||
AI_URL_V2: Joi.string().uri(),
|
||||
AI_USERNAME: Joi.string(),
|
||||
AI_PASSWORD: Joi.string(),
|
||||
SMS_PROVIDER: Joi.string()
|
||||
.valid("kavenegar", "parsian")
|
||||
.default("kavenegar"),
|
||||
SMS_API_KEY: Joi.string(),
|
||||
AUTH_SMS_TEMPLATE: Joi.string(),
|
||||
EXP_OTP_TIME: Joi.number(),
|
||||
}),
|
||||
}),
|
||||
CronModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, "..", "files"),
|
||||
serveRoot: "/files",
|
||||
}),
|
||||
MongooseModule.forRoot(
|
||||
`mongodb://${process.env.MONGO_URL}:${process.env.MONGO_PORT}/`,
|
||||
{
|
||||
dbName: "yara724",
|
||||
autoIndex: true,
|
||||
user: process.env.MONGO_USER,
|
||||
pass: process.env.MONGO_PASS,
|
||||
authMechanism: "SCRAM-SHA-256",
|
||||
tls: true,
|
||||
tlsAllowInvalidCertificates: true,
|
||||
connectionFactory: (connection) => {
|
||||
applyIranFaTimestampPlugin(connection);
|
||||
return connection;
|
||||
},
|
||||
MongooseModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => {
|
||||
return {
|
||||
uri: `mongodb://${configService.get<string>("MONGO_USER")}:${configService.get<string>("MONGO_PASS")}@${configService.get<string>("MONGO_HOST")}:${configService.get<string>("MONGO_PORT")}/${configService.get<string>("MONGO_DB_NAME")}?authSource=admin&${configService.get<string>("MONGO_OPTIONS")}`,
|
||||
tls: true,
|
||||
tlsAllowInvalidCertificates: true,
|
||||
autoIndex: configService.get<string>("NODE_ENV") !== "production",
|
||||
connectionFactory: (connection) => {
|
||||
applyIranFaTimestampPlugin(connection);
|
||||
return connection;
|
||||
},
|
||||
};
|
||||
},
|
||||
),
|
||||
}),
|
||||
UsersModule,
|
||||
AuthModule,
|
||||
ClientModule,
|
||||
|
||||
@@ -78,8 +78,7 @@ export class ActorAuthService {
|
||||
res = await this.fieldExpertDbService.findOne({
|
||||
_id: new Types.ObjectId(userId),
|
||||
});
|
||||
else
|
||||
res = await this.fieldExpertDbService.findOne({ email: username });
|
||||
else res = await this.fieldExpertDbService.findOne({ email: username });
|
||||
break;
|
||||
case RoleEnum.REGISTRAR:
|
||||
if (username == null && userId)
|
||||
@@ -139,7 +138,7 @@ export class ActorAuthService {
|
||||
};
|
||||
|
||||
const access_token = this.jwtService.sign(payload, {
|
||||
secret: `${process.env.SECRET}`,
|
||||
secret: `${process.env.JWT_SECRET}`,
|
||||
expiresIn: "1h",
|
||||
});
|
||||
|
||||
@@ -159,9 +158,7 @@ export class ActorAuthService {
|
||||
throw new UnauthorizedException("user not assigned to this role");
|
||||
}
|
||||
if (!(await this.hashService.compare(pass, user.password))) {
|
||||
throw new UnauthorizedException(
|
||||
"password is incorrect or access Denied",
|
||||
);
|
||||
throw new UnauthorizedException("password is incorrect or access Denied");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
@@ -225,7 +222,7 @@ export class ActorAuthService {
|
||||
firstName: body.firstName,
|
||||
lastName: body.lastName,
|
||||
phone: body.phone,
|
||||
mobile:body.mobile,
|
||||
mobile: body.mobile,
|
||||
city: body.city,
|
||||
state: body.state,
|
||||
address: body.address,
|
||||
@@ -445,13 +442,7 @@ export class ActorAuthService {
|
||||
"state",
|
||||
"address",
|
||||
],
|
||||
field_expert: [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"phone",
|
||||
"mobile",
|
||||
],
|
||||
field_expert: ["firstName", "lastName", "email", "phone", "mobile"],
|
||||
registrar: ["email"],
|
||||
};
|
||||
|
||||
@@ -477,7 +468,11 @@ export class ActorAuthService {
|
||||
}
|
||||
|
||||
// fetch user detail (document or plain object)
|
||||
const document = await this.dynamicDbController(role, currentUser.role === "company" ? currentUser.username : null, userId);
|
||||
const document = await this.dynamicDbController(
|
||||
role,
|
||||
currentUser.role === "company" ? currentUser.username : null,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (!document) throw new NotFoundException("Profile not found");
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export class UserAuthService {
|
||||
role: "user",
|
||||
};
|
||||
const accToken = this.jwtService.sign(payload, {
|
||||
secret: `${process.env.SECRET}`,
|
||||
secret: `${process.env.JWT_SECRET}`,
|
||||
});
|
||||
await this.userDbService.findOneAndUpdate(
|
||||
{ username: user.username },
|
||||
@@ -162,10 +162,7 @@ export class UserAuthService {
|
||||
process.env.AUTH_SMS_TEMPLATE,
|
||||
);
|
||||
if (!ok) {
|
||||
throw new HttpException(
|
||||
"auth sms send failed",
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
);
|
||||
throw new HttpException("auth sms send failed", HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
this.logger.log(
|
||||
`Auth OTP SMS accepted by provider phone=${mobile} otp=${otp}`,
|
||||
|
||||
@@ -49,9 +49,9 @@ import { CaptchaModule } from "src/captcha/captcha.module";
|
||||
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
||||
]),
|
||||
JwtModule.register({
|
||||
signOptions: { expiresIn: "1h" },
|
||||
signOptions: { expiresIn: "1h" }, // TODO: MAKE IT ENV
|
||||
global: true,
|
||||
secret: `${process.env.SECRET}`,
|
||||
secret: `${process.env.JWT_SECRET}`,
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
|
||||
@@ -33,7 +33,7 @@ export class LocalActorAuthGuard implements CanActivate {
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: `${process.env.SECRET}`,
|
||||
secret: `${process.env.JWT_SECRET}`,
|
||||
});
|
||||
|
||||
if (
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ClaimAccessGuard implements CanActivate {
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: `${process.env.SECRET}`,
|
||||
secret: `${process.env.JWT_SECRET}`,
|
||||
});
|
||||
|
||||
// Allow users to pass through (they will be checked by service methods)
|
||||
@@ -81,7 +81,10 @@ export class ClaimAccessGuard implements CanActivate {
|
||||
|
||||
throw new UnauthorizedException("Invalid role");
|
||||
} catch (error) {
|
||||
if (error instanceof ForbiddenException || error instanceof UnauthorizedException) {
|
||||
if (
|
||||
error instanceof ForbiddenException ||
|
||||
error instanceof UnauthorizedException
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new UnauthorizedException();
|
||||
@@ -124,4 +127,3 @@ export class ClaimAccessGuard implements CanActivate {
|
||||
return type === "Bearer" ? token : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,13 @@ export class GlobalGuard implements CanActivate {
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: `${process.env.SECRET}`,
|
||||
secret: `${process.env.JWT_SECRET}`,
|
||||
});
|
||||
|
||||
if (payload.role !== RoleEnum.USER && payload.role !== RoleEnum.FIELD_EXPERT) {
|
||||
if (
|
||||
payload.role !== RoleEnum.USER &&
|
||||
payload.role !== RoleEnum.FIELD_EXPERT
|
||||
) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export class SettingsJwtGuard implements CanActivate {
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: `${process.env.SECRET}`,
|
||||
secret: `${process.env.JWT_SECRET}`,
|
||||
});
|
||||
(request as any).user = payload;
|
||||
(request as any).identity = payload;
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
60
src/main.ts
60
src/main.ts
@@ -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,29 +15,64 @@ 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_DEV", "");
|
||||
const expectedPassword = configService.get<string>(
|
||||
"SWAGGER_PASSWORD_DEV",
|
||||
"",
|
||||
);
|
||||
|
||||
if (username !== expectedUser || password !== expectedPassword) {
|
||||
return challenge();
|
||||
}
|
||||
|
||||
next();
|
||||
} catch {
|
||||
return challenge();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle("yara724-backend")
|
||||
.setVersion("1.0.0")
|
||||
.addServer(process.env.BASE_URL + "/api")
|
||||
.addServer(process.env.BASE_URL_DEV + "/api")
|
||||
.addServer("http://192.168.20.170:9001")
|
||||
.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
@@ -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,
|
||||
{
|
||||
email: process.env.SANDHUB_USERNAME,
|
||||
password: process.env.SANDHUB_PASSWORD,
|
||||
},
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.post(process.env.SANHUB_URL_LOGIN, {
|
||||
email: process.env.SANHUB_USERNAME,
|
||||
password: process.env.SANHUB_PASSWORD,
|
||||
}),
|
||||
);
|
||||
|
||||
const token = response.data.accessToken;
|
||||
@@ -237,16 +235,18 @@ export class SandHubService {
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await this.httpService.axiosRef.post(
|
||||
`${baseUrl}/user/login`,
|
||||
{ email, password },
|
||||
{
|
||||
headers: {
|
||||
Accept: "*/*",
|
||||
"Content-Type": "application/json",
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.post(
|
||||
`${baseUrl}/user/login`,
|
||||
{ email, password },
|
||||
{
|
||||
headers: {
|
||||
Accept: "*/*",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout: 30000,
|
||||
},
|
||||
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, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
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, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
timeout: 50000,
|
||||
});
|
||||
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) {
|
||||
@@ -394,53 +401,67 @@ export class SandHubService {
|
||||
// Company information
|
||||
CompanyCode: newResponse.companyId || newResponse.CompanyCode,
|
||||
CompanyName: newResponse.companyPersianName || newResponse.CompanyName,
|
||||
|
||||
|
||||
// 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,
|
||||
EngineNumberField: newResponse.EngineNumberField || newResponse.mtrnum,
|
||||
ModelField: newResponse.ModelField || newResponse.ModelField,
|
||||
|
||||
|
||||
// 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,
|
||||
|
||||
|
||||
// Usage and vehicle system codes
|
||||
UsageCode: newResponse.UsageCode || newResponse.usgCod,
|
||||
VehicleSystemCode: newResponse.VehicleSystemCode || newResponse.vehSysCod,
|
||||
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,
|
||||
|
||||
|
||||
// National code
|
||||
nationalCode: newResponse.nationalCode || newResponse.ntnlId,
|
||||
};
|
||||
@@ -455,7 +476,7 @@ export class SandHubService {
|
||||
rightTwoDigits: String(userDetail.plate.ir),
|
||||
nationalCode: userDetail.nationalCodeOfInsurer,
|
||||
};
|
||||
const base = process.env.SANDHUB_BASE_URL ?? "";
|
||||
const base = process.env.SANHUB_BASE_URL ?? "";
|
||||
const requestUrl = `${base}/block-inquiry-tejarat`;
|
||||
|
||||
let response: any;
|
||||
@@ -486,12 +507,9 @@ 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`;
|
||||
const requestUrl = `${process.env.SANHUB_BASE_URL}/personal-inquiry/tejarat-no`;
|
||||
|
||||
const gregorianBirthdate = jalaliToGregorianDate(birthDate);
|
||||
if (!gregorianBirthdate) {
|
||||
@@ -531,7 +549,7 @@ export class SandHubService {
|
||||
nationalCode: string,
|
||||
driverLicenseNumber: string,
|
||||
) {
|
||||
const requestUrl = `${process.env.SANDHUB_BASE_URL}/driver-license-check`;
|
||||
const requestUrl = `${process.env.SANHUB_BASE_URL}/driver-license-check`;
|
||||
const requestPayload = {
|
||||
driverLicenseNumber,
|
||||
nationalCode,
|
||||
@@ -570,7 +588,7 @@ export class SandHubService {
|
||||
|
||||
async getCarOwnershipInfo(plate: any, nationalCode: string) {
|
||||
try {
|
||||
const requestUrl = `${process.env.SANDHUB_BASE_URL}/ownership`;
|
||||
const requestUrl = `${process.env.SANHUB_BASE_URL}/ownership`;
|
||||
const requestPayload = {
|
||||
Plk1: String(plate.leftDigits),
|
||||
Plk2: String(plate.centerAlphabet),
|
||||
@@ -606,7 +624,7 @@ export class SandHubService {
|
||||
|
||||
async getShebaValidation(nationalId: string, shebaId: string) {
|
||||
try {
|
||||
const requestUrl = `${process.env.SANDHUB_BASE_URL}/sheba/sheba-tejaratno`;
|
||||
const requestUrl = `${process.env.SANHUB_BASE_URL}/sheba/sheba-tejaratno`;
|
||||
const requestPayload = {
|
||||
AccountOwnerType: "1",
|
||||
NationalId: nationalId,
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
45
src/sms-orchestration/provider/kavenegar.service.ts
Normal file
45
src/sms-orchestration/provider/kavenegar.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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", {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }]),
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user