fix(currency): глобальные курсы без явного курса хостера и нормализация Valute ЦБ
- parseProviderFxRate: пусто/auto/невалид → конвертация по ссылке из настроек - normalizeRatesPayload + App: JSON ЦБ с Valute → base+rates как у latest.js - Подсказка в форме хостера про курсы USD/EUR Made-with: Cursor
This commit is contained in:
+2
-1
@@ -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) => {
|
||||
|
||||
+58
-5
@@ -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' }
|
||||
}
|
||||
|
||||
|
||||
@@ -222,6 +222,12 @@ export function ProvidersPage({ db, actions }) {
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, eurRate: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<div className="text-secondary small">
|
||||
Курс USD/EUR: рублей (или единиц валюты отображения в настройках) за 1 USD/EUR. Пусто или «auto» —
|
||||
конвертация по ссылке на курсы валют из настроек.
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label">Заметки</label>
|
||||
<textarea {...noBrowserSuggestProps}
|
||||
|
||||
Reference in New Issue
Block a user