fix(userapi): корректно определять суточные ставки Macloud/VDSina при синке
Docker / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Учитываются server-group, full_cost при нулевом cost и неактивные тарифные планы из индекса. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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([
|
||||
[
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user