1
0
forked from Yara724/api

Initial commit after migration to gitea

This commit is contained in:
2026-01-18 11:27:43 +03:30
parent a21039410c
commit ea4b8eb543
196 changed files with 45567 additions and 9 deletions

26
.gitignore vendored
View File

@@ -1,4 +1,4 @@
# ---> Node
### Node ###
# Logs
logs
*.log
@@ -79,7 +79,7 @@ web_modules/
.env.test.local
.env.production.local
.env.local
.env.development.env
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
@@ -103,13 +103,6 @@ dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
@@ -136,3 +129,18 @@ dist
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node
/docker
.development.env
*.env
/files

13
build_n_deploy.sh Normal file
View File

@@ -0,0 +1,13 @@
git pull
docker compose -f dev.docker-compose.yml up -d --build
docker run --rm \
-e SONAR_HOST_URL="https://sq.ittalie.ir" \
-e SONAR_TOKEN="sqp_42fe574fde1e2d527813f4fb6912ad0040c6850a" \
-v "$(pwd):/usr/src" \
sonarsource/sonar-scanner-cli \
-Dsonar.scm.provider=git \
-Dsonar.projectKey=yara724-main-api \
-Dsonar.sources=.

20
dev.Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
# Use the official Node.js image as the base image
FROM node:20-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install the project dependencies
RUN npm i --force
# Copy the rest of the application code to the working directory
COPY . .
# Expose the port on which your NestJS application will run
# EXPOSE 9000
# Start the NestJS application
CMD ["npm", "start"]

35
dev.docker-compose.yml Normal file
View File

@@ -0,0 +1,35 @@
version: '3.7'
services:
api:
build:
context: .
dockerfile: dev.Dockerfile
container_name: yara724-main-api-dev
image: yara724-main-api:dev
restart: unless-stopped
volumes:
- yara724-main-api-dev-files:/app/files/:rw
env_file:
- .development.env
#environment:
# - "MONGODB_USERNAME=ittalie_dev"
# - "MONGODB_PASSWORD=t3hYwwj9jbu8QB6aMxt8KLnj3qEQDuMM"
# - "MONGODB_HOSTNAME=db.ittalie.ir"
# - "MONGODB_PORT=3082"
# - "NODE_ENV=development"
# - "PORT=3002"
# - "SWAGGER_USER=admin"
# - "SWAGGER_PASSWORD=123321"
# - "SECRET=h43h$24qaw849f3912d@3as"
ports:
- 3080:3002
networks:
default:
driver: bridge
ipam:
config:
- subnet: 172.16.58.0/24
volumes:
yara724-main-api-dev-files:
name: yara724-main-api-dev-files

1
err.json Normal file
View File

@@ -0,0 +1 @@
{"1000":{"info":"start","message":"start"},"1001":{"info":"Access Denied Other Actor Lock File","message":""},"1004":{"info":"g","message":""},"1005":{"info":"fSF","message":""},"1006":{"info":"request not found ","message":""}}

8
nest-cli.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

19051
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

109
package.json Normal file
View File

