fix(userapi): корректно определять суточные ставки Macloud/VDSina при синке
Docker / build (push) Has been cancelled

Учитываются server-group, full_cost при нулевом cost и неактивные тарифные планы из индекса.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denozordec
2026-06-29 10:04:42 +07:00
co-authored by Cursor
parent 031fc3aaa0
commit b15e10f117
4 changed files with 133 additions and 8 deletions
@@ -53,6 +53,59 @@ describe('mapServerToVps', () => {
expect(vps.currency).toBe('RUB')
})
it('resolves plan by server-group scoped index key', () => {
const planIndex = new Map([
[
'11:1',
{
id: 1,
name: 'Wrong group plan',
cost: 0,
full_cost: 9.99,
period: 'day',
'server-group': 11,
},
],
[
'5:1',
{
id: 1,
name: '2 RAM / 1 CPU / 40 NVMe',
cost: 2.5,
period: 'day',
'server-group': 5,
},
],
])
const vps = mapServerToVps(
{ ...server, 'server-group': { id: 5, name: 'VDS' } },
'vdsina',
'prov-1',
'acc-1',
planIndex,
'USD',
)
expect(vps.dailyRate).toBe(2.5)
})
it('uses full_cost when discounted cost is zero', () => {
const planIndex = new Map([
[
'1',
{
id: 1,
name: '2 RAM / 1 CPU / 40 NVMe',
cost: 0,
full_cost: 1.55,
period: 'day',
},
],
])
const vps = mapServerToVps(server, 'macloud', 'prov-1', 'acc-1', planIndex)
expect(vps.dailyRate).toBe(1.55)
expect(vps.monthlyRate).toBe(46.5)
})
it('parses string plan cost from index', () => {
const planIndex = new Map([
[
+3 -3
View File
@@ -12,7 +12,7 @@ import type {
UserApiServerPlan,
UserApiTariffItem,
} from './operations.js'
import { normalizePlanPeriod, parsePlanCost } from './operations.js'
import { normalizePlanPeriod, effectivePlanCost, findPlanInIndex, parsePlanCost } from './operations.js'
const STATUS_MAP: Record<string, string> = {
active: 'active',
@@ -112,8 +112,8 @@ function resolvePlanRates(
const planKey = String(planId)
if (planIndex) {
const plan = planIndex.get(planKey)
const baseCost = plan ? parsePlanCost(plan.cost ?? plan.full_cost) : null
const plan = findPlanInIndex(planIndex, server)
const baseCost = plan ? effectivePlanCost(plan) : null
if (plan && baseCost != null) {
const cost = baseCost + calculateConstructorExtraCost(server, plan)
return ratesFromCost(cost, normalizePlanPeriod(plan.period))
+75 -5
View File
@@ -47,6 +47,7 @@ export interface UserApiServerListItem {
status_text?: string
ip?: { id?: number; ip?: string; type?: string } | null
'server-plan'?: { id?: number; name?: string }
'server-group'?: { id?: number; name?: string }
template?: { id?: number; name?: string }
datacenter?: UserApiDatacenter | null
}
@@ -77,6 +78,7 @@ export interface UserApiServerPlan {
has_params?: boolean
params?: UserApiPlanParams | null
data?: UserApiTariffSpec | null
'server-group'?: number
}
export type UserApiPlanCostIndex = Map<string, UserApiServerPlan>
@@ -151,6 +153,52 @@ export function parsePlanCost(raw: string | number | undefined | null): number |
return Number.isFinite(n) ? n : null
}
/** Ключ индекса тарифов: группа + plan id (ID плана уникален в рамках группы). */
export function planIndexKey(groupId: number | string, planId: number | string): string {
return `${groupId}:${planId}`
}
/**
* Эффективная цена плана: cost со скидкой, при 0 — full_cost.
* UserAPI может вернуть cost=0 при 100% скидке, а реальную ставку — в full_cost.
*/
export function effectivePlanCost(plan: UserApiServerPlan): number | null {
const discounted = parsePlanCost(plan.cost)
const full = parsePlanCost(plan.full_cost)
if (discounted != null && discounted > 0) return discounted
if (full != null && full > 0) return full
return discounted ?? full
}
export function findPlanInIndex(
planIndex: UserApiPlanCostIndex,
server: UserApiServerDetail,
): UserApiServerPlan | undefined {
const planId = server['server-plan']?.id
if (planId == null) return undefined
const planKey = String(planId)
const groupId = server['server-group']?.id
if (groupId != null) {
const scoped = planIndex.get(planIndexKey(groupId, planKey))
if (scoped) return scoped
}
const direct = planIndex.get(planKey)
if (direct) return direct
const planName = server['server-plan']?.name?.trim().toLowerCase()
if (!planName) return undefined
for (const [key, plan] of planIndex) {
if (groupId != null && key.includes(':') && !key.startsWith(`${groupId}:`)) continue
if (plan.name?.trim().toLowerCase() === planName) return plan
}
return undefined
}
export function normalizePlanPeriod(period?: string): 'day' | 'month' {
const p = (period || 'day').toLowerCase()
if (p === 'month' || p === 'monthly') return 'month'
@@ -186,7 +234,7 @@ function mapPlanToTariffItem(
): UserApiTariffItem {
const data = plan.data ?? {}
const diskGb = data.disk?.value ?? 0
const cost = parsePlanCost(plan.cost ?? plan.full_cost)
const cost = effectivePlanCost(plan)
const descParts = [plan.description || '']
if (plan.has_params) descParts.push('конструктор')
return {
@@ -209,15 +257,33 @@ function mapPlanToTariffItem(
}
}
function normalizePlanInIndex(plan: UserApiServerPlan): UserApiServerPlan {
const cost = parsePlanCost(plan.cost ?? plan.full_cost)
function normalizePlanInIndex(plan: UserApiServerPlan, groupId: number): UserApiServerPlan {
const cost = effectivePlanCost(plan)
return {
...plan,
cost: cost ?? 0,
'server-group': plan['server-group'] ?? groupId,
cost: cost ?? plan.cost,
period: normalizePlanPeriod(plan.period) === 'month' ? 'month' : 'day',
}
}
/** Дополняет карту тарифов планами из индекса (в т.ч. неактивными / снятыми с заказа). */
export function augmentTariffMapFromPlanIndex(
planIndex: UserApiPlanCostIndex,
currency: string,
target: Map<string, UserApiTariffItem>,
): void {
for (const [key, plan] of planIndex) {
if (!key.includes(':')) continue
const planId = key.split(':')[1]
if (!planId || target.has(planId)) continue
const cost = effectivePlanCost(plan)
if (cost == null || cost <= 0) continue
const groupId = key.split(':')[0] ?? ''
target.set(planId, mapPlanToTariffItem(plan, groupId, '', currency))
}
}
export async function fetchAccount(baseUrl: string, credentials: string): Promise<UserApiAccountInfo> {
const { baseUrl: url, token } = parseCredentials(baseUrl, credentials)
const data = await userApiRequest<UserApiAccountInfo>(url, token, '/account')
@@ -336,7 +402,11 @@ export async function fetchPlanCostIndex(
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), normalizePlanInIndex(plan))
if (plan?.id == null) continue
const normalized = normalizePlanInIndex(plan, group.id)
const groupKey = String(plan['server-group'] ?? group.id)
index.set(planIndexKey(groupKey, plan.id), normalized)
index.set(String(plan.id), normalized)
}
}
+2
View File
@@ -14,6 +14,7 @@ import {
fetchPlanCostIndex,
fetchServersWithDetails,
fetchTariffList,
augmentTariffMapFromPlanIndex,
type UserApiBalanceResult,
type UserApiPlanCostIndex,
} from './operations.js'
@@ -92,6 +93,7 @@ export async function syncFromUserApi(
])
const tariffByPlanId = new Map(tariffItems.map((t) => [t.externalId, t]))
augmentTariffMapFromPlanIndex(planIndex, fallbackCurrency, tariffByPlanId)
let vpsCount = 0
const syncSummary: SyncSummary = { added: [], updated: [], paymentsAdded: 0 }