1
0
forked from Yara724/api
Files
yara724-api/scripts/refresh-blame-inquiries.js
2026-06-01 18:14:21 +03:30

666 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/*
* One-time script to replace mocked blameCases party vehicle inquiry data.
* THIRD_PARTY cases use Tejarat block inquiry; CAR_BODY cases use both
* Tejarat third-party block inquiry and car-body inquiry.
*
* Defaults to DRY_RUN=true. Set DRY_RUN=false to write changes.
*/
const fs = require("fs");
const path = require("path");
const mongoose = require("mongoose");
const loadedEnvFiles = loadEnvFiles(process.env.ENV_FILE || ".env");
const LETTER_TO_NUMBER = {
"الف": 1,
"ب": 2,
"ت": 3,
"ج": 4,
"د": 5,
"س": 6,
"ص": 7,
"ط": 8,
"ع": 9,
"ق": 10,
"ل": 11,
"م": 12,
"ن": 13,
"و": 14,
"ه": 15,
"ی": 16,
"ر": 17,
"ک": 18,
"ژ": 19,
"پ": 20,
"ظ": 24,
"ض": 25,
"ز": 41,
"ش": 42,
"گ": 43,
"ث": 44,
D: 45,
S: 46,
"ح": 47,
"ف": 48,
};
const NUMBER_TO_LETTER = Object.fromEntries(
Object.entries(LETTER_TO_NUMBER).map(([letter, number]) => [String(number), letter]),
);
const config = {
mongoUri: requiredEnv("MONGO_URL", "MONGODB_URI", "DATABASE_URL"),
collectionName: process.env.BLAME_COLLECTION || "blameCases",
mongoDbName: process.env.MONGO_DB_NAME || "",
thirdPartyUrl:
process.env.TEJARAT_THIRD_PARTY_URL ||
"http://82.99.202.245:3027/block-inquiry-tejarat",
carBodyUrl:
process.env.TEJARAT_CAR_BODY_URL ||
"http://82.99.202.245:3027/block-inquiry-tejarat/badane",
thirdPartyToken:
process.env.TEJARAT_THIRD_PARTY_TOKEN || process.env.TEJARAT_TOKEN || "",
carBodyToken:
process.env.TEJARAT_CAR_BODY_TOKEN || process.env.TEJARAT_TOKEN || "",
rateLimitPerMinute: Number(process.env.RATE_LIMIT_PER_MINUTE || 5),
retryEnabled: String(process.env.RETRY_ENABLED ?? "true").toLowerCase() !== "false",
retryCount: Number(process.env.RETRY_COUNT || 3),
retryDelayMs: Number(process.env.RETRY_DELAY_MS || 2000),
dryRun: String(process.env.DRY_RUN ?? "true").toLowerCase() !== "false",
limit: process.env.LIMIT ? Number(process.env.LIMIT) : 0,
publicId: process.env.PUBLIC_ID || "",
};
let lastRequestAt = 0;
main().catch(async (error) => {
console.error("[fatal]", error && error.stack ? error.stack : error);
await mongoose.disconnect().catch(() => undefined);
process.exitCode = 1;
});
async function main() {
validateConfig();
console.log("script runned successfully");
console.log(
"[config] envFiles=" +
(loadedEnvFiles.length ? loadedEnvFiles.join(",") : "none") +
", dbName=" +
(config.mongoDbName || "from-url-or-driver-default") +
", collection=" +
config.collectionName +
", dryRun=" +
config.dryRun +
", rateLimitPerMinute=" +
config.rateLimitPerMinute +
", retryEnabled=" +
config.retryEnabled +
", retryCount=" +
config.retryCount,
);
await mongoose.connect(config.mongoUri, {
autoIndex: false,
dbName: config.mongoDbName || undefined,
});
const collection = mongoose.connection.collection(config.collectionName);
const query = {
type: { $in: ["THIRD_PARTY", "CAR_BODY"] },
};
if (config.publicId) query.publicId = config.publicId;
const totalDocs = await collection.countDocuments(query);
console.log(`total docs that we have to edit: ${totalDocs}`);
const cursor = collection
.find(query, {
projection: {
publicId: 1,
requestNo: 1,
type: 1,
parties: 1,
},
})
.sort({ createdAt: 1, _id: 1 });
if (config.limit > 0) cursor.limit(config.limit);
const summary = {
docsSeen: 0,
docsChanged: 0,
partiesInquired: 0,
partiesSkipped: 0,
partiesFailed: 0,
};
for await (const doc of cursor) {
summary.docsSeen += 1;
const label = doc.publicId || doc.requestNo || String(doc._id);
const requestId = String(doc._id);
console.log(`currently inquiry for doc with ${label} and requestId ${requestId}`);
const parties = Array.isArray(doc.parties) ? clone(doc.parties) : [];
let docChanged = false;
for (let index = 0; index < parties.length; index += 1) {
const party = parties[index];
const partyLabel = `${label} parties[${index}] role=${party && party.role ? party.role : "-"}`;
const input = buildInquiryInputs(doc, party);
if (!input.ok) {
summary.partiesSkipped += 1;
console.warn(`[skip] ${partyLabel}: ${input.reason}`);
continue;
}
let nextParty = party;
let partyChanged = false;
for (const request of input.requests) {
console.log(
`[request] ${partyLabel} type=${request.type} body=${JSON.stringify(request.body)}`,
);
try {
const inquiryResponse = await inquiryWithRetry(request.type, request.body);
const successful = isInquirySuccessful(request.type, inquiryResponse);
console.log(
`[response] ${partyLabel} type=${request.type} successful=${successful} body=${JSON.stringify(inquiryResponse)}`,
);
if (!successful) {
summary.partiesFailed += 1;
continue;
}
nextParty = applyInquiryToParty(request.type, nextParty, inquiryResponse, input.plate);
summary.partiesInquired += 1;
partyChanged = true;
} catch (error) {
summary.partiesFailed += 1;
console.error(`[error] ${partyLabel} type=${request.type}: ${error.message}`);
}
}
if (partyChanged) {
parties[index] = nextParty;
docChanged = true;
}
}
if (!docChanged) {
console.log(`[doc] ${label} no changes`);
continue;
}
if (config.dryRun) {
console.log(`[dry-run] ${label} would update parties array`);
continue;
}
const result = await collection.updateOne(
{ _id: doc._id },
{
$set: {
parties,
updatedAt: new Date(),
},
},
);
summary.docsChanged += result.modifiedCount;
console.log(`[update] ${label} matched=${result.matchedCount} modified=${result.modifiedCount}`);
}
await mongoose.disconnect();
console.log(`[done] ${JSON.stringify(summary)}`);
}
function buildInquiryInputs(doc, party) {
if (!party || typeof party !== "object") return { ok: false, reason: "party is empty" };
if (!party.vehicle || typeof party.vehicle !== "object") {
return { ok: false, reason: "party.vehicle is missing" };
}
const plate = extractPlate(party);
const nationalCode =
cleanString(party.person && party.person.nationalCodeOfInsurer) ||
cleanString(party.person && party.person.nationalCodeOfDriver);
if (!plate) return { ok: false, reason: "Plk1/Plk2/Plk3/PlkSrl not found" };
if (!nationalCode) return { ok: false, reason: "nationalCodeOfInsurer/nationalCodeOfDriver missing" };
const serialLetter = NUMBER_TO_LETTER[String(plate.Plk2)] || cleanString(plate.Plk2);
if (!serialLetter) return { ok: false, reason: `no Persian letter mapping for Plk2=${plate.Plk2}` };
if (doc.type === "THIRD_PARTY") {
return {
ok: true,
plate,
requests: [buildThirdPartyRequest(plate, serialLetter, nationalCode)],
};
}
if (doc.type === "CAR_BODY") {
return {
ok: true,
plate,
requests: [
buildThirdPartyRequest(plate, serialLetter, nationalCode),
buildCarBodyRequest(plate, serialLetter, nationalCode),
],
};
}
return { ok: false, reason: `unsupported type=${doc.type}` };
}
function buildThirdPartyRequest(plate, serialLetter, nationalCode) {
return {
type: "THIRD_PARTY",
body: {
leftTwoDigits: String(plate.Plk1),
serialLetter,
threeDigits: String(plate.Plk3),
rightTwoDigits: String(plate.PlkSrl),
nationalCode,
},
};
}
function buildCarBodyRequest(plate, serialLetter, nationalCode) {
return {
type: "CAR_BODY",
body: {
part1: toNumber(plate.Plk1),
part2: serialLetter,
part3: toNumber(plate.Plk3),
part4: toNumber(plate.PlkSrl),
nationalCode,
},
};
}
function parsePlateId(plateId) {
const value = cleanString(plateId);
if (!value) return null;
const parts = value.split("-").map((part) => normalizePlateNumber(part));
if (parts.length !== 4) return null;
const thirdPartIsLetter = LETTER_TO_NUMBER[parts[2]] !== undefined;
const fourthPartIsLetter = LETTER_TO_NUMBER[parts[3]] !== undefined;
if (thirdPartIsLetter) {
return {
Plk1: parts[1],
Plk2: String(LETTER_TO_NUMBER[parts[2]]),
Plk3: parts[3],
PlkSrl: parts[0],
};
}
if (fourthPartIsLetter) {
return {
Plk1: parts[2],
Plk2: String(LETTER_TO_NUMBER[parts[3]]),
Plk3: parts[1],
PlkSrl: parts[0],
};
}
return null;
}
function extractPlate(party) {
const plateFromPlateId = parsePlateId(party.vehicle && party.vehicle.plateId);
if (plateFromPlateId) return plateFromPlateId;
const candidates = [
party.vehicle && party.vehicle.inquiry && party.vehicle.inquiry.mapped,
party.vehicle && party.vehicle.inquiry && party.vehicle.inquiry.raw,
party.vehicle && party.vehicle.inquiry,
party.vehicle,
].filter(Boolean);
for (const candidate of candidates) {
const Plk1 = firstPresent(candidate.Plk1, candidate.platePartOne);
const Plk2 = firstPresent(candidate.Plk2, candidate.plateLetterid, candidate.plateLetterId);
const Plk3 = firstPresent(candidate.Plk3, candidate.platePartThree);
const PlkSrl = firstPresent(candidate.PlkSrl, candidate.plkSrl, candidate.plateSerialNumber);
if (
Plk1 !== undefined &&
Plk2 !== undefined &&
Plk3 !== undefined &&
PlkSrl !== undefined
) {
return {
Plk1: normalizePlateNumber(Plk1),
Plk2: normalizePlateNumber(Plk2),
Plk3: normalizePlateNumber(Plk3),
PlkSrl: normalizePlateNumber(PlkSrl),
};
}
}
return null;
}
async function inquiryWithRetry(type, body) {
const url = type === "CAR_BODY" ? config.carBodyUrl : config.thirdPartyUrl;
const token = type === "CAR_BODY" ? config.carBodyToken : config.thirdPartyToken;
let lastError;
const maxAttempts = config.retryEnabled ? config.retryCount : 1;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
await waitForRateLimit();
try {
console.log("[http] " + type + " attempt=" + attempt + "/" + maxAttempts);
return await postJson(url, token, body);
} catch (error) {
lastError = error;
console.error(`[retry] ${type} attempt=${attempt} failed: ${error.message}`);
if (attempt < maxAttempts) {
await sleep(config.retryDelayMs * attempt);
}
}
}
throw lastError;
}
async function postJson(url, token, body) {
const response = await fetch(url, {
method: "POST",
headers: {
accept: "application/json",
authorization: `Bearer ${token}`,
"content-type": "application/json",
},
body: JSON.stringify(body),
});
const text = await response.text();
let data;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = text;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${JSON.stringify(data)}`);
}
return data;
}
function isInquirySuccessful(type, response) {
if (type === "CAR_BODY") return response && response.isSuccess === true && response.data;
return response && response.resultStatus === true;
}
function applyInquiryToParty(type, party, response, originalPlate) {
if (type === "CAR_BODY") return applyCarBodyInquiryToParty(party, response, originalPlate);
return applyThirdPartyInquiryToParty(party, response, originalPlate);
}
function applyThirdPartyInquiryToParty(party, response, originalPlate) {
const next = clone(party);
if (!next.vehicle) next.vehicle = {};
if (!next.insurance) next.insurance = {};
const mapped = normalizeThirdPartyResponse(response, originalPlate);
const plateId = buildPlateId(originalPlate);
next.vehicle.plateId = plateId || next.vehicle.plateId;
next.vehicle.inquiry = {
source: "TEJARAT_BLOCK_INQUIRY",
raw: response,
mapped,
refreshedAt: new Date().toISOString(),
};
if (party.vehicle && party.vehicle.inquiry && party.vehicle.inquiry.carBody) {
next.vehicle.inquiry.carBody = party.vehicle.inquiry.carBody;
}
next.vehicle.name = mapped.vehiclePersianName || mapped.MapTypNam || "اطلاعات این گزینه در استعلام موجود نیست";
next.vehicle.type = mapped.persianCarType || mapped.MapUsageName || next.vehicle.type;
next.insurance.policyNumber =
mapped.LastCompanyDocumentNumber || mapped.insuranceNumber || next.insurance.policyNumber;
next.insurance.company = mapped.companyPersianName || next.insurance.company;
next.insurance.financialCeiling = mapped.financeCoverage || next.insurance.financialCeiling;
next.insurance.startDate = mapped.persianStartDate || next.insurance.startDate;
next.insurance.endDate = mapped.persianEndDate || next.insurance.endDate;
return next;
}
function applyCarBodyInquiryToParty(party, response, originalPlate) {
const next = clone(party);
if (!next.vehicle) next.vehicle = {};
if (!next.insurance) next.insurance = {};
const mapped = normalizeCarBodyResponse(response, originalPlate);
const plateId = buildPlateId(originalPlate);
next.vehicle.plateId = plateId || next.vehicle.plateId;
if (!next.vehicle.inquiry || typeof next.vehicle.inquiry !== "object") {
next.vehicle.inquiry = {};
}
next.vehicle.inquiry.carBody = {
source: "TEJARAT_CAR_BODY_INQUIRY",
raw: response,
mapped,
refreshedAt: new Date().toISOString(),
};
next.vehicle.name = mapped.vehicleSystemTitle || next.vehicle.name;
next.vehicle.type = mapped.vehicleGroupTitle || next.vehicle.type;
next.insurance.carBodyInsurance = {
policyNumber: mapped.policyNumber,
companyId: mapped.companyId,
companyName: mapped.CompanyName,
insurerName: mapped.insurerName,
insurerNationalCode: mapped.insurerNationalCode,
ownerNationalCode: mapped.ownerNationalCode,
chassisNumber: mapped.chassisNumber,
vin: mapped.vin,
motorNumber: mapped.motorNumber,
vehicleGroup: mapped.vehicleGroupTitle,
vehicleSystem: mapped.vehicleSystemTitle,
startDate: mapped.StartDate,
endDate: mapped.EndDate,
issueDate: mapped.IssueDate,
noLossYearsCount: mapped.noLossYearsCount,
lossDocuments: Array.isArray(mapped.lossDocuments) ? mapped.lossDocuments : [],
hasEndorsement: mapped.hasEndorsement,
};
return next;
}
function buildPlateId(plate) {
if (!plate) return "";
const serialLetter = NUMBER_TO_LETTER[String(plate.Plk2)] || cleanString(plate.Plk2);
if (!serialLetter) return "";
return (
cleanString(plate.PlkSrl) +
"-" +
cleanString(plate.Plk1) +
"-" +
serialLetter +
"-" +
cleanString(plate.Plk3)
);
}
function normalizeThirdPartyResponse(response, originalPlate) {
return {
...response,
Plk1: toNumber(originalPlate.Plk1),
Plk2: toNumber(originalPlate.Plk2),
Plk3: toNumber(originalPlate.Plk3),
PlkSrl: toNumber(originalPlate.PlkSrl),
CompanyName: response.companyPersianName,
CompanyCode: response.companyId,
LastCompanyDocumentNumber: response.lastCompanyInsuranceNumber || response.insuranceNumber,
FinancialCvrCptl: response.financeCoverage,
IssueDate: response.hIsuDte || response.persianStartDate,
SatrtDate: response.persianStartDate,
EndDate: response.persianEndDate,
MapTypNam: response.vehiclePersianName,
MapUsageName: response.MapUsageName || response.persianCarType,
UsageField: response.persianCarType,
UsageCode: response.usgCod,
VehicleSystemCode: response.vehSysCod,
CarGroupCode: response.carGrpCod,
EdrsJson: Array.isArray(response.edrSes) ? JSON.stringify(response.edrSes) : response.EdrsJson,
InsuranceFullName: response.fullname,
};
}
function normalizeCarBodyResponse(response, originalPlate) {
const data = response.data || {};
return {
...data,
Plk1: toNumber(originalPlate.Plk1),
Plk2: toNumber(originalPlate.Plk2),
Plk3: toNumber(originalPlate.Plk3),
PlkSrl: toNumber(originalPlate.PlkSrl),
policyNumber: data.printNumber,
CompanyName: data.companyName,
CompanyCode: data.companyId,
LastCompanyDocumentNumber: data.printNumber,
InsuranceFullName: data.insurerName,
IssueDate: data.issueDate,
StartDate: data.beginDate,
SatrtDate: data.beginDate,
EndDate: data.endDate,
EngineNumberField: data.motorNumber,
MtrNum: data.motorNumber,
ChassisNumberField: data.chassisNumber,
ShsNum: data.chassisNumber,
VinNumberField: data.vin,
MapTypNam: data.vehicleGroupTitle,
isSuccess: response.isSuccess,
statusCode: response.statusCode,
message: response.message,
};
}
async function waitForRateLimit() {
const minDelayMs = Math.ceil(60000 / config.rateLimitPerMinute);
const elapsed = Date.now() - lastRequestAt;
if (lastRequestAt > 0 && elapsed < minDelayMs) {
const waitMs = minDelayMs - elapsed;
console.log(`[rate-limit] waiting ${waitMs}ms`);
await sleep(waitMs);
}
lastRequestAt = Date.now();
}
function validateConfig() {
if (!config.mongoUri) throw new Error("MONGO_URL is required");
if (!Number.isFinite(config.rateLimitPerMinute) || config.rateLimitPerMinute <= 0) {
throw new Error("RATE_LIMIT_PER_MINUTE must be a positive number");
}
if (!Number.isFinite(config.retryCount) || config.retryCount <= 0) {
throw new Error("RETRY_COUNT must be a positive number");
}
if (!config.thirdPartyToken) {
throw new Error("TEJARAT_THIRD_PARTY_TOKEN or TEJARAT_TOKEN is required");
}
if (!config.carBodyToken) {
throw new Error("TEJARAT_CAR_BODY_TOKEN or TEJARAT_TOKEN is required");
}
}
function loadEnvFiles(filePath) {
const candidates = path.isAbsolute(filePath)
? [filePath]
: [
path.resolve(process.cwd(), filePath),
path.resolve(__dirname, "..", filePath),
];
const loaded = [];
for (const candidate of [...new Set(candidates)]) {
if (!fs.existsSync(candidate)) continue;
loadEnvFile(candidate);
loaded.push(candidate);
}
return loaded;
}
function loadEnvFile(filePath) {
const resolved = path.resolve(process.cwd(), filePath);
if (!fs.existsSync(resolved)) return;
const lines = fs.readFileSync(resolved, "utf8").split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const equalIndex = trimmed.indexOf("=");
if (equalIndex === -1) continue;
const key = trimmed.slice(0, equalIndex).trim();
let value = trimmed.slice(equalIndex + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (key && process.env[key] === undefined) process.env[key] = value;
}
}
function requiredEnv(...names) {
for (const name of names) {
if (process.env[name]) return process.env[name];
}
return "";
}
function firstPresent(...values) {
return values.find((value) => value !== undefined && value !== null && value !== "");
}
function normalizePlateNumber(value) {
const cleaned = normalizeDigits(cleanString(value));
return cleaned === "" ? value : cleaned;
}
function normalizeDigits(value) {
return cleanString(value).replace(/./g, (char) => {
const code = char.charCodeAt(0);
if (code >= 0x06f0 && code <= 0x06f9) return String(code - 0x06f0);
if (code >= 0x0660 && code <= 0x0669) return String(code - 0x0660);
return char;
});
}
function toNumber(value) {
const number = Number(value);
return Number.isFinite(number) ? number : value;
}
function cleanString(value) {
return value === undefined || value === null ? "" : String(value).trim();
}
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}