added offline inquiry in system setting also fix some bugs

This commit is contained in:
2026-08-03 11:49:24 +03:30
parent ab4f667c8d
commit 7f672541ae
19 changed files with 965 additions and 26 deletions

View File

@@ -0,0 +1,20 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { SystemSettingsModule } from "src/system-settings/system-settings.module";
import { OfflineInquiryService } from "./offline-inquiry.service";
import {
OfflineInquiry,
OfflineInquirySchema,
} from "./schema/offline-inquiry.schema";
@Module({
imports: [
MongooseModule.forFeature([
{ name: OfflineInquiry.name, schema: OfflineInquirySchema },
]),
SystemSettingsModule,
],
providers: [OfflineInquiryService],
exports: [OfflineInquiryService],
})
export class OfflineInquiryModule {}

View File

@@ -0,0 +1,287 @@
import type { FanavaranClientKey } from "src/core/config/fanavaran-client.config";
/**
* Seed payloads for offline plate/policy inquiry.
* Inserted into `offlineInquiries` on boot when the unique key is missing.
*/
export const OFFLINE_INQUIRY_SEEDS: Array<{
clientKey: FanavaranClientKey;
inquiryType: string;
nationalCode: string;
plate: {
leftDigits: string;
centerAlphabet: string;
centerDigits: string;
ir: string;
};
plateId: string;
label: string;
fanavaranDriverId?: number;
person?: Record<string, unknown>;
insurance?: Record<string, unknown>;
inquiry: {
source: string;
raw: Record<string, unknown>;
mapped: Record<string, unknown>;
};
}> = [
{
clientKey: "parsian",
inquiryType: "thirdPartyPlate",
nationalCode: "0651828562",
plate: {
leftDigits: "56",
centerAlphabet: "د",
centerDigits: "394",
ir: "66",
},
plateId: "66-56-د-394",
label: "روح اله حيدري — Parsian FIRST party (offline seed)",
person: {
driverBirthday: "13571201",
insurerBirthday: 13571201,
driverIsInsurer: true,
nationalCodeOfDriver: "0651828562",
nationalCodeOfInsurer: "0651828562",
phoneNumber: "09120723528",
},
insurance: {
coverages: [],
policyNumber: null,
company: "بيمه پارسيان",
financialCeiling: "0",
startDate: "1404/10/21",
endDate: "1405/10/23",
},
inquiry: {
source: "OFFLINE_SEED_INQUIRY",
raw: {
success: true,
provider: "PARSIAN",
trackingCode: "OFFLINE-PARSIAN-0651828562",
message: "offline seeded policy by plate",
duration: 0,
data: {
nationalCode: "0651828562",
plk1: "56",
plk2: "د",
plk3: "394",
plksrl: "66",
CarGrpCod: "2",
CmpCod: "8",
CmpNam: "بيمه پارسيان",
DisFnYrPrcnt: "35",
DisLfYrPrcnt: "35",
DisPrsnYrPrcnt: "70",
FndCst: "0.00",
HBgnDte: "1404/10/23",
HEndDte: "1405/10/23",
HIsuDte: "1404/10/21",
InsNam: "روح اله حيدري",
LastCmpCod: "8",
LastCmpDocNo: "1110/111130/403/002650",
MtrNum: "13389030167",
NtnlId: "0651828562",
PlcyUnqCod: "16018466143",
Plk: "394د56 66",
PrdDte: "1389",
PrntCmpDocNo: "1110/111130/404/003412",
ShsNum: "NAAP41FD5BJ301065",
TypPlcy: "بيمه ثالث و حوادث",
UsgCod: "8",
VIN: "IRFC891V7D2301065",
VehSysCod: "2",
},
error: null,
},
mapped: {
nationalCode: "0651828562",
plk1: "56",
plk2: "د",
plk3: "394",
plksrl: "66",
CarGrpCod: "2",
CmpCod: "8",
CmpNam: "بيمه پارسيان",
DisFnYrPrcnt: "35",
DisLfYrPrcnt: "35",
DisPrsnYrPrcnt: "70",
FndCst: "0.00",
HBgnDte: "1404/10/23",
HEndDte: "1405/10/23",
HIsuDte: "1404/10/21",
InsNam: "روح اله حيدري",
LastCmpCod: "8",
LastCmpDocNo: "1110/111130/403/002650",
MtrNum: "13389030167",
NtnlId: "0651828562",
PlcyUnqCod: "16018466143",
Plk: "394د56 66",
PrdDte: "1389",
PrntCmpDocNo: "1110/111130/404/003412",
ShsNum: "NAAP41FD5BJ301065",
TypPlcy: "بيمه ثالث و حوادث",
UsgCod: "8",
VIN: "IRFC891V7D2301065",
VehSysCod: "2",
companyId: "8",
companyPersianName: "بيمه پارسيان",
carGrpCod: "2",
usgCod: "8",
vehSysCod: "2",
mtrnum: "13389030167",
shsNam: "NAAP41FD5BJ301065",
vin: "IRFC891V7D2301065",
ntnlId: "0651828562",
Name: "روح اله حيدري",
ThirdPolicyCode: "16018466143",
LastCompanyDocumentNumber: "1110/111130/403/002650",
IssueDate: "1404/10/21",
StartDate: "1404/10/23",
EndDate: "1405/10/23",
CompanyCode: "8",
CompanyName: "بيمه پارسيان",
UsageField: "شخصی",
FinancialCvrCptl: "0",
PrntPlcyCmpDocNo: "1110/111130/404/003412",
VinNumberField: "IRFC891V7D2301065",
ChassisNumberField: "NAAP41FD5BJ301065",
EngineNumberField: "13389030167",
UsageCode: "8",
VehicleSystemCode: "2",
CarGroupCode: "2",
InsuranceFullName: "روح اله حيدري",
},
},
},
{
clientKey: "parsian",
inquiryType: "thirdPartyPlate",
nationalCode: "0021557985",
plate: {
leftDigits: "77",
centerAlphabet: "ه",
centerDigits: "339",
ir: "20",
},
plateId: "20-77-ه-339",
label: "سيدنويد صالحي — Parsian SECOND party (offline seed)",
fanavaranDriverId: 2426953,
person: {
driverBirthday: "13770819",
insurerBirthday: 13770819,
driverIsInsurer: true,
nationalCodeOfDriver: "0021557985",
nationalCodeOfInsurer: "0021557985",
phoneNumber: "09912536917",
},
insurance: {
coverages: [],
policyNumber: null,
company: "بيمه پارسيان",
financialCeiling: "0",
startDate: "1405/02/29",
endDate: "1406/02/29",
},
inquiry: {
source: "OFFLINE_SEED_INQUIRY",
raw: {
success: true,
provider: "PARSIAN",
trackingCode: "OFFLINE-PARSIAN-0021557985",
message: "offline seeded policy by plate",
duration: 0,
data: {
nationalCode: "0021557985",
plk1: "77",
plk2: "ه",
plk3: "339",
plksrl: "20",
CarGrpCod: "2",
CmpCod: "8",
CmpNam: "بيمه پارسيان",
DisFnYrPrcnt: "15",
DisLfYrPrcnt: "15",
DisPrsnYrPrcnt: "15",
FndCst: "716122.00",
HBgnDte: "1405/02/29",
HEndDte: "1406/02/29",
HIsuDte: "1405/02/29",
InsNam: "سيدنويد صالحي",
LastCmpCod: "8",
LastCmpDocNo: "1110/111130/404/000575",
MtrNum: "M159895611",
NtnlId: "0021557985",
PlcyUnqCod: "16035590431",
Plk: "339ه77 20",
PrdDte: "1402",
PrntCmpDocNo: "1110/111130/405/001098",
ShsNum: "NAPX212AAP1173773",
TypPlcy: "بيمه ثالث و حوادث",
UsgCod: "8",
VIN: "NAPX212AAP1173773",
VehSysCod: "315",
},
error: null,
},
mapped: {
nationalCode: "0021557985",
plk1: "77",
plk2: "ه",
plk3: "339",
plksrl: "20",
CarGrpCod: "2",
CmpCod: "8",
CmpNam: "بيمه پارسيان",
DisFnYrPrcnt: "15",
DisLfYrPrcnt: "15",
DisPrsnYrPrcnt: "15",
FndCst: "716122.00",
HBgnDte: "1405/02/29",
HEndDte: "1406/02/29",
HIsuDte: "1405/02/29",
InsNam: "سيدنويد صالحي",
LastCmpCod: "8",
LastCmpDocNo: "1110/111130/404/000575",
MtrNum: "M159895611",
NtnlId: "0021557985",
PlcyUnqCod: "16035590431",
Plk: "339ه77 20",
PrdDte: "1402",
PrntCmpDocNo: "1110/111130/405/001098",
ShsNum: "NAPX212AAP1173773",
TypPlcy: "بيمه ثالث و حوادث",
UsgCod: "8",
VIN: "NAPX212AAP1173773",
VehSysCod: "315",
companyId: "8",
companyPersianName: "بيمه پارسيان",
carGrpCod: "2",
usgCod: "8",
vehSysCod: "315",
mtrnum: "M159895611",
shsNam: "NAPX212AAP1173773",
vin: "NAPX212AAP1173773",
ntnlId: "0021557985",
Name: "سيدنويد صالحي",
ThirdPolicyCode: "16035590431",
LastCompanyDocumentNumber: "1110/111130/404/000575",
IssueDate: "1405/02/29",
StartDate: "1405/02/29",
EndDate: "1406/02/29",
CompanyCode: "8",
CompanyName: "بيمه پارسيان",
UsageField: "شخصی",
FinancialCvrCptl: "0",
PrntPlcyCmpDocNo: "1110/111130/405/001098",
VinNumberField: "NAPX212AAP1173773",
ChassisNumberField: "NAPX212AAP1173773",
EngineNumberField: "M159895611",
UsageCode: "8",
VehicleSystemCode: "315",
CarGroupCode: "2",
InsuranceFullName: "سيدنويد صالحي",
},
},
},
];

