forked from Yara724/api
666 lines
20 KiB
JavaScript
666 lines
20 KiB
JavaScript
#!/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));
|
||
}
|