fix(userapi): подтягивать стоимость VPS Macloud/VDSina из тарифных планов
Docker / build (push) Has been cancelled

При синке UserAPI цена и период берутся по server-plan.id из индекса тарифов, а не остаются нулевыми.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-29 02:23:34 +07:00
co-authored by Cursor
parent 87ab13e9f6
commit 6c5eb04fae
5 changed files with 186 additions and 12 deletions
+69 -1
View File
@@ -25,7 +25,18 @@ const server: UserApiServerDetail = {
describe('mapServerToVps', () => {
it('maps macloud server with apiType prefix in notes', () => {
const vps = mapServerToVps(server, 'macloud', 'prov-1', 'acc-1')
const planIndex = new Map([
[
'1',
{
id: 1,
name: '2 RAM / 1 CPU / 40 NVMe',
cost: 1.55,
period: 'day',
},
],
])
const vps = mapServerToVps(server, 'macloud', 'prov-1', 'acc-1', planIndex)
expect(vps.externalId).toBe('12345')
expect(vps.ip).toBe('91.84.101.78')
expect(vps.os).toBe('Ubuntu 24.04')
@@ -37,6 +48,63 @@ describe('mapServerToVps', () => {
expect(vps.paidUntil).toBe('2029-02-20')
expect(vps.notes).toContain('macloud-12345')
expect(vps.tariffType).toBe('daily')
expect(vps.dailyRate).toBe(1.55)
expect(vps.monthlyRate).toBeNull()
})
it('maps monthly plan cost from plan index', () => {
const planIndex = new Map([
[
'1',
{
id: 1,
name: '2 RAM / 1 CPU / 40 NVMe',
cost: 500,
period: 'month',
},
],
])
const vps = mapServerToVps(server, 'vdsina', 'prov-1', 'acc-1', planIndex)
expect(vps.tariffType).toBe('monthly')
expect(vps.monthlyRate).toBe(500)
expect(vps.dailyRate).toBeNull()
})
it('adds constructor plan extra cost from server totals', () => {
const planIndex = new Map([
[
'1',
{
id: 1,
name: 'Constructor',
cost: 10,
period: 'day',
has_params: true,
params: {
cpu: { cost: 1 },
ram: { cost: 0.5 },
disk: { cost: 0.1 },
},
data: { cpu: { value: 1 }, ram: { value: 1 }, disk: { value: 1 } },
},
],
])
const vps = mapServerToVps(
{
...server,
data: {
cpu: { value: 1, total: 4 },
ram: { value: 1, total: 8 },
disk: { value: 1, total: 10 },
},
},
'vdsina',
'prov-1',
'acc-1',
planIndex,
)
// 10 + 3*1 + 7*0.5 + 9*0.1 = 10 + 3 + 3.5 + 0.9 = 17.4
expect(vps.dailyRate).toBe(17.4)
})
it('maps vdsina server with vdsina prefix in notes', () => {
+53 -4
View File
@@ -4,7 +4,7 @@
import type { UserApiType } from '@cfdm/shared/contracts/provider'
import type { UserApiOperation, UserApiServerDetail } from './operations.js'
import type { UserApiOperation, UserApiServerDetail, UserApiServerPlan, UserApiPlanCostIndex } from './operations.js'
const STATUS_MAP: Record<string, string> = {
active: 'active',
@@ -34,6 +34,53 @@ function extractIp(server: UserApiServerDetail): { ip: string; ipv6: string } {
return { ip: String(ipObj.ip).trim(), ipv6: '' }
}
function calculateConstructorExtraCost(
server: UserApiServerDetail,
plan: UserApiServerPlan,
): number {
if (!plan.has_params || !plan.params) return 0
const serverData = server.data ?? {}
const planData = plan.data ?? {}
let extra = 0
for (const key of ['cpu', 'ram', 'disk'] as const) {
const param = plan.params[key]
if (!param?.cost) continue
const serverRes = serverData[key]
const planBase = planData[key]?.value ?? 0
const serverTotal = serverRes?.total ?? serverRes?.value ?? planBase
const units = Math.max(0, serverTotal - planBase)
if (units > 0) extra += units * param.cost
}
return extra
}
function resolvePlanRates(
server: UserApiServerDetail,
planIndex?: UserApiPlanCostIndex,
): { tariffType: string; dailyRate: number | null; monthlyRate: number | null } {
const planId = server['server-plan']?.id
if (planId == null || !planIndex) {
return { tariffType: 'daily', dailyRate: null, monthlyRate: null }
}
const plan = planIndex.get(String(planId))
if (!plan || !Number.isFinite(plan.cost)) {
return { tariffType: 'daily', dailyRate: null, monthlyRate: null }
}
const cost = plan.cost + calculateConstructorExtraCost(server, plan)
const period = (plan.period || 'day').toLowerCase()
if (period === 'month') {
return { tariffType: 'monthly', dailyRate: null, monthlyRate: cost }
}
return { tariffType: 'daily', dailyRate: cost, monthlyRate: null }
}
export interface MappedVps {
externalId: string
ip: string
@@ -85,6 +132,7 @@ export function mapServerToVps(
apiType: UserApiType,
providerId: string,
providerAccountId: string,
planIndex?: UserApiPlanCostIndex,
): MappedVps {
const { ip, ipv6 } = extractIp(server)
const data = server.data ?? {}
@@ -101,6 +149,7 @@ export function mapServerToVps(
: traffGb > 0
? Math.round((traffGb / 1024) * 100) / 100
: 0
const { tariffType, dailyRate, monthlyRate } = resolvePlanRates(server, planIndex)
return {
externalId: String(server.id),
@@ -128,10 +177,10 @@ export function mapServerToVps(
monitoringEnabled: false,
backupEnabled: false,
status,
tariffType: 'daily',
tariffType,
currency: 'RUB',
dailyRate: null,
monthlyRate: null,
dailyRate,
monthlyRate,
createdAt: dateToIso(server.created),
paidUntil: dateToIso(server.end),
notes: name ? `${name} [${apiType}-${server.id}]` : `${apiType}-${server.id}`,
+36 -3
View File
@@ -16,13 +16,26 @@ export interface UserApiDatacenter {
}
export interface UserApiTariffSpec {
cpu?: { value?: number; for?: string }
ram?: { value?: number; for?: string }
disk?: { value?: number; for?: string }
cpu?: { value?: number; total?: number; for?: string }
ram?: { value?: number; total?: number; for?: string }
disk?: { value?: number; total?: number; for?: string }
gpu?: { value?: number; for?: string } | null
traff?: { value?: number; for?: string }
}
export interface UserApiPlanParamCost {
cost?: number
min?: number
max?: number
}
export interface UserApiPlanParams {
cpu?: UserApiPlanParamCost
ram?: UserApiPlanParamCost
disk?: UserApiPlanParamCost
ip4?: UserApiPlanParamCost
}
export interface UserApiServerListItem {
id: number
name: string
@@ -62,9 +75,12 @@ export interface UserApiServerPlan {
active?: boolean
enable?: boolean
has_params?: boolean
params?: UserApiPlanParams | null
data?: UserApiTariffSpec | null
}
export type UserApiPlanCostIndex = Map<string, UserApiServerPlan>
export interface UserApiOperation {
id: number
purse?: 'real' | 'bonus' | 'partner'
@@ -265,6 +281,23 @@ export async function fetchServerPlans(
return Array.isArray(data) ? data : []
}
export async function fetchPlanCostIndex(
baseUrl: string,
credentials: string,
): Promise<UserApiPlanCostIndex> {
const groups = await fetchServerGroups(baseUrl, credentials)
const index: UserApiPlanCostIndex = new Map()
for (const group of groups) {
const plans = await fetchServerPlans(baseUrl, credentials, group.id)
for (const plan of plans) {
if (plan?.id != null) index.set(String(plan.id), plan)
}
}
return index
}
export async function fetchTariffList(
baseUrl: string,
credentials: string,
+21 -2
View File
@@ -10,12 +10,14 @@ vi.mock('./operations.js', () => ({
fetchServersWithDetails: vi.fn(),
fetchBalance: vi.fn(),
fetchTariffList: vi.fn(),
fetchPlanCostIndex: vi.fn(),
fetchOperations: vi.fn(),
}))
import {
fetchBalance,
fetchOperations,
fetchPlanCostIndex,
fetchServersWithDetails,
fetchTariffList,
} from './operations.js'
@@ -56,7 +58,7 @@ describe('syncFromUserApi', () => {
ip: { ip: '1.2.3.4', type: '4' },
template: { name: 'Debian 12' },
datacenter: { id: 1, name: 'DC1', country: 'ru' },
'server-plan': { name: '2 RAM / 1 CPU / 40 NVMe' },
'server-plan': { id: 13, name: '2 RAM / 1 CPU / 40 NVMe' },
data: { cpu: { value: 1 }, ram: { value: 2 }, disk: { value: 40 } },
},
])
@@ -85,6 +87,19 @@ describe('syncFromUserApi', () => {
price: '1.55 ₽/день',
},
])
vi.mocked(fetchPlanCostIndex).mockResolvedValue(
new Map([
[
'13',
{
id: 13,
name: '2 RAM / 1 CPU / 40 NVMe',
cost: 1.55,
period: 'day',
},
],
]),
)
vi.mocked(fetchOperations).mockResolvedValue([
{
id: 999,
@@ -122,11 +137,15 @@ describe('syncFromUserApi', () => {
expect(result.tariffsCount).toBe(1)
expect(result.balance?.balance).toBe(500)
const vps = getSqlite().prepare('SELECT id, notes FROM vps WHERE id = ?').get('vps-macloud-acc-macloud-100') as {
const vps = getSqlite().prepare('SELECT id, notes, dailyRate, tariffType FROM vps WHERE id = ?').get('vps-macloud-acc-macloud-100') as {
id: string
notes: string
dailyRate: number
tariffType: string
}
expect(vps.notes).toContain('macloud-100')
expect(vps.dailyRate).toBe(1.55)
expect(vps.tariffType).toBe('daily')
})
it('syncs vdsina account with vdsina id prefix', async () => {
+7 -2
View File
@@ -11,9 +11,11 @@ import { mapOperationToPayment, mapServerToVps } from './mappers.js'
import {
fetchBalance,
fetchOperations,
fetchPlanCostIndex,
fetchServersWithDetails,
fetchTariffList,
type UserApiBalanceResult,
type UserApiPlanCostIndex,
} from './operations.js'
export interface SyncFromUserApiOptions {
@@ -75,13 +77,16 @@ export async function syncFromUserApi(
const fallbackCurrency = syncFallbackCurrency(account)
const [servers, balanceInfo, tariffItems, operations] = await Promise.all([
const [servers, balanceInfo, tariffItems, operations, planIndex] = await Promise.all([
fetchVpsData ? fetchServersWithDetails(apiBaseUrl, credentials) : [],
fetchVpsData
? fetchBalance(apiBaseUrl, credentials, fallbackCurrency).catch(() => null)
: null,
fetchTariffs ? fetchTariffList(apiBaseUrl, credentials).catch(() => []) : [],
fetchVpsData ? fetchOperations(apiBaseUrl, credentials).catch(() => []) : [],
fetchVpsData
? fetchPlanCostIndex(apiBaseUrl, credentials).catch(() => new Map() as UserApiPlanCostIndex)
: (new Map() as UserApiPlanCostIndex),
])
let vpsCount = 0
@@ -89,7 +94,7 @@ export async function syncFromUserApi(
if (fetchVpsData) {
for (const server of servers) {
const vps = mapServerToVps(server, apiType, providerId, accountId)
const vps = mapServerToVps(server, apiType, providerId, accountId, planIndex)
const id = `vps-${idPrefix}-${accountId}-${vps.externalId}`
const additionalIps = JSON.stringify(vps.additionalIps || [])
const dailyRate = vps.dailyRate