View File

@@ -0,0 +1,154 @@
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import {
resolveFanavaranClientKey,
type FanavaranClientKey,
} from "src/core/config/fanavaran-client.config";
import { SystemSettingsService } from "src/system-settings/system-settings.service";
import { OFFLINE_INQUIRY_SEEDS } from "./offline-inquiry.seeds";
import {
OfflineInquiry,
OfflineInquiryDocument,
} from "./schema/offline-inquiry.schema";
export interface OfflinePlateLookupInput {
nationalCode: string;
leftDigits: string;
centerAlphabet: string;
centerDigits: string;
ir: string;
clientKey?: FanavaranClientKey;
}
export interface OfflinePlateInquiryHit {
raw: Record<string, unknown>;
mapped: Record<string, unknown>;
source: string;
fanavaranDriverId?: number;
insurance?: Record<string, unknown>;
person?: Record<string, unknown>;
plateId?: string;
label?: string;
}
@Injectable()
export class OfflineInquiryService implements OnModuleInit {
private readonly logger = new Logger(OfflineInquiryService.name);
constructor(
@InjectModel(OfflineInquiry.name)
private readonly offlineInquiryModel: Model<OfflineInquiryDocument>,
private readonly systemSettingsService: SystemSettingsService,
) {}
async onModuleInit(): Promise<void> {
await this.seedMissing();
}
/** Normalize Persian plate letter (strip ZWNJ / Arabic variants). */
static normalizePlateAlphabet(value: string): string {
return String(value ?? "")
.replace(/[\u200c\u200d\uFEFF]/g, "")
.replace(/ي/g, "ی")
.replace(/ك/g, "ک")
.trim();
}
static normalizeDigits(value: string | number): string {
return String(value ?? "").trim();
}
async seedMissing(): Promise<void> {
for (const seed of OFFLINE_INQUIRY_SEEDS) {
const plate = {
leftDigits: OfflineInquiryService.normalizeDigits(seed.plate.leftDigits),
centerAlphabet: OfflineInquiryService.normalizePlateAlphabet(
seed.plate.centerAlphabet,
),
centerDigits: OfflineInquiryService.normalizeDigits(
seed.plate.centerDigits,
),
ir: OfflineInquiryService.normalizeDigits(seed.plate.ir),
};
const existing = await this.offlineInquiryModel
.findOne({
clientKey: seed.clientKey,
inquiryType: seed.inquiryType,
nationalCode: seed.nationalCode,
"plate.leftDigits": plate.leftDigits,
"plate.centerAlphabet": plate.centerAlphabet,
"plate.centerDigits": plate.centerDigits,
"plate.ir": plate.ir,
})
.select({ _id: 1 })
.lean()
.exec();
if (existing) {
continue;
}
await this.offlineInquiryModel.create({
...seed,
plate,
enabled: true,
});
this.logger.log(
`Seeded offlineInquiry ${seed.clientKey} ${seed.nationalCode} plate=${seed.plateId}`,
);
}
}
async findPlateInquiry(
input: OfflinePlateLookupInput,
): Promise<OfflinePlateInquiryHit | null> {
if (!(await this.systemSettingsService.isOfflineInquiryEnabled())) {
return null;
}
const clientKey = input.clientKey ?? resolveFanavaranClientKey();
const nationalCode = OfflineInquiryService.normalizeDigits(
input.nationalCode,
);
const leftDigits = OfflineInquiryService.normalizeDigits(input.leftDigits);
const centerAlphabet = OfflineInquiryService.normalizePlateAlphabet(
input.centerAlphabet,
);
const centerDigits = OfflineInquiryService.normalizeDigits(
input.centerDigits,
);
const ir = OfflineInquiryService.normalizeDigits(input.ir);
const doc = await this.offlineInquiryModel
.findOne({
clientKey,
inquiryType: "thirdPartyPlate",
nationalCode,
enabled: true,
"plate.leftDigits": leftDigits,
"plate.centerAlphabet": centerAlphabet,
"plate.centerDigits": centerDigits,
"plate.ir": ir,
})
.lean()
.exec();
if (!doc?.inquiry?.raw || !doc?.inquiry?.mapped) {
return null;
}
this.logger.log(
`[OFFLINE] Hit plate inquiry client=${clientKey} nationalCode=${nationalCode} plate=${doc.plateId ?? `${ir}-${leftDigits}-${centerAlphabet}-${centerDigits}`}`,
);
return {
raw: doc.inquiry.raw as Record<string, unknown>,
mapped: doc.inquiry.mapped as Record<string, unknown>,
source: doc.inquiry.source || "OFFLINE_SEED_INQUIRY",
fanavaranDriverId: doc.fanavaranDriverId,
insurance: doc.insurance,
person: doc.person,
plateId: doc.plateId,
label: doc.label,
};
}
}

