diff --git a/apps/api/src/services/userapi/mappers.test.ts b/apps/api/src/services/userapi/mappers.test.ts index 6baf58f..3af9ba6 100644 --- a/apps/api/src/services/userapi/mappers.test.ts +++ b/apps/api/src/services/userapi/mappers.test.ts @@ -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([ [ diff --git a/apps/api/src/services/userapi/mappers.ts b/apps/api/src/services/userapi/mappers.ts index d55d4fa..24ae78f 100644 --- a/apps/api/src/services/userapi/mappers.ts +++ b/apps/api/src/services/userapi/mappers.ts @@ -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 = { 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)) diff --git a/apps/api/src/services/userapi/operations.ts b/apps/api/src/services/userapi/operations.ts index 5ca7d0e..f20dec7 100644 --- a/apps/api/src/services/userapi/operations.ts +++ b/apps/api/src/services/userapi/operations.ts @@ -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 @@ -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, +): 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 { const { baseUrl: url, token } = parseCredentials(baseUrl, credentials) const data = await userApiRequest(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) } } diff --git a/apps/api/src/services/userapi/sync.ts b/apps/api/src/services/userapi/sync.ts index 4f6dbc6..2743487 100644 --- a/apps/api/src/services/userapi/sync.ts +++ b/apps/api/src/services/userapi/sync.ts @@ -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 }