From c161eaf84ac936237bf1497119e5ccc73256e268 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 23 Mar 2026 21:25:01 +0700 Subject: [PATCH] =?UTF-8?q?fix(currency):=20=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=BA=D1=83=D1=80=D1=81=D1=8B?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B7=20=D1=8F=D0=B2=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BA=D1=83=D1=80=D1=81=D0=B0=20=D1=85=D0=BE=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=20=D0=B8=20=D0=BD=D0=BE=D1=80=D0=BC=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20Valute=20=D0=A6=D0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - parseProviderFxRate: пусто/auto/невалид → конвертация по ссылке из настроек - normalizeRatesPayload + App: JSON ЦБ с Valute → base+rates как у latest.js - Подсказка в форме хостера про курсы USD/EUR Made-with: Cursor --- src/App.jsx | 3 +- src/lib/utils.js | 63 ++++++++++++++++++++++++++++++++++--- src/pages/ProvidersPage.jsx | 6 ++++ 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index b8a5005..7113135 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,6 +18,7 @@ import { loadDataSet, updateRecord, } from './lib/api' +import { normalizeRatesPayload } from './lib/utils' function App() { const [isReady, setIsReady] = useState(false) @@ -63,7 +64,7 @@ function App() { return response.json() }) .then((payload) => { - setRatesData(payload) + setRatesData(normalizeRatesPayload(payload) ?? payload) setRatesError('') }) .catch((error) => { diff --git a/src/lib/utils.js b/src/lib/utils.js index ff7a0b4..5077cfe 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -208,6 +208,59 @@ export function formatCurrency(amount, currency = 'USD') { }).format(safeAmount) } +/** + * Числовой курс хостера из справочника: только явное положительное число. + * Пусто, «auto», невалид — нет своего курса, используется ссылка на курсы из настроек. + * @param {unknown} raw + * @returns {number} + */ +export function parseProviderFxRate(raw) { + if (raw == null) return NaN + const s = String(raw).trim() + if (!s) return NaN + if (s.toLowerCase() === 'auto') return NaN + const n = Number(s.replace(',', '.')) + if (!Number.isFinite(n) || n <= 0) return NaN + return n +} + +/** + * Приводит ответ API курсов к виду { base, rates }. + * Поддержка JSON ЦБ с полем Valute (daily_json.js) и уже нормализованного latest.js. + * @param {object|null|undefined} payload + * @returns {{ base: string, rates: object, date?: string }|null} + */ +export function normalizeRatesPayload(payload) { + if (!payload || typeof payload !== 'object') return null + if (payload.base && payload.rates && typeof payload.rates === 'object') { + return payload + } + if (payload.Valute && typeof payload.Valute === 'object') { + const rates = {} + for (const v of Object.values(payload.Valute)) { + const code = v?.CharCode + const val = Number(v?.Value) + const nom = Number(v?.Nominal) || 1 + if ( + typeof code === 'string' && + code.length === 3 && + Number.isFinite(val) && + val > 0 && + nom > 0 + ) { + rates[code] = nom / val + } + } + if (Object.keys(rates).length === 0) return null + return { + base: 'RUB', + rates, + date: typeof payload.Date === 'string' ? payload.Date : '', + } + } + return null +} + /** * Конвертирует сумму из одной валюты в другую по ratesData (CBR и т.п.) * @param {number} amount @@ -267,7 +320,7 @@ export function formatInBaseCurrency(amount, currency, appSettings, ratesData) { * Конвертирует сумму в валюту отображения (из настроек). * provider.baseCurrency — валюта, в которой хостер принимает платежи. * settings.baseCurrency — валюта отображения на дашбордах. - * Курсы хостера (usdRate, eurRate) — курс 1 USD/EUR в валюту отображения. + * Курсы хостера (usdRate, eurRate) — только если заданы явные числа; иначе — курсы по ссылке из настроек. */ export function convertWithProviderRate(amount, currency, provider, appSettings, ratesData) { const safeAmount = Number(amount) @@ -282,12 +335,12 @@ export function convertWithProviderRate(amount, currency, provider, appSettings, return { value: safeAmount, currency: appBase, source: 'native' } } - const usdRate = Number(provider?.usdRate) - const eurRate = Number(provider?.eurRate) - if (fromCurrency === 'USD' && Number.isFinite(usdRate) && usdRate > 0) { + const usdRate = parseProviderFxRate(provider?.usdRate) + const eurRate = parseProviderFxRate(provider?.eurRate) + if (fromCurrency === 'USD' && Number.isFinite(usdRate)) { return { value: safeAmount * usdRate, currency: appBase, source: 'provider' } } - if (fromCurrency === 'EUR' && Number.isFinite(eurRate) && eurRate > 0) { + if (fromCurrency === 'EUR' && Number.isFinite(eurRate)) { return { value: safeAmount * eurRate, currency: appBase, source: 'provider' } } diff --git a/src/pages/ProvidersPage.jsx b/src/pages/ProvidersPage.jsx index cd63dd1..1c6aed9 100644 --- a/src/pages/ProvidersPage.jsx +++ b/src/pages/ProvidersPage.jsx @@ -222,6 +222,12 @@ export function ProvidersPage({ db, actions }) { onChange={(e) => setForm((prev) => ({ ...prev, eurRate: e.target.value }))} /> +
+
+ Курс USD/EUR: рублей (или единиц валюты отображения в настройках) за 1 USD/EUR. Пусто или «auto» — + конвертация по ссылке на курсы валют из настроек. +
+