1
0
forked from Yara724/api

Compare commits

..

8 Commits

19 changed files with 925 additions and 379 deletions

View File

@@ -5,6 +5,10 @@ NODE_ENV =
PORT =
CLIENT_ID =
CLIENT_NAME =
FANAVARAN_CLIENT=parsian
INSURANCE_CORP_ID='شرکت بيمه پارسيان(بيمه گر)'
CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED=false
CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN=53000000
# ---------------------------------------------
# 🌐 Application URLs
# ---------------------------------------------
@@ -63,6 +67,23 @@ AUTH_SMS_TEMPLATE =
EXP_OTP_TIME =
FAKE_OTP =
# ---------------------------------------------
# 🌐 Proxy Configuration (Local Development Only)
# ---------------------------------------------
# NOTE: These proxy settings are for local development only.
# When deploying to the server, comment out or remove these lines
# as the server IP is already whitelisted by Fanavaran.
# SOCKS_PROXY_HOST = localhost
# SOCKS_PROXY_PORT = 6565
# ---------------------------------------------
# 🏢 Fanavaran Insurance Corp
# ---------------------------------------------
# Caption from Fanavaran insurance-corp lookup used to resolve InsuranceCorpId
# for damage-case payloads. Must match a Caption in the insurance-corp code-list.
# Example: "شرکت بيمه تجارت نو"
INSURANCE_CORP_ID =
# ---------------------------------------------
# ⚙️ Application Features / Flags
# ---------------------------------------------

View File

