fix(userapi): подтягивать стоимость VPS Macloud/VDSina из тарифных планов
Docker / build (push) Has been cancelled
Docker / build (push) Has been cancelled
При синке UserAPI цена и период берутся по server-plan.id из индекса тарифов, а не остаются нулевыми. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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}`,
|
||||
|
||||
@@ -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<string, UserApiServerPlan>
|
||||
|
||||
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<UserApiPlanCostIndex> {
|
||||
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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user