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 <[email protected]>
This commit is contained in:
@@ -53,6 +53,59 @@ describe('mapServerToVps', () => {
|
|||||||
expect(vps.currency).toBe('RUB')
|
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', () => {
|
it('parses string plan cost from index', () => {
|
||||||
const planIndex = new Map([
|
const planIndex = new Map([
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type {
|
|||||||
UserApiServerPlan,
|
UserApiServerPlan,
|
||||||
UserApiTariffItem,
|
UserApiTariffItem,
|
||||||
} from './operations.js'
|
} from './operations.js'
|
||||||
import { normalizePlanPeriod, parsePlanCost } from './operations.js'
|
import { normalizePlanPeriod, effectivePlanCost, findPlanInIndex, parsePlanCost } from './operations.js'
|
||||||
|
|
||||||
const STATUS_MAP: Record<string, string> = {
|
const STATUS_MAP: Record<string, string> = {
|
||||||
active: 'active',
|
active: 'active',
|
||||||
@@ -112,8 +112,8 @@ function resolvePlanRates(
|
|||||||
const planKey = String(planId)
|
const planKey = String(planId)
|
||||||
|
|
||||||
if (planIndex) {
|
if (planIndex) {
|
||||||
const plan = planIndex.get(planKey)
|
const plan = findPlanInIndex(planIndex, server)
|
||||||
const baseCost = plan ? parsePlanCost(plan.cost ?? plan.full_cost) : null
|
const baseCost = plan ? effectivePlanCost(plan) : null
|
||||||
if (plan && baseCost != null) {
|
if (plan && baseCost != null) {
|
||||||
const cost = baseCost + calculateConstructorExtraCost(server, plan)
|
const cost = baseCost + calculateConstructorExtraCost(server, plan)
|
||||||
return ratesFromCost(cost, normalizePlanPeriod(plan.period))
|
return ratesFromCost(cost, normalizePlanPeriod(plan.period))
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export interface UserApiServerListItem {
|
|||||||
status_text?: string
|
status_text?: string
|
||||||
ip?: { id?: number; ip?: string; type?: string } | null
|
ip?: { id?: number; ip?: string; type?: string } | null
|
||||||
'server-plan'?: { id?: number; name?: string }
|
'server-plan'?: { id?: number; name?: string }
|
||||||
|
'server-group'?: { id?: number; name?: string }
|
||||||
template?: { id?: number; name?: string }
|
template?: { id?: number; name?: string }
|
||||||
datacenter?: UserApiDatacenter | null
|
datacenter?: UserApiDatacenter | null
|
||||||
}
|
}
|
||||||
@@ -77,6 +78,7 @@ export interface UserApiServerPlan {
|
|||||||
has_params?: boolean
|
has_params?: boolean
|
||||||
params?: UserApiPlanParams | null
|
params?: UserApiPlanParams | null
|
||||||
data?: UserApiTariffSpec | null
|
data?: UserApiTariffSpec | null
|
||||||
|
'server-group'?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserApiPlanCostIndex = Map<string, UserApiServerPlan>
|
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
|
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' {
|
export function normalizePlanPeriod(period?: string): 'day' | 'month' {
|
||||||
const p = (period || 'day').toLowerCase()
|
const p = (period || 'day').toLowerCase()
|
||||||
if (p === 'month' || p === 'monthly') return 'month'
|
if (p === 'month' || p === 'monthly') return 'month'
|
||||||
@@ -186,7 +234,7 @@ function mapPlanToTariffItem(
|
|||||||
): UserApiTariffItem {
|
): UserApiTariffItem {
|
||||||
const data = plan.data ?? {}
|
const data = plan.data ?? {}
|
||||||
const diskGb = data.disk?.value ?? 0
|
const diskGb = data.disk?.value ?? 0
|
||||||
const cost = parsePlanCost(plan.cost ?? plan.full_cost)
|
const cost = effectivePlanCost(plan)
|
||||||
const descParts = [plan.description || '']
|
const descParts = [plan.description || '']
|
||||||
if (plan.has_params) descParts.push('конструктор')
|
if (plan.has_params) descParts.push('конструктор')
|
||||||
return {
|
return {
|
||||||
@@ -209,15 +257,33 @@ function mapPlanToTariffItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePlanInIndex(plan: UserApiServerPlan): UserApiServerPlan {
|
function normalizePlanInIndex(plan: UserApiServerPlan, groupId: number): UserApiServerPlan {
|
||||||
const cost = parsePlanCost(plan.cost ?? plan.full_cost)
|
const cost = effectivePlanCost(plan)
|
||||||
return {
|
return {
|
||||||
...plan,
|
...plan,
|
||||||
cost: cost ?? 0,
|
'server-group': plan['server-group'] ?? groupId,
|
||||||
|
cost: cost ?? plan.cost,
|
||||||
period: normalizePlanPeriod(plan.period) === 'month' ? 'month' : 'day',
|
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> {
|
export async function fetchAccount(baseUrl: string, credentials: string): Promise<UserApiAccountInfo> {
|
||||||
const { baseUrl: url, token } = parseCredentials(baseUrl, credentials)
|
const { baseUrl: url, token } = parseCredentials(baseUrl, credentials)
|
||||||
const data = await userApiRequest<UserApiAccountInfo>(url, token, '/account')
|
const data = await userApiRequest<UserApiAccountInfo>(url, token, '/account')
|
||||||
@@ -336,7 +402,11 @@ export async function fetchPlanCostIndex(
|
|||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
const plans = await fetchServerPlans(baseUrl, credentials, group.id)
|
const plans = await fetchServerPlans(baseUrl, credentials, group.id)
|
||||||
for (const plan of plans) {
|
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,
|
fetchPlanCostIndex,
|
||||||
fetchServersWithDetails,
|
fetchServersWithDetails,
|
||||||
fetchTariffList,
|
fetchTariffList,
|
||||||
|
augmentTariffMapFromPlanIndex,
|
||||||
type UserApiBalanceResult,
|
type UserApiBalanceResult,
|
||||||
type UserApiPlanCostIndex,
|
type UserApiPlanCostIndex,
|
||||||
} from './operations.js'
|
} from './operations.js'
|
||||||
@@ -92,6 +93,7 @@ export async function syncFromUserApi(
|
|||||||
])
|
])
|
||||||
|
|
||||||
const tariffByPlanId = new Map(tariffItems.map((t) => [t.externalId, t]))
|
const tariffByPlanId = new Map(tariffItems.map((t) => [t.externalId, t]))
|
||||||
|
augmentTariffMapFromPlanIndex(planIndex, fallbackCurrency, tariffByPlanId)
|
||||||
|
|
||||||
let vpsCount = 0
|
let vpsCount = 0
|
||||||
const syncSummary: SyncSummary = { added: [], updated: [], paymentsAdded: 0 }
|
const syncSummary: SyncSummary = { added: [], updated: [], paymentsAdded: 0 }
|
||||||
|
|||||||
Reference in New Issue
Block a user