@@ -0,0 +1,109 @@
{
"name": "yara724",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"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:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@arashioz/errjson-talieh": "^2.2.5",
"@fraybabak/kavenegar_nest": "^1.0.5",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/axios": "^3.1.3",
"@nestjs/common": "^10.4.15",
"@nestjs/core": "^10.4.15",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "*",
"@nestjs/mongoose": "^10.1.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.15",
"@nestjs/platform-fastify": "^10.4.15",
"@nestjs/platform-socket.io": "^10.4.15",
"@nestjs/schedule": "^4.1.2",
"@nestjs/serve-static": "^5.0.3",
"@nestjs/swagger": "^8.1.0",
"@nestjs/websockets": "^10.4.15",
"@types/uuid": "^10.0.0",
"axios": "^1.9.0",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"crypto": "^1.0.1",
"dotenv": "^16.4.7",
"express-basic-auth": "^1.2.1",
"fastest-levenshtein": "^1.0.16",
"form-data": "^4.0.2",
"jalali-moment": "^3.3.11",
"kavenegar": "^1.1.4",
"mongoose": "^8.9.2",
"nestjs-command": "^3.1.4",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"short-unique-id": "^5.2.0",
"standard": "^17.1.2",
"standardjs": "^1.0.0-alpha",
"uuid": "^11.0.3",
"yargs": "^17.7.2"
},
"devDependencies": {
"@nestjs/cli": "^10.4.9",
"@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.4.15",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.2",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"@types/yargs": "^17.0.33",
"@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",
"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.1",
"ts-node": "^10.9.2",
"ts-standard": "^12.0.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.2"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@@ -0,0 +1,23 @@
// Weather conditions for accident
export enum WeatherCondition {
CLEAR = "صاف",
RAINY = "بارونی",
SNOWY = "برفی",
FOGGY = "مه آلود",
}
// Road conditions for accident
export enum RoadCondition {
MUDDY = "گلی",
ICY = "یخ زده",
WET = "مرطوب",
DRY = "خشک",
}
// Light conditions for accident
export enum LightCondition {
DAYLIGHT = "روز",
NIGHT = "شب",
LOW_LIGHT = "کم نور",
}

View File

@@ -0,0 +1,14 @@
export enum ReqBlameStatus {
PendingForSecondParty = "PendingForSecondParty",
PendingForFirstParty = "PendingForFirstParty",
UnChecked = "UnChecked",
CheckedRequest = "CheckedRequest",
ReviewRequest = "ReviewRequest",
CheckAgain = "CheckAgain",
UserPending = "UserPending",
CloseRequest = "CloseRequest",
WaitForUserAccept = "WaitForUserAccept",
WaitingForSignatures = "WaitingForSignatures",
PartiesDisagree = "PartiesDisagree",
InPersonVisit = "InPersonVisit",
}

View File

@@ -0,0 +1,26 @@
export enum StepsEnum {
createRequest = "createRequest",
F_InitialForm = "firstParty-initialForm",
F_addPlate = "firstParty-addPlate",
F_videoUpload = "firstParty-videoUpload",
F_addLocation = "firstParty-addLocation",
F_addVoice = "firstParty-addVoice",
F_addDescription = "firstParty-addDescription",
F_addSecondPartyPhoneNumber = "firstParty-addSecondPartyPhoneNumber",
F_addSign = "firstParty-addSign",
F_completed = "firstParty-completed",
S_InitialForm = "secondParty-initialForm",
S_addPlate = "secondParty-addPlate",
S_addLocation = "secondParty-addLocation",
S_addVoice = "secondParty-addVoice",
S_addDescription = "secondParty-addDescription",
S_addSecondPartyPhoneNumber = "S_addSecondPartyPhoneNumber",
S_addSign = "secondParty-addSign",
S_completed = "secondParty-completed",
AddSignatures = "AddSignatures",
CarBodyVideo = "CarBodyVideo",
CarBodyForm = "CarBodyForm",
CarBodySecondForm = "CarBodySecondForm",
CarBodyDamaged = "CarBodyDamaged",
CarBodyGuilty = "CarBodyGuilty",
}

View File

@@ -0,0 +1,21 @@
export enum CarPartEnum {
LeftBackFender = "leftBackFender",
RightBackFender = "rightBackFender",
BackWheel = "backWheel",
LeftBackDoor = "leftBackDoor",
RightBackDoor = "rightBackDoor",
LeftFrontDoor = "leftFrontDoor",
RightFrontDoor = "rightFrontDoor",
LeftMirror = "leftMirror",
RightMirror = "rightMirror",
FrontWheel = "frontWheel",
LeftFrontFender = "leftFrontFender",
RightFrontFender = "RightFrontFender",
FrontBumper = "frontBumper",
FrontCarWindow = "frontCarWindow",
CarHood = "carHood",
BackBumper = "backBumper",
CarTrunk = "carTrunk",
BackCarWindow = "backCarWindow",
Roof = "roof",
}

View File

@@ -0,0 +1,7 @@
export enum DaghiOption {
RECYCLED_PARTS_VALUE = "ارزش لوازم بازیافتی",
DELIVER_DAMAGED_PART = "تحویل داغی",
NO_VALUE = "فاقد ارزش",
WITH_DAMAGED_PART_CALCULATION = "با احتساب داغی",
}

View File

@@ -0,0 +1,7 @@
export class PriceDropIF {
total: number;
carPrice: number;
carModel: number;
carValue: number[];
sumOfSeverity: number;
}

View File

@@ -0,0 +1,5 @@
export enum FactorStatus {
PENDING = "PENDING",
APPROVED = "APPROVED",
REJECTED = "REJECTED",
}

View File

@@ -0,0 +1,6 @@
export enum InPersonDocumentsEnum {
NationalCertificate = "nationalCertificate",
CarCertificate = "carCertificate",
DrivingLicense = "drivingLicense",
CarGreenCard = "carGreenCard",
}

View File

@@ -0,0 +1,18 @@
export enum ClaimRequiredDocumentType {
// Damaged party documents
DAMAGED_DRIVING_LICENSE_BACK = "damaged_driving_license_back",
DAMAGED_DRIVING_LICENSE_FRONT = "damaged_driving_license_front",
DAMAGED_CHASSIS_NUMBER = "damaged_chassis_number",
DAMAGED_ENGINE_PHOTO = "damaged_engine_photo",
DAMAGED_CAR_CARD_FRONT = "damaged_car_card_front",
DAMAGED_CAR_CARD_BACK = "damaged_car_card_back",
DAMAGED_METAL_PLATE = "damaged_metal_plate",
// Guilty party documents
GUILTY_DRIVING_LICENSE_FRONT = "guilty_driving_license_front",
GUILTY_DRIVING_LICENSE_BACK = "guilty_driving_license_back",
GUILTY_CAR_CARD_FRONT = "guilty_car_card_front",
GUILTY_CAR_CARD_BACK = "guilty_car_card_back",
GUILTY_METAL_PLATE = "guilty_metal_plate",
}

View File

@@ -0,0 +1,15 @@
export enum ReqClaimStatus {
WaitingForUserCompleted = "WaitingForUserCompleted",
UnChecked = "UnChecked",
CheckedRequest = "CheckedRequest",
ReviewRequest = "ReviewRequest",
CheckAgain = "CheckAgain",
UserPending = "UserPending",
CloseRequest = "CloseRequest",
WaitingForUserToResend = "WaitingForUserToResend",
InPersonVisit = "InPersonVisit",
PendingFactorUpload = "PendingFactorUpload",
PendingFactorValidation = "PendingFactorValidation",
FactorRejected = "FactorRejected",
UploadingRequiredDocuments = "UploadingRequiredDocuments",
}

View File

@@ -0,0 +1,10 @@
export enum ClaimStepsEnum {
CreateClaimFile = "createClaimFile",
SelectDamagePart = "selectDamagePart",
SelectOtherParts = "SelectOtherParts",
UploadRequiredDocuments = "uploadRequiredDocuments",
ImageRequired = "ImageRequired",
waitForDamageExpertComment = "waitForDamageExpertComment",
WaitingForUserToReact = "WaitingForUserToReact",
WaitingForFactorUpload = "WaitingForFactorUpload",
}

View File

@@ -0,0 +1,4 @@
export enum TypeOfDamage {
Repair = "repair",
Change = "change",
}

View File

@@ -0,0 +1,5 @@
export enum UserReplyEnum {
HOLD = "HOLD",
ACCEPTED = "ACCEPTED",
REJECTED = "REJECTED",
}

View File

@@ -0,0 +1,32 @@
export enum ExpertizedAtEnum {
BADANE = "badane",
SAALES = "saales",
FANI = "fani",
SPECIALIZED = "specialized",
}
export enum PreviousWorkEnum {
INSURANCE_COMPANY = "insuranceCompany",
BROKER = "broker",
GENUINE_EXPERT = "genuineExpert",
}
export enum SkillEnum {
SMOOTHING = "smothing",
COLORING = "coloring",
PARTS_AUTH = "partsAuth",
MEDIA_EVAL = "mediaEval",
SCENE_EXPERT = "sceneExpert",
DYNAMIC_ANALYSIS = "dynamicAnalysis",
}

View File

@@ -0,0 +1,6 @@
export enum Degrees {
DIPLOMA = "diploma",
EXPERT = "expert",
SENIOR_EXPERT = "senior_expert",
PHD = "phd",
}

View File

@@ -0,0 +1,8 @@
export interface Plates {
leftDigits: number;
centerAlphabet: string;
centerDigits: number;
ir: number;
nationalCode: string;
carDetail?: any;
}

View File

@@ -0,0 +1,7 @@
export enum RoleEnum {
EXPERT = "expert",
DAMAGE_EXPERT = "damage_expert",
COMPANY = "company",
ADMIN = "admin",
USER = "user",
}

View File

@@ -0,0 +1,4 @@
export enum UploaderModeEnum {
Sign = "uploadService",
Certificate = "certService",
}

View File

@@ -0,0 +1,5 @@
export enum UserType {
GENUINE = "genuine",
LEGAL = "legal",
INSURER = "insurer",
}

10
src/ai/ai.module.ts Normal file
View File

@@ -0,0 +1,10 @@
import { HttpModule } from "@nestjs/axios";
import { Module } from "@nestjs/common";
import { AiService } from "./ai.service";
@Module({
imports: [HttpModule],
providers: [AiService],
exports: [AiService],
})
export class AiModule {}

273
src/ai/ai.service.ts Normal file
View File

@@ -0,0 +1,273 @@
import { createReadStream, existsSync } from "node:fs";
import { join } from "node:path";
import {
HttpException,
HttpStatus,
Injectable,
Logger,
OnModuleInit,
} from "@nestjs/common";
import axios, { AxiosRequestConfig } from "axios";
import * as FormData from "form-data";
@Injectable()
export class AiService implements OnModuleInit {
private readonly logger = new Logger(AiService.name);
private apiKey: string;
private accessToken: string = null;
// These configurations are for authentication and getting the API key.
private readonly loginOptions: AxiosRequestConfig = {
method: "POST",
headers: { "Content-Type": "application/json" },
url: `${process.env.AI_URL_V2}/auth/login`,
data: {
username: process.env.AI_USERNAME,
password: process.env.AI_PASSWORD,
},
timeout: 30000, // 30 second timeout
};
private get profileOptions(): AxiosRequestConfig {
return {
method: "GET",
url: `${process.env.AI_URL_V2}/auth/profile`,
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
};
}
// This getter dynamically creates the base options for the image processing request.
private get imageProcessOptions(): AxiosRequestConfig {
return {
method: "POST",
url: `${process.env.AI_URL_V2}/services/car-damage/detector?version=ai-v7`,
headers: {
Authorization: `Bearer ${this.accessToken}`,
"gateway-api-key": `${this.apiKey}`,
},
};
}
constructor() {}
async onModuleInit() {
try {
const res = await this.login();
if (res?.accessToken) {
this.logger.verbose("AI Service Authenticated Successfully.");
this.accessToken = res.accessToken;
await this.getApiKey();
this.logger.log("AI Service initialized and ready.");
} else {
this.logger.warn(
"AI Service Unavailable: Login did not return an access token. Will retry on first request.",
);
}
} catch (error) {
// Don't prevent app startup if AI service is temporarily unavailable
// The service will attempt to re-authenticate when aiRequestImage is called
this.logger.warn(
"AI Service Unavailable: Failed during initial login. Will retry on first request.",
);
this.logger.warn(`Error: ${error.message}`);
// Reset tokens so re-authentication will be attempted
this.accessToken = null;
this.apiKey = null;
}
}
private async login() {
try {
const loginResponse = await axios.request(this.loginOptions);
return loginResponse.data;
} catch (err) {
const errorMessage = err.response?.data?.message || err.message || "Unknown error";
const statusCode = err.response?.status || 500;
this.logger.error(`AI login failed: ${errorMessage} (Status: ${statusCode})`);
if (err.response?.data) {
this.logger.error(`AI login error details: ${JSON.stringify(err.response.data, null, 2)}`);
}
throw new HttpException(
`Could not authenticate with AI service: ${errorMessage}`,
statusCode >= 400 && statusCode < 500 ? statusCode : HttpStatus.UNAUTHORIZED,
);
}
}
private async getApiKey() {
try {
const profileResponse = await axios.request(this.profileOptions);
this.apiKey = profileResponse.data.apiKey.key;
this.logger.log("Successfully retrieved AI gateway API key.");
return this.apiKey;
} catch (err) {
this.logger.error("Failed to retrieve AI API key:", err.message);
throw new HttpException(
"Could not get API key from AI service",
HttpStatus.FAILED_DEPENDENCY,
);
}
}
public async aiRequestImage(file: { path: string; fileName?: string }): Promise<any> {
// Ensure authentication is set up
if (!this.accessToken || !this.apiKey) {
this.logger.warn("AI service not authenticated, attempting to re-authenticate...");
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,
);
}
} catch (error) {
this.logger.error("Failed to re-authenticate AI service:", error.message);
throw new HttpException(
"AI Service authentication failed",
HttpStatus.UNAUTHORIZED,
);
}
}
// Resolve relative paths to absolute paths
const filePath = file.path.startsWith("/")
? file.path
: join(process.cwd(), file.path.replace(/^\.\//, ""));
this.logger.log(`Processing AI image request for: ${filePath}`);
// Check if file exists
if (!existsSync(filePath)) {
this.logger.error(`File not found at path: ${filePath}`);
throw new HttpException(
`File not found: ${file.path}`,
HttpStatus.NOT_FOUND,
);
}
const form = new FormData();
const fileStream = createReadStream(filePath);
// Append file with filename if available
if (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);
}
try {
const requestHeaders = {
...this.imageProcessOptions.headers,
...form.getHeaders(),
};
this.logger.log(`[STEP 1/4] Sending request to AI service: ${this.imageProcessOptions.url}`);
this.logger.log(`[STEP 1/4] File: ${filePath}, Filename: ${file.fileName || 'extracted from path'}`);
this.logger.log(`[STEP 1/4] FormData Content-Type: ${form.getHeaders()['content-type']}`);
this.logger.log(`[STEP 1/4] Authorization header present: ${!!requestHeaders.Authorization}`);
this.logger.log(`[STEP 1/4] Gateway API key present: ${!!this.apiKey}`);
this.logger.log(`[STEP 1/4] Request method: POST`);
this.logger.log(`[STEP 1/4] FormData field name: "images"`);
// Get file stats for debugging
const fs = require('fs');
const stats = fs.statSync(filePath);
this.logger.log(`[STEP 1/4] File size: ${stats.size} bytes`);
this.logger.log(`[STEP 1/4] File exists: ${existsSync(filePath)}`);
const response = await axios.request({
...this.imageProcessOptions,
headers: requestHeaders,
data: form,
maxContentLength: Infinity,
maxBodyLength: Infinity,
});
this.logger.log(`[STEP 2/4] Successfully received response from AI service (Status: ${response.status})`);
// Validate response structure
if (!response.data) {
this.logger.error(`[ERROR] AI response is empty or missing 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) {
this.logger.error(`[ERROR] AI service returned an error in response body`);
this.logger.error(`[ERROR] Error message: ${response.data.error}`);
this.logger.error(`[ERROR] Full response: ${JSON.stringify(response.data, null, 2)}`);
throw new HttpException(
`AI Service error: ${response.data.error}`,
HttpStatus.BAD_GATEWAY,
);
}
// Check for processed image (downloadLink)
if (!response.data.downloadLink) {
this.logger.error(`[ERROR] AI response missing processed image (downloadLink)`);
this.logger.error(`[ERROR] Response structure: ${JSON.stringify(Object.keys(response.data))}`);
this.logger.error(`[ERROR] Full response: ${JSON.stringify(response.data, null, 2)}`);
throw new HttpException(
"AI Service did not return processed image (downloadLink missing)",
HttpStatus.BAD_GATEWAY,
);
}
// Check for reports
if (!response.data.reports) {
this.logger.warn(`[WARNING] AI response missing reports object, but downloadLink exists`);
this.logger.warn(`[WARNING] Response keys: ${JSON.stringify(Object.keys(response.data))}`);
}
this.logger.log(`[STEP 3/4] Validated AI response - downloadLink: ${response.data.downloadLink ? 'present' : 'missing'}, reports: ${response.data.reports ? 'present' : 'missing'}`);
return response.data;
} catch (er) {
// Determine error source
let errorSource = "UNKNOWN";
let errorMessage = er.message;
let errorDetails = "No error details available";
if (er.response) {
errorSource = "AI_SERVICE_RESPONSE";
errorMessage = er.response?.data?.message || er.message || `HTTP ${er.response.status}`;
errorDetails = er.response?.data
? JSON.stringify(er.response.data, null, 2)
: `Status: ${er.response.status}, StatusText: ${er.response.statusText}`;
} else if (er.request) {
errorSource = "NETWORK_ERROR";
errorMessage = "Network error - AI service did not respond";
errorDetails = "Request was made but no response received";
} else {
errorSource = "REQUEST_SETUP_ERROR";
errorMessage = er.message || "Error setting up request";
}
this.logger.error(`[ERROR] AI request failed - Source: ${errorSource}`);
this.logger.error(`[ERROR] File path: ${filePath}`);
this.logger.error(`[ERROR] Error message: ${errorMessage}`);
this.logger.error(`[ERROR] Error details: ${errorDetails}`);
if (er.stack) {
this.logger.error(`[ERROR] Stack trace: ${er.stack}`);
}
// Re-throw with detailed error information
throw new HttpException(
`[${errorSource}] ${errorMessage}`,
er.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

66
src/app.module.ts Normal file
View File

@@ -0,0 +1,66 @@
import { join } from "node:path";
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { ScheduleModule } from "@nestjs/schedule";
import { ServeStaticModule } from "@nestjs/serve-static";
import * as dotenv from "dotenv";
import { CommandModule } from "nestjs-command";
import { AiModule } from "./ai/ai.module";
import { AuthModule } from "./auth/auth.module";
import { ClaimRequestManagementModule } from "./claim-request-management/claim-request-management.module";
import { ClientModule } from "./client/client.module";
import { ExpertBlameModule } from "./expert-blame/expert-blame.module";
import { ExpertClaimModule } from "./expert-claim/expert-claim.module";
import { ExpertInsurerModule } from "./expert-insurer/expert-insurer.module";
import { LookupsModule } from "./lookups/lookups.module";
import { PlatesModule } from "./plates/plates.module";
import { ProfileModule } from "./profile/profile.module";
import { SandHubModule } from "./sand-hub/sand-hub.module";
import { ReportsModule } from "./reports/reports.module";
import { RequestManagementModule } from "./request-management/request-management.module";
import { UsersModule } from "./users/users.module";
import { CronModule } from "./utils/cron/cron.module";
dotenv.config();
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
@Module({
imports: [
CommandModule,
ScheduleModule.forRoot(),
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,
},
),
UsersModule,
AuthModule,
ClientModule,
ProfileModule,
PlatesModule,
RequestManagementModule,
SandHubModule,
ExpertBlameModule,
ClaimRequestManagementModule,
ExpertClaimModule,
AiModule,
ReportsModule,
ExpertInsurerModule,
LookupsModule,
],
controllers: [],
providers: [],
})
export class AppModule {}

View File

@@ -0,0 +1,123 @@
import {
Body,
Controller,
Get,
Param,
Patch,
Post,
Req,
UseGuards,
} from "@nestjs/common";
import {
ApiBody,
ApiAcceptedResponse,
ApiResponse,
ApiTags,
ApiBearerAuth,
} from "@nestjs/swagger";
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
import {
ForgetPasswordSendCodeDto,
ForgetPasswordVerifyCodeDto,
} from "src/auth/dto/actor/forget-password.actor.dto";
import { LoginActorDto } from "src/auth/dto/actor/login.actor.dto";
import { ActorEditUserProfileDto } from "src/auth/dto/actor/profile.actor.dto";
import {
GenuineRegisterDto,
InsurerRegisterDto,
LegalRegisterDto,
} from "src/auth/dto/actor/register.actor.dto";
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
import { ClientKey } from "src/decorators/clientKey.decorator";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
@Controller("actor")
@ApiTags("actor")
export class ActorAuthController {
constructor(private readonly actorAuthService: ActorAuthService) {}
@Post("register/genuine")
@ApiBody({ type: GenuineRegisterDto })
async registerGenuine(@Body() body: GenuineRegisterDto) {
return await this.actorAuthService.genuineRegister(body);
}
@Post("register/legal")
@ApiBody({ type: LegalRegisterDto })
async registerLegal(@Body() body: LegalRegisterDto) {
return await this.actorAuthService.legalRegister(body);
}
@Post("register/insurer")
@ApiBody({ type: InsurerRegisterDto })
async registerInsurer(@Body() body: InsurerRegisterDto) {
return await this.actorAuthService.insurerRegister(body);
}
@UseGuards(LocalActorAuthGuard)
@Post("login")
@Roles()
@ApiBody({
type: LoginActorDto,
description: "user verify otp -- call this api and get a tokens",
})
@ApiAcceptedResponse()
async login(@Body() body, @Req() req, @ClientKey() client) {
return await this.actorAuthService.loginActors(req.user);
}
@Post("forget-password")
@ApiBody({
type: ForgetPasswordSendCodeDto,
description: "send otp when call this api",
})
@ApiAcceptedResponse()
@ApiResponse({ type: ForgetPasswordSendCodeDto })
async forgetPassword(@Body() body: ForgetPasswordSendCodeDto) {
return await this.actorAuthService.forgetPasswordSendMail(body.email);
}
@Post("forget-password-verify")
@ApiBody({
type: ForgetPasswordVerifyCodeDto,
description: "send otp when call this api",
})
@ApiAcceptedResponse()
@ApiResponse({ type: ForgetPasswordVerifyCodeDto })
async forgetPasswordVerify(@Body() body: ForgetPasswordVerifyCodeDto) {
const { email, otp, newPassword } = body;
return await this.actorAuthService.forgetPasswordVerify(
email,
otp,
newPassword,
);
}
@Get("register/form/states/list")
async getStates() {
return await this.actorAuthService.getStates();
}
@Get("register/form/cities/list/:stateId")
async getCities(@Param("stateId") stateId: number) {
return await this.actorAuthService.getCities(stateId);
}
@Get("/profile")
@UseGuards(LocalActorAuthGuard)
@ApiBearerAuth()
async getProfile(@CurrentUser() user) {
return await this.actorAuthService.getProfiles(user);
}
@Patch("/profile")
@UseGuards(LocalActorAuthGuard)
@ApiBearerAuth()
async editProfile(
@CurrentUser() user,
@Body() update: ActorEditUserProfileDto,
) {
return await this.actorAuthService.modifyProfile(user, update);
}
}

View File

@@ -0,0 +1,45 @@
import {
Body,
Controller,
HttpException,
HttpStatus,
Post,
Req,
UseGuards,
} from "@nestjs/common";
import { ApiAcceptedResponse, ApiBody, ApiTags } from "@nestjs/swagger";
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
import { UserLoginDto } from "src/auth/dto/user/login.dto";
import { UserVerifyOtp } from "src/auth/dto/user/verify.dto";
import { LocalUserAuthGuard } from "src/auth/guards/user-local.guard";
import { CurrentUser } from "src/decorators/user.decorator";
@Controller("user")
@ApiTags("user")
export class UserAuthController {
constructor(private readonly userAuthService: UserAuthService) {}
@Post("/send-otp")
@ApiBody({
type: UserLoginDto,
description: "user login api -- call this api and send otp",
})
@ApiAcceptedResponse()
async sendOtpRq(@Body() body: UserLoginDto) {
const res = await this.userAuthService.sendOtpRequest(body.mobile);
if (res) {
throw new HttpException(res, HttpStatus.ACCEPTED);
}
}
@Post("/login")
@UseGuards(LocalUserAuthGuard)
@ApiBody({
type: UserVerifyOtp,
description: "user verify otp -- call this api and get a tokens",
})
@ApiAcceptedResponse()
async login(@Body() body, @Req() req, @CurrentUser() user) {
return await this.userAuthService.login(req.user);
}
}

View File

@@ -0,0 +1,397 @@
import { readFileSync } from "node:fs";
import {
BadGatewayException,
BadRequestException,
ForbiddenException,
Injectable,
InternalServerErrorException,
NotFoundException,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Types } from "mongoose";
import { ForgetPasswordVerifyCodeDtoRs } from "src/auth/dto/actor/forget-password.actor.dto";
import { ProfileActor } from "src/auth/dto/actor/profile.actor.dto";
import {
InsurerRegisterDto,
RegisterDto,
RegisterDtoRs,
} from "src/auth/dto/actor/register.actor.dto";
import { StateListDtoRs } from "src/auth/dto/actor/states.dto";
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { UserType } from "src/Types&Enums/userType.enum";
import { InsurerExpertDbService } from "src/users/entities/db-service/insurer-expert.db.service";
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
import { HashService } from "src/utils/hash/hash.service";
import { MailService } from "src/utils/mail/mail.service";
import { OtpService } from "src/utils/otp/otp.service";
function pick(obj: Record<string, any>, keys: string[]) {
const out: Record<string, any> = {};
for (const key of keys) {
if (obj[key] !== undefined) out[key] = obj[key];
}
return out;
}
// TODO FIX REGISTER TO ACTOR.SERVICE AND AUTH IN THIS MODULE
@Injectable()
export class ActorAuthService {
constructor(
private readonly jwtService: JwtService,
private readonly hashService: HashService,
private readonly expertDbService: ExpertDbService,
private readonly damageExpertDbService: DamageExpertDbService,
private readonly insurerExpertDbService: InsurerExpertDbService,
private readonly mailService: MailService,
private readonly clientDbService: ClientDbService,
private readonly otpService: OtpService,
) {}
// TODO convrt to class for dynamic controller
public async dynamicDbController(role, username, userId?: string) {
let res;
switch (role) {
case RoleEnum.EXPERT:
if (username == null && userId)
res = await this.expertDbService.findOne({
_id: new Types.ObjectId(userId),
});
else res = await this.expertDbService.findOne({ email: username });
break;
case RoleEnum.DAMAGE_EXPERT:
if (username == null && userId)
res = await this.damageExpertDbService.findOne({
_id: new Types.ObjectId(userId),
});
else
res = await this.damageExpertDbService.findOne({ email: username });
break;
case RoleEnum.COMPANY:
res = await this.insurerExpertDbService.findOne({ email: username });
break;
default:
return null;
}
return res;
}
async validateActor(username: string, pass: string, role): Promise<any> {
const user = await this.dynamicDbController(role, username);
if (user) {
if (user.role !== role) {
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",
);
} else {
return user;
}
}
return null;
}
async loginActors(user: any) {
let foundedUser = await this.dynamicDbController(user.role, user.username);
if (foundedUser) {
const payload = {
username: foundedUser.username || foundedUser.email,
sub: foundedUser._id,
fullName:
`${foundedUser.firstName || ""} ${foundedUser.lastName || ""}`.trim(),
role: foundedUser.role || "User",
userType: foundedUser.userType || "UserType",
clientKey: foundedUser.clientKey || null,
};
const accToken = this.jwtService.sign(payload, {
secret: `${process.env.SECRET}`,
expiresIn: "1h",
});
return {
...payload,
access_token: accToken,
};
} else {
throw new UnauthorizedException("expert or damage_expert not found");
}
}
async registerActors(
body: RegisterDto | InsurerRegisterDto,
userType: UserType,
) {
const hashPassword = await this.hashService.hash(body.password);
body.password = hashPassword;
(body as any).userType = userType;
try {
if ("username" in body) {
if (userType === UserType.INSURER) {
if (!body.clientKey) {
throw new BadRequestException(
"clientKey is required for this user type.",
);
}
const clientName = await this.clientDbService.find({
_id: new Types.ObjectId(body.clientKey),
});
if (!clientName) throw new NotFoundException("Client Not Found");
}
const newCompanyPayload = {
email: body.username,
password: body.password,
role: RoleEnum.COMPANY,
userType: (body as any).userType,
clientKey: body.clientKey,
firstName: body.firstName,
lastName: body.lastName,
};
return new RegisterDtoRs(
await this.insurerExpertDbService.create(newCompanyPayload),
);
} else {
if (userType === UserType.LEGAL || userType === UserType.GENUINE) {
if (!body.insuActivityCo) {
throw new BadRequestException(
"insuActivityCo is required for this user type.",
);
}
const clientName = await this.clientDbService.find({
_id: body.insuActivityCo,
});
if (!clientName) throw new NotFoundException("Client Not Found");
body.clientKey = body.insuActivityCo;
body.insuActivityCo = clientName.clientName.english;
}
switch (body.role) {
case RoleEnum.EXPERT:
return new RegisterDtoRs(await this.expertDbService.create(body));
case RoleEnum.DAMAGE_EXPERT:
return new RegisterDtoRs(
await this.damageExpertDbService.create(body),
);
default:
throw new BadRequestException(
`Invalid role for this registration type: ${body.role}`,
);
}
}
} catch (er) {
if (er.code === 11000) {
throw new BadRequestException(
"A user with these details already exists.",
);
} else if (er.errors) {
const errObjKey = Object.keys(er.errors)[0];
const { path, kind } = er.errors[errObjKey];
throw new BadRequestException(
`Validation failed for field '${path}'. Expected a valid ${kind}.`,
);
} else {
throw er;
}
}
}
async legalRegister(body: RegisterDto) {
return await this.registerActors(body, UserType.LEGAL);
}
async genuineRegister(body: RegisterDto) {
return await this.registerActors(body, UserType.GENUINE);
}
async insurerRegister(body: InsurerRegisterDto) {
Object.assign(body, { role: RoleEnum.COMPANY });
return await this.registerActors(body, UserType.INSURER);
}
/// TODO need to seed
async getStates() {
const states = JSON.parse(
readFileSync(`${process.cwd()}/src/static/states.json`, "utf8"),
);
return new StateListDtoRs(states);
}
/// TODO need to seeds
async getCities(id: number) {
const cities = JSON.parse(
readFileSync(`${process.cwd()}/src/static/cities.json`, "utf8"),
);
const citiesList = cities.filter((c) => c.province_id == id);
return new StateListDtoRs(citiesList);
}
async forgetPasswordSendMail(email: string) {
const normalizedEmail = email.toLowerCase().trim();
const filter = { email: normalizedEmail };
let actor;
// expert
actor = await this.expertDbService.findOne(filter);
let dbServiceToUpdate = this.expertDbService;
if (!actor) {
actor = (await this.damageExpertDbService.findOne(filter)) as any;
dbServiceToUpdate = this.damageExpertDbService as any;
}
if (!actor) {
actor = await this.insurerExpertDbService.findOne(filter);
dbServiceToUpdate = this.insurerExpertDbService as any;
}
if (!actor) {
throw new NotFoundException("actor not found");
}
const otp = this.otpService.create();
const sendEmail = await this.mailService.sendMail(normalizedEmail, otp);
if (sendEmail) {
const hashOtp = await this.hashService.hash(otp);
await dbServiceToUpdate.findOneAndUpdate(filter, { otp: hashOtp });
return sendEmail;
} else {
throw new BadGatewayException("mailServer in unavailable");
}
}
async forgetPasswordVerify(email, otp, newPassword) {
const userExist = await this.expertDbService.findOne({ email });
if (!userExist) throw new NotFoundException("user not found");
const decodeOtp = await this.hashService.compare(otp, userExist.otp);
if (!decodeOtp) throw new UnauthorizedException("otp invalid");
if (decodeOtp) {
const hashNewPassword = await this.hashService.hash(newPassword);
await this.expertDbService.findOneAndUpdate(
{ email },
{ password: hashNewPassword },
);
return new ForgetPasswordVerifyCodeDtoRs(userExist, "update password");
}
}
async getProfiles(currentUser) {
const userDetail = await this.dynamicDbController(
currentUser.role,
null,
currentUser.sub,
);
return new ProfileActor(userDetail);
}
async modifyProfile(currentUser: any, update: Record<string, any>) {
const role = currentUser?.role;
const userId = currentUser?.sub;
if (!role || !userId) {
throw new BadRequestException("Invalid actor payload");
}
const allowedFieldsByRole: Record<string, string[]> = {
expert: [
"fullName",
"firstName",
"lastName",
"email",
"phone",
"city",
"state",
"bio",
"nationalCode",
"birthDate",
"avatarUrl",
"address",
],
damage_expert: [
"fullName",
"email",
"phone",
"companyName",
"city",
"state",
"bio",
"nationalCode",
"birthDate",
"avatarUrl",
"address",
],
};
const allowedFields = allowedFieldsByRole[role];
if (!allowedFields) {
throw new ForbiddenException("Profile update not allowed for this role");
}
const sanitized = Object.fromEntries(
Object.entries(update).filter(([key]) => allowedFields.includes(key)),
);
if (Object.keys(sanitized).length === 0) {
throw new BadRequestException("No valid fields to update");
}
if (sanitized.birthDate && typeof sanitized.birthDate === "string") {
const parsed = new Date(sanitized.birthDate);
if (isNaN(parsed.getTime())) {
throw new BadRequestException("birthDate must be a valid date");
}
sanitized.birthDate = parsed;
}
// fetch user detail (document or plain object)
const document = await this.dynamicDbController(role, null, userId);
if (!document) throw new NotFoundException("Profile not found");
try {
// if its a mongoose document (has .save)
if (typeof document.save === "function") {
Object.assign(document, sanitized);
await document.save();
return document;
}
// else, update directly via corresponding DB service
switch (role) {
case "expert":
await this.expertDbService.updateOne(
{ _id: new Types.ObjectId(userId) },
{ $set: sanitized },
);
break;
case "damage_expert":
await this.damageExpertDbService.updateOne(
{ _id: new Types.ObjectId(userId) },
{ $set: sanitized },
);
break;
default:
throw new ForbiddenException("Unsupported role for update");
}
// return the fresh document
return await this.dynamicDbController(role, null, userId);
} catch (err) {
throw new InternalServerErrorException("Failed to update profile");
}
}
}

View File

@@ -0,0 +1,137 @@
import {
HttpException,
HttpStatus,
Injectable,
Logger,
NotAcceptableException,
NotFoundException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Types } from "mongoose";
import { LoginDtoRs } from "src/auth/dto/user/login.dto";
import { UserDbService } from "src/users/entities/db-service/user.db.service";
import { HashService } from "src/utils/hash/hash.service";
import { OtpService } from "src/utils/otp/otp.service";
import { SmsManagerService } from "src/utils/sms-manager/sms-manager.service";
// TODO FIX REGISTER TO USER.SERVICE AND AUTH IN THIS MODULE
@Injectable()
export class UserAuthService {
private readonly logger = new Logger(UserAuthService.name);
constructor(
private readonly jwtService: JwtService,
private readonly userDbService: UserDbService,
private readonly hashService: HashService,
private readonly otpCreator: OtpService,
private readonly smsManagerService: SmsManagerService,
) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userDbService.findOne({ username });
if (!user) throw new NotFoundException("user not found");
const now = new Date().getTime();
if (user.otp == null) throw new NotAcceptableException("please get otp");
if (user.otpExpire < now) {
throw new NotAcceptableException("expire otp");
}
if (await this.hashService.compare(pass, user.otp)) {
return user;
}
return false;
}
async login(user: any) {
const payload = {
username: user.username,
sub: user.id,
role: "user",
};
const accToken = this.jwtService.sign(payload, {
secret: `${process.env.SECRET}`,
});
await this.userDbService.findOneAndUpdate(
{ username: user.username },
{
tokens: { token: accToken },
otp: null,
},
);
return {
userId: user._id,
access_token: accToken,
};
}
async sendOtpRequest(mobile: string): Promise<LoginDtoRs> {
const userExist = await this.userDbService.findOne({
mobile,
});
const otp = this.otpCreator.create();
const hashOtp = await this.hashService.hash(otp);
if (!userExist) {
await this.smsSender(otp, mobile);
/// create otp request
const newUser = await this.userDbService.createUser({
mobile,
username: mobile,
otp: hashOtp,
tokens: {
token: "",
rfToken: "",
},
fullName: "",
nationalCode: "",
lastLogin: new Date(),
clientKey: new Types.ObjectId(),
birthDay: "",
city: "",
address: "",
state: "",
otpExpire: new Date(
new Date().getTime() + +process.env.EXP_OTP_TIME * 60 * 1000,
).getTime(),
});
return new LoginDtoRs(newUser);
}
if (userExist) {
await this.smsSender(otp, mobile);
const updateTokens = await this.userDbService.findOneAndUpdate(
{
username: userExist.username,
},
{
otp: hashOtp,
otpExpire: new Date(
new Date().getTime() + +process.env.EXP_OTP_TIME * 60 * 1000,
).getTime(),
},
);
if (updateTokens) return new LoginDtoRs(userExist);
}
}
private async smsSender(otp: string, mobile: string) {
return this.smsManagerService
.verifyLookUp({
token: otp,
template: process.env.AUTH_SMS_TEMPLATE,
receptor: mobile,
})
.then((smsRes) => {
this.logger.log(
`${"phone : " + mobile + " " + ", status : " + smsRes["return"].status + ", otp : " + otp} `,
);
})
.catch((er) => {
this.logger.error(
`${"phone : " + mobile + " " + ", status : " + er["return"].status + ", otp : " + otp} `,
);
throw new HttpException(
" auth sms send failed",
HttpStatus.INTERNAL_SERVER_ERROR,
);
});
}
}

42
src/auth/auth.module.ts Normal file
View File

@@ -0,0 +1,42 @@
import { Module } from "@nestjs/common";
import { JwtModule, JwtService } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { LocalStrategy } from "src/auth/stratregys/local.strategy";
import { LocalActorStrategy } from "src/auth/stratregys/local-actor.strategy";
import { ActorAuthController } from "src/auth/auth-controllers/actor/actor.auth.controller";
import { UserAuthController } from "src/auth/auth-controllers/user/user.auth.controller";
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
import { ClientModule } from "src/client/client.module";
import { UsersModule } from "src/users/users.module";
import { HashModule } from "src/utils/hash/hash.module";
import { MailModule } from "src/utils/mail/mail.module";
import { OtpModule } from "src/utils/otp/otp.module";
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
@Module({
imports: [
MailModule,
UsersModule,
ClientModule,
HashModule,
OtpModule,
PassportModule,
SmsManagerModule,
JwtModule.register({
signOptions: { expiresIn: "1h" },
global: true,
secret: `${process.env.SECRET}`,
}),
],
providers: [
UserAuthService,
ActorAuthService,
LocalStrategy,
LocalActorStrategy,
JwtService,
],
exports: [LocalStrategy, UserAuthService, ActorAuthService, JwtService],
controllers: [UserAuthController, ActorAuthController],
})
export class AuthModule {}

View File

@@ -0,0 +1,30 @@
import { ApiProperty } from "@nestjs/swagger";
export class ForgetPasswordSendCodeDto {
@ApiProperty({ example: "balali.arash@gmail.com", type: String })
email: string;
}
export class ForgetPasswordVerifyCodeDto {
@ApiProperty({ example: "09331009989", type: String })
email: string;
@ApiProperty({ type: String, examples: { true: "22222" } })
otp: string;
@ApiProperty({ type: String })
newPassword: string;
}
export class ForgetPasswordVerifyCodeDtoRs {
@ApiProperty({ example: "balali.arash@gmail.com", type: String })
email: string;
@ApiProperty({ type: String, examples: { true: "22222" } })
message: string;
constructor(user, message) {
this.email = user.email;
this.message = message;
}
}

View File

@@ -0,0 +1,41 @@
import { ApiProperty } from "@nestjs/swagger";
import { RoleEnum } from "src/Types&Enums/role.enum";
export class LoginActorDto {
@ApiProperty({ example: RoleEnum, type: "array", description: "LOGIN_DTO" })
role: RoleEnum[];
@ApiProperty({})
username: string;
@ApiProperty({})
password: string;
}
export class LoginActorDtoRs extends LoginActorDto {
private readonly userId;
constructor(userData) {
super();
this.userId = userData._id;
this.role = userData.role;
this.username = userData.email;
this.clientKey = userData.clientKey;
this.token = userData.token;
this.refreshToken = userData.refreshToken;
}
@ApiProperty({ type: "string", description: "LOGIN_DTO_RS" })
fullName: string;
@ApiProperty({ type: "string", description: "LOGIN_DTO_RS" })
role: RoleEnum[];
@ApiProperty({ type: "string", description: "LOGIN_DTO_RS" })
token: string;
@ApiProperty({ type: "string", description: "LOGIN_DTO_RS" })
refreshToken: string;
@ApiProperty({ type: "string", description: "LOGIN_DTO_RS" })
clientKey: string;
}

View File

@@ -0,0 +1,98 @@
import { ApiProperty, ApiSchema } from "@nestjs/swagger";
import { IsMobilePhone, IsString, Length } from "class-validator";
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
export class ProfileActor {
public email: string;
public fullName: string;
public nationalCode: string;
public insuActivityCo: string;
public userType: string;
public mobile: string;
public address: string;
public state: string;
public city: string;
constructor(Profile: DamageExpertModel) {
this.email = Profile?.email;
this.fullName = Profile?.firstName + " " + Profile?.lastName;
this.nationalCode = Profile?.nationalCode;
this.insuActivityCo = Profile?.insuActivityCo;
this.userType = Profile?.userType;
this.mobile = Profile?.mobile;
this.address = Profile?.address;
this.state = Profile?.state;
this.city = Profile?.city;
}
}
@ApiSchema({
name: "Edit Actor Profile",
description: "Body of the request to edit the actor's profile",
})
export class ActorEditUserProfileDto {
@ApiProperty({
type: "string",
description: "First name of the actor",
example: "heshmat",
nullable: true,
})
@IsString()
@Length(2, 40)
firstName?: string;
@ApiProperty({
type: "string",
description: "Last name of the actor",
example: "Heshmati",
nullable: true,
})
@IsString()
@Length(1, 10)
lastName?: string;
@ApiProperty({
type: "string",
description: "Mobile phone of the actor",
example: "09123456789",
nullable: true,
})
@IsMobilePhone("fa-IR")
mobile?: string;
@ApiProperty({
type: "string",
description: "تلفن خط ثابت",
example: "02122222222",
nullable: true,
})
@IsString()
phone?: string;
@ApiProperty({
type: "string",
description: "City of the user",
example: "استان",
nullable: true,
})
@IsString()
city?: string;
@ApiProperty({
type: "string",
description: "State of the user",
example: "شهر",
nullable: true,
})
@IsString()
state?: string;
@ApiProperty({
type: "string",
description: "Address of the user",
example: "آدرس",
nullable: true,
})
@IsString()
address?: string;
}

View File

@@ -0,0 +1,341 @@
import { ApiProperty } from "@nestjs/swagger";
import { Exclude } from "class-transformer";
import { IsEmail } from "class-validator";
import { Types } from "mongoose";
import { Degrees } from "src/Types&Enums/degrees.enum";
import {
ExpertizedAtEnum,
PreviousWorkEnum,
SkillEnum,
} from "src/Types&Enums/damage-expert.enum";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { UserType } from "src/Types&Enums/userType.enum";
@Exclude()
export class RegisterDto {
clientKey?: string;
@ApiProperty({
example: "Soheil",
type: "string",
description: "firstname of actor",
})
firstName: string;
@ApiProperty({
enum: RoleEnum,
examples: RoleEnum,
type: "string",
description: "firstname of actor",
})
role: RoleEnum;
userType: UserType;
@ApiProperty({
example: "Hajizadeh",
type: "string",
description: "lastname of actor",
})
lastName: string;
@ApiProperty({
example: "4311402422",
type: "string",
description: "nationalCode of actor",
})
nationalCode: string;
@ApiProperty({
example: "dev.callmeskylark@gmail.com",
type: "string",
description: "email of actor",
})
email: string;
@ApiProperty({
example: "123321",
type: "string",
description: "password of actor",
})
password: string;
@ApiProperty({
example: "7522312365495123",
type: "string",
description: "sheba of actor",
})
sheba?: string;
@ApiProperty({
example: "02133564521",
type: "string",
description: "phone of actor",
})
phone?: string;
@ApiProperty({
example: "09226187419",
type: "string",
description: "mobile of actor",
})
mobile: string;
@ApiProperty({
example: "Tehran",
type: "string",
description: "province of actor",
})
state?: string;
@ApiProperty({
example: "Tehran",
type: "string",
description: "city of actor",
})
city?: string;
@ApiProperty({
example: "Gisha , No 7",
type: "string",
description: "address of actor",
})
address?: string;
@ApiProperty({
enum: Degrees,
examples: Degrees,
type: String,
description: "expDegree of actor",
})
expDegree?: Degrees;
@ApiProperty({
example: "5",
type: "number",
description: "insuActivityTime of actor",
})
insuActivityTime?: number;
@ApiProperty({
example: "64fc4978b74d670939b08920",
type: String,
description: "insuActivityCo of actor",
})
insuActivityCo: string;
@ApiProperty({
example: "5",
type: "number",
description: "policeActivityTime of actor",
})
policeActivityTime?: number;
@ApiProperty({
examples: ["headquarters", "queue"],
type: "string",
description: "policeActivityKind of actor",
})
policeActivityKind?: string;
@ApiProperty({
example: "Tehran",
type: String,
description: "policeServicePlace of actor",
})
policeServicePlace?: string;
}
export class GenuineRegisterDto extends RegisterDto {
@ApiProperty({
type: () => ({
fullName: { type: "string", example: "John Doe" },
nationalCode: { type: "string", example: "4311402422" },
dob: {
type: "string",
example: "1990-01-01",
description: "date of birth in ISO string or yyyy-mm-dd",
},
mobile: { type: "string", example: "09226187419" },
}),
description: "personal information of damage expert (duplicated with some flat fields)",
required: false,
})
personalInfo?: {
fullName: string;
nationalCode: string;
dob: string;
mobile: string;
};
@ApiProperty({
type: () => ({
nationalCard: {
type: "string",
example: "665f0e5ab74d670939b08920",
description: "fileId of uploaded national card",
},
selfie: {
type: "string",
example: "665f0e5ab74d670939b08921",
description: "fileId of uploaded selfie",
},
activityCert: {
type: "string",
example: "665f0e5ab74d670939b08922",
description: "fileId of uploaded activity certificate",
},
}),
description:
"IDs of already uploaded documents in upload module (no files uploaded directly here)",
required: false,
})
personalDocs?: {
nationalCard?: string;
selfie?: string;
activityCert?: string;
};
@ApiProperty({
type: () => ({
state: {
type: "number",
example: 8,
description: "state id from /actor/register/form/states/list",
},
city: {
type: "number",
example: 101,
description: "city id from /actor/register/form/cities/list/:stateId",
},
expertizedAt: {
type: "array",
items: { enum: Object.values(ExpertizedAtEnum) },
example: [ExpertizedAtEnum.BADANE, ExpertizedAtEnum.FANI],
description: "one or more expertized fields",
},
}),
required: false,
})
workExperience?: {
state: number;
city: number;
expertizedAt: ExpertizedAtEnum[];
};
@ApiProperty({
type: () => ({
workingYears: {
type: "number",
example: 5,
},
casesCount: {
type: "number",
example: 120,
},
previousWork: {
enum: PreviousWorkEnum,
example: PreviousWorkEnum.INSURANCE_COMPANY,
},
}),
required: false,
})
workRecords?: {
workingYears: number;
casesCount: number;
previousWork: PreviousWorkEnum;
};
@ApiProperty({
type: "array",
items: {
type: "string",
enum: Object.values(SkillEnum),
},
required: false,
description: "one or more skills of the damage expert",
example: [SkillEnum.SMOOTHING, SkillEnum.SCENE_EXPERT],
})
skills?: SkillEnum[];
@ApiProperty({
type: () => ({
IBAN: {
type: "string",
example: "IR820540102680020817909002",
},
cardNum: {
type: "string",
example: "6037991234567890",
},
accountNum: {
type: "string",
example: "0101234567000",
},
}),
required: false,
})
financialInfo?: {
IBAN: string;
cardNum: string;
accountNum: string;
};
}
export class LegalRegisterDto implements RegisterDto {
userType: UserType;
@ApiProperty()
firstName: string;
@ApiProperty()
role: RoleEnum;
@ApiProperty()
lastName: string;
@ApiProperty()
nationalCode: string;
@ApiProperty()
email: string;
@ApiProperty()
password: string;
@ApiProperty()
mobile: string;
@ApiProperty({
example: "64fc4978b74d670939b08920",
type: String,
description: "insuActivityCo of actor",
})
insuActivityCo: string;
}
export class InsurerRegisterDto {
@ApiProperty()
firstName: string;
@ApiProperty()
lastName: string;
@ApiProperty({ description: "just submit by Email" })
@IsEmail()
username: string;
@ApiProperty()
password: string;
@ApiProperty({ example: "{saman : 651abe5814ed4bb6ee20cd81}" })
clientKey: Types.ObjectId;
}
export class RegisterDtoRs extends RegisterDto {
@ApiProperty({ type: "string", description: "REGISTER_DTO_RS" })
message: string;
constructor(registerData) {
super();
this.message = registerData.message || "ثبت نام با موفقیت انجام شد.";
}
}

View File

@@ -0,0 +1,15 @@
export class StatesDtoRs {
name: string;
id: string;
constructor(states) {
this.name = states.name;
this.id = states.id;
}
}
export class StateListDtoRs {
list: StatesDtoRs[];
constructor(statesLis: []) {
this.list = statesLis.map((s) => new StatesDtoRs(s));
}
}

View File

View File

@@ -0,0 +1,44 @@
import { ApiProperty } from "@nestjs/swagger";
import { UserModel } from "src/users/entities/schema/user.schema";
export class UserLoginDto {
@ApiProperty({
example: "09226187419",
type: "string",
description: "User login dto",
})
mobile: string;
}
export class LoginDtoRs extends UserModel {
message: string;
@ApiProperty({
example: "09226187419",
type: "string",
description: "User login dto",
})
tokens: { token: string; rfToken: string };
@ApiProperty({
example: "09226187419",
type: "string",
description: "User login dto",
})
userId: string;
@ApiProperty({
example: "09226187419",
type: "string",
description: "User login dto",
})
firstName: string;
lastName: string;
username: string;
mobile: string;
nationalCode: string;
constructor(loginData) {
super();
this.mobile = loginData.mobile;
}
}

View File

@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
export class UserVerifyOtp {
@ApiProperty({
example: "09226187419",
type: "string",
description: "User login dto",
})
username: string;
@ApiProperty({
example: "258567",
type: "string",
description: "User login verify dto",
})
password: string;
}

View File

@@ -0,0 +1,63 @@
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { AuthGuard } from "@nestjs/passport";
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
import { RoleEnum } from "src/Types&Enums/role.enum";
@Injectable()
export class LocalActorAuthGuard extends AuthGuard("actor") {
constructor(
private readonly actorAuthService: ActorAuthService,
private readonly jwtService: JwtService,
) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
const path = request.url;
if (!token) {
if (path === "/actor/login") {
const loginData = await this.actorAuthService.loginActors(request.body);
request.user = loginData;
request.identity = request;
return true;
} else {
throw new UnauthorizedException("Token not found");
}
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: `${process.env.SECRET}`,
});
if (
![RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT, RoleEnum.COMPANY].includes(
payload.role,
)
) {
throw new UnauthorizedException("User role is not authorized");
}
request.user = payload;
request.identity = request.user;
} catch {
throw new UnauthorizedException("Invalid token");
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
//@ts-ignore
const [type, token] = request.headers.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}

View File

@@ -0,0 +1,127 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
ForbiddenException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { ClaimRequestManagementDbService } from "src/claim-request-management/entites/db-service/claim-request-management.db.service";
import { Types } from "mongoose";
/**
* Guard that allows:
* - Users to access their own claim files
* - Experts to access IN_PERSON claim files they initiated
*/
@Injectable()
export class ClaimAccessGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly claimDbService: ClaimRequestManagementDbService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: `${process.env.SECRET}`,
});
// Allow users to pass through (they will be checked by service methods)
if (payload.role === RoleEnum.USER) {
request.user = payload;
request.identity = request.user;
return true;
}
// For experts, check if they're accessing an IN_PERSON claim file they initiated
if (
payload.role === RoleEnum.EXPERT ||
payload.role === RoleEnum.DAMAGE_EXPERT
) {
// Extract claimRequestId from route params
const claimRequestId =
request.params?.claimRequestId ||
request.params?.claimRequestID ||
request.params?.id;
if (!claimRequestId) {
// If no claim ID in params, allow access (e.g., for listing endpoints)
// The service will filter appropriately
request.user = payload;
request.actor = payload;
request.identity = payload;
return true;
}
// Verify expert has access to this specific claim file
const hasAccess = await this.verifyExpertClaimAccess(
claimRequestId,
payload.sub,
);
if (!hasAccess) {
throw new ForbiddenException(
"You can only access IN_PERSON claim files that you initiated",
);
}
request.user = payload;
request.actor = payload;
request.identity = payload;
return true;
}
throw new UnauthorizedException("Invalid role");
} catch (error) {
if (error instanceof ForbiddenException || error instanceof UnauthorizedException) {
throw error;
}
throw new UnauthorizedException();
}
}
private async verifyExpertClaimAccess(
claimRequestId: string,
expertId: string,
): Promise<boolean> {
try {
const claim = await this.claimDbService.findOne(claimRequestId);
if (!claim) {
return false;
}
const blameFile = claim.blameFile;
if (!blameFile) {
return false;
}
// Check if it's an expert-initiated IN_PERSON file
if (
blameFile.expertInitiated &&
blameFile.creationMethod === "IN_PERSON" &&
blameFile.initiatedBy
) {
// Verify the expert is the one who initiated it
return String(blameFile.initiatedBy) === expertId;
}
return false;
} catch (error) {
return false;
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}

View File

@@ -0,0 +1,44 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Request } from "express";
import { RoleEnum } from "src/Types&Enums/role.enum";
@Injectable()
export class GlobalGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: `${process.env.SECRET}`,
});
if (payload.role !== RoleEnum.USER) {
console.log(
"🚀 ~ GlobalGuard ~ canActivate ~ request.user.role:",
request.user.role,
);
throw new UnauthorizedException();
}
request.user = payload;
request.identity = request.user;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}

View File

@@ -0,0 +1,24 @@
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// get the roles required
const roles = this.reflector.getAllAndOverride<string[]>("role", [
context.getHandler(),
context.getClass(),
]);
if (!roles) {
return false;
}
const request = context.switchToHttp().getRequest();
const userRoles = request.user?.role?.split(",");
return this.validateRoles(roles, userRoles);
}
validateRoles(roles: string[], userRoles: string[]) {
return roles.some((role) => userRoles.includes(role));
}
}

View File

@@ -0,0 +1,27 @@
import {
ExecutionContext,
Injectable,
NotAcceptableException,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
@Injectable()
export class LocalUserAuthGuard extends AuthGuard("local") {
constructor(private readonly userAuthService: UserAuthService) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const { username, password } = request.body;
let isValidUser = await this.userAuthService.validateUser(
username,
password,
);
if (!isValidUser) {
throw new NotAcceptableException("otp is wrong");
}
request["user"] = isValidUser;
return true;
}
}

View File

@@ -0,0 +1,22 @@
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
@Injectable()
export class LocalActorStrategy extends PassportStrategy(Strategy, "actor") {
constructor(private readonly actorAuthService: ActorAuthService) {
super();
}
// async validate(username, password): Promise<any> {
// const user = await this.actorAuthService.validateActor(
// username,
// password,
// );
// if (!user) {
// throw new UnauthorizedException("user not found");
// }
// return user;
// }
}

View File

@@ -0,0 +1,19 @@
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly userAuthService: UserAuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.userAuthService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException("user not found please register");
}
return user;
}
}

View File

@@ -0,0 +1,483 @@
import { readFile } from "node:fs/promises";
import { extname, parse } from "node:path";
import {
BadRequestException,
Body,
Controller,
Get,
Param,
Patch,
Post,
Put,
Query,
UploadedFile,
UseGuards,
UseInterceptors,
} from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiParam,
ApiQuery,
ApiTags,
} from "@nestjs/swagger";
import { diskStorage } from "multer";
import { GlobalGuard } from "src/auth/guards/global.guard";
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
import { RolesGuard } from "src/auth/guards/role.guard";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { ClaimRequestManagementService } from "./claim-request-management.service";
import { CarDamagePartDto, OtherCarDamagePartDto } from "./dto/car-part.dto";
import { UserCommentDto } from "./dto/user-comment.dto";
import { UserObjectionDto } from "./dto/user-objection.dto";
import { InPersonVisitDto } from "./dto/in-person-visit.dto";
@Controller("claim-request-management")
@ApiTags("claim-request-management")
@Roles(RoleEnum.USER, RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT)
@UseGuards(ClaimAccessGuard, RolesGuard)
@ApiBearerAuth()
export class ClaimRequestManagementController {
constructor(
private readonly claimRequestManagementService: ClaimRequestManagementService,
) {}
@ApiParam({ name: "blameId" })
@Post("/:blameId")
async createClaimRequest(
@Param("blameId") requestId: string,
@CurrentUser() user,
) {
return await this.claimRequestManagementService.createClaimRequest(
requestId,
user.role === RoleEnum.USER ? user.sub : undefined,
user,
);
}
@ApiBody({ type: CarDamagePartDto })
@Patch("/car-part-damage/:claimRequestID")
@ApiParam({ name: "claimRequestID" })
async carPartDamage(
@Param("claimRequestID") requestId: string,
@Body() body: CarDamagePartDto,
@CurrentUser() user,
) {
return await this.claimRequestManagementService.selectCarPartDamage(
requestId,
body,
user,
);
}
@Get("/car-other-part")
async getCarOtherParts() {
const carOtherPart = await readFile(
`${process.cwd()}/src/static/car-part.json`,
"utf-8",
);
return carOtherPart;
}
@ApiBody({ type: OtherCarDamagePartDto })
@ApiParam({ name: "claimRequestID" })
@UseInterceptors(
FileInterceptor("file", {
limits: {
fileSize: 10 * 1024 * 1024,
},
storage: diskStorage({
destination: "./files/car-green-cards",
filename: (req, file, callback) => {
const unique = Date.now();
const ex = extname(file.originalname);
const filename = `${file.originalname.split(" ")[0]}-${unique}${ex}`;
callback(null, filename);
},
}),
}),
)
@ApiConsumes("multipart/form-data")
@Patch("/car-other-part-damage/:claimRequestID")
async carOtherPartDamage(
@Param("claimRequestID") requestId: string,
@UploadedFile() file,
@Body() body: OtherCarDamagePartDto,
@CurrentUser() user,
) {
return await this.claimRequestManagementService.selectCarOtherPartDamage(
requestId,
body,
file,
user,
);
}
@Get("required-documents-status/:claimRequestID")
@ApiParam({ name: "claimRequestID" })
async getRequiredDocumentsStatus(
@Param("claimRequestID") requestId: string,
) {
return await this.claimRequestManagementService.getRequiredDocumentsStatus(
requestId,
);
}
@Get("car-part-image-required/:claimRequestID")
@ApiParam({ name: "claimRequestID" })
async getImageRequired(@Param("claimRequestID") requestId) {
return await this.claimRequestManagementService.getImageRequiredList(
requestId,
);
}
@ApiBody({
schema: {
type: "object",
properties: {
file: { type: "string", format: "binary" },
},
},
})
@UseInterceptors(
FileInterceptor("file", {
limits: { fileSize: 10 * 1024 * 1024 },
storage: diskStorage({
destination: "./files/claim-required-documents/",
filename: (req, file, callback) => {
const extension = extname(file.originalname);
const basename = parse(file.originalname).name;
const unique = Date.now();
const sanitizedBasename = basename
.replace(/\s/g, "_")
.replace(/[^\w\-_]/g, "")
.substring(0, 50);
const filename = `${sanitizedBasename}-${unique}${extension}`;
callback(null, filename);
},
}),
}),
)
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestID" })
@ApiQuery({
name: "documentType",
enum: [
"damaged_driving_license_back",
"damaged_driving_license_front",
"damaged_chassis_number",
"damaged_engine_photo",
"damaged_car_card_front",
"damaged_car_card_back",
"damaged_metal_plate",
"guilty_driving_license_front",
"guilty_driving_license_back",
"guilty_car_card_front",
"guilty_car_card_back",
"guilty_metal_plate",
],
description: "Type of required document to upload",
})
@Patch("upload-required-document/:claimRequestID")
async uploadRequiredDocument(
@Param("claimRequestID") requestId: string,
@Query("documentType") documentType: string,
@UploadedFile("file") file: Express.Multer.File,
@CurrentUser() user,
) {
if (!file) {
throw new BadRequestException("File is required");
}
return await this.claimRequestManagementService.uploadRequiredDocument(
requestId,
documentType as any,
file,
user,
);
}
@ApiBody({
schema: {
type: "object",
properties: {
file: { type: "string", format: "binary" },
},
},
})
@UseInterceptors(
FileInterceptor("file", {
limits: { fileSize: 10 * 1024 * 1024 },
storage: diskStorage({
destination: "./files/car-parts/",
filename: (req, file, callback) => {
const extension = extname(file.originalname);
const basename = parse(file.originalname).name;
const unique = Date.now();
// Sanitize filename: remove non-ASCII characters and special chars to avoid encoding issues
const sanitizedBasename = basename
.replace(/\s/g, "_")
.replace(/[^\w\-_]/g, "") // Remove all non-word characters except hyphens and underscores
.substring(0, 50); // Limit length
const filename = `${sanitizedBasename}-${unique}${extension}`;
callback(null, filename);
},
}),
}),
)
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestID" })
@ApiParam({
name: "partId",
description: "The ID of the specific car part being photographed.",
})
@Patch("capture-car-part-damage/:claimRequestID/:partId")
async captureCarPartDamage(
@Param("partId") partId: string,
@Param("claimRequestID") requestId: string,
@UploadedFile("file") file: Express.Multer.File,
) {
if (!file) {
throw new BadRequestException("Image file is required.");
}
return await this.claimRequestManagementService.setDamageImage(
requestId,
partId,
file,
);
}
@ApiBody({
schema: {
type: "object",
properties: {
file: { type: "string", format: "binary" },
},
},
})
@UseInterceptors(
FileInterceptor("file", {
limits: { fileSize: 50 * 1024 * 1024 },
storage: diskStorage({
destination: "./files/car-capture-videos/",
filename: (req, file, callback) => {
const unique = Date.now();
const ex = extname(file.originalname);
const filename = `claim-video-${unique}${ex}`;
callback(null, filename);
},
}),
}),
)
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestID" })
@Patch("car-capture/:claimRequestID")
async captureVideoCapture(
@Param("claimRequestID") requestId: string,
@UploadedFile("file") file: Express.Multer.File,
) {
return await this.claimRequestManagementService.setVideoCapture(
requestId,
file,
);
}
@Get("requests/")
async getRequest(@CurrentUser() currentUser) {
return await this.claimRequestManagementService.myRequests(currentUser);
}
@Get("request/:claimRequestId")
@ApiParam({ name: "claimRequestId" })
myRequests(
@Param("claimRequestId") requestId: string,
@CurrentUser() user,
) {
return this.claimRequestManagementService.requestDetails(requestId, user);
}
@Put("request/reply/:claimRequestId")
@ApiParam({ name: "claimRequestId" })
@UseInterceptors(
FileInterceptor("file", {
limits: {
fileSize: 10 * 1024 * 1024,
},
storage: diskStorage({
destination: "./files/claim-sign",
filename: (req, file, callback) => {
const unique = Date.now();
const ex = extname(file.originalname);
const filename = `${file.originalname.split(" ")[0]}-${unique}${ex}`;
callback(null, filename);
},
}),
}),
)
@ApiBody({
type: UserCommentDto,
description: "if partId null , you can upload video capture",
})
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestId" })
async submitReply(
@Param("claimRequestId") requestId,
@Body() body,
@UploadedFile() file: Express.Multer.File,
@CurrentUser() user,
) {
return await this.claimRequestManagementService.submitUserReply(
requestId,
body,
file,
user,
);
}
@Put("request/resend/:claimRequestId/objection")
@ApiParam({ name: "claimRequestId" })
@ApiConsumes("application/json")
@ApiBody({
type: UserObjectionDto,
description: "Objection details with optional new parts",
})
async handleUserObjection(
@Param("claimRequestId") claimRequestId: string,
@Body() userObjectionDto: UserObjectionDto,
) {
return await this.claimRequestManagementService.handleUserObjectionAndParts(
claimRequestId,
userObjectionDto,
);
}
@Patch("request/resend/:claimRequestId")
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestId" })
@ApiQuery({ name: "fields", enum: ["resendDocuments", "resendCarParts"] })
@ApiQuery({ name: "partId", required: false })
@ApiQuery({ name: "documentName", required: false })
@ApiQuery({ name: "side", required: false })
@ApiBody({
schema: {
type: "object",
properties: {
file: {
type: "string",
format: "binary",
},
},
},
})
@UseInterceptors(
FileInterceptor("file", {
storage: diskStorage({
destination: "./files/claim-resend-documents",
filename: (req, file, callback) => {
const unique = Date.now();
const ext = extname(file.originalname);
const filename = `${file.originalname.split(" ")[0]}-${unique}${ext}`;
callback(null, filename);
},
}),
limits: { fileSize: 10 * 1024 * 1024 },
}),
)
async uploadDocuments(
@Param("claimRequestId") claimId: string,
@Query() query: string,
@UploadedFile() file: Express.Multer.File,
@CurrentUser() user,
) {
return await this.claimRequestManagementService.resendFiles(
claimId,
file,
query,
user,
);
}
@Patch("request/reply/:claimRequestId/:partId/upload-factor")
@ApiConsumes("multipart/form-data")
@ApiParam({ name: "claimRequestId" })
@ApiParam({ name: "partId" })
@ApiBody({
schema: {
type: "object",
properties: {
file: {
type: "string",
format: "binary",
},
},
},
})
@UseInterceptors(
FileInterceptor("file", {
storage: diskStorage({
destination: "./files/claim-factors",
filename: (req, file, callback) => {
const unique = Date.now();
const filename = `-${unique}-${file.originalname}`;
callback(null, filename);
},
}),
limits: { fileSize: 10 * 1024 * 1024 },
}),
)
async uploadFactorForPart(
@Param("claimRequestId") claimId: string,
@Param("partId") partId: string,
@UploadedFile() file: Express.Multer.File,
@CurrentUser() user,
) {
return await this.claimRequestManagementService.uploadClaimFactor(
claimId,
partId,
file,
user,
);
}
@ApiBody({ type: InPersonVisitDto })
@ApiParam({ name: "id" })
@Patch(":id/visit")
async inPersonVisit(
@Param("id") requestId: string,
@Body() body: InPersonVisitDto,
@CurrentUser() actor,
) {
// Pass the branchId from the body to the service
return await this.claimRequestManagementService.inPersonVisit(
requestId,
body.branchId,
actor,
);
}
@Get("branches/:insuranceId")
async insuranceBranches(@Param("insuranceId") insuranceId: string) {
return await this.claimRequestManagementService.retrieveInsuranceBranches(
insuranceId,
);
}
@Get("fanavaran-submit/:claimRequestId")
@ApiParam({ name: "claimRequestId" })
async fanavaranSubmit(@Param("claimRequestId") claimRequestId: string) {
return await this.claimRequestManagementService.fanavaranSubmit(
claimRequestId,
);
}
@Post("fanavaran-submit/:claimRequestId")
@ApiParam({ name: "claimRequestId" })
async submitToFanavaran(@Param("claimRequestId") claimRequestId: string) {
return await this.claimRequestManagementService.submitToFanavaran(
claimRequestId,
);
}
}

View File

@@ -0,0 +1,88 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { AiModule } from "src/ai/ai.module";
import { SandHubModule } from "src/sand-hub/sand-hub.module";
import { RequestManagementModule } from "src/request-management/request-management.module";
import { UsersModule } from "src/users/users.module";
import { ClaimRequestManagementController } from "./claim-request-management.controller";
import { ClaimRequestManagementService } from "./claim-request-management.service";
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
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";
import { VideoCaptureDbService } from "./entites/db-service/video-capture.db.service";
import { ClaimRequiredDocumentDbService } from "./entites/db-service/claim-required-document.db.service";
import {
CarGreenCardModel,
CarGreenCardSchema,
} from "./entites/schema/car-green-card.schema";
import {
ClaimRequiredDocument,
ClaimRequiredDocumentSchema,
} from "./entites/schema/claim-required-document.schema";
import {
ClaimRequestManagementModel,
ClaimRequestManagementSchema,
} from "./entites/schema/claim-request-management.schema";
import { ClaimSignModel, ClaimSignSchema } from "./entites/schema/claim-sign";
import {
DamageImageModelSchema,
DamagePartImageModel,
} from "./entites/schema/damage-image-part.schema";
import {
ClaimFactorsImage,
ClaimFactorsImageSchema,
} from "./entites/schema/factor-image.schema";
import {
VideoCaptureModel,
VideoCaptureSchema,
} from "./entites/schema/video-capture.schema";
import { ClientModule } from "src/client/client.module";
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
import { JwtModule } from "@nestjs/jwt";
@Module({
imports: [
UsersModule,
RequestManagementModule,
AiModule,
SandHubModule,
ClientModule,
JwtModule.register({}),
MongooseModule.forFeature([
{
name: ClaimRequestManagementModel.name,
schema: ClaimRequestManagementSchema,
},
{ name: CarGreenCardModel.name, schema: CarGreenCardSchema },
{ name: DamagePartImageModel.name, schema: DamageImageModelSchema },
{ name: ClaimSignModel.name, schema: ClaimSignSchema },
{ name: ClaimFactorsImage.name, schema: ClaimFactorsImageSchema },
{ name: VideoCaptureModel.name, schema: VideoCaptureSchema },
{
name: ClaimRequiredDocument.name,
schema: ClaimRequiredDocumentSchema,
},
]),
],
providers: [
ClaimRequestManagementService,
ClaimRequestManagementDbService,
CarGreenCardDbService,
DamageImageDbService,
ClaimSignDbService,
ClaimFactorsImageDbService,
VideoCaptureDbService,
ClaimRequiredDocumentDbService,
ClaimAccessGuard,
],
controllers: [ClaimRequestManagementController],
exports: [
ClaimRequestManagementDbService,
DamageImageDbService,
VideoCaptureDbService,
ClaimRequiredDocumentDbService,
],
})
export class ClaimRequestManagementModule {}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,108 @@
import { ApiProperty } from "@nestjs/swagger";
export class MainParts {
@ApiProperty({ default: false })
backFender: boolean;
@ApiProperty({ default: false })
backWheel: boolean;
@ApiProperty({ default: false })
backDoor: boolean;
@ApiProperty({ default: false })
frontDoor: boolean;
@ApiProperty({ default: false })
mirror: boolean;
@ApiProperty({ default: false })
frontWheel: boolean;
@ApiProperty({ default: false })
frontFender: boolean;
@ApiProperty({ default: false })
backWindow: boolean;
@ApiProperty({ default: false })
frontWindow: boolean;
}
export class FrontParts {
@ApiProperty({ default: false })
frontBumper: boolean;
@ApiProperty({ default: false })
frontCarWindshield: boolean;
@ApiProperty({ default: false })
carHood: boolean;
@ApiProperty({ default: false })
leftLight: boolean;
@ApiProperty({ default: false })
rightLight: boolean;
@ApiProperty({ default: false, description: "جلو پنجره " })
frontGrille: boolean;
}
export class BackParts {
@ApiProperty({ default: false })
backBumper: boolean;
@ApiProperty({ default: false })
carTrunk: boolean;
@ApiProperty({ default: false })
backCarWindshield: boolean;
@ApiProperty({ default: false })
leftLight: boolean;
@ApiProperty({ default: false })
rightLight: boolean;
}
export class TopParts {
@ApiProperty({ default: false })
roof: boolean;
}
export class CarDamagePartDto {
@ApiProperty({ type: MainParts })
left: MainParts[];
@ApiProperty({ type: MainParts })
right: MainParts[];
@ApiProperty({ type: FrontParts })
front: FrontParts[];
@ApiProperty({ type: BackParts })
back: BackParts[];
@ApiProperty({ type: TopParts })
top: TopParts[];
}
export class OtherCarDamagePartDto {
@ApiProperty({
format: "array",
description: "please add items of json into array",
example: [{ "حسگر درها": true }],
})
otherParts: [];
@ApiProperty({ format: "string" })
sheba: string;
@ApiProperty({ format: "string" })
nationalCodeOfInsurer: string;
@ApiProperty({ type: "string", format: "binary", required: true })
file: Express.Multer.File;
}
export class CaptureCarPartDto {
@ApiProperty({ type: "string", format: "binary", required: true })
file: Express.Multer.File;
}

View File

@@ -0,0 +1,16 @@
import { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema";
export class ClaimPartUploadDetail {
list: any;
constructor(claimFile: ClaimRequestManagementModel[]) {
this.list = claimFile
.map((c) => {
return {
carPartDamage: c.carPartDamage,
carOtherPartDamage: c.otherParts,
greenCardUpload: !!c.carGreenCard.path,
};
})
.flat(2);
}
}

View File

@@ -0,0 +1,17 @@
import { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema";
import { ReqClaimStatus } from "src/Types&Enums/claim-request-management/status.enum";
export class ClaimRequestDtoRs {
requestDetail: any;
messsage: string;
status: ReqClaimStatus;
constructor(
req: ClaimRequestManagementModel,
message: string,
status: ReqClaimStatus,
) {
this.requestDetail = req;
this.messsage = message;
this.status = status;
}
}

View File

@@ -0,0 +1,8 @@
export class CreateClaimRequestDtoRs {
message: string;
requestId: string;
constructor(requestId: string, message: string) {
this.requestId = requestId;
this.message = message;
}
}

View File

@@ -0,0 +1,8 @@
import { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema";
export class ImageRequiredDto {
public list: {} = {};
constructor(imageModel: ClaimRequestManagementModel) {
this.list = imageModel.imageRequired;
}
}

View File

@@ -0,0 +1,12 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsMongoId, IsNotEmpty } from "class-validator";
export class InPersonVisitDto {
@ApiProperty({
example: "60d5ec49e7b2f8001c8e4d2a",
description: "The unique ID of the branch the user is being sent to.",
})
@IsNotEmpty()
@IsMongoId()
branchId: string;
}

View File

@@ -0,0 +1,23 @@
import { Types } from "mongoose";
import { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema";
import { ReqClaimStatus } from "src/Types&Enums/claim-request-management/status.enum";
export class MyRequestsDtoList {
status: ReqClaimStatus;
submitDate: string;
numberOfRequest: number;
_id: Types.ObjectId;
constructor(request: ClaimRequestManagementModel) {
this._id = request["_id"];
this.status = request.claimStatus;
this.submitDate = new Date(request.createdAt).toLocaleString("fa-IR");
this.numberOfRequest = request.requestNumber;
}
}
export class MyRequestsDto {
public list = [];
constructor(requests: ClaimRequestManagementModel[]) {
this.list = requests.map((r) => new MyRequestsDtoList(r));
}
}

View File

@@ -0,0 +1,19 @@
import { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema";
import { UserCommentDto } from "src/claim-request-management/dto/user-comment.dto";
export class SubmitUserReplyDtoRs {
requestId: string;
partsNeedFactorDetail: object;
partsNeedFactor: boolean;
userComment: UserCommentDto;
constructor(
claim: ClaimRequestManagementModel,
partsFactorDetail?,
partsNeedFactor?,
) {
this.requestId = claim["_id"];
this.partsNeedFactorDetail = partsFactorDetail;
this.partsNeedFactor = partsNeedFactor;
this.userComment = claim.damageExpertReply.userComment;
}
}

View File

@@ -0,0 +1,12 @@
import { ApiProperty } from "@nestjs/swagger";
export class UserCommentDto {
@ApiProperty({ type: Boolean })
isAccept: boolean;
@ApiProperty({ type: "string", format: "binary", required: false })
file?: Express.Multer.File;
@ApiProperty({ type: String })
branch?: string;
}

View File

@@ -0,0 +1,47 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { TypeOfDamage } from "src/Types&Enums/claim-request-management/type-of-damage.enum";
export class UserObjectionPartDto {
@ApiProperty()
partId: string;
@ApiProperty({ required: false })
reason?: string;
@ApiProperty({ required: false })
partPrice?: string;
@ApiProperty({ required: false })
partSalary?: string;
@ApiProperty({ required: false })
typeOfDamage?: TypeOfDamage;
@ApiProperty({ required: false })
carPartDamage?: string;
@ApiProperty({ required: false })
side?: string;
}
export class NewPartDto {
@ApiProperty({ required: false, nullable: true })
partId: string | null;
@ApiProperty()
partName: string;
@ApiProperty({ required: false })
side?: string;
}
export class UserObjectionDto {
@ApiProperty({ type: [UserObjectionPartDto], required: false })
@Type(() => UserObjectionPartDto)
objectionParts?: UserObjectionPartDto[];
@ApiProperty({ type: [NewPartDto], required: false })
@Type(() => NewPartDto)
newParts?: NewPartDto[];
}

View File

@@ -0,0 +1,4 @@
export class UserReplyDtoRs {
requestId: string;
isAccept: string;
}

View File

@@ -0,0 +1,19 @@
import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model } from "mongoose";
import { CarGreenCardModel } from "src/claim-request-management/entites/schema/car-green-card.schema";
export class CarGreenCardDbService {
constructor(
@InjectModel(CarGreenCardModel.name)
private readonly model: Model<CarGreenCardModel>,
) {}
async create(greenCard): Promise<CarGreenCardModel> {
return await this.model.create(greenCard);
}
async findOne(
filter: FilterQuery<CarGreenCardModel>,
): Promise<CarGreenCardModel> {
return await this.model.findOne({ filter });
}
}

View File

@@ -0,0 +1,121 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model, Types, UpdateQuery } from "mongoose";
import { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema";
const crypto = require("node:crypto");
@Injectable()
export class ClaimRequestManagementDbService {
constructor(
@InjectModel(ClaimRequestManagementModel.name)
private readonly model: Model<ClaimRequestManagementModel>,
) {}
async create(
claimRequest: ClaimRequestManagementModel,
): Promise<ClaimRequestManagementModel> {
const uniqueRequestNumber = await this.generateUniqueNumbers();
return this.model.create({
...claimRequest,
requestNumber: uniqueRequestNumber,
});
}
async findOne(
id?: string,
filter?: FilterQuery<ClaimRequestManagementModel>,
) {
if (filter) return await this.model.findOne(filter);
return await this.model.findOne({ _id: new Types.ObjectId(id) });
}
async findOneDocument(
id: string,
filter?: FilterQuery<ClaimRequestManagementModel>,
) {
if (filter) return await this.model.findOne(filter);
return await this.model.findOne({ _id: new Types.ObjectId(id) }).lean();
}
async findOneAndUpdate(
filter: FilterQuery<ClaimRequestManagementModel>,
update: UpdateQuery<ClaimRequestManagementModel>,
option?,
) {
return await this.model.findOneAndUpdate(filter, update, option);
}
async findAllByStatus(filter: FilterQuery<ClaimRequestManagementModel>) {
return await this.model.find(filter);
}
async findAndDelete(
filter: FilterQuery<ClaimRequestManagementModel>,
option,
) {
return await this.model.deleteMany(filter, option);
}
async findAllAndPagination(
filter: FilterQuery<ClaimRequestManagementModel>,
currentPage: number,
countPerPage: number,
) {
const responsePerPage = countPerPage | 1;
const skipPge = responsePerPage * (Number(currentPage) - 1);
return await this.model.find(filter).limit(responsePerPage).skip(skipPge);
}
async aggregate(filter?) {
return await this.model.aggregate(filter);
}
async findAndUpdate(
id: string,
update: UpdateQuery<ClaimRequestManagementModel>,
option?: FilterQuery<ClaimRequestManagementModel>,
) {
return await this.model.findByIdAndUpdate(
{ _id: new Types.ObjectId(id) },
update,
option,
);
}
async findAllByAnyFilter(
filter: FilterQuery<ClaimRequestManagementModel>,
): Promise<ClaimRequestManagementModel[]> {
return await this.model.find(filter);
}
async countByFilter(
filter: FilterQuery<ClaimRequestManagementModel>,
): Promise<number> {
return await this.model.countDocuments(filter);
}
async findAll(): Promise<ClaimRequestManagementModel[]> {
return await this.model.find();
}
async generateUniqueNumbers(digits = 5) {
try {
const max = Math.pow(10, digits);
const randomBytes = crypto.randomBytes(Math.ceil(digits / 2));
const randomNumber = parseInt(randomBytes.toString("hex"), 16) % max;
return randomNumber.toString().padStart(digits, "0");
} catch (error) {
console.error("Error generating unique numbers:", error);
throw error;
}
}
async findAllWithFilter(filter?: FilterQuery<ClaimRequestManagementModel>) {
return await this.model.find(filter);
}
async findByIdAndUpdate(
id: string,
updateDto: UpdateQuery<ClaimRequestManagementModel>,
): Promise<ClaimRequestManagementModel | null> {
return this.model.findByIdAndUpdate(id, updateDto, { new: true }).lean();
}
}

View File

@@ -0,0 +1,41 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model, Types } from "mongoose";
import { ClaimRequiredDocument } from "src/claim-request-management/entites/schema/claim-required-document.schema";
@Injectable()
export class ClaimRequiredDocumentDbService {
constructor(
@InjectModel(ClaimRequiredDocument.name)
private readonly model: Model<ClaimRequiredDocument>,
) {}
async create(document: Partial<ClaimRequiredDocument>): Promise<ClaimRequiredDocument> {
return await this.model.create(document);
}
async findOne(
filter: FilterQuery<ClaimRequiredDocument>,
): Promise<ClaimRequiredDocument | null> {
return await this.model.findOne(filter);
}
async findAll(
filter: FilterQuery<ClaimRequiredDocument>,
): Promise<ClaimRequiredDocument[]> {
return await this.model.find(filter);
}
async findById(id: string): Promise<ClaimRequiredDocument | null> {
return this.model.findById(id).lean();
}
async findByClaimId(claimId: string): Promise<ClaimRequiredDocument[]> {
return this.model.find({ claimId: new Types.ObjectId(claimId) });
}
async delete(id: string): Promise<void> {
await this.model.findByIdAndDelete(id);
}
}

View File

@@ -0,0 +1,20 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model } from "mongoose";
import { ClaimSignModel } from "src/claim-request-management/entites/schema/claim-sign";
@Injectable()
export class ClaimSignDbService {
constructor(
@InjectModel(ClaimSignModel.name)
private readonly claimSignService: Model<ClaimSignModel>,
) {}
async create(claimSign: ClaimSignModel): Promise<ClaimSignModel> {
return await this.claimSignService.create(claimSign);
}
async findOne(filter: FilterQuery<ClaimSignModel>): Promise<ClaimSignModel> {
return await this.claimSignService.findOne(filter);
}
}

View File

@@ -0,0 +1,18 @@
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import { DamagePartImageModel } from "src/claim-request-management/entites/schema/damage-image-part.schema";
export class DamageImageDbService {
constructor(
@InjectModel(DamagePartImageModel.name)
private readonly model: Model<DamagePartImageModel>,
) {}
async create(image): Promise<DamagePartImageModel> {
return await this.model.create(image);
}
async findOne(id: string): Promise<DamagePartImageModel> {
return await this.model.findById(new Types.ObjectId(id));
}
}

View File

@@ -0,0 +1,25 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model } from "mongoose";
import { ClaimFactorsImage } from "src/claim-request-management/entites/schema/factor-image.schema";
@Injectable()
export class ClaimFactorsImageDbService {
constructor(
@InjectModel(ClaimFactorsImage.name)
private readonly model: Model<ClaimFactorsImage>,
) {}
async create(image): Promise<ClaimFactorsImage> {
return await this.model.create(image);
}
async findOne(
filter: FilterQuery<ClaimFactorsImage>,
): Promise<ClaimFactorsImage> {
return await this.model.findOne({ filter });
}
async findById(id: string): Promise<ClaimFactorsImage | null> {
return this.model.findById(id).lean();
}
}

View File

@@ -0,0 +1,25 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model } from "mongoose";
import { VideoCaptureModel } from "src/claim-request-management/entites/schema/video-capture.schema";
@Injectable()
export class VideoCaptureDbService {
constructor(
@InjectModel(VideoCaptureModel.name)
private readonly videoCapture: Model<VideoCaptureModel>,
) {}
async create(video): Promise<VideoCaptureModel> {
return await this.videoCapture.create(video);
}
async findOne(
filter: FilterQuery<VideoCaptureModel>,
): Promise<VideoCaptureModel> {
return await this.videoCapture.findOne(filter);
}
async findById(id: string): Promise<VideoCaptureModel | null> {
return this.videoCapture.findById(id).lean();
}
}

View File

@@ -0,0 +1,13 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import mongoose, { Types } from "mongoose";
@Schema({ versionKey: false })
export class ActionUserModel extends mongoose.Document {
@Prop({ required: true, type: Types.ObjectId })
userId: Types.ObjectId;
@Prop({ required: true, type: Date })
date: Date;
}
export const ActionActorSchema = SchemaFactory.createForClass(ActionUserModel);

View File

@@ -0,0 +1,7 @@
import { Prop, Schema } from "@nestjs/mongoose";
@Schema({ versionKey: false, _id: true })
export class AiImagesModel {
@Prop({ type: "array" })
imagesAddress: string[];
}

View File

@@ -0,0 +1,17 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import mongoose, { Types } from "mongoose";
@Schema({ versionKey: false, collection: "car-green-cards" })
export class CarGreenCardModel extends mongoose.Document {
@Prop({ required: false, type: String })
path?: string;
@Prop({ required: false, type: String })
fileName?: string;
@Prop({ required: false, type: Types.ObjectId })
claimId?: Types.ObjectId;
}
export const CarGreenCardSchema =
SchemaFactory.createForClass(CarGreenCardModel);

View File

@@ -0,0 +1,18 @@
import { Prop } from "@nestjs/mongoose";
export class CarDamagePartOtherModel {
@Prop({
format: "array",
description: "please add items of json into array",
example: [{ "حسگر درها": true }],
})
otherParts?: [];
}
export class CarDamagePartModel {
@Prop({ type: String })
side: string;
@Prop({ type: String })
part: string;
}

View File

@@ -0,0 +1,29 @@
import { Prop } from "@nestjs/mongoose";
import mongoose, { Types } from "mongoose";
import { ActionUserModel } from "./action-user.schema";
export class ClaimBaseModel extends mongoose.Document {
@Prop()
readonly _id: Types.ObjectId;
@Prop()
readonly created: Date;
@Prop({
required: false,
type: ActionUserModel,
})
createdBy: ActionUserModel;
@Prop({ required: false })
readonly updated: Date;
@Prop({
required: false,
type: [ActionUserModel],
})
updatedBy: ActionUserModel[];
@Prop()
readonly deleted: boolean;
}

View File

@@ -0,0 +1,330 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
import {
CarDetail,
RequestManagementModel,
} from "src/request-management/entities/schema/request-management.schema";
import { UserSignModel } from "src/request-management/entities/schema/sign.schema";
import { ReqClaimStatus } from "src/Types&Enums/claim-request-management/status.enum";
import { ClaimStepsEnum } from "src/Types&Enums/claim-request-management/steps.enum";
import { UserReplyEnum } from "src/Types&Enums/claim-request-management/userReply.enum";
import { AiImagesModel } from "./ai-image.schema";
import { CarGreenCardModel } from "./car-green-card.schema";
import {
CarDamagePartModel,
CarDamagePartOtherModel,
} from "./car-parts.schema";
import { ImageRequiredModel } from "./image-required.schema";
import { Plates } from "src/Types&Enums/plate.interface";
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
import { DaghiOption } from "src/Types&Enums/claim-request-management/daghi-option.enum";
// main schema
export type ClaimRequestManagementDoc = ClaimRequestManagementModel & Document;
export class EffectedUserReply {
@Prop({ required: true, type: String })
reply?: UserReplyEnum;
@Prop({ required: true })
signDetail?: UserSignModel;
}
export class UserComment {
@Prop({ type: Boolean })
isAccept: boolean;
@Prop({ required: false })
signDetail?: Types.ObjectId;
}
export class DaghiDetails {
@Prop({ required: true, type: String, enum: DaghiOption })
option: DaghiOption;
@Prop({ required: false, type: String })
price?: string;
@Prop({ required: false, type: Types.ObjectId })
branchId?: Types.ObjectId;
}
export class PartsList {
@Prop({ required: true, type: String })
partId: string;
@Prop({ required: true, type: String })
carPartDamage: string;
@Prop({ required: true, type: String })
typeOfDamage: string;
@Prop({ required: true, type: String })
price: string;
@Prop({ required: true, type: String })
salary: string;
@Prop({ required: true, type: String })
totalPayment: string;
@Prop({ required: true, type: DaghiDetails })
daghi: DaghiDetails;
@Prop({ required: true, type: Boolean })
factorNeeded: boolean;
@Prop({ type: Types.ObjectId })
factorLink?: Types.ObjectId;
@Prop({ type: String, enum: FactorStatus, default: null })
factorStatus?: FactorStatus;
@Prop({ type: String, default: null })
rejectionReason?: string;
}
export class ActorDetail {
@Prop()
actorName: string;
@Prop()
actorId: string;
}
export class InPersonDocuments {
@Prop()
NationalCertificate: string;
@Prop()
CarCertificate: string;
@Prop()
DrivingLicense: string;
@Prop()
CarGreenCard: string;
}
export class SubmitReply {
@Prop({ required: true })
description: string;
@Prop({ required: true })
actorDetail: ActorDetail;
@Prop({ required: true, type: [PartsList] })
parts: PartsList[];
@Prop({ required: true, default: () => new Date() })
submitTime: Date;
@Prop()
userComment?: UserComment;
}
export class ActorLockDetails {
@Prop({ type: String })
fullName?: string;
@Prop({ type: Types.ObjectId })
actorId?: Types.ObjectId;
}
export class ResendCarPartsDto {
@Prop({ required: true })
partId: string;
@Prop({ required: true })
partName: string;
}
export class ClaimSubmitResend {
@Prop({ required: true })
resendDescription: string;
@Prop({ required: true, type: [InPersonDocuments] })
resendDocuments: InPersonDocuments[];
@Prop({ required: true })
resendCarParts: ResendCarPartsDto[];
}
export class UserResendDocuments {
@Prop({ required: false })
documents: [];
@Prop({ required: false })
carParts: [];
}
export class UserObjection {
@Prop({ type: String, required: true })
partId: string;
@Prop({ type: String })
reason: string;
@Prop({ type: String })
partPrice: string;
@Prop({ type: String })
partSalary: string;
@Prop({ type: String })
typeOfDamage: string;
}
@Schema({ _id: false })
export class FileRating {
@Prop({ type: Number, min: 0, max: 5 })
collisionMethodAccuracy: number;
@Prop({ type: Number, min: 0, max: 5 })
evaluationTimeliness: number;
@Prop({ type: Number, min: 0, max: 5 })
accidentCauseAccuracy: number;
@Prop({ type: Number, min: 0, max: 5 })
guiltyVehicleIdentification: number;
}
export class PriceDrop {
@Prop({ type: Number })
total: number;
@Prop({ type: Number })
carPrice: number;
@Prop({ type: Number })
carModel: number;
@Prop({ type: [Number] })
carValue: number[];
@Prop({ type: Number })
sumOfSeverity: number;
}
@Schema({
collection: "claim-requests-management",
versionKey: false,
timestamps: true,
autoIndex: false,
})
export class ClaimRequestManagementModel {
readonly aiImage?: any;
constructor() {}
@Prop({ type: RequestManagementModel, unique: true })
blameFile: RequestManagementModel;
@Prop({ type: Number, default: 100 })
requestNumber?: number;
@Prop({ type: Types.ObjectId })
userClientKey?: Types.ObjectId;
@Prop({ type: Types.ObjectId })
userId: Types.ObjectId;
@Prop()
fullName: string;
@Prop()
carDetail: CarDetail;
@Prop({ type: AddPlateDto })
carPlate: Plates;
@Prop({})
claimStatus: ReqClaimStatus;
@Prop({})
steps: ClaimStepsEnum[];
@Prop({})
currentStep: ClaimStepsEnum;
@Prop({})
nextStep?: ClaimStepsEnum;
@Prop({ type: CarDamagePartModel })
carPartDamage?: CarDamagePartModel[];
@Prop({ type: CarDamagePartOtherModel })
otherParts?: CarDamagePartOtherModel[];
@Prop({ format: "string" })
sheba?: string;
@Prop({ format: "string" })
nationalCodeOfInsurer?: string;
@Prop({ type: CarGreenCardModel, required: false })
carGreenCard?: CarGreenCardModel;
@Prop({ type: ImageRequiredModel })
imageRequired?: ImageRequiredModel;
@Prop({ type: AiImagesModel })
aiImages?: AiImagesModel;
@Prop({ type: EffectedUserReply })
effectedUserReply?: EffectedUserReply;
@Prop()
lockFile?: boolean;
@Prop({ type: Date })
lockTime?: Date | string | number;
@Prop({ type: Date })
unlockTime?: Date | number;
@Prop()
actorLocked?: ActorLockDetails | null;
@Prop()
actorsChecker?: [];
@Prop({ required: false })
videoCaptureId?: Types.ObjectId;
@Prop({ required: false })
damageExpertReply?: SubmitReply | null;
@Prop({ required: false })
damageExpertReplyFinal?: SubmitReply | null;
@Prop({ required: false, type: ClaimSubmitResend })
damageExpertResend?: ClaimSubmitResend | null;
@Prop({ type: UserObjection, required: false })
objection?: UserObjection;
@Prop({ type: UserResendDocuments })
userResendDocuments?: UserResendDocuments;
@Prop({ type: PriceDrop, required: false })
priceDrop?: PriceDrop;
@Prop({ type: Map, of: Types.ObjectId, required: false })
requiredDocuments?: { [key: string]: Types.ObjectId };
createdAt: any;
updatedAt: any;
@Prop({ type: FileRating, required: false })
rating?: FileRating;
@Prop({ type: String, required: false })
visitLocation?: string;
@Prop({ type: Number, required: false })
claimNo?: number;
@Prop({ type: Number, required: false })
claimId?: number;
}
export const ClaimRequestManagementSchema = SchemaFactory.createForClass(
ClaimRequestManagementModel,
);

View File

@@ -0,0 +1,27 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
@Schema({ versionKey: false, collection: "claim-required-documents" })
export class ClaimRequiredDocument {
@Prop({ required: true, type: String })
path: string;
@Prop({ required: true, type: String })
fileName: string;
@Prop({ required: true, type: Types.ObjectId })
claimId: Types.ObjectId;
@Prop({ required: true, enum: ClaimRequiredDocumentType })
documentType: ClaimRequiredDocumentType;
@Prop({ default: () => new Date() })
uploadedAt: Date;
_id?: Types.ObjectId;
}
export const ClaimRequiredDocumentSchema =
SchemaFactory.createForClass(ClaimRequiredDocument);

View File

@@ -0,0 +1,6 @@
import { Schema, SchemaFactory } from "@nestjs/mongoose";
import { UserSignModel } from "src/request-management/entities/schema/sign.schema";
@Schema({ collection: "claim-sign", versionKey: false })
export class ClaimSignModel extends UserSignModel {}
export const ClaimSignSchema = SchemaFactory.createForClass(ClaimSignModel);

View File

@@ -0,0 +1,18 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
export type DamagePartImage = DamagePartImageModel & Document;
@Schema({ versionKey: false, collection: "damage-part-image" })
export class DamagePartImageModel {
@Prop({ default: null })
path: string | null = null;
@Prop({ default: null })
fileName: string | null = null;
@Prop({ default: null })
claimId: string = null;
}
export const DamageImageModelSchema =
SchemaFactory.createForClass(DamagePartImageModel);

View File

@@ -0,0 +1,28 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
@Schema({ versionKey: false, collection: "claim-factors-image" })
export class ClaimFactorsImage {
@Prop({ required: true, type: String })
path: string;
@Prop({ required: true, type: String })
fileName: string;
@Prop({ required: true, type: Types.ObjectId })
claimId: Types.ObjectId;
@Prop({ required: true, type: String })
partId: string;
@Prop({ required: true, type: Object })
partName: any;
@Prop({ default: () => new Date() })
uploadedAt: Date;
_id?: Types.ObjectId;
}
export const ClaimFactorsImageSchema =
SchemaFactory.createForClass(ClaimFactorsImage);

View File

@@ -0,0 +1,38 @@
import { Prop, Schema } from "@nestjs/mongoose";
import { v4 as uuidv4 } from "uuid";
@Schema({ versionKey: false, _id: false })
export class ImageRequiredModel {
@Prop({ type: "array" })
public aroundTheCar = [
{ side: "left" },
{ side: "back" },
{ side: "front" },
{ side: "right" },
];
@Prop({ type: "array" })
public selectPartOfCar = [];
@Prop({ type: "string" })
public aiReportText: string;
constructor(claimFile: any[]) {
this.aroundTheCar.forEach((a) => {
Object.assign(a, {
partId: uuidv4(),
imageId: null,
aiReport: {},
upload: false,
});
});
this.selectPartOfCar = claimFile.map((c, idx) =>
Object.assign(c, {
partId: uuidv4(),
aiReport: {},
imageId: null,
upload: false,
}),
);
}
}

View File

@@ -0,0 +1,17 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import mongoose, { Types } from "mongoose";
@Schema({ versionKey: false, collection: "claim-video-capture" })
export class VideoCaptureModel extends mongoose.Document {
@Prop({ required: false, type: String })
path?: string;
@Prop({ required: false, type: String })
fileName?: string;
@Prop({ required: false, type: Types.ObjectId })
claimId?: Types.ObjectId;
}
export const VideoCaptureSchema =
SchemaFactory.createForClass(VideoCaptureModel);

View File

@@ -0,0 +1,181 @@
{
"قاب موتور": true,
"گلگیرهاتایرها": true,
"دیسک ترمز": true,
"لنت ترمز": true,
"روغن ترمز": true,
"ترمز دستی": true,
"پیچ چرخ‌ها": true,
"پیستون ترمز": true,
"پدال ترمز": true,
"شلنگ‌های ترمز": true,
"سوزن هواگیر": true,
"سامانه ABS": true,
"پمپ ترمز": true,
"کالیپر": true,
"بوستر": true,
"انواع سوپاپ‌های ترمز": true,
"فنرهای نگهدارنده": true,
"لنگر": true,
"کفشک ترمز": true,
"مخزن روغن ترمز": true,
"واحد تقویت‌کننده هیدرولیکی": true,
"پایه باتری": true,
"کابل باتری": true,
"سینی باتری": true,
"سامانه مدیریت باتری": true,
"دینام": true,
"ترمینال سیم‌کشی باتری": true,
"چراغ‌های جلو و عقب": true,
"چراغ‌های مه‌شکن": true,
"چراغ‌هایی داخل داشبورد": true,
"چراغ سقفی داخل کابین": true,
"حسگر دنده اتوماتیک": true,
"حسگر سرعت": true,
"حسگر دمای مایع خنک‌کننده": true,
"حسگر ترمز ABS": true,
"حسگر اکسیژن": true,
"حسگر جریان هوا": true,
"حسگر کیسه هوا": true,
"حسگر روغن": true,
"حسگر سوخت": true,
"حسگر میل‌لنگ": true,
"حسگر میل بادامک": true,
"حسگر کمربند ایمنی": true,
"حسگر نور": true,
"حسگر درها": true,
"جعبه احتراق": true,
"شمع": true,
"دلکو": true,
"کنترل‌گر زمان": true,
"سیم‌های اتصال": true,
"سوپاپ مغناطیسی": true,
"سیم‌پیچ احتراق": true,
"وایر شمع": true,
"رله فن": true,
"سوئیچ درها": true,
"سوئیچ استارت": true,
"کلید شیشه بالابر": true,
"سوئیچ قفل فرمان": true,
"ترموستات": true,
"تهویه": true,
"موتور": true,
"کف کابین": true,
"ادوات داخل کابین": true,
"اصلی": true,
"دوربین دنده عقب": true,
"دوربین‌های 360 درجه": true,
"صال به زمین": true,
"سامانه قفل مرکزی": true,
"اتصالات برقی درها": true,
"ماژول کنترل ایربگ": true,
"سامانه کنترل سرعت": true,
"سامانه مدیریت موتور": true,
"کروز کنترل": true,
"سامانه ناوبری": true,
"سوکت‌ها": true,
"سیستم قفل از راه دور": true,
"رایانه جعبه‌دنده": true,
"فیوزها": true,
"بلوک سیلندر": true,
"پوشش میل‌لنگ": true,
"پولی میل‌لنگ": true,
"میل‌لنگ": true,
"پولی پمپ آب": true,
"تسمه پروانه": true,
"پیستون": true,
"تسمه دینام": true,
"تسمه تایم": true,
"توربو شارژ": true,
"درپوش سوپاپ": true,
"دسته موتور": true,
"سرسیلندر": true,
"سوپاپ پایت": true,
"سوپاپ تهویه": true,
"شاتون": true,
"پین انگشتی": true,
"بخاری": true,
"میل بادامک": true,
"لرزش‌گیر موتور": true,
"فیلر": true,
"جعبه‌دنده": true,
"پوسته گیربکس": true,
"چرخ‌دنده‌های انتقال قدرت، جناحی، هرزگرد، سرعت‌سنج، چرخ لنگر، فرمان": true,
"پمپ دنده": true,
"دنده": true,
"اهرم تعویض دنده": true,
"دوشاخ دنده": true,
"کوپلینگ دنده": true,
"دیفرانسیل": true,
"سیلندر": true,
"فنر جعبه‌دنده": true,
"محور جعبه‌دنده": true,
"میل‌گاردان": true,
"شفت خروجی": true,
"پولوس‌ها": true,
"سامانه کلاچ": true,
"کابل تعویض دنده": true,
"فیلتر بنزین": true,
"فیلتر هوا": true,
"کاربراتور": true,
"باک سوخت": true,
"سیستم LPG": true,
"کابل ساسات": true,
"منیفولد ورودی": true,
"جداکننده آب از سوخت": true,
"پیل سوختی": true,
"سیستم CNG": true,
"پمپ‌بنزین": true,
"انژکتور": true,
"بدنه دریچه گاز": true,
"خنک‌کننده سوخت": true,
"رگلاتور": true,
"ریل": true,
"غربیلک فرمان": true,
"بازوی فرمان": true,
"جعبه فرمان": true,
"دوک": true,
"شفت فرمان": true,
"سامانه فرمان خودکار": true,
"اتصال جانبی": true,
"اتصال میل موج‌گیر": true,
"بازوی خمیده": true,
"بازوی آزاد": true,
"بازوی پیت من": true,
"بست و اتصالات": true,
"سیبک فرمان": true,
"کمک‌فنرها": true,
"اتصالات تعادلی": true,
"فنر": true,
"محور خودرو": true,
"عایق حرارتی": true,
"گیره‌ها": true,
"لوله اگزوز": true,
"سپر حرارتی": true,
"صداخفه‌کن": true,
"رزیناتور": true,
"کاتالیست": true,
"حلقه فاصله": true,
"انواع واشرها": true,
"لوله‌های روغن": true,
"کارتل روغن": true,
"پمپ روغن": true,
"واشر پمپ روغن": true,
"صافی روغن": true,
"فیلتر روغن": true,
"تسمه فن": true,
"تیغه فن": true,
"واتر پمپ": true,
"واشر پمپ آب": true,
"دمنده هوا": true,
"بوش فن": true,
"مخزن": true,
"درپوش فشار": true,
"ترموستات": true,
"پوشش فن": true,
"کلاچ فن": true,
"پروانه یا فن": true,
"لوله‌های ورودی و خروجی آب": true,
"زانویی آب": true,
"شلنگ مایع خنک‌کننده": true
}

View File

@@ -0,0 +1,37 @@
import {
Body,
Controller,
Get,
Post,
UseGuards,
} from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { GlobalGuard } from "src/auth/guards/global.guard";
import { CurrentUser } from "src/decorators/user.decorator";
import { ClientService } from "./client.service";
import { ClientDto } from "./dto/create-client.dto";
@Controller("client")
@ApiTags("client-management")
export class ClientController {
constructor(private readonly clientService: ClientService) {}
@Post()
@UseGuards(GlobalGuard)
@ApiBearerAuth()
async addClient(@Body() client: ClientDto) {
return await this.clientService.addClient(client);
}
@Get()
@ApiBearerAuth()
@UseGuards(GlobalGuard)
async getClient(@CurrentUser() user) {
return await this.clientService.getClients();
}
@Get("list")
async getClientList(@CurrentUser() user) {
return await this.clientService.getClientList();
}
}

View File

@@ -0,0 +1,24 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { ClientController } from "./client.controller";
import { ClientService } from "./client.service";
import { BranchDbService } from "./entities/db-service/branch.db.service";
import { ClientDbService } from "./entities/db-service/client.db.service";
import { BranchModel, BranchSchema } from "./entities/schema/branch.schema";
import { ClientDbSchema, ClientModel } from "./entities/schema/client.schema";
@Module({
imports: [
MongooseModule.forFeature([
{ name: ClientModel.name, schema: ClientDbSchema },
{
name: BranchModel.name,
schema: BranchSchema,
},
]),
],
controllers: [ClientController],
providers: [ClientService, ClientDbService, BranchDbService],
exports: [ClientService, ClientDbService, BranchDbService],
})
export class ClientModule {}

View File

@@ -0,0 +1,56 @@
import { BadGatewayException, GoneException, Injectable } from "@nestjs/common";
import { Types } from "mongoose";
import {
ClientDto,
ClientDtoRs,
ClientLists,
} from "src/client/dto/create-client.dto";
import { ClientDbService } from "./entities/db-service/client.db.service";
@Injectable()
export class ClientService {
constructor(private readonly clientDbService: ClientDbService) {}
async addClient(client: ClientDto): Promise<ClientDtoRs> {
try {
const newClient = await this.clientDbService.create({
clientCode: client.clientCode,
clientName: {
persian: client.clientName.persian,
english: client.clientName.english || null,
},
property: {
smsApiKey: client.property.smsApiKey || null,
},
useExpertMode: client.useExpertMode || null,
});
if (newClient) return new ClientDtoRs(newClient);
else throw new GoneException("database not connected");
} catch (er) {
throw new BadGatewayException(er.errors);
}
}
findOne(filter) {
return this.clientDbService.findOne(filter);
}
async findClientWithPersianName(filter: string) {
return await this.clientDbService.find({ "clientName.persian": filter });
}
async findClientWithCompanyCode(companyCode: number) {
return await this.clientDbService.find({ clientCode: companyCode });
}
async getClients(): Promise<ClientDtoRs[]> {
const clients = await this.clientDbService.findAll();
const show = clients.map((c) => new ClientDtoRs(c));
return show;
}
async getClientList(): Promise<ClientLists[]> {
const client = await this.clientDbService.findAll();
const list = client.map((element) => new ClientLists(element));
return list;
}
}

View File

@@ -0,0 +1,34 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsNotEmpty, IsOptional } from "class-validator";
export class CreateBranchDto {
@ApiProperty({ example: "شهرک غرب" })
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty({ example: "1234" })
@IsString()
@IsNotEmpty()
code: string;
@ApiProperty({ example: "استان" })
@IsString()
@IsNotEmpty()
city: string;
@ApiProperty({ example: "شهر" })
@IsString()
@IsNotEmpty()
state: string;
@ApiProperty({ example: "فلان آدرس" })
@IsString()
@IsNotEmpty()
address: string;
@ApiProperty({ required: false, example: "0912345678" })
@IsOptional()
@IsString()
phoneNumber?: string;
}

View File

@@ -0,0 +1,48 @@
import { Injectable } from "@nestjs/common";
import { ApiProperty } from "@nestjs/swagger";
import { Types } from "mongoose";
class ClientName {
@ApiProperty({})
persian: string;
@ApiProperty({})
english: string;
}
class Property {
@ApiProperty({})
smsApiKey: string;
}
@Injectable()
export class ClientDto {
@ApiProperty({ required: true })
clientName: ClientName;
@ApiProperty({ required: true })
clientCode: number;
@ApiProperty({ required: false })
property: Property;
@ApiProperty({ examples: ["legal", "genuine"] })
useExpertMode: "legal" | "genuine";
}
export class ClientDtoRs {
persian: string;
english: string;
clientId: Types.ObjectId;
useExpertsMode: string;
constructor(readonly client) {
this.persian = client.clientName.persian;
}
}
export class ClientLists {
name: string;
id: Types.ObjectId;
constructor(client: ClientDto) {
this.name = client.clientName.persian;
this.id = client["_id"];
}
}

View File

@@ -0,0 +1,34 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model, Types } from "mongoose";
import { BranchModel, BranchDocument } from "../schema/branch.schema";
@Injectable()
export class BranchDbService {
constructor(
@InjectModel(BranchModel.name)
private readonly branchModel: Model<BranchModel>,
) {}
async create(branch: BranchModel): Promise<BranchModel> {
return await this.branchModel.create(branch);
}
async find(branch: FilterQuery<BranchModel>): Promise<BranchDocument> {
return await this.branchModel.findOne(branch);
}
async findOne(branch: FilterQuery<BranchModel>) {
return this.branchModel.findOne(branch);
}
async findAll(insuranceId: string): Promise<BranchModel[]> {
return await this.branchModel.find({
clientKey: new Types.ObjectId(insuranceId),
});
}
async findById(id: string): Promise<BranchModel | null> {
return this.branchModel.findById(id).lean();
}
}

View File

@@ -0,0 +1,28 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model } from "mongoose";
import { ClientModel, ClientDocument } from "../schema/client.schema";
@Injectable()
export class ClientDbService {
constructor(
@InjectModel(ClientModel.name)
private readonly clientModel: Model<ClientModel>,
) {}
async create(client: ClientModel): Promise<ClientModel> {
return await this.clientModel.create(client);
}
async find(client: FilterQuery<ClientModel>): Promise<ClientDocument> {
return await this.clientModel.findOne(client);
}
async findOne(client: FilterQuery<ClientModel>) {
return this.clientModel.findOne(client);
}
async findAll(): Promise<ClientModel[]> {
return await this.clientModel.find();
}
}

View File

@@ -0,0 +1,32 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
export type BranchDocument = BranchModel & Document;
@Schema({ collection: "branches", versionKey: false, timestamps: true })
export class BranchModel {
@Prop({ required: true, type: Types.ObjectId, ref: "ClientModel" })
clientKey: Types.ObjectId;
@Prop({ required: true })
name: string;
@Prop({ required: true })
code: string;
@Prop({ required: true })
city: string;
@Prop({ required: true })
state: string;
@Prop({ required: true })
address: string;
@Prop()
phoneNumber?: string;
}
export const BranchSchema = SchemaFactory.createForClass(BranchModel);
BranchSchema.index({ clientKey: 1, code: 1 }, { unique: true });

View File

@@ -0,0 +1,25 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
export type ClientDocument = ClientModel & Document;
@Schema({ collection: "clients", versionKey: false })
export class ClientModel {
@Prop({ required: true, unique: true, type: Object })
clientName: {
persian: string;
english: string;
};
@Prop({ required: false, unique: true, type: Object })
property: {
smsApiKey: string;
};
@Prop({ required: true, unique: false })
useExpertMode: "legal" | "genuine";
@Prop({ required: true, unique: false })
clientCode: number;
}
export const ClientDbSchema = SchemaFactory.createForClass(ClientModel);

View File

@@ -0,0 +1,9 @@
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const ClientKey = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user.clientKey;
},
);

View File

@@ -0,0 +1,8 @@
import { ExecutionContext, createParamDecorator } from "@nestjs/common";
export const CustomHeader = createParamDecorator(
(data, ctx: ExecutionContext) => {
console.log(ctx.switchToHttp().getRequest());
console.log(ctx.getType());
},
);

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from "@nestjs/common";
import { RoleEnum } from "src/Types&Enums/role.enum";
export const Roles = (...args: RoleEnum[]) => SetMetadata("role", args);

View File

@@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);

View File

@@ -0,0 +1,103 @@
import { RequestManagementModel } from "src/request-management/entities/schema/request-management.schema";
export class AllRequestDto {
// TODO fix interface for class
firstPartyPlate: object;
secondPartyPlate: object;
secondPartyCar: string | undefined;
firstPartyCar: string | undefined;
requestId: string;
submitTime: string;
requestCode: string;
date: Date | string;
time: Date | string;
firstPartyDetail: Object | undefined;
secondPartyDetail: Object | undefined;
requestStatus: string;
lockFile: boolean | undefined;
lockTime: any;
userComment: boolean | null;
status: string;
partiesInitialForms: Object;
type: string;
constructor(request: RequestManagementModel) {
// TODO FIX THIS OBJECT AND CLASS FOR CLEAN CODE
this.requestId = request["_id"];
this.status = request.blameStatus;
this.userComment = this.userCommentVoid(request);
this.requestCode = request.requestNumber;
// Format date and time with Iran timezone (Asia/Tehran)
if (request.createdAt) {
const dateFormatOptions: Intl.DateTimeFormatOptions = {
timeZone: "Asia/Tehran",
year: "numeric",
month: "2-digit",
day: "2-digit",
};
const timeFormatOptions: Intl.DateTimeFormatOptions = {
timeZone: "Asia/Tehran",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
this.date = `${new Date(request.createdAt).toLocaleDateString("fa-IR", dateFormatOptions)} `;
this.time = new Date(request.createdAt).toLocaleTimeString("fa-IR", timeFormatOptions);
} else {
this.date = "";
this.time = "";
}
this.lockFile = request.lockFile;
this.lockTime = request.lockTime;
this.type = request.type || "THIRD_PARTY"; // Include type field, default to THIRD_PARTY for backward compatibility
this.partiesInitialForms = {
firstParty: Object.entries(
request.firstPartyDetails.firstPartyInitialForm,
)
.filter(([key, value]) => value)
.flat()[0],
secondParty: Object.entries(
request.secondPartyDetails.secondPartyInitialForm,
)
.filter(([key, value]) => (value ? key : null))
.flat()[0],
};
this.firstPartyCar =
request.firstPartyDetails?.firstPartyCarDetail?.carName;
this.secondPartyCar =
request.secondPartyDetails?.secondPartyCarDetail?.carName;
}
userCommentVoid(request: RequestManagementModel): boolean | null {
if (
request?.expertSubmitReply?.firstPartyComment &&
request?.expertSubmitReply?.secondPartyComment
) {
if (
request.expertSubmitReply.firstPartyComment.isAccept ||
request.expertSubmitReply.secondPartyComment.isAccept
) {
return true;
} else {
return false;
}
} else {
return null;
}
}
getFirstValidKey(obj: Record<string, boolean>) {
return (
Object.entries(obj).find(([_, value]) => value === true)?.[0] ?? null
);
}
}
export class AllRequestDtoRs {
public data;
constructor(requests: RequestManagementModel[]) {
this.data = requests.map((r) => new AllRequestDto(r));
}
}

View File

@@ -0,0 +1,72 @@
import { ApiProperty } from "@nestjs/swagger";
import { Types } from "mongoose";
import { BlameDocumentType } from "src/request-management/entities/schema/blame-document.schema";
export class AccidentWayIF {
@ApiProperty({ required: true })
id: string;
@ApiProperty({ required: true })
label: string;
}
export class AccidentReasonIF {
@ApiProperty({ required: true })
id: string;
@ApiProperty({ required: true })
label: string;
@ApiProperty({ required: true })
fanavaran: number;
}
export class FieldsInterface {
@ApiProperty({ required: true })
accidentWay: AccidentWayIF;
@ApiProperty({ required: true })
accidentReason: AccidentReasonIF;
@ApiProperty({ required: true })
accidentType: AccidentWayIF;
}
export class SubmitReplyDto {
@ApiProperty({ required: true })
description: string;
@ApiProperty({ required: true, type: Types.ObjectId })
guiltyUserId: Types.ObjectId;
@ApiProperty({ required: true })
fields: FieldsInterface;
}
export class ResendFirstPartyDto {
voice?: string;
userReply?: string;
@ApiProperty({ required: false })
firstPartyId: string | null;
@ApiProperty({ required: false })
firstPartyDescription: string | null;
documents?: { [key in BlameDocumentType]?: Types.ObjectId | string };
}
export class ResendSecondPartyDto {
voice?: string;
userReply?: string;
@ApiProperty({ required: false })
secondPartyId: string | null;
@ApiProperty({ required: false })
secondPartyDescription: string | null;
documents?: { [key in BlameDocumentType]?: Types.ObjectId | string };
}
export interface SendAginIF {
first: ResendFirstPartyDto;
second: ResendSecondPartyDto;
}

View File

@@ -0,0 +1,156 @@
import {
Controller,
Get,
Body,
Param,
UseGuards,
Put,
Req,
Res,
Headers,
UseInterceptors,
Patch,
} from "@nestjs/common";
import {
ApiBearerAuth,
ApiBody,
ApiOkResponse,
ApiParam,
ApiProduces,
ApiTags,
} from "@nestjs/swagger";
import { Response, Request } from "express";
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
import { RolesGuard } from "src/auth/guards/role.guard";
import { ClientKey } from "src/decorators/clientKey.decorator";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
import { LoggingInterceptor } from "src/interceptor/logging.interceptors";
import { RoleEnum } from "src/Types&Enums/role.enum";
import {
ResendFirstPartyDto,
ResendSecondPartyDto,
SendAginIF,
SubmitReplyDto,
} from "./dto/reply.dto";
import { ExpertBlameService } from "./expert-blame.service";
@ApiTags("expert-blame-panel")
@Controller("expert-blame")
@ApiBearerAuth()
@UseGuards(LocalActorAuthGuard, RolesGuard)
@Roles(RoleEnum.EXPERT)
export class ExpertBlameController {
constructor(private readonly expertBlameService: ExpertBlameService) {}
// TODO role guard for expert fix
@Roles(RoleEnum.EXPERT)
@Get()
async findAll(@CurrentUser() actor, @ClientKey() client) {
return await this.expertBlameService.findAll(actor);
}
@Roles(RoleEnum.EXPERT)
@Get(":id")
async findOne(@Param("id") id: string, @CurrentUser() actor) {
return await this.expertBlameService.findOne(id, actor.sub);
}
@Roles(RoleEnum.EXPERT)
@Put("lock/:id")
async lockRequest(@Param("id") id: string, @CurrentUser() actor) {
return await this.expertBlameService.lockRequest(id, actor);
}
@Roles(RoleEnum.EXPERT)
@Get("request/accident-fields")
async getAccidentFields() {
return await this.expertBlameService.getAccidentField();
}
@Roles(RoleEnum.EXPERT)
@Put("reply/submit/:id")
@ApiBody({ type: SubmitReplyDto })
@ApiParam({ name: "id" })
async submitReply(
@Param("id") id: string,
@Body() body: SubmitReplyDto,
@CurrentUser() actor,
) {
return await this.expertBlameService.replyRequest(id, body, actor.sub);
}
private async handleResendRequest(
id: string,
body: SendAginIF,
actorSub: string,
req: Request,
) {
return await this.expertBlameService.sendAgainRequest(
id,
body,
actorSub,
req,
);
}
@Roles(RoleEnum.EXPERT)
@Put("reply/resend/first/:id")
@ApiBody({ type: ResendFirstPartyDto })
@ApiParam({ name: "id" })
async resendFirstParty(
@Param("id") id: string,
@Body() body: SendAginIF,
@CurrentUser() actor,
@Req() req: Request,
) {
return this.handleResendRequest(id, body, actor.sub, req);
}
@Roles(RoleEnum.EXPERT)
@Put("reply/resend/second/:id")
@Roles(RoleEnum.EXPERT)
@ApiBody({ type: ResendSecondPartyDto })
@ApiParam({ name: "id" })
async resendSecondParty(
@Param("id") id: string,
@Body() body: SendAginIF,
@CurrentUser() actor,
@Req() req: Request,
) {
return this.handleResendRequest(id, body, actor.sub, req);
}
@Roles(RoleEnum.EXPERT)
@Get("stream/:requestId")
@ApiParam({ name: "requestId" })
async streamVideo(@Param("requestId") requestId: string) {
return this.expertBlameService.streamVideo(requestId);
}
@UseInterceptors(LoggingInterceptor)
@Get("voice/:requestId/:voiceId")
@Roles(RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT)
@ApiParam({ name: "requestId" })
@ApiParam({ name: "voiceId" })
@ApiOkResponse({
schema: {
type: "string",
format: "binary",
},
})
@ApiProduces("mp3")
async downloadVoice(
@Param("voiceId") voiceId,
@Param("requestId") requestId: string,
@Res({ passthrough: true }) res: Response,
@Req() req: Request,
@Headers() headers,
) {
return await this.expertBlameService.streamVoice(requestId, voiceId);
}
@ApiParam({ name: "id" })
@Patch(":id/visit")
async inPersonVisit(@Param("id") requestId: string, @CurrentUser() actor) {
return await this.expertBlameService.inPersonVisit(requestId, actor);
}
}

Some files were not shown because too many files have changed in this diff Show More