refresh blame inquirys script

This commit is contained in:
2026-06-01 16:01:41 +03:30
parent 6ddb06594b
commit 4bc5889ccd

View File

@@ -0,0 +1,606 @@
#!/usr/bin/env node
/*
* One-time script to replace mocked blameCases party vehicle inquiry data with
* Tejarat block inquiry responses.
*
* 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 = buildInquiryInput(doc, party);
if (!input.ok) {
summary.partiesSkipped += 1;
console.warn(`[skip] ${partyLabel}: ${input.reason}`);
continue;
}
console.log(`[request] ${partyLabel} body=${JSON.stringify(input.body)}`);
try {
const inquiryResponse = await inquiryWithRetry(doc.type, input.body);
const successful = isInquirySuccessful(doc.type, inquiryResponse);
console.log(
`[response] ${partyLabel} successful=${successful} body=${JSON.stringify(inquiryResponse)}`,
);
if (!successful) {
summary.partiesFailed += 1;
continue;
}
parties[index] = applyInquiryToParty(doc.type, party, inquiryResponse, input.plate);
summary.partiesInquired += 1;
docChanged = true;
} catch (error) {
summary.partiesFailed += 1;
console.error(`[error] ${partyLabel}: ${error.message}`);
}
}
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 buildInquiryInput(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 === "CAR_BODY") {
return {
ok: true,
plate,
body: {
part1: toNumber(plate.Plk1),
part2: serialLetter,
part3: toNumber(plate.Plk3),
part4: toNumber(plate.PlkSrl),
nationalCode,
},
};
}
if (doc.type === "THIRD_PARTY") {
return {
ok: true,
plate,
body: {
leftTwoDigits: String(plate.Plk1),
serialLetter,
threeDigits: String(plate.Plk3),
rightTwoDigits: String(plate.PlkSrl),
nationalCode,
},
};
}
return { ok: false, reason: `unsupported type=${doc.type}` };
}
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) {
const next = clone(party);
if (!next.vehicle) next.vehicle = {};
if (!next.insurance) next.insurance = {};
const mapped = type === "CAR_BODY"
? normalizeCarBodyResponse(response, originalPlate)
: normalizeThirdPartyResponse(response, originalPlate);
const plateId = buildPlateId(originalPlate);
next.vehicle.plateId = plateId || next.vehicle.plateId;
next.vehicle.inquiry = {
source: type === "CAR_BODY" ? "TEJARAT_CAR_BODY_BLOCK_INQUIRY" : "TEJARAT_BLOCK_INQUIRY",
raw: type === "CAR_BODY" ? normalizeCarBodyRaw(response, originalPlate) : response,
mapped,
refreshedAt: new Date().toISOString(),
};
if (type === "CAR_BODY") {
next.vehicle.inquiry.responseMeta = {
isSuccess: response.isSuccess,
statusCode: response.statusCode,
message: response.message,
};
}
if (type === "CAR_BODY") {
next.vehicle.name = mapped.vehicleSystemTitle || next.vehicle.name;
next.vehicle.type = mapped.vehicleGroupTitle || next.vehicle.type;
next.insurance.policyNumber = mapped.printNumber || next.insurance.policyNumber;
next.insurance.company = mapped.companyName || next.insurance.company;
next.insurance.startDate = mapped.beginDate || next.insurance.startDate;
next.insurance.endDate = mapped.endDate || next.insurance.endDate;
next.insurance.carBodyInsurance = {
policyNumber: mapped.printNumber,
startDate: mapped.beginDate,
endDate: mapped.endDate,
insurerCompany: mapped.companyName,
coverages: Array.isArray(mapped.coverages) ? mapped.coverages : [],
};
return next;
}
next.vehicle.name = mapped.vehiclePersianName || mapped.MapTypNam || "اطلاعات این گزینه در استعلام موجود نیست";
next.vehicle.type = mapped.persianCarType || mapped.MapUsageName || next.vehicle.type;
next.insurance.policyNumber = 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 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.insuranceNumber,
FinancialCvrCptl: response.financeCoverage,
IssueDate: response.hIsuDte || response.persianStartDate,
SatrtDate: response.persianStartDate,
EndDate: response.persianEndDate,
MapTypNam: response.vehiclePersianName,
MapUsageName: response.MapUsageName,
};
}
function normalizeCarBodyRaw(response, originalPlate) {
const data = response.data || {};
return {
...data,
Plk1: toNumber(originalPlate.Plk1),
Plk2: toNumber(originalPlate.Plk2),
Plk3: toNumber(originalPlate.Plk3),
PlkSrl: toNumber(originalPlate.PlkSrl),
};
}
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),
CompanyName: data.companyName,
CompanyCode: data.companyId,
LastCompanyDocumentNumber: data.printNumber,
IssueDate: data.issueDate,
SatrtDate: data.beginDate,
EndDate: data.endDate,
MtrNum: data.motorNumber,
ShsNum: data.chassisNumber,
VinNumberField: data.vin,
MapTypNam: data.vehicleSystemTitle,
};
}
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));
}