View File

@@ -0,0 +1,85 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";
import type { FanavaranClientKey } from "src/core/config/fanavaran-client.config";
@Schema({ _id: false })
export class OfflineInquiryPlateParts {
@Prop({ type: String, required: true })
leftDigits: string;
@Prop({ type: String, required: true })
centerAlphabet: string;
@Prop({ type: String, required: true })
centerDigits: string;
@Prop({ type: String, required: true })
ir: string;
}
export const OfflineInquiryPlatePartsSchema = SchemaFactory.createForClass(
OfflineInquiryPlateParts,
);
/**
* Captured plate/policy inquiry (+ optional Fanavaran driver id) for offline use
* when live ESG/Tejarat inquiry is unavailable (e.g. Parsian off their LAN).
*/
@Schema({ collection: "offlineInquiries", timestamps: true })
export class OfflineInquiry {
@Prop({ type: String, required: true, index: true })
clientKey: FanavaranClientKey;
/** thirdPartyPlate | personalIdentity (extensible). */
@Prop({ type: String, required: true, index: true, default: "thirdPartyPlate" })
inquiryType: string;
@Prop({ type: String, required: true, index: true })
nationalCode: string;
@Prop({ type: OfflineInquiryPlatePartsSchema, required: false })
plate?: OfflineInquiryPlateParts;
/** Human plate id e.g. 66-56-د-394 */
@Prop({ type: String, required: false })
plateId?: string;
@Prop({ type: String, required: false })
label?: string;
@Prop({ type: Object, required: true })
inquiry: {
source: string;
raw: Record<string, unknown>;
mapped: Record<string, unknown>;
};
@Prop({ type: Object, required: false })
insurance?: Record<string, unknown>;
/** Cached Fanavaran DriverId from parties inquiry-by-unique-identifier. */
@Prop({ type: Number, required: false })
fanavaranDriverId?: number;
@Prop({ type: Object, required: false })
person?: Record<string, unknown>;
@Prop({ type: Boolean, default: true })
enabled: boolean;
}
export type OfflineInquiryDocument = HydratedDocument<OfflineInquiry>;
export const OfflineInquirySchema =
SchemaFactory.createForClass(OfflineInquiry);
OfflineInquirySchema.index(
{
clientKey: 1,
inquiryType: 1,
nationalCode: 1,
"plate.leftDigits": 1,
"plate.centerAlphabet": 1,
"plate.centerDigits": 1,
"plate.ir": 1,
},
{ unique: true, sparse: true },
);