#!/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. All cases also * refresh available party personal inquiries into inquiries.person. * * 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", personUrl: process.env.TEJARAT_PERSON_URL || "http://82.99.202.245:3027/personal-inquiry/tejarat-no", thirdPartyToken: process.env.TEJARAT_THIRD_PARTY_TOKEN || process.env.TEJARAT_TOKEN || "", carBodyToken: process.env.TEJARAT_CAR_BODY_TOKEN || process.env.TEJARAT_TOKEN || "", personToken: process.env.TEJARAT_PERSON_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, inquiries: 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, personInquired: 0, personSkipped: 0, personFailed: 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) : []; const inquiries = doc.inquiries && typeof doc.inquiries === "object" ? clone(doc.inquiries) : {}; let docChanged = false; let inquiriesChanged = 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}`); } else { 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; } } const personInput = buildPersonInquiryInput(party); if (!personInput.ok) { summary.personSkipped += 1; console.warn(`[skip] ${partyLabel} person: ${personInput.reason}`); continue; } console.log(`[request] ${partyLabel} type=PERSON body=${JSON.stringify(personInput.body)}`); try { const personResponse = await inquiryWithRetry("PERSON", personInput.body); const successful = isInquirySuccessful("PERSON", personResponse); console.log( `[response] ${partyLabel} type=PERSON successful=${successful} body=${JSON.stringify(personResponse)}`, ); if (!successful) { summary.personFailed += 1; applyPersonInquiryToCaseInquiries(inquiries, party, index, false, {}, personResponse); inquiriesChanged = true; continue; } applyPersonInquiryToCaseInquiries( inquiries, party, index, true, normalizePersonResponse(personResponse), ); summary.personInquired += 1; inquiriesChanged = true; } catch (error) { summary.personFailed += 1; applyPersonInquiryToCaseInquiries(inquiries, party, index, false, {}, error); inquiriesChanged = true; console.error(`[error] ${partyLabel} type=PERSON: ${error.message}`); } } if (!docChanged && !inquiriesChanged) { console.log(`[doc] ${label} no changes`); continue; } if (config.dryRun) { console.log(`[dry-run] ${label} would update parties/inquiries`); continue; } const updateSet = { updatedAt: new Date(), }; if (docChanged) updateSet.parties = parties; if (inquiriesChanged) updateSet.inquiries = inquiries; const result = await collection.updateOne( { _id: doc._id }, { $set: updateSet, }, ); 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 buildPersonInquiryInput(party) { if (!party || typeof party !== "object") return { ok: false, reason: "party is empty" }; const person = party.person && typeof party.person === "object" ? party.person : null; if (!person) return { ok: false, reason: "party.person is missing" }; const nationalCode = cleanString(person.nationalCodeOfInsurer) || cleanString(person.nationalCodeOfDriver); const birthDate = firstPresent( person.insurerBirthday, person.driverBirthday, person.birthday, ); const gregorianBirthdate = jalaliToGregorianDate(birthDate); if (!nationalCode) { return { ok: false, reason: "nationalCodeOfInsurer/nationalCodeOfDriver missing" }; } if (!gregorianBirthdate) { return { ok: false, reason: `invalid insurerBirthday/driverBirthday=${cleanString(birthDate)}`, }; } return { ok: true, body: { nationalCode, birthdate: gregorianBirthdate, }, }; } 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 endpoint = getInquiryEndpoint(type); const url = endpoint.url; const token = endpoint.token; const accept = endpoint.accept; 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, accept); } 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, accept = "application/json") { const response = await fetch(url, { method: "POST", headers: { accept, 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) { const error = new Error(`HTTP ${response.status}: ${JSON.stringify(data)}`); error.status = response.status; error.data = data; throw error; } return data; } function isInquirySuccessful(type, response) { if (type === "CAR_BODY") return response && response.isSuccess === true && response.data; if (type === "PERSON") return response && response.status === 200 && response.data; return response && response.resultStatus === true; } function getInquiryEndpoint(type) { if (type === "CAR_BODY") { return { url: config.carBodyUrl, token: config.carBodyToken, accept: "application/json" }; } if (type === "PERSON") { return { url: config.personUrl, token: config.personToken, accept: "*/*" }; } return { url: config.thirdPartyUrl, token: config.thirdPartyToken, accept: "application/json" }; } 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 applyPersonInquiryToCaseInquiries(inquiries, party, index, has, data, error) { if (!inquiries.person || typeof inquiries.person !== "object") { inquiries.person = {}; } const existingData = inquiries.person.data && typeof inquiries.person.data === "object" && !Array.isArray(inquiries.person.data) ? inquiries.person.data : {}; const existingError = inquiries.person.error && typeof inquiries.person.error === "object" && !Array.isArray(inquiries.person.error) ? inquiries.person.error : {}; const roleKey = party && party.role ? party.role : `party_${index}`; const nextData = { ...existingData }; const nextError = { ...existingError }; if (has) { nextData[roleKey] = data || {}; delete nextError[roleKey]; } else { nextError[roleKey] = normalizeInquiryError(error); } inquiries.person = { has: Object.keys(nextData).length > 0, data: nextData, ...(Object.keys(nextError).length > 0 ? { error: nextError } : {}), updatedAt: new Date(), }; } function normalizePersonResponse(response) { return response && response.data ? response.data : response; } function normalizeInquiryError(error) { if (!error) return undefined; return { message: error.message || String(error), status: error.status, data: error.data, }; } 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, }; } function jalaliToGregorianDate(input) { if (input === null || input === undefined) return null; const raw = normalizeDigits(typeof input === "number" ? String(input) : String(input).trim()); if (!raw) return null; let year = 0; let month = 0; let day = 0; const separated = raw.match(/^(\d{4})[\-/](\d{1,2})[\-/](\d{1,2})$/); if (separated) { year = parseInt(separated[1], 10); month = parseInt(separated[2], 10); day = parseInt(separated[3], 10); } else { const digits = raw.replace(/\D/g, ""); if (digits.length !== 8) return null; year = parseInt(digits.slice(0, 4), 10); month = parseInt(digits.slice(4, 6), 10); day = parseInt(digits.slice(6, 8), 10); } if (!year || !month || !day) return null; if (year >= 1900) { const mm = String(month).padStart(2, "0"); const dd = String(day).padStart(2, "0"); const result = `${year}-${mm}-${dd}`; return isNaN(new Date(result).getTime()) ? null : result; } return jalaliPartsToGregorian(year, month, day); } function jalaliPartsToGregorian(jYear, jMonth, jDay) { const jy = jYear - 979; const jm = jMonth - 1; const jd = jDay - 1; let jDayNo = 365 * jy + Math.floor(jy / 33) * 8 + Math.floor(((jy % 33) + 3) / 4); const jalaliMonthDays = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29]; for (let i = 0; i < jm; i += 1) { jDayNo += jalaliMonthDays[i]; } jDayNo += jd; let gDayNo = jDayNo + 79; let gy = 1600 + 400 * Math.floor(gDayNo / 146097); gDayNo %= 146097; let leap = true; if (gDayNo >= 36525) { gDayNo -= 1; gy += 100 * Math.floor(gDayNo / 36524); gDayNo %= 36524; if (gDayNo >= 365) gDayNo += 1; else leap = false; } gy += 4 * Math.floor(gDayNo / 1461); gDayNo %= 1461; if (gDayNo >= 366) { leap = false; gDayNo -= 1; gy += Math.floor(gDayNo / 365); gDayNo %= 365; } const gregorianMonthDays = [ 31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, ]; let gm = 0; for (let i = 0; i < 12; i += 1) { if (gDayNo < gregorianMonthDays[i]) { gm = i + 1; break; } gDayNo -= gregorianMonthDays[i]; } const gd = gDayNo + 1; const mm = String(gm).padStart(2, "0"); const dd = String(gd).padStart(2, "0"); const result = `${gy}-${mm}-${dd}`; return isNaN(new Date(result).getTime()) ? null : result; } 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"); } if (!config.personToken) { throw new Error("TEJARAT_PERSON_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)); }