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', () => {
|
describe('mapServerToVps', () => {
|
||||||
it('maps macloud server with apiType prefix in notes', () => {
|
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.externalId).toBe('12345')
|
||||||
expect(vps.ip).toBe('91.84.101.78')
|
expect(vps.ip).toBe('91.84.101.78')
|
||||||
expect(vps.os).toBe('Ubuntu 24.04')
|
expect(vps.os).toBe('Ubuntu 24.04')
|
||||||
@@ -37,6 +48,63 @@ describe('mapServerToVps', () => {
|
|||||||
expect(vps.paidUntil).toBe('2029-02-20')
|
expect(vps.paidUntil).toBe('2029-02-20')
|
||||||
expect(vps.notes).toContain('macloud-12345')
|
expect(vps.notes).toContain('macloud-12345')
|
||||||
expect(vps.tariffType).toBe('daily')
|
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', () => {
|
it('maps vdsina server with vdsina prefix in notes', () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import type { UserApiType } from '@cfdm/shared/contracts/provider'
|
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> = {
|
const STATUS_MAP: Record<string, string> = {
|
||||||
active: 'active',
|
active: 'active',
|
||||||
@@ -34,6 +34,53 @@ function extractIp(server: UserApiServerDetail): { ip: string; ipv6: string } {
|
|||||||
return { ip: String(ipObj.ip).trim(), ipv6: '' }
|
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 {
|
export interface MappedVps {
|
||||||
externalId: string
|
externalId: string
|
||||||
ip: string
|
ip: string
|
||||||
@@ -85,6 +132,7 @@ export function mapServerToVps(
|
|||||||
apiType: UserApiType,
|
apiType: UserApiType,
|
||||||
providerId: string,
|
providerId: string,
|
||||||
providerAccountId: string,
|
providerAccountId: string,
|
||||||
|
planIndex?: UserApiPlanCostIndex,
|
||||||
): MappedVps {
|
): MappedVps {
|
||||||
const { ip, ipv6 } = extractIp(server)
|
const { ip, ipv6 } = extractIp(server)
|
||||||
const data = server.data ?? {}
|
const data = server.data ?? {}
|
||||||
@@ -101,6 +149,7 @@ export function mapServerToVps(
|
|||||||
: traffGb > 0
|
: traffGb > 0
|
||||||
? Math.round((traffGb / 1024) * 100) / 100
|
? Math.round((traffGb / 1024) * 100) / 100
|
||||||
: 0
|
: 0
|
||||||
|
const { tariffType, dailyRate, monthlyRate } = resolvePlanRates(server, planIndex)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
externalId: String(server.id),
|
externalId: String(server.id),
|
||||||
@@ -128,10 +177,10 @@ export function mapServerToVps(
|
|||||||
monitoringEnabled: false,
|
monitoringEnabled: false,
|
||||||
backupEnabled: false,
|
backupEnabled: false,
|
||||||
status,
|
status,
|
||||||
tariffType: 'daily',
|
tariffType,
|
||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
dailyRate: null,
|
dailyRate,
|
||||||
monthlyRate: null,
|
monthlyRate,
|
||||||
createdAt: dateToIso(server.created),
|
createdAt: dateToIso(server.created),
|
||||||
paidUntil: dateToIso(server.end),
|
paidUntil: dateToIso(server.end),
|
||||||
notes: name ? `${name} [${apiType}-${server.id}]` : `${apiType}-${server.id}`,
|
notes: name ? `${name} [${apiType}-${server.id}]` : `${apiType}-${server.id}`,
|
||||||
|
|||||||
@@ -16,13 +16,26 @@ export interface UserApiDatacenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserApiTariffSpec {
|
export interface UserApiTariffSpec {
|
||||||
cpu?: { value?: number; for?: string }
|
cpu?: { value?: number; total?: number; for?: string }
|
||||||
ram?: { value?: number; for?: string }
|
ram?: { value?: number; total?: number; for?: string }
|
||||||
disk?: { value?: number; for?: string }
|
disk?: { value?: number; total?: number; for?: string }
|
||||||
gpu?: { value?: number; for?: string } | null
|
gpu?: { value?: number; for?: string } | null
|
||||||
traff?: { value?: number; for?: string }
|
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 {
|
export interface UserApiServerListItem {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
@@ -62,9 +75,12 @@ export interface UserApiServerPlan {
|
|||||||
active?: boolean
|
active?: boolean
|
||||||
enable?: boolean
|
enable?: boolean
|
||||||
has_params?: boolean
|
has_params?: boolean
|
||||||
|
params?: UserApiPlanParams | null
|
||||||
data?: UserApiTariffSpec | null
|
data?: UserApiTariffSpec | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserApiPlanCostIndex = Map<string, UserApiServerPlan>
|
||||||
|
|
||||||
export interface UserApiOperation {
|
export interface UserApiOperation {
|
||||||
id: number
|
id: number
|
||||||
purse?: 'real' | 'bonus' | 'partner'
|
purse?: 'real' | 'bonus' | 'partner'
|
||||||
@@ -265,6 +281,23 @@ export async function fetchServerPlans(
|
|||||||
return Array.isArray(data) ? data : []
|
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(
|
export async function fetchTariffList(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
credentials: string,
|
credentials: string,
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ vi.mock('./operations.js', () => ({
|
|||||||
fetchServersWithDetails: vi.fn(),
|
fetchServersWithDetails: vi.fn(),
|
||||||
fetchBalance: vi.fn(),
|
fetchBalance: vi.fn(),
|
||||||
fetchTariffList: vi.fn(),
|
fetchTariffList: vi.fn(),
|
||||||
|
fetchPlanCostIndex: vi.fn(),
|
||||||
fetchOperations: vi.fn(),
|
fetchOperations: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchBalance,
|
fetchBalance,
|
||||||
fetchOperations,
|
fetchOperations,
|
||||||
|
fetchPlanCostIndex,
|
||||||
fetchServersWithDetails,
|
fetchServersWithDetails,
|
||||||
fetchTariffList,
|
fetchTariffList,
|
||||||
} from './operations.js'
|
} from './operations.js'
|
||||||
@@ -56,7 +58,7 @@ describe('syncFromUserApi', () => {
|
|||||||
ip: { ip: '1.2.3.4', type: '4' },
|
ip: { ip: '1.2.3.4', type: '4' },
|
||||||
template: { name: 'Debian 12' },
|
template: { name: 'Debian 12' },
|
||||||
datacenter: { id: 1, name: 'DC1', country: 'ru' },
|
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 } },
|
data: { cpu: { value: 1 }, ram: { value: 2 }, disk: { value: 40 } },
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -85,6 +87,19 @@ describe('syncFromUserApi', () => {
|
|||||||
price: '1.55 ₽/день',
|
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([
|
vi.mocked(fetchOperations).mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 999,
|
id: 999,
|
||||||
@@ -122,11 +137,15 @@ describe('syncFromUserApi', () => {
|
|||||||
expect(result.tariffsCount).toBe(1)
|
expect(result.tariffsCount).toBe(1)
|
||||||
expect(result.balance?.balance).toBe(500)
|
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
|
id: string
|
||||||
notes: string
|
notes: string
|
||||||
|
dailyRate: number
|
||||||
|
tariffType: string
|
||||||
}
|
}
|
||||||
expect(vps.notes).toContain('macloud-100')
|
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 () => {
|
it('syncs vdsina account with vdsina id prefix', async () => {
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import { mapOperationToPayment, mapServerToVps } from './mappers.js'
|
|||||||
import {
|
import {
|
||||||
fetchBalance,
|
fetchBalance,
|
||||||
fetchOperations,
|
fetchOperations,
|
||||||
|
fetchPlanCostIndex,
|
||||||
fetchServersWithDetails,
|
fetchServersWithDetails,
|
||||||
fetchTariffList,
|
fetchTariffList,
|
||||||
type UserApiBalanceResult,
|
type UserApiBalanceResult,
|
||||||
|
type UserApiPlanCostIndex,
|
||||||
} from './operations.js'
|
} from './operations.js'
|
||||||
|
|
||||||
export interface SyncFromUserApiOptions {
|
export interface SyncFromUserApiOptions {
|
||||||
@@ -75,13 +77,16 @@ export async function syncFromUserApi(
|
|||||||
|
|
||||||
const fallbackCurrency = syncFallbackCurrency(account)
|
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 ? fetchServersWithDetails(apiBaseUrl, credentials) : [],
|
||||||
fetchVpsData
|
fetchVpsData
|
||||||
? fetchBalance(apiBaseUrl, credentials, fallbackCurrency).catch(() => null)
|
? fetchBalance(apiBaseUrl, credentials, fallbackCurrency).catch(() => null)
|
||||||
: null,
|
: null,
|
||||||
fetchTariffs ? fetchTariffList(apiBaseUrl, credentials).catch(() => []) : [],
|
fetchTariffs ? fetchTariffList(apiBaseUrl, credentials).catch(() => []) : [],
|
||||||
fetchVpsData ? fetchOperations(apiBaseUrl, credentials).catch(() => []) : [],
|
fetchVpsData ? fetchOperations(apiBaseUrl, credentials).catch(() => []) : [],
|
||||||
|
fetchVpsData
|
||||||
|
? fetchPlanCostIndex(apiBaseUrl, credentials).catch(() => new Map() as UserApiPlanCostIndex)
|
||||||
|
: (new Map() as UserApiPlanCostIndex),
|
||||||
])
|
])
|
||||||
|
|
||||||
let vpsCount = 0
|
let vpsCount = 0
|
||||||
@@ -89,7 +94,7 @@ export async function syncFromUserApi(
|
|||||||
|
|
||||||
if (fetchVpsData) {
|
if (fetchVpsData) {
|
||||||
for (const server of servers) {
|
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 id = `vps-${idPrefix}-${accountId}-${vps.externalId}`
|
||||||
const additionalIps = JSON.stringify(vps.additionalIps || [])
|
const additionalIps = JSON.stringify(vps.additionalIps || [])
|
||||||
const dailyRate = vps.dailyRate
|
const dailyRate = vps.dailyRate
|
||||||
|
|||||||
Reference in New Issue
Block a user