181 lines
4.7 KiB
JavaScript
181 lines
4.7 KiB
JavaScript
/**
|
||
* Live EUR→toman pricing (TGJU free-market EUR).
|
||
* Markups: <3€ +100%, 3–5€ +80%, >5€ +65%.
|
||
*/
|
||
|
||
const FALLBACK_EUR_TOMAN = 302700;
|
||
const EUR_DETECT_MAX = 500;
|
||
const CACHE_KEY = "kikomiko-eur-toman-v1";
|
||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||
|
||
let cachedRate = 0;
|
||
let inflight = null;
|
||
|
||
export function markupForEuro(eur) {
|
||
if (!Number.isFinite(eur) || eur <= 0) return 1;
|
||
if (eur < 3) return 2;
|
||
if (eur <= 5) return 1.8;
|
||
return 1.65;
|
||
}
|
||
|
||
function parseTgjuRial(raw) {
|
||
const n = Number(String(raw ?? "").replace(/,/g, "").trim());
|
||
return Number.isFinite(n) && n > 0 ? n : 0;
|
||
}
|
||
|
||
function rialToToman(rial) {
|
||
return rial > 0 ? rial / 10 : 0;
|
||
}
|
||
|
||
function readBodyFallback() {
|
||
const raw = Number(document.body?.dataset?.eurRate || "");
|
||
return Number.isFinite(raw) && raw > 0 ? raw : FALLBACK_EUR_TOMAN;
|
||
}
|
||
|
||
function readSessionCache() {
|
||
try {
|
||
const raw = sessionStorage.getItem(CACHE_KEY);
|
||
if (!raw) return 0;
|
||
const parsed = JSON.parse(raw);
|
||
if (
|
||
parsed &&
|
||
Number(parsed.rate) > 0 &&
|
||
Date.now() - Number(parsed.at || 0) < CACHE_TTL_MS
|
||
) {
|
||
return Number(parsed.rate);
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
function writeSessionCache(rate) {
|
||
try {
|
||
sessionStorage.setItem(
|
||
CACHE_KEY,
|
||
JSON.stringify({ rate, at: Date.now() })
|
||
);
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
function applyRateToDom(rate) {
|
||
if (!(rate > 0) || !document.body) return;
|
||
document.body.dataset.eurRate = String(Math.round(rate));
|
||
document.querySelectorAll("[data-eur-rate-display]").forEach((el) => {
|
||
el.textContent = new Intl.NumberFormat("fa-IR", {
|
||
maximumFractionDigits: 0,
|
||
}).format(Math.round(rate));
|
||
});
|
||
}
|
||
|
||
/** Prefer same-origin proxy (avoids CORS); fall back to TGJU ajax JSON. */
|
||
async function fetchTgjuEurRial() {
|
||
const endpoints = ["/api/fx/eur", "https://call1.tgju.org/ajax.json"];
|
||
let lastErr;
|
||
for (const url of endpoints) {
|
||
try {
|
||
const res = await fetch(url, {
|
||
credentials: "omit",
|
||
headers: { Accept: "application/json" },
|
||
});
|
||
if (!res.ok) throw new Error(`FX HTTP ${res.status}`);
|
||
const data = await res.json();
|
||
const row = data?.current?.price_eur || data?.price_eur;
|
||
const rial = parseTgjuRial(row?.p ?? row);
|
||
if (rial > 0) return rial;
|
||
throw new Error("TGJU price_eur missing");
|
||
} catch (err) {
|
||
lastErr = err;
|
||
}
|
||
}
|
||
throw lastErr || new Error("FX fetch failed");
|
||
}
|
||
|
||
export function getEurToTomanRate() {
|
||
if (cachedRate > 0) return cachedRate;
|
||
const fromSession = readSessionCache();
|
||
if (fromSession > 0) {
|
||
cachedRate = fromSession;
|
||
return cachedRate;
|
||
}
|
||
return readBodyFallback();
|
||
}
|
||
|
||
export async function ensureEurRate() {
|
||
if (cachedRate > 0) return cachedRate;
|
||
const fromSession = readSessionCache();
|
||
if (fromSession > 0) {
|
||
cachedRate = fromSession;
|
||
applyRateToDom(cachedRate);
|
||
return cachedRate;
|
||
}
|
||
if (!inflight) {
|
||
inflight = fetchTgjuEurRial()
|
||
.then((rial) => {
|
||
const toman = rialToToman(rial);
|
||
cachedRate = toman > 0 ? toman : readBodyFallback();
|
||
writeSessionCache(cachedRate);
|
||
applyRateToDom(cachedRate);
|
||
return cachedRate;
|
||
})
|
||
.catch(() => {
|
||
cachedRate = readBodyFallback();
|
||
applyRateToDom(cachedRate);
|
||
return cachedRate;
|
||
})
|
||
.finally(() => {
|
||
inflight = null;
|
||
});
|
||
}
|
||
return inflight;
|
||
}
|
||
|
||
export function euroBase(item) {
|
||
if (!item) return null;
|
||
for (const candidate of [item.priceEur, item.sourcePrice]) {
|
||
const n = Number(candidate);
|
||
if (Number.isFinite(n) && n > 0 && n < EUR_DETECT_MAX) return n;
|
||
}
|
||
const cur = String(item.sourceCurrency || "").toUpperCase();
|
||
const price = Number(item.price);
|
||
if (
|
||
(cur === "EUR" || cur === "EU" || cur === "€") &&
|
||
Number.isFinite(price) &&
|
||
price > 0 &&
|
||
price < EUR_DETECT_MAX
|
||
) {
|
||
return price;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function eurToTomanSell(eur, rate = getEurToTomanRate()) {
|
||
const r = rate > 0 ? rate : FALLBACK_EUR_TOMAN;
|
||
return Math.round(Number(eur) * r * markupForEuro(Number(eur)));
|
||
}
|
||
|
||
export function sellPriceFromItem(item, rate = getEurToTomanRate()) {
|
||
const eur = euroBase(item);
|
||
if (eur != null) return eurToTomanSell(eur, rate);
|
||
const n = Number(item?.price);
|
||
return Number.isFinite(n) && n > 0 ? Math.round(n) : 0;
|
||
}
|
||
|
||
export function sellCompareFromItem(item, rate = getEurToTomanRate()) {
|
||
const eur = euroBase(item);
|
||
const compare = Number(item?.compareAtPrice);
|
||
if (
|
||
eur != null &&
|
||
Number.isFinite(compare) &&
|
||
compare > 0 &&
|
||
compare < EUR_DETECT_MAX
|
||
) {
|
||
return eurToTomanSell(compare, rate);
|
||
}
|
||
if (Number.isFinite(compare) && compare > 0) return Math.round(compare);
|
||
return null;
|
||
}
|