From 6c5eb04fae1d1f18a4382f47f86775147b5a945e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 29 Jun 2026 02:23:34 +0700 Subject: [PATCH] =?UTF-8?q?fix(userapi):=20=D0=BF=D0=BE=D0=B4=D1=82=D1=8F?= =?UTF-8?q?=D0=B3=D0=B8=D0=B2=D0=B0=D1=82=D1=8C=20=D1=81=D1=82=D0=BE=D0=B8?= =?UTF-8?q?=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20VPS=20Macloud/VDSina=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D1=82=D0=B0=D1=80=D0=B8=D1=84=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BF=D0=BB=D0=B0=D0=BD=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При синке UserAPI цена и период берутся по server-plan.id из индекса тарифов, а не остаются нулевыми. Co-authored-by: Cursor --- apps/api/src/services/userapi/mappers.test.ts | 70 ++++++++++++++++++- apps/api/src/services/userapi/mappers.ts | 57 +++++++++++++-- apps/api/src/services/userapi/operations.ts | 39 ++++++++++- apps/api/src/services/userapi/sync.test.ts | 23 +++++- apps/api/src/services/userapi/sync.ts | 9 ++- 5 files changed, 186 insertions(+), 12 deletions(-) diff --git a/apps/api/src/services/userapi/mappers.test.ts b/apps/api/src/services/userapi/mappers.test.ts index 55690a4..08bfdee 100644 --- a/apps/api/src/services/userapi/mappers.test.ts +++ b/apps/api/src/services/userapi/mappers.test.ts @@ -25,7 +25,18 @@ const server: UserApiServerDetail = { describe('mapServerToVps', () => { it('maps macloud server with apiType prefix in notes', () => { - const vps = mapServerToVps(server, 'macloud', 'prov-1', 'acc-1') + const planIndex = new Map([ + [ + '1', + { + id: 1, + name: '2 RAM / 1 CPU / 40 NVMe', + cost: 1.55, + period: 'day', + }, + ], + ]) + const vps = mapServerToVps(server, 'macloud', 'prov-1', 'acc-1', planIndex) expect(vps.externalId).toBe('12345') expect(vps.ip).toBe('91.84.101.78') expect(vps.os).toBe('Ubuntu 24.04') @@ -37,6 +48,63 @@ describe('mapServerToVps', () => { expect(vps.paidUntil).toBe('2029-02-20') expect(vps.notes).toContain('macloud-12345') expect(vps.tariffType).toBe('daily') + expect(vps.dailyRate).toBe(1.55) + expect(vps.monthlyRate).toBeNull() + }) + + it('maps monthly plan cost from plan index', () => { + const planIndex = new Map([ + [ + '1', + { + id: 1, + name: '2 RAM / 1 CPU / 40 NVMe', + cost: 500, + period: 'month', + }, + ], + ]) + const vps = mapServerToVps(server, 'vdsina', 'prov-1', 'acc-1', planIndex) + expect(vps.tariffType).toBe('monthly') + expect(vps.monthlyRate).toBe(500) + expect(vps.dailyRate).toBeNull() + }) + + it('adds constructor plan extra cost from server totals', () => { + const planIndex = new Map([ + [ + '1', + { + id: 1, + name: 'Constructor', + cost: 10, + period: 'day', + has_params: true, + params: { + cpu: { cost: 1 }, + ram: { cost: 0.5 }, + disk: { cost: 0.1 }, + }, + data: { cpu: { value: 1 }, ram: { value: 1 }, disk: { value: 1 } }, + }, + ], + ]) + const vps = mapServerToVps( + { + ...server, + data: { + cpu: { value: 1, total: 4 }, + ram: { value: 1, total: 8 }, + disk: { value: 1, total: 10 }, + }, + }, + 'vdsina', + 'prov-1', + 'acc-1', + planIndex, + ) + // 10 + 3*1 + 7*0.5 + 9*0.1 = 10 + 3 + 3.5 + 0.9 = 17.4 + expect(vps.dailyRate).toBe(17.4) }) it('maps vdsina server with vdsina prefix in notes', () => { diff --git a/apps/api/src/services/userapi/mappers.ts b/apps/api/src/services/userapi/mappers.ts index 98f9154..0c598dc 100644 --- a/apps/api/src/services/userapi/mappers.ts +++ b/apps/api/src/services/userapi/mappers.ts @@ -4,7 +4,7 @@ import type { UserApiType } from '@cfdm/shared/contracts/provider' -import type { UserApiOperation, UserApiServerDetail } from './operations.js' +import type { UserApiOperation, UserApiServerDetail, UserApiServerPlan, UserApiPlanCostIndex } from './operations.js' const STATUS_MAP: Record = { active: 'active', @@ -34,6 +34,53 @@ function extractIp(server: UserApiServerDetail): { ip: string; ipv6: string } { return { ip: String(ipObj.ip).trim(), ipv6: '' } } +function calculateConstructorExtraCost( + server: UserApiServerDetail, + plan: UserApiServerPlan, +): number { + if (!plan.has_params || !plan.params) return 0 + + const serverData = server.data ?? {} + const planData = plan.data ?? {} + let extra = 0 + + for (const key of ['cpu', 'ram', 'disk'] as const) { + const param = plan.params[key] + if (!param?.cost) continue + const serverRes = serverData[key] + const planBase = planData[key]?.value ?? 0 + const serverTotal = serverRes?.total ?? serverRes?.value ?? planBase + const units = Math.max(0, serverTotal - planBase) + if (units > 0) extra += units * param.cost + } + + return extra +} + +function resolvePlanRates( + server: UserApiServerDetail, + planIndex?: UserApiPlanCostIndex, +): { tariffType: string; dailyRate: number | null; monthlyRate: number | null } { + const planId = server['server-plan']?.id + if (planId == null || !planIndex) { + return { tariffType: 'daily', dailyRate: null, monthlyRate: null } + } + + const plan = planIndex.get(String(planId)) + if (!plan || !Number.isFinite(plan.cost)) { + return { tariffType: 'daily', dailyRate: null, monthlyRate: null } + } + + const cost = plan.cost + calculateConstructorExtraCost(server, plan) + const period = (plan.period || 'day').toLowerCase() + + if (period === 'month') { + return { tariffType: 'monthly', dailyRate: null, monthlyRate: cost } + } + + return { tariffType: 'daily', dailyRate: cost, monthlyRate: null } +} + export interface MappedVps { externalId: string ip: string @@ -85,6 +132,7 @@ export function mapServerToVps( apiType: UserApiType, providerId: string, providerAccountId: string, + planIndex?: UserApiPlanCostIndex, ): MappedVps { const { ip, ipv6 } = extractIp(server) const data = server.data ?? {} @@ -101,6 +149,7 @@ export function mapServerToVps( : traffGb > 0 ? Math.round((traffGb / 1024) * 100) / 100 : 0 + const { tariffType, dailyRate, monthlyRate } = resolvePlanRates(server, planIndex) return { externalId: String(server.id), @@ -128,10 +177,10 @@ export function mapServerToVps( monitoringEnabled: false, backupEnabled: false, status, - tariffType: 'daily', + tariffType, currency: 'RUB', - dailyRate: null, - monthlyRate: null, + dailyRate, + monthlyRate, createdAt: dateToIso(server.created), paidUntil: dateToIso(server.end), notes: name ? `${name} [${apiType}-${server.id}]` : `${apiType}-${server.id}`, diff --git a/apps/api/src/services/userapi/operations.ts b/apps/api/src/services/userapi/operations.ts index 6a73e1a..520bcfe 100644 --- a/apps/api/src/services/userapi/operations.ts +++ b/apps/api/src/services/userapi/operations.ts @@ -16,13 +16,26 @@ export interface UserApiDatacenter { } export interface UserApiTariffSpec { - cpu?: { value?: number; for?: string } - ram?: { value?: number; for?: string } - disk?: { value?: number; for?: string } + cpu?: { value?: number; total?: number; for?: string } + ram?: { value?: number; total?: number; for?: string } + disk?: { value?: number; total?: number; for?: string } gpu?: { value?: number; for?: string } | null traff?: { value?: number; for?: string } } +export interface UserApiPlanParamCost { + cost?: number + min?: number + max?: number +} + +export interface UserApiPlanParams { + cpu?: UserApiPlanParamCost + ram?: UserApiPlanParamCost + disk?: UserApiPlanParamCost + ip4?: UserApiPlanParamCost +} + export interface UserApiServerListItem { id: number name: string @@ -62,9 +75,12 @@ export interface UserApiServerPlan { active?: boolean enable?: boolean has_params?: boolean + params?: UserApiPlanParams | null data?: UserApiTariffSpec | null } +export type UserApiPlanCostIndex = Map + export interface UserApiOperation { id: number purse?: 'real' | 'bonus' | 'partner' @@ -265,6 +281,23 @@ export async function fetchServerPlans( return Array.isArray(data) ? data : [] } +export async function fetchPlanCostIndex( + baseUrl: string, + credentials: string, +): Promise { + const groups = await fetchServerGroups(baseUrl, credentials) + const index: UserApiPlanCostIndex = new Map() + + 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), plan) + } + } + + return index +} + export async function fetchTariffList( baseUrl: string, credentials: string, diff --git a/apps/api/src/services/userapi/sync.test.ts b/apps/api/src/services/userapi/sync.test.ts index ad5aab0..b8e1efc 100644 --- a/apps/api/src/services/userapi/sync.test.ts +++ b/apps/api/src/services/userapi/sync.test.ts @@ -10,12 +10,14 @@ vi.mock('./operations.js', () => ({ fetchServersWithDetails: vi.fn(), fetchBalance: vi.fn(), fetchTariffList: vi.fn(), + fetchPlanCostIndex: vi.fn(), fetchOperations: vi.fn(), })) import { fetchBalance, fetchOperations, + fetchPlanCostIndex, fetchServersWithDetails, fetchTariffList, } from './operations.js' @@ -56,7 +58,7 @@ describe('syncFromUserApi', () => { ip: { ip: '1.2.3.4', type: '4' }, template: { name: 'Debian 12' }, datacenter: { id: 1, name: 'DC1', country: 'ru' }, - 'server-plan': { name: '2 RAM / 1 CPU / 40 NVMe' }, + 'server-plan': { id: 13, name: '2 RAM / 1 CPU / 40 NVMe' }, data: { cpu: { value: 1 }, ram: { value: 2 }, disk: { value: 40 } }, }, ]) @@ -85,6 +87,19 @@ describe('syncFromUserApi', () => { price: '1.55 ₽/день', }, ]) + vi.mocked(fetchPlanCostIndex).mockResolvedValue( + new Map([ + [ + '13', + { + id: 13, + name: '2 RAM / 1 CPU / 40 NVMe', + cost: 1.55, + period: 'day', + }, + ], + ]), + ) vi.mocked(fetchOperations).mockResolvedValue([ { id: 999, @@ -122,11 +137,15 @@ describe('syncFromUserApi', () => { expect(result.tariffsCount).toBe(1) expect(result.balance?.balance).toBe(500) - const vps = getSqlite().prepare('SELECT id, notes FROM vps WHERE id = ?').get('vps-macloud-acc-macloud-100') as { + const vps = getSqlite().prepare('SELECT id, notes, dailyRate, tariffType FROM vps WHERE id = ?').get('vps-macloud-acc-macloud-100') as { id: string notes: string + dailyRate: number + tariffType: string } expect(vps.notes).toContain('macloud-100') + expect(vps.dailyRate).toBe(1.55) + expect(vps.tariffType).toBe('daily') }) it('syncs vdsina account with vdsina id prefix', async () => { diff --git a/apps/api/src/services/userapi/sync.ts b/apps/api/src/services/userapi/sync.ts index d13a07c..9197c0e 100644 --- a/apps/api/src/services/userapi/sync.ts +++ b/apps/api/src/services/userapi/sync.ts @@ -11,9 +11,11 @@ import { mapOperationToPayment, mapServerToVps } from './mappers.js' import { fetchBalance, fetchOperations, + fetchPlanCostIndex, fetchServersWithDetails, fetchTariffList, type UserApiBalanceResult, + type UserApiPlanCostIndex, } from './operations.js' export interface SyncFromUserApiOptions { @@ -75,13 +77,16 @@ export async function syncFromUserApi( const fallbackCurrency = syncFallbackCurrency(account) - const [servers, balanceInfo, tariffItems, operations] = await Promise.all([ + const [servers, balanceInfo, tariffItems, operations, planIndex] = await Promise.all([ fetchVpsData ? fetchServersWithDetails(apiBaseUrl, credentials) : [], fetchVpsData ? fetchBalance(apiBaseUrl, credentials, fallbackCurrency).catch(() => null) : null, fetchTariffs ? fetchTariffList(apiBaseUrl, credentials).catch(() => []) : [], fetchVpsData ? fetchOperations(apiBaseUrl, credentials).catch(() => []) : [], + fetchVpsData + ? fetchPlanCostIndex(apiBaseUrl, credentials).catch(() => new Map() as UserApiPlanCostIndex) + : (new Map() as UserApiPlanCostIndex), ]) let vpsCount = 0 @@ -89,7 +94,7 @@ export async function syncFromUserApi( if (fetchVpsData) { for (const server of servers) { - const vps = mapServerToVps(server, apiType, providerId, accountId) + const vps = mapServerToVps(server, apiType, providerId, accountId, planIndex) const id = `vps-${idPrefix}-${accountId}-${vps.externalId}` const additionalIps = JSON.stringify(vps.additionalIps || []) const dailyRate = vps.dailyRate