fix(accounts): учитывать baseCurrency хостера при отображении и синке баланса
Docker / build (push) Has been cancelled

API не всегда возвращает валюту (VDSina UserAPI); раньше подставлялся RUB вместо USD из настроек провайдера.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denozordec
2026-06-29 02:18:40 +07:00
co-authored by Cursor
parent f2c4c7a412
commit 87ab13e9f6
13 changed files with 80 additions and 19 deletions
+2 -1
View File
@@ -6,6 +6,7 @@ type ProviderRow = typeof schema.providers.$inferSelect
export interface BillmanagerSyncAccount extends AccountRow {
apiType: 'billmanager'
apiBaseUrl: string
providerBaseCurrency?: string | null
}
export function resolveBillmanagerApi(
@@ -25,5 +26,5 @@ export function billmanagerAccountRowForSync(
const { apiType, apiBaseUrl } = resolveBillmanagerApi(accountRow, providerRow)
const cred = String(accountRow.apiCredentials || '').trim()
if (apiType !== 'billmanager' || !apiBaseUrl || !cred) return null
return { ...accountRow, apiType: 'billmanager', apiBaseUrl }
return { ...accountRow, apiType: 'billmanager', apiBaseUrl, providerBaseCurrency: providerRow?.baseCurrency ?? null }
}
+5 -2
View File
@@ -6,6 +6,7 @@ import { and, eq, like, or } from 'drizzle-orm'
import { getDb, schema } from '@cfdm/db'
import type { BillmanagerSyncAccount } from './context.js'
import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance'
import { mapPaymentToPayment, mapVdsToVps } from './mappers.js'
import {
fetchDashboardInfo,
@@ -72,11 +73,13 @@ export async function syncFromBillmanager(
const fetchVpsPayments = !skipVpsPayments
const fetchTariffs = !skipTariffs
const fallbackCurrency = syncFallbackCurrency(account)
const [vdsItems, paymentItems, dashboardInfo, tariffResult] = await Promise.all([
fetchVpsPayments ? fetchVds(apiBaseUrl, authinfo) : [],
fetchVpsPayments ? fetchPayments(apiBaseUrl, authinfo, {}) : [],
fetchVpsPayments
? fetchDashboardInfo(apiBaseUrl, authinfo, { fallbackCurrency: account.currency }).catch(() => null)
? fetchDashboardInfo(apiBaseUrl, authinfo, { fallbackCurrency }).catch(() => null)
: null,
fetchTariffs
? fetchVdsOrderPricelistAllDatacenters(apiBaseUrl, authinfo).catch((err) => {
@@ -253,7 +256,7 @@ export async function syncFromBillmanager(
db.update(schema.providerAccounts)
.set({
balanceApi: dashboardInfo.balance,
balanceCurrency: dashboardInfo.currency || 'RUB',
balanceCurrency: dashboardInfo.currency || fallbackCurrency,
balanceUpdatedAt: new Date().toISOString(),
enoughmoneyto: dashboardInfo.enoughmoneyto || '',
})
+2 -1
View File
@@ -9,6 +9,7 @@ export interface FourvpsSyncAccount extends AccountRow {
apiBaseUrl: string
panelId: number | null
apiKey: string
providerBaseCurrency?: string | null
}
export function resolveFourvpsApi(
@@ -29,5 +30,5 @@ export function fourvpsAccountRowForSync(
const cred = String(accountRow.apiCredentials || '').trim()
const { panelId, apiKey } = parseFourVpsCredentials(cred)
if (apiType !== '4vps' || !apiBaseUrl || !apiKey) return null
return { ...accountRow, apiType: '4vps', apiBaseUrl, panelId, apiKey }
return { ...accountRow, apiType: '4vps', apiBaseUrl, panelId, apiKey, providerBaseCurrency: providerRow?.baseCurrency ?? null }
}
+5 -2
View File
@@ -6,6 +6,7 @@ import { and, eq, like, or } from 'drizzle-orm'
import { getDb, schema } from '@cfdm/db'
import type { FourvpsSyncAccount } from './context.js'
import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance'
import { mapServerToVps } from './mappers.js'
import {
fetchDcList,
@@ -77,10 +78,12 @@ export async function syncFromFourvps(
const fetchVpsData = !skipVpsPayments
const fetchTariffs = !skipTariffs
const fallbackCurrency = syncFallbackCurrency(account)
const [servers, balanceInfo, tariffItems, dcMap] = await Promise.all([
fetchVpsData ? fetchMyServers(apiBaseUrl, credentials) : [],
fetchVpsData
? fetchUserBalance(apiBaseUrl, credentials, account.currency || 'RUB').catch(() => null)
? fetchUserBalance(apiBaseUrl, credentials, fallbackCurrency).catch(() => null)
: null,
fetchTariffs ? fetchTarifList(apiBaseUrl, credentials).catch(() => []) : [],
fetchVpsData || fetchTariffs ? fetchDcList(apiBaseUrl, credentials).catch(() => new Map()) : new Map(),
@@ -217,7 +220,7 @@ export async function syncFromFourvps(
db.update(schema.providerAccounts)
.set({
balanceApi: balanceInfo.balance,
balanceCurrency: balanceInfo.currency || 'RUB',
balanceCurrency: balanceInfo.currency || fallbackCurrency,
balanceUpdatedAt: new Date().toISOString(),
enoughmoneyto: '',
})
@@ -1,6 +1,7 @@
import { syncFromBillmanager } from '../billmanager/sync.js'
import { testConnection as bmTestConnection, fetchDashboardInfo } from '../billmanager/operations.js'
import type { BillmanagerSyncAccount } from '../billmanager/context.js'
import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance'
import type { ProviderAdapter, SyncResult } from './types.js'
@@ -28,12 +29,13 @@ export const billmanagerAdapter: ProviderAdapter = {
},
async fetchBalance(account: BillmanagerSyncAccount) {
const fallbackCurrency = syncFallbackCurrency(account)
const info = await fetchDashboardInfo(account.apiBaseUrl, String(account.apiCredentials).trim(), {
fallbackCurrency: account.currency,
fallbackCurrency,
})
return {
balance: info.balance,
currency: info.currency || 'RUB',
currency: info.currency || fallbackCurrency,
enoughmoneyto: info.enoughmoneyto || '',
}
},
@@ -4,6 +4,7 @@ import {
testConnection as fourvpsTestConnection,
} from '../fourvps/operations.js'
import type { FourvpsSyncAccount } from '../fourvps/context.js'
import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance'
import type { ProviderAdapter, SyncResult } from './types.js'
@@ -33,10 +34,11 @@ export const fourvpsAdapter: ProviderAdapter = {
async fetchBalance(account: FourvpsSyncAccount) {
const cred =
account.panelId != null ? `${account.panelId}:${account.apiKey}` : account.apiKey
const info = await fetchFourvpsBalance(account.apiBaseUrl, cred, account.currency || 'RUB')
const fallbackCurrency = syncFallbackCurrency(account)
const info = await fetchFourvpsBalance(account.apiBaseUrl, cred, fallbackCurrency)
return {
balance: info.balance,
currency: info.currency || 'RUB',
currency: info.currency || fallbackCurrency,
enoughmoneyto: '',
}
},
@@ -1,6 +1,7 @@
import { syncFromUserApi } from '../userapi/sync.js'
import { fetchBalance, testConnection } from '../userapi/operations.js'
import type { UserApiSyncAccount } from '../userapi/context.js'
import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance'
import type { ProviderAdapter, SyncResult } from './types.js'
@@ -28,14 +29,15 @@ export const userapiAdapter: ProviderAdapter = {
},
async fetchBalance(account: UserApiSyncAccount) {
const fallbackCurrency = syncFallbackCurrency(account)
const info = await fetchBalance(
account.apiBaseUrl,
account.apiToken,
account.currency || 'RUB',
fallbackCurrency,
)
return {
balance: info.balance,
currency: info.currency || 'RUB',
currency: info.currency || fallbackCurrency,
enoughmoneyto: info.enoughmoneyto || '',
}
},
+8 -1
View File
@@ -9,6 +9,7 @@ export interface UserApiSyncAccount extends AccountRow {
apiType: UserApiType
apiBaseUrl: string
apiToken: string
providerBaseCurrency?: string | null
}
export function resolveUserApi(
@@ -31,5 +32,11 @@ export function userApiAccountRowForSync(
const resolved = resolveUserApi(accountRow, providerRow)
const apiToken = parseUserApiToken(accountRow.apiCredentials)
if (!resolved || !resolved.apiBaseUrl || !apiToken) return null
return { ...accountRow, apiType: resolved.apiType, apiBaseUrl: resolved.apiBaseUrl, apiToken }
return {
...accountRow,
apiType: resolved.apiType,
apiBaseUrl: resolved.apiBaseUrl,
apiToken,
providerBaseCurrency: providerRow?.baseCurrency ?? null,
}
}
+6 -3
View File
@@ -6,6 +6,7 @@ import { and, eq, like, or } from 'drizzle-orm'
import { getDb, schema } from '@cfdm/db'
import type { UserApiSyncAccount } from './context.js'
import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance'
import { mapOperationToPayment, mapServerToVps } from './mappers.js'
import {
fetchBalance,
@@ -72,10 +73,12 @@ export async function syncFromUserApi(
const fetchVpsData = !skipVpsPayments
const fetchTariffs = !skipTariffs
const fallbackCurrency = syncFallbackCurrency(account)
const [servers, balanceInfo, tariffItems, operations] = await Promise.all([
fetchVpsData ? fetchServersWithDetails(apiBaseUrl, credentials) : [],
fetchVpsData
? fetchBalance(apiBaseUrl, credentials, account.currency || 'RUB').catch(() => null)
? fetchBalance(apiBaseUrl, credentials, fallbackCurrency).catch(() => null)
: null,
fetchTariffs ? fetchTariffList(apiBaseUrl, credentials).catch(() => []) : [],
fetchVpsData ? fetchOperations(apiBaseUrl, credentials).catch(() => []) : [],
@@ -223,7 +226,7 @@ export async function syncFromUserApi(
)
for (const item of operations) {
const payment = mapOperationToPayment(item, apiType, accountId, account.currency || 'RUB')
const payment = mapOperationToPayment(item, apiType, accountId, fallbackCurrency)
if (!payment) continue
const note = payment.note
if (existingPayments.has(note)) continue
@@ -250,7 +253,7 @@ export async function syncFromUserApi(
db.update(schema.providerAccounts)
.set({
balanceApi: balanceInfo.balance,
balanceCurrency: balanceInfo.currency || 'RUB',
balanceCurrency: balanceInfo.currency || fallbackCurrency,
balanceUpdatedAt: new Date().toISOString(),
enoughmoneyto: balanceInfo.enoughmoneyto || '',
})
+1 -1
View File
@@ -1 +1 @@
export { accountBalanceApi, accountBalanceCurrency } from '@cfdm/shared/utils/account-balance'
export { accountBalanceApi, accountBalanceCurrency, effectiveAccountBalanceCurrency } from '@cfdm/shared/utils/account-balance'
+2 -2
View File
@@ -33,7 +33,7 @@ import {
providerAccountFormDefaults,
} from '@/components/domain/account-edit-sheet'
import type { ProviderAccountFormValues } from '@/lib/schemas'
import { accountBalanceApi, accountBalanceCurrency } from '@/lib/account'
import { accountBalanceApi, effectiveAccountBalanceCurrency } from '@/lib/account'
import type { ProviderAccount } from '@/types/entities'
import { providerByIdMap, accountBillmanagerUiReady, billmanagerSyncableAccounts } from '@/lib/billmanager'
import { billingModeLabel, formatCurrency, formatRelativeTime } from '@/lib/format'
@@ -337,7 +337,7 @@ function AccountsPage() {
cell: (a) => {
const provider = providerById.get(a.providerId)
if (!accountBillmanagerUiReady(a, provider)) return <span className="text-muted-foreground"></span>
const cur = accountBalanceCurrency(a)
const cur = effectiveAccountBalanceCurrency(a, provider)
const ext = a as ProviderAccount & { enoughmoneyto?: string }
return dataGridCellStack(
formatCurrency(accountBalanceApi(a) ?? 0, cur),
@@ -1,3 +1,5 @@
import { resolveProviderCurrency } from './currency.js'
/** Баланс API аккаунта (camelCase из API и snake_case в типах). */
export function accountBalanceApi(account: {
balance_api?: number | null
@@ -16,3 +18,26 @@ export function accountBalanceCurrency(account: {
}): string {
return account.balance_currency ?? account.balanceCurrency ?? account.currency ?? 'RUB'
}
/** Валюта баланса с учётом baseCurrency хостера (как effectiveVpsTariffCurrency для тарифов). */
export function effectiveAccountBalanceCurrency(
account: {
balance_currency?: string
balanceCurrency?: string
currency?: string
},
provider?: { baseCurrency?: string | null } | null,
): string {
const provRaw = (provider?.baseCurrency ?? '').trim()
if (provRaw) return provRaw
return accountBalanceCurrency(account)
}
export function syncFallbackCurrency(
account: { currency?: string | null; providerBaseCurrency?: string | null },
): string {
return resolveProviderCurrency(
{ baseCurrency: account.providerBaseCurrency },
account.currency,
)
}
+12
View File
@@ -0,0 +1,12 @@
/** Валюта хостера: настройки провайдера → аккаунт → fallback. */
export function resolveProviderCurrency(
provider?: { baseCurrency?: string | null } | null,
accountCurrency?: string | null,
fallback = 'RUB',
): string {
const fromProvider = (provider?.baseCurrency ?? '').trim()
if (fromProvider) return fromProvider
const fromAccount = (accountCurrency ?? '').trim()
if (fromAccount) return fromAccount
return fallback
}