fix(userapi): период тарифа Macloud и отображение ставки на списке VPS
Docker / build (push) Failing after 1m45s
Docker / build (push) Failing after 1m45s
Месячные планы Macloud определяются через inferPlanPeriod; в UI показывается суточная или месячная ставка по tariffType, а не месячный эквивалент. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -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([
|
||||
[
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 || [])
|
||||
|
||||
Reference in New Issue
Block a user