fix(userapi): период тарифа Macloud и отображение ставки на списке VPS
Docker / build (push) Failing after 1m45s

Месячные планы Macloud определяются через inferPlanPeriod; в UI показывается суточная или месячная ставка по tariffType, а не месячный эквивалент.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denozordec
2026-06-29 11:21:44 +07:00
co-authored by Cursor
parent 579c00392d
commit 87aa6949b3
8 changed files with 149 additions and 30 deletions
@@ -106,6 +106,37 @@ describe('mapServerToVps', () => {
expect(vps.monthlyRate).toBe(46.5)
})
it('maps macloud monthly plan when period fields are missing', () => {
const planIndex = new Map([
[
'3:7',
{
id: 7,
name: '1 RAM / 1 CPU / 10 NVMe',
cost: 150,
period: '',
'server-group': 3,
},
],
])
const vps = mapServerToVps(
{
...server,
'server-plan': { id: 7, name: '1 RAM / 1 CPU / 10 NVMe' },
'server-group': { id: 3, name: 'VDS' },
data: { cpu: { value: 1 }, ram: { value: 1 }, disk: { value: 10 } },
},
'macloud',
'prov-1',
'acc-1',
planIndex,
'RUB',
)
expect(vps.tariffType).toBe('monthly')
expect(vps.monthlyRate).toBe(150)
expect(vps.dailyRate).toBeNull()
})
it('parses string plan cost from index', () => {
const planIndex = new Map([
[
+15 -5
View File
@@ -12,7 +12,7 @@ import type {
UserApiServerPlan,
UserApiTariffItem,
} from './operations.js'
import { normalizePlanPeriod, effectivePlanCost, findPlanInIndex, parsePlanCost } from './operations.js'
import { inferPlanPeriod, effectivePlanCost, findPlanInIndex, parsePlanCost } from './operations.js'
const STATUS_MAP: Record<string, string> = {
active: 'active',
@@ -101,12 +101,15 @@ function resolveRatesFromTariffItem(
function resolvePlanRates(
server: UserApiServerDetail,
apiType: UserApiType,
billingMode: string | null | undefined,
planIndex?: UserApiPlanCostIndex,
tariffByPlanId?: Map<string, UserApiTariffItem>,
): { tariffType: string; dailyRate: number | null; monthlyRate: number | null } {
const planId = server['server-plan']?.id
const defaultType = inferPlanPeriod(undefined, apiType, billingMode) === 'month' ? 'monthly' : 'daily'
if (planId == null) {
return { tariffType: 'daily', dailyRate: null, monthlyRate: null }
return { tariffType: defaultType, dailyRate: null, monthlyRate: null }
}
const planKey = String(planId)
@@ -116,7 +119,7 @@ function resolvePlanRates(
const baseCost = plan ? effectivePlanCost(plan) : null
if (plan && baseCost != null) {
const cost = baseCost + calculateConstructorExtraCost(server, plan)
return ratesFromCost(cost, normalizePlanPeriod(plan.period))
return ratesFromCost(cost, inferPlanPeriod(plan, apiType, billingMode))
}
}
@@ -126,7 +129,7 @@ function resolvePlanRates(
if (fromTariff) return fromTariff
}
return { tariffType: 'daily', dailyRate: null, monthlyRate: null }
return { tariffType: defaultType, dailyRate: null, monthlyRate: null }
}
export interface MappedVps {
@@ -183,6 +186,7 @@ export function mapServerToVps(
planIndex?: UserApiPlanCostIndex,
currency = 'RUB',
tariffByPlanId?: Map<string, UserApiTariffItem>,
billingMode?: string | null,
): MappedVps {
const { ip, ipv6 } = extractIp(server)
const data = server.data ?? {}
@@ -199,7 +203,13 @@ export function mapServerToVps(
: traffGb > 0
? Math.round((traffGb / 1024) * 100) / 100
: 0
const { tariffType, dailyRate, monthlyRate } = resolvePlanRates(server, planIndex, tariffByPlanId)
const { tariffType, dailyRate, monthlyRate } = resolvePlanRates(
server,
apiType,
billingMode,
planIndex,
tariffByPlanId,
)
const resolvedCurrency = (currency || 'RUB').trim().toUpperCase() || 'RUB'
return {
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { inferPlanPeriod, normalizePlanPeriod } from './operations.js'
describe('normalizePlanPeriod', () => {
it('detects day and month from period and period_name', () => {
expect(normalizePlanPeriod('day')).toBe('day')
expect(normalizePlanPeriod('month')).toBe('month')
expect(normalizePlanPeriod(undefined, 'день')).toBe('day')
expect(normalizePlanPeriod(undefined, 'месяц')).toBe('month')
})
it('returns null when period is unknown', () => {
expect(normalizePlanPeriod()).toBeNull()
expect(normalizePlanPeriod('', '')).toBeNull()
})
})
describe('inferPlanPeriod', () => {
it('defaults macloud to month when plan has no period', () => {
expect(inferPlanPeriod({ id: 1, name: 'x', cost: 150 }, 'macloud')).toBe('month')
})
it('defaults vdsina to day when plan has no period', () => {
expect(inferPlanPeriod({ id: 1, name: 'x', cost: 2.1 }, 'vdsina')).toBe('day')
})
it('respects explicit plan period over apiType default', () => {
expect(
inferPlanPeriod({ id: 1, name: 'x', cost: 2.1, period: 'day' }, 'macloud'),
).toBe('day')
})
it('uses account billingMode when plan period is missing', () => {
expect(inferPlanPeriod({ id: 1, name: 'x', cost: 90 }, 'vdsina', 'monthly')).toBe('month')
})
})
+28 -7
View File
@@ -72,6 +72,7 @@ export interface UserApiServerPlan {
cost: number | string
full_cost?: number | string
period?: string
period_name?: string
description?: string
active?: boolean
enable?: boolean
@@ -199,20 +200,39 @@ export function findPlanInIndex(
return undefined
}
export function normalizePlanPeriod(period?: string): 'day' | 'month' {
const p = (period || 'day').toLowerCase()
if (p === 'month' || p === 'monthly') return 'month'
/** Распознаёт период из полей UserAPI (period, period_name). */
export function normalizePlanPeriod(period?: string, periodName?: string): 'day' | 'month' | null {
const sources = [period, periodName].filter((s) => Boolean(String(s ?? '').trim()))
for (const raw of sources) {
const p = String(raw).toLowerCase().trim()
if (['month', 'monthly', 'mo', 'месяц', 'мес'].some((k) => p === k || p.includes(k))) return 'month'
if (['day', 'daily', 'день', 'дн', 'сут', 'сутки'].some((k) => p === k || p.includes(k))) return 'day'
}
return null
}
/** Период тарифа: поля плана → billingMode аккаунта → дефолт по apiType (macloud — месяц). */
export function inferPlanPeriod(
plan: UserApiServerPlan | undefined,
apiType?: string,
billingMode?: string | null,
): 'day' | 'month' {
const fromPlan = plan ? normalizePlanPeriod(plan.period, plan.period_name) : null
if (fromPlan) return fromPlan
if (billingMode === 'monthly') return 'month'
if (billingMode === 'daily') return 'day'
if (apiType === 'macloud') return 'month'
return 'day'
}
/** Формат цены для active_tariffs.price — понятен parseTariffPrice. */
export function formatUserApiTariffPrice(
cost: number,
period: string | undefined,
period: 'day' | 'month',
currency: string,
): string {
const cur = (currency || 'RUB').trim().toUpperCase()
if (normalizePlanPeriod(period) === 'day') {
if (period === 'day') {
return `${cost} ${cur}/day`
}
return `${cost} ${cur}`
@@ -253,17 +273,18 @@ function mapPlanToTariffItem(
country: '',
cpuModel: '',
orderAvailable: Boolean(plan.active && plan.enable),
price: cost != null ? formatUserApiTariffPrice(cost, plan.period, currency) : '',
price: cost != null ? formatUserApiTariffPrice(cost, inferPlanPeriod(plan), currency) : '',
}
}
function normalizePlanInIndex(plan: UserApiServerPlan, groupId: number): UserApiServerPlan {
const cost = effectivePlanCost(plan)
const period = inferPlanPeriod(plan)
return {
...plan,
'server-group': plan['server-group'] ?? groupId,
cost: cost ?? plan.cost,
period: normalizePlanPeriod(plan.period) === 'month' ? 'month' : 'day',
period,
}
}
+1
View File
@@ -108,6 +108,7 @@ export async function syncFromUserApi(
planIndex,
fallbackCurrency,
tariffByPlanId,
account.billingMode,
)
const id = `vps-${idPrefix}-${accountId}-${vps.externalId}`
const additionalIps = JSON.stringify(vps.additionalIps || [])