@@ -45,6 +45,7 @@
"pdfkit": "^0.19.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socks-proxy-agent": "^8.0.4",
"svg-captcha": "^1.4.0"
},
"devDependencies": {

View File

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

View File

@@ -1,7 +1,8 @@
import { join } from "node:path";
import { APP_INTERCEPTOR, APP_PIPE } from "@nestjs/core";
import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { HttpModule } from "@nestjs/axios";
import { UnicodeDigitsNormalizeInterceptor } from "./common/interceptors/unicode-digits-normalize.interceptor";
import { MongooseModule } from "@nestjs/mongoose";
import { ServeStaticModule } from "@nestjs/serve-static";
@@ -28,9 +29,15 @@ import { WorkflowStepManagementModule } from "./workflow-step-management/workflo
import { DatabaseModule } from "./core/database/database.module";
import { AppConfigModule } from "./core/config/config.module";
import { SuperAdminModule } from "./super-admin/super-admin.module";
import { createHttpModuleOptions } from "./core/config/http-proxy.factory";
@Module({
imports: [
HttpModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: createHttpModuleOptions,
}),
AppConfigModule,
DatabaseModule,
CronModule,

View File

@@ -1,5 +1,8 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { HttpModule } from "@nestjs/axios";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { createHttpModuleOptions } from "src/core/config/http-proxy.factory";
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";
@@ -52,13 +55,16 @@ import { ClientModule } from "src/client/client.module";
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
import { JwtModule } from "@nestjs/jwt";
import { MediaPolicyModule } from "src/media-policy/media-policy.module";
import { HttpModule } from "@nestjs/axios";
import { FanavaranAuditModule } from "src/fanavaran/fanavaran-audit.module";
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
@Module({
imports: [
HttpModule,
HttpModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: createHttpModuleOptions,
}),
FanavaranAuditModule,
FanavaranLookupModule,
PublicIdModule,

View File

@@ -86,6 +86,15 @@ export class FanavaranSyncStage {
@Prop({ type: MongooseSchema.Types.Mixed })
response?: unknown;
@Prop({ type: Number, default: 0 })
retryCount?: number;
@Prop({ type: Number, default: 2 })
maxRetries?: number;
@Prop({ type: Date })
nextRetryAt?: Date;
}
export const FanavaranSyncStageSchema =
SchemaFactory.createForClass(FanavaranSyncStage);

View File

@@ -20,7 +20,9 @@ export class AuthGuard implements CanActivate {
}
try {
const payload: JwtPayload =
await this.jwtService.verifyAsync<JwtPayload>(token);
await this.jwtService.verifyAsync<JwtPayload>(token, {
secret: `${process.env.JWT_SECRET}`,
});
request["user"] = payload;
} catch {
throw new UnauthorizedException("Invalid or expired token");

View File

@@ -37,6 +37,7 @@ export interface FanavaranPayloadDefaults {
AccidentReportTypeId: number;
AccidentVehicleUsedId: number;
ClaimExpertId: number;
ExpertiseClaimExpertId: number;
CompensationReferenceId: number;
CulpritLicenceTypeId: number;
CulpritTypeId: number;
@@ -74,7 +75,8 @@ const FANAVARAN_CLIENT_PROFILES: Record<
AccidentCityId: 701,
AccidentReportTypeId: 155,
AccidentVehicleUsedId: 1,
ClaimExpertId: 1589,
ClaimExpertId: 4543092,
ExpertiseClaimExpertId: 4543092,
CompensationReferenceId: 167,
CulpritLicenceTypeId: 2,
CulpritTypeId: 337,
@@ -84,7 +86,7 @@ const FANAVARAN_CLIENT_PROFILES: Record<
PlaqueSampleId: 10,
DriverIsOwner: 0,
FaultPercent: 100,
ClaimFileTypeId: 70,
ClaimFileTypeId: 23,
},
},
parsian: {
@@ -103,6 +105,7 @@ const FANAVARAN_CLIENT_PROFILES: Record<
AccidentReportTypeId: 155,
AccidentVehicleUsedId: 1,
ClaimExpertId: 154,
ExpertiseClaimExpertId: 29,
CompensationReferenceId: 167,
CulpritLicenceTypeId: 2,
CulpritTypeId: 337,

View File

@@ -0,0 +1,26 @@
import { ConfigService } from "@nestjs/config";
import { HttpModuleOptions } from "@nestjs/axios";
import { SocksProxyAgent } from "socks-proxy-agent";
/**
* Shared HttpModule.registerAsync factory that applies the SOCKS proxy
* when SOCKS_PROXY_HOST + SOCKS_PROXY_PORT env vars are set.
* Used by every module that imports HttpModule.
*/
export function createHttpModuleOptions(
configService: ConfigService,
): HttpModuleOptions | Promise<HttpModuleOptions> {
const socksHost = configService.get<string>("SOCKS_PROXY_HOST");
const socksPort = configService.get<string>("SOCKS_PROXY_PORT");
if (socksHost && socksPort) {
const proxyUrl = `socks5://${socksHost}:${socksPort}`;
const agent = new SocksProxyAgent(proxyUrl);
return {
httpsAgent: agent,
httpAgent: agent,
proxy: false,
};
}
return {};
}

View File

@@ -1,5 +1,7 @@
import { HttpModule } from "@nestjs/axios";
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { createHttpModuleOptions } from "src/core/config/http-proxy.factory";
import { MongooseModule } from "@nestjs/mongoose";
import { AiModule } from "src/ai/ai.module";
import { AuthModule } from "src/auth/auth.module";
@@ -21,7 +23,11 @@ import { ExpertClaimService } from "./expert-claim.service";
@Module({
imports: [
HttpModule,
HttpModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: createHttpModuleOptions,
}),
SandHubModule,
MongooseModule.forFeature([
{ name: ClaimFactorsImage.name, schema: ClaimFactorsImageSchema },

View File

@@ -3466,17 +3466,7 @@ export class ExpertClaimService {
});
}
const fanavaranExpertise = !needsFactorUpload
? await this.claimRequestManagementService.autoSubmitFanavaranExpertiseOnExpertReply(
claimRequestId,
)
: {
attempted: false,
submitted: false,
skipped: true,
skipReason:
"Expertise submit waits until factor-needed repair lines are validated.",
};
// Fanavaran expertise is now triggered on owner final sign, not here.
return {
claimRequestId,
@@ -3490,7 +3480,6 @@ export class ExpertClaimService {
mixedPricingAndFactors: mixedFactorAndPrice,
allPartsFactorNeeded: !!needsFactorUpload && !!allFactorLines,
isFinalReplyAfterObjection,
fanavaranExpertise,
};
}

View File

@@ -76,6 +76,16 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/person-role`,
cacheFile: "person-role.json",
},
{
name: "insurance-corp",
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/insurance-corp`,
cacheFile: "insurance-corp.json",
},
{
name: "file-types",
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/base-info/file-types`,
cacheFile: "file-types.json",
},
];
export const TEJARAT_STATIC_ACCIDENT_FILES = {

View File

@@ -1,9 +1,17 @@
import { Module } from "@nestjs/common";
import { HttpModule } from "@nestjs/axios";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { createHttpModuleOptions } from "src/core/config/http-proxy.factory";
import { FanavaranLookupService } from "./fanavaran-lookup.service";
@Module({
imports: [HttpModule],
imports: [
HttpModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: createHttpModuleOptions,
}),
],
providers: [FanavaranLookupService],
exports: [FanavaranLookupService],
})

View File

@@ -183,24 +183,6 @@ export class FanavaranLookupService {
this.logger.log(
`[${clientKey}] Fanavaran lookup response status=${response.status} dataCount=${dataCount}`,
);
this.logger.log(
`[${clientKey}] Fanavaran lookup raw response: ${JSON.stringify(
response.data,
null,
2,
)}`,
);
if (Array.isArray(response.data) && response.data.length > 0) {
const firstItem = response.data[0];
if (firstItem && typeof firstItem === "object") {
this.logger.log(
`[${clientKey}] Fanavaran lookup first item keys: ${Object.keys(
firstItem,
).join(", ")}`,
);
}
}
return response.data;
} catch (error) {
@@ -255,6 +237,34 @@ export class FanavaranLookupService {
return this.fetchFromFanavaran(clientKey, url);
}
async myPolicies(
clientKey: FanavaranClientKey,
nationalCode: string,
insuranceLineId: number = 5,
): Promise<unknown> {
const url =
`${FANAVARAN_LOOKUP_BASE_URL}/common/Policies/inquiry-my-policies` +
`?InsuranceLineId=${insuranceLineId}` +
`&NationalCode=${encodeURIComponent(nationalCode)}`;
return this.fetchFromFanavaran(clientKey, url);
}
async thirdPartyPolicyById(
clientKey: FanavaranClientKey,
policyId: number,
): Promise<unknown> {
const url = `${FANAVARAN_LOOKUP_BASE_URL}/car/third-party-car-policies/${policyId}`;
return this.fetchFromFanavaran(clientKey, url);
}
async bodyPolicyById(
clientKey: FanavaranClientKey,
policyId: number,
): Promise<unknown> {
const url = `${FANAVARAN_LOOKUP_BASE_URL}/car/vehicle-hull-policies/${policyId}`;
return this.fetchFromFanavaran(clientKey, url);
}
async inquiryByUniqueIdentifier(
clientKey: FanavaranClientKey,
params: {
@@ -273,6 +283,91 @@ export class FanavaranLookupService {
return this.fetchFromFanavaran(clientKey, url);
}
async resolveInsuranceCorpId(
clientKey: FanavaranClientKey,
): Promise<number | null> {
const caption = process.env.INSURANCE_CORP_ID?.trim();
if (!caption) {
this.logger.warn("resolveInsuranceCorpId: INSURANCE_CORP_ID env not set");
return null;
}
// Check resolved cache first
const cachedId = await this.readCacheFile<number>(clientKey, "insurance-corp-id-resolved.json");
if (cachedId !== null) {
this.logger.log(`resolveInsuranceCorpId: using cached id=${cachedId} for "${caption}"`);
return cachedId;
}
// Try fetching the full list directly from Fanavaran
let companies: unknown;
const url = `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/insurance-corp`;
try {
companies = await this.fetchFromFanavaran(clientKey, url);
} catch (error) {
this.logger.warn(
`resolveInsuranceCorpId: Fanavaran fetch failed, trying local lookup endpoint`,
);
// Fallback: call our own local lookup endpoint
try {
const localPort = process.env.PORT || 3000;
const response = await firstValueFrom(
this.httpService.get(`http://localhost:${localPort}/lookups/fanavaran/insurance-corp`, {
timeout: 10000,
}),
);
companies = response.data;
} catch (localError) {
this.logger.error(
`resolveInsuranceCorpId: both Fanavaran and local lookup failed`,
);
return null;
}
}
if (!Array.isArray(companies)) {
this.logger.warn(
`resolveInsuranceCorpId: insurance-corp response is not an array, type=${typeof companies}`,
);
return null;
}
this.logger.log(
`resolveInsuranceCorpId: got ${companies.length} companies, searching for "${caption}"`,
);
const normalizedCaption = caption
.replace(/\s+/g, " ")
.toLowerCase()
.trim();
const match = companies.find((c: any) => {
if (c?.IsActive !== 1) return false;
if (typeof c?.Id !== "number") return false;
const cCaption = typeof c?.Caption === "string" ? c.Caption : "";
const normalized = cCaption.replace(/\s+/g, " ").toLowerCase().trim();
return normalized.includes(normalizedCaption) || normalizedCaption.includes(normalized);
});
if (!match) {
this.logger.warn(
`resolveInsuranceCorpId: no active company matching "${caption}". Available: ` +
companies
.filter((c: any) => c?.IsActive === 1)
.map((c: any) => `${c.Caption} (${c.Id})`)
.join(", "),
);
return null;
}
const corpId = match.Id as number;
// Cache both the resolved id and the full list for future use
await this.writeCacheFile(clientKey, "insurance-corp-id-resolved.json", corpId);
await this.writeCacheFile(clientKey, "insurance-corp.json", companies);
this.logger.log(`resolveInsuranceCorpId: resolved id=${corpId} for "${caption}"`);
return corpId;
}
mapFanavaranAccidentCausesToReasonOptions(
causes: unknown,
): { id: number; label: string; fanavaran: number }[] {
@@ -297,4 +392,5 @@ export class FanavaranLookupService {
};
});
}
}

View File

@@ -1,14 +1,18 @@
import { Controller, Get, Param, Query } from "@nestjs/common";
import { Controller, Get, Param, ParseIntPipe, Query, UseGuards } from "@nestjs/common";
import {
ApiBearerAuth,
ApiOkResponse,
ApiOperation,
ApiParam,
ApiQuery,
ApiTags,
} from "@nestjs/swagger";
import { AuthGuard } from "src/common/auth/guards";
import { LookupsService } from "./lookups.service";
@ApiTags("lookups")
@ApiBearerAuth()
@UseGuards(AuthGuard)
@Controller("lookups")
export class LookupsController {
constructor(private readonly lookupsService: LookupsService) {}
@@ -210,6 +214,30 @@ export class LookupsController {
return await this.lookupsService.getPersonRole();
}
@Get("file-types")
@ApiOperation({
summary: "Fanavaran file types lookup",
description:
"Returns file type codes from common/base-info/file-types. Use FileTypeId in attachment uploads.",
})
@ApiOkResponse({
description: "Returns Fanavaran file types lookup data",
schema: {
type: "array",
items: {
type: "object",
properties: {
Caption: { type: "string" },
Id: { type: "number" },
IsActive: { type: "number" },
},
},
},
})
async getFileTypes() {
return await this.lookupsService.getFileTypes();
}
@Get("inquiry-by-vin")
@ApiOperation({
summary: "Fanavaran vehicle inquiry by VIN",
@@ -371,4 +399,75 @@ export class LookupsController {
async getAccidentFields() {
return await this.lookupsService.getAccidentFields();
}
@Get("my-policies")
@ApiOperation({
summary: "My policies inquiry",
description:
"Returns the list of policies for the given national code via common/Policies/inquiry-my-policies.",
})
@ApiQuery({
name: "nationalCode",
description: "National code of the insured person",
example: "0012345678",
})
@ApiQuery({
name: "insuranceLineId",
description: "Insurance line ID (default: 5 for car)",
required: false,
example: 5,
})
@ApiOkResponse({
description: "Returns list of policies",
schema: { type: "array", items: { type: "object" } },
})
async myPolicies(
@Query("nationalCode") nationalCode: string,
@Query("insuranceLineId") insuranceLineId?: number,
) {
return await this.lookupsService.myPolicies(
nationalCode,
insuranceLineId ?? 5,
);
}
@Get("third-party-policy/:policyId")
@ApiOperation({
summary: "Third-party car policy inquiry by policy ID",
description:
"Returns third-party car insurance policy details for the given policy ID via car/third-party-car-policies/{policyId}.",
})
@ApiParam({
name: "policyId",
description: "Fanavaran policy ID",
example: 12345,
})
@ApiOkResponse({
description: "Returns third-party policy details",
schema: { type: "object" },
})
async thirdPartyPolicyById(
@Param("policyId", ParseIntPipe) policyId: number,
) {
return await this.lookupsService.thirdPartyPolicyById(policyId);
}
@Get("body-policy/:policyId")
@ApiOperation({
summary: "Vehicle hull (body) policy inquiry by policy ID",
description:
"Returns vehicle hull (body) insurance policy details for the given policy ID via car/vehicle-hull-policies/{policyId}.",
})
@ApiParam({
name: "policyId",
description: "Fanavaran policy ID",
example: 12345,
})
@ApiOkResponse({
description: "Returns body policy details",
schema: { type: "object" },
})
async bodyPolicyById(@Param("policyId", ParseIntPipe) policyId: number) {
return await this.lookupsService.bodyPolicyById(policyId);
}
}

View File

@@ -123,11 +123,37 @@ export class LookupsService {
return await this.getClientRemoteLookup("person-role");
}
async getFileTypes(): Promise<any> {
return await this.getClientRemoteLookup("file-types");
}
async inquiryByVin(vin: string): Promise<unknown> {
const clientKey = this.activeClientKey();
return this.fanavaranLookupService.inquiryByVin(clientKey, vin);
}
async myPolicies(
nationalCode: string,
insuranceLineId: number = 5,
): Promise<unknown> {
const clientKey = this.activeClientKey();
return this.fanavaranLookupService.myPolicies(
clientKey,
nationalCode,
insuranceLineId,
);
}
async thirdPartyPolicyById(policyId: number): Promise<unknown> {
const clientKey = this.activeClientKey();
return this.fanavaranLookupService.thirdPartyPolicyById(clientKey, policyId);
}
async bodyPolicyById(policyId: number): Promise<unknown> {
const clientKey = this.activeClientKey();
return this.fanavaranLookupService.bodyPolicyById(clientKey, policyId);
}
async getAccidentWay(): Promise<{ id: number; label: string }[]> {
const clientKey = this.activeClientKey();
const fileName = TEJARAT_STATIC_ACCIDENT_FILES.accidentWay;

View File

@@ -41,6 +41,10 @@ export class Person {
@Prop({ type: String })
driverBirthday?: string | null;
/** Cached Fanavaran party ID resolved from nationalCodeOfDriver + driverBirthday + driverIsInsurer */
@Prop({ type: Number })
fanavaranDriverId?: number;
}
export const PersonSchema = SchemaFactory.createForClass(Person);

View File

@@ -1,5 +1,7 @@
import { Module } from "@nestjs/common";
import { HttpModule } from "@nestjs/axios";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { createHttpModuleOptions } from "src/core/config/http-proxy.factory";
import { MongooseModule } from "@nestjs/mongoose";
import { ClientModule } from "src/client/client.module";
import { SystemSettingsModule } from "src/system-settings/system-settings.module";
@@ -9,7 +11,11 @@ import { SandHubService } from "./sand-hub.service";
@Module({
imports: [
HttpModule,
HttpModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: createHttpModuleOptions,
}),
SystemSettingsModule,
ClientModule,
MongooseModule.forFeature([