feat(sync): FirstByte daily paidUntil из баланса и гео 4VPS
Docker / build (push) Failing after 21s

Для daily FirstByte считать dailyRate из cost/30 и общий paidUntil по балансу аккаунта; для 4VPS парсить страну/город из dc_name+flag.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-19 23:26:50 +07:00
co-authored by Cursor
parent 83ed3b8a42
commit 4c6240c636
12 changed files with 456 additions and 36 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ export interface MappedVps {
status: string
tariffType: string
currency: string
dailyRate: null
dailyRate: number | null
monthlyRate: number | null
createdAt: string
paidUntil: string
@@ -13,6 +13,10 @@ import {
enrichMappedVpsFromTariffs,
parseSpecsFromVdsEdit,
} from './vds-specs.js'
import {
applyFirstbyteSharedDailyPaidUntil,
monthlyToDailyRate,
} from './profiles/firstbyte.js'
/** Minimal Waicore-style bjson (no credentials / addon noise). */
const WAICORE_VDS_FIXTURE = {
@@ -156,6 +160,7 @@ describe('billmanager profiles', () => {
expect(monthly.diskType).toBe('SSD')
expect(monthly.virtualization).toBe('KVM')
expect(monthly.vcpu).toBe(0)
expect(monthly.dailyRate).toBeNull()
const daily = mapVdsWithProfile(
profile,
@@ -165,9 +170,61 @@ describe('billmanager profiles', () => {
)
expect(daily.country).toBe('Россия')
expect(daily.city).toBe('Москва')
expect(daily.paidUntil).toBe('2026-07-20')
expect(daily.paidUntil).toBe('')
expect(daily.tariffType).toBe('daily')
expect(daily.diskType).toBe('SAS')
expect(daily.monthlyRate).toBe(129)
expect(daily.dailyRate).toBe(monthlyToDailyRate(129))
})
it('FirstByte: shared balance → same paidUntil for all daily VPS', () => {
const profile = resolveBillmanagerProfile('https://my.firstbyte.ru/')
expect(profile.map.enrichVdsBatch).toBeTypeOf('function')
const a = mapVdsWithProfile(
profile,
elemToObject({
id: '1',
billdaily: 'on',
expiredate: 'Ежедневное списание',
cost: '129.00 RUB / Месяц',
currency_str: 'RUB',
item_status_orig: '2',
ip: '1.1.1.1',
datacentername: '1 Датацентр Россия, Москва',
}),
'p',
'a',
)
const b = mapVdsWithProfile(
profile,
elemToObject({
id: '2',
billdaily: 'on',
expiredate: 'Ежедневное списание',
cost: '129.00 RUB / Месяц',
currency_str: 'RUB',
item_status_orig: '2',
ip: '2.2.2.2',
datacentername: '1 Датацентр Россия, Москва',
}),
'p',
'a',
)
const monthly = mapVdsWithProfile(
profile,
elemToObject(FIRSTBYTE_VDS_FIXTURE.elem[0]!),
'p',
'a',
)
// 2 × (129/30) = 8.6/day; balance 86 → 10 days
const asOf = new Date(Date.UTC(2026, 6, 19))
const out = applyFirstbyteSharedDailyPaidUntil([a, b, monthly], 86, asOf)
expect(out[0]!.paidUntil).toBe('2026-07-29')
expect(out[1]!.paidUntil).toBe('2026-07-29')
expect(out[2]!.paidUntil).toBe(monthly.paidUntil)
expect(out[0]!.dailyRate).toBe(monthlyToDailyRate(129))
})
it('FirstByte: specs from vds.order by pricelist_id', () => {
@@ -50,4 +50,4 @@ Sync и operations **не** содержат `if (hoster)` — только пр
| id | Match | Что переопределяет |
|----|-------|-------------------|
| `waicore` | `waicore.com` / keyword | `funcs.listVds = vds.vps` |
| `firstbyte` | `firstbyte.ru`, `firstbyte.club`, `1byte.ru` | `enrichVds`: страна/город, `paidUntil` ISO, `billdaily`→daily, diskType/KVM из имени; `options.fetchVdsEditForSpecs`; sync: specs из `vds.order` по `pricelist_id`, иначе `vds.edit` |
| `firstbyte` | `firstbyte.ru`, `firstbyte.club`, `1byte.ru` | `enrichVds`: страна/город, daily из `billdaily`/`Ежедневное списание`, `dailyRate=cost/30`; `enrichVdsBatch`: общий баланс ÷ сумма daily → один `paidUntil`; specs из `vds.order` / `vds.edit` |
@@ -1,9 +1,15 @@
import { parseDatacenterName } from '../parsers.js'
import type { MappedVps } from '../mappers.js'
import type { BillmanagerProfileOverrides } from './types.js'
import type {
BillmanagerMapOverrides,
BillmanagerProfileOverrides,
} from './types.js'
/** BILLmanager daily billing usually divides monthly price by 30. */
export const FIRSTBYTE_DAYS_PER_MONTH = 30
/** YYYY-MM-DD only — skip labels like «Ежедневное списание». */
function pickPaidUntilDate(item: Record<string, string>): string {
export function pickPaidUntilDate(item: Record<string, string>): string {
for (const key of ['real_expiredate', 'expiredate'] as const) {
const raw = String(item[key] ?? '').trim()
const m = raw.match(/^(\d{4}-\d{2}-\d{2})/)
@@ -12,6 +18,66 @@ function pickPaidUntilDate(item: Record<string, string>): string {
return ''
}
export function isFirstbyteDailyBilling(item: Record<string, string>): boolean {
if (String(item.billdaily || '').toLowerCase() === 'on') return true
const expire = String(item.expiredate || '').trim()
return /ежедневн/i.test(expire)
}
/** Parse «129.00 RUB / Месяц» or item_cost → monthly amount. */
export function parseFirstbyteMonthlyCost(item: Record<string, string>): number {
const fromCost = parseFloat(
String(item.cost || '')
.replace(/[^\d.,-]/g, '')
.replace(',', '.'),
)
if (Number.isFinite(fromCost) && fromCost > 0) return fromCost
const fromItem = parseFloat(String(item.item_cost || '').replace(',', '.'))
return Number.isFinite(fromItem) && fromItem > 0 ? fromItem : 0
}
export function monthlyToDailyRate(monthly: number): number {
if (!monthly || monthly <= 0) return 0
return Math.round((monthly / FIRSTBYTE_DAYS_PER_MONTH) * 10000) / 10000
}
function addDaysIso(from: Date, days: number): string {
const d = new Date(Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), from.getUTCDate()))
d.setUTCDate(d.getUTCDate() + days)
return d.toISOString().slice(0, 10)
}
/**
* Shared account balance ÷ sum(dailyRate of all daily VPS) → same paidUntil for each.
* Monthly VPS are left unchanged.
*/
export function applyFirstbyteSharedDailyPaidUntil(
list: MappedVps[],
balance: number | null | undefined,
asOf: Date = new Date(),
): MappedVps[] {
const dailyOnes = list.filter((v) => v.tariffType === 'daily')
if (dailyOnes.length === 0) return list
const totalDaily = dailyOnes.reduce((sum, v) => sum + (Number(v.dailyRate) || 0), 0)
const bal = typeof balance === 'number' && Number.isFinite(balance) ? balance : 0
let paidUntil = ''
if (totalDaily > 0 && bal > 0) {
const daysLeft = Math.floor(bal / totalDaily)
paidUntil = addDaysIso(asOf, daysLeft)
} else if (totalDaily > 0) {
// Нет средств — считаем оплаченным до сегодня (0 полных дней)
paidUntil = addDaysIso(asOf, 0)
}
if (!paidUntil) return list
return list.map((v) =>
v.tariffType === 'daily' ? { ...v, paidUntil } : v,
)
}
/** Disk / virt hints from FirstByte tariff codes (MSK-KVM-SSD-START, KVM-SAS-1). */
export function inferFirstbyteHardwareHints(item: Record<string, string>): {
diskType?: string
@@ -36,8 +102,10 @@ export function inferFirstbyteHardwareHints(item: Record<string, string>): {
* Sync additionally fills specs via:
* 1) vds.order match by pricelist_id (`enrichMappedVpsFromTariffs`)
* 2) func=vds.edit&elid= (`options.fetchVdsEditForSpecs`)
* 3) enrichVdsBatch: shared balance → paidUntil for daily VPS
*
* This enrich: geo, paidUntil, daily billing, diskType/KVM from tariff name.
* This enrich: geo, rates, daily flag, diskType/KVM from tariff name.
* For daily: paidUntil left empty until enrichVdsBatch (shared balance).
*/
export function enrichFirstbyteVds(
item: Record<string, string>,
@@ -45,32 +113,61 @@ export function enrichFirstbyteVds(
): MappedVps {
const dcRaw = (item.datacentername || item.datacenter || mapped.datacenter || '').trim()
const { country, location } = parseDatacenterName(dcRaw)
const paidUntil = pickPaidUntilDate(item) || mapped.paidUntil
const tariffType =
String(item.billdaily || '').toLowerCase() === 'on' ? 'daily' : mapped.tariffType
const daily = isFirstbyteDailyBilling(item)
const monthly = parseFirstbyteMonthlyCost(item) || mapped.monthlyRate || 0
const hints = inferFirstbyteHardwareHints(item)
if (daily) {
const dailyRate = monthlyToDailyRate(monthly) || mapped.dailyRate
return {
...mapped,
country: country || mapped.country,
city: location || mapped.city,
datacenter: dcRaw || mapped.datacenter,
tariffType: 'daily',
monthlyRate: monthly || mapped.monthlyRate,
dailyRate,
// Не брать real_expiredate — дата из общего баланса в enrichVdsBatch
paidUntil: '',
diskType: mapped.diskGb ? mapped.diskType : hints.diskType || mapped.diskType,
virtualization: hints.virtualization || mapped.virtualization,
}
}
const paidUntil = pickPaidUntilDate(item) || mapped.paidUntil
return {
...mapped,
country: country || mapped.country,
city: location || mapped.city,
datacenter: dcRaw || mapped.datacenter,
paidUntil,
tariffType,
tariffType: mapped.tariffType || 'monthly',
monthlyRate: monthly || mapped.monthlyRate,
dailyRate: null,
diskType: mapped.diskGb ? mapped.diskType : hints.diskType || mapped.diskType,
virtualization: hints.virtualization || mapped.virtualization,
}
}
export function enrichFirstbyteVdsBatch(
list: MappedVps[],
ctx: { balance: number | null },
): MappedVps[] {
return applyFirstbyteSharedDailyPaidUntil(list, ctx.balance)
}
const firstbyteMap: BillmanagerMapOverrides = {
enrichVds: enrichFirstbyteVds,
enrichVdsBatch: enrichFirstbyteVdsBatch,
}
export const firstbyteOverrides: BillmanagerProfileOverrides = {
id: 'firstbyte',
match: {
hostnames: ['firstbyte.ru', 'firstbyte.club', '1byte.ru'],
keywords: ['firstbyte'],
},
map: {
enrichVds: enrichFirstbyteVds,
},
map: firstbyteMap,
options: {
fetchVdsEditForSpecs: true,
},
@@ -1,5 +1,5 @@
export { DEFAULT_PROFILE } from './default.js'
export { enrichFirstbyteVds, firstbyteOverrides } from './firstbyte.js'
export { enrichFirstbyteVds, enrichFirstbyteVdsBatch, firstbyteOverrides } from './firstbyte.js'
export { mergeProfile } from './merge.js'
export { PROFILE_OVERRIDES, resolveBillmanagerProfile } from './registry.js'
export type {
@@ -24,6 +24,16 @@ export type BillmanagerExtract = {
paymentsKey: string
}
export type EnrichVdsFn = (
item: Record<string, string>,
mapped: MappedVps,
) => MappedVps
export type EnrichVdsBatchFn = (
list: MappedVps[],
ctx: { balance: number | null },
) => MappedVps[]
export type BillmanagerMap = {
vds: (
item: Record<string, string>,
@@ -35,10 +45,20 @@ export type BillmanagerMap = {
accountId: string,
) => MappedPayment | null
/** Optional post-process after map.vds (specs / geo / etc.) */
enrichVds?: (
item: Record<string, string>,
mapped: MappedVps,
) => MappedVps
enrichVds?: EnrichVdsFn
/**
* Optional post-process after all VDS mapped (e.g. shared balance → paidUntil).
* Receives list in same order as sync will upsert.
*/
enrichVdsBatch?: EnrichVdsBatchFn
}
/** Partial map for hoster overrides — lists every hook explicitly (IDE/Partial safe). */
export type BillmanagerMapOverrides = {
vds?: BillmanagerMap['vds']
payment?: BillmanagerMap['payment']
enrichVds?: EnrichVdsFn
enrichVdsBatch?: EnrichVdsBatchFn
}
export type BillmanagerRequestParams = {
@@ -72,7 +92,7 @@ export type BillmanagerProfileOverrides = {
match?: BillmanagerMatch
funcs?: Partial<BillmanagerFuncs>
extract?: Partial<BillmanagerExtract>
map?: Partial<BillmanagerMap>
map?: BillmanagerMapOverrides
requestParams?: BillmanagerRequestParams
options?: BillmanagerProfileOptions
}
+19 -8
View File
@@ -137,16 +137,27 @@ export async function syncFromBillmanager(
const syncSummary: SyncSummary = { added: [], updated: [], paymentsAdded: 0 }
if (fetchVpsPayments) {
const mappedRaw: Awaited<ReturnType<typeof mapVdsWithSpecs>>[] = []
for (const item of vdsItems) {
const vps = await mapVdsWithSpecs(
profile,
item,
providerId,
accountId,
tariffItems,
apiBaseUrl,
authinfo,
mappedRaw.push(
await mapVdsWithSpecs(
profile,
item,
providerId,
accountId,
tariffItems,
apiBaseUrl,
authinfo,
),
)
}
const mappedList = profile.map.enrichVdsBatch
? profile.map.enrichVdsBatch(mappedRaw, {
balance: dashboardInfo?.balance ?? null,
})
: mappedRaw
for (const vps of mappedList) {
const id = `vps-bm-${accountId}-${vps.externalId}`
const additionalIps = JSON.stringify(vps.additionalIps || [])
const dailyRate = vps.dailyRate
+168
View File
@@ -0,0 +1,168 @@
/**
* 4VPS datacenter location helpers
* @see https://4vps.su/page/api — getDcList: dc_name + flag
*/
/** ISO 3166-1 alpha-2 (flag) → русское название страны */
export const FOURVPS_FLAG_COUNTRY: Record<string, string> = {
AE: 'ОАЭ',
AT: 'Австрия',
AU: 'Австралия',
BE: 'Бельгия',
BG: 'Болгария',
CA: 'Канада',
CH: 'Швейцария',
CZ: 'Чехия',
DE: 'Германия',
DK: 'Дания',
EE: 'Эстония',
ES: 'Испания',
FI: 'Финляндия',
FR: 'Франция',
GB: 'Великобритания',
HK: 'Гонконг',
HU: 'Венгрия',
IE: 'Ирландия',
IL: 'Израиль',
IT: 'Италия',
JP: 'Япония',
KZ: 'Казахстан',
LT: 'Литва',
LV: 'Латвия',
MD: 'Молдова',
NL: 'Нидерланды',
NO: 'Норвегия',
PL: 'Польша',
PT: 'Португалия',
RO: 'Румыния',
RU: 'Россия',
SE: 'Швеция',
SG: 'Сингапур',
TR: 'Турция',
UA: 'Украина',
UK: 'Великобритания',
US: 'США',
}
/** English / short codes often used in dc_name / tname prefixes */
const NAME_COUNTRY_ALIASES: Record<string, string> = {
UAE: 'ОАЭ',
USA: 'США',
UK: 'Великобритания',
GB: 'Великобритания',
NL: 'Нидерланды',
DE: 'Германия',
FR: 'Франция',
FI: 'Финляндия',
SE: 'Швеция',
PL: 'Польша',
CZ: 'Чехия',
RU: 'Россия',
AE: 'ОАЭ',
CA: 'Канада',
HK: 'Гонконг',
SG: 'Сингапур',
TR: 'Турция',
KZ: 'Казахстан',
}
function countryFromFlag(flag: unknown): string {
const code = String(flag || '')
.trim()
.toUpperCase()
if (!code) return ''
return FOURVPS_FLAG_COUNTRY[code] || NAME_COUNTRY_ALIASES[code] || ''
}
/** Strip trailing «ДЦ1» / «DC 2» labels (not a city). */
function stripDcOrdinal(s: string): string {
return s.replace(/\s*(?:ДЦ|DC)\s*\d+\s*$/i, '').trim()
}
/**
* Parse 4VPS dc_name + flag → country / city.
* Examples:
* - («ОАЭ ДЦ1», ae) → ОАЭ / ''
* - («USA DC1», us) → США / ''
* - («Нидерланды Амстердам», nl) → Нидерланды / Амстердам
* - («Германия, Франкфурт», de) → Германия / Франкфурт
*/
export function parseFourVpsDcLocation(
dcName: unknown,
flag?: unknown,
): { country: string; city: string } {
const raw = String(dcName || '').trim()
const fromFlag = countryFromFlag(flag)
if (!raw) return { country: fromFlag, city: '' }
const comma = raw.match(/^([^,]+),\s*(.+)$/)
if (comma) {
const left = comma[1]!.trim()
const right = stripDcOrdinal(comma[2]!.trim())
const leftCountry =
NAME_COUNTRY_ALIASES[left.toUpperCase()] ||
FOURVPS_FLAG_COUNTRY[left.toUpperCase()] ||
(fromFlag && left.toUpperCase() === String(flag).toUpperCase() ? fromFlag : left)
return {
country: fromFlag || leftCountry,
city: right,
}
}
const withoutOrdinal = stripDcOrdinal(raw)
if (!withoutOrdinal) {
return { country: fromFlag || raw, city: '' }
}
// Exact country / alias match for whole string (e.g. «ОАЭ», «USA»)
const upper = withoutOrdinal.toUpperCase()
if (NAME_COUNTRY_ALIASES[upper] || FOURVPS_FLAG_COUNTRY[upper]) {
return {
country: fromFlag || NAME_COUNTRY_ALIASES[upper] || FOURVPS_FLAG_COUNTRY[upper]!,
city: '',
}
}
// Country name at start (RU list + aliases), rest = city
const countryNames = [
...Object.values(FOURVPS_FLAG_COUNTRY),
...Object.keys(NAME_COUNTRY_ALIASES),
].sort((a, b) => b.length - a.length)
for (const name of countryNames) {
const re = new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s+|$)`, 'i')
if (re.test(withoutOrdinal)) {
const rest = withoutOrdinal.replace(re, '').trim()
const country =
fromFlag ||
NAME_COUNTRY_ALIASES[name.toUpperCase()] ||
FOURVPS_FLAG_COUNTRY[name.toUpperCase()] ||
name
return { country, city: rest }
}
}
// Fallback: first token country-ish, rest city
const parts = withoutOrdinal.split(/\s+/).filter(Boolean)
if (parts.length >= 2) {
const first = parts[0]!
const firstCountry =
NAME_COUNTRY_ALIASES[first.toUpperCase()] ||
FOURVPS_FLAG_COUNTRY[first.toUpperCase()] ||
''
if (firstCountry || fromFlag) {
return {
country: fromFlag || firstCountry,
city: parts.slice(1).join(' '),
}
}
}
return { country: fromFlag || withoutOrdinal, city: '' }
}
/** Country for tariffs when only flag is known. */
export function countryFromFourVpsFlag(flag: unknown): string {
return countryFromFlag(flag)
}
+41 -2
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { parseFourVpsDcLocation } from './location.js'
import { mapServerToVps } from './mappers.js'
import type { FourVpsServer } from './operations.js'
@@ -19,8 +20,39 @@ const sampleServer: FourVpsServer = {
expired: 1666781101,
}
describe('parseFourVpsDcLocation', () => {
it('parses UAE DC ordinal → country only', () => {
expect(parseFourVpsDcLocation('ОАЭ ДЦ1', 'ae')).toEqual({
country: 'ОАЭ',
city: '',
})
expect(parseFourVpsDcLocation('AE DC1', 'ae')).toEqual({
country: 'ОАЭ',
city: '',
})
})
it('parses USA DC1 → США', () => {
expect(parseFourVpsDcLocation('USA DC1', 'us')).toEqual({
country: 'США',
city: '',
})
})
it('parses country + city', () => {
expect(parseFourVpsDcLocation('Нидерланды Амстердам', 'nl')).toEqual({
country: 'Нидерланды',
city: 'Амстердам',
})
expect(parseFourVpsDcLocation('Германия, Франкфурт', 'de')).toEqual({
country: 'Германия',
city: 'Франкфурт',
})
})
})
describe('mapServerToVps', () => {
it('maps myservers fields to VPS model', () => {
it('maps myservers fields to VPS model with country/city from DC', () => {
const dcMap = new Map([
[7, { id: 7, dc_name: 'USA DC1', flag: 'us', cpu_name: 'E5' }],
])
@@ -35,8 +67,15 @@ describe('mapServerToVps', () => {
expect(vps.status).toBe('active')
expect(vps.monthlyRate).toBe(420)
expect(vps.datacenter).toBe('USA DC1')
expect(vps.country).toBe('US')
expect(vps.country).toBe('США')
expect(vps.city).toBe('')
expect(vps.paidUntil).toBe('2022-10-26')
expect(vps.notes).toContain('4vps-4140')
})
it('falls back to tname prefix when DC map empty', () => {
const vps = mapServerToVps(sampleServer, 'prov-1', 'acc-1', new Map())
expect(vps.country).toBe('США')
expect(vps.datacenter).toBe('7')
})
})
+16 -2
View File
@@ -3,6 +3,7 @@
*/
import type { FourVpsDatacenter, FourVpsServer } from './operations.js'
import { parseFourVpsDcLocation } from './location.js'
const STATUS_MAP: Record<string, string> = {
active: 'active',
@@ -16,6 +17,14 @@ function unixToIso(ts: number | null | undefined): string {
return new Date(ts * 1000).toISOString().slice(0, 10)
}
/** Fallback when getDcList miss: «USA-cx01» / «AE-cx01» → flag-like prefix. */
function locationFromTariffName(tname: unknown): { country: string; city: string } {
const raw = String(tname || '').trim()
const m = raw.match(/^([A-Za-z]{2,3})-/)
if (!m) return { country: '', city: '' }
return parseFourVpsDcLocation(m[1], m[1])
}
export interface MappedVps {
externalId: string
ip: string
@@ -59,7 +68,12 @@ export function mapServerToVps(
): MappedVps {
const dc = dcMap.get(server.dc)
const datacenter = dc?.dc_name ?? String(server.dc)
const country = (dc?.flag ?? '').toUpperCase()
let { country, city } = parseFourVpsDcLocation(dc?.dc_name, dc?.flag)
if (!country) {
const fromTariff = locationFromTariffName(server.tname)
country = fromTariff.country
city = city || fromTariff.city
}
const status = STATUS_MAP[String(server.status).toLowerCase()] ?? 'active'
const monthlyRate = Number.isFinite(server.price) ? server.price : null
const name = String(server.name || '').trim()
@@ -73,7 +87,7 @@ export function mapServerToVps(
providerId,
providerAccountId,
country,
city: '',
city,
datacenter,
os: String(server.image || '').trim(),
vcpu: server.cpu ?? 0,
+8 -3
View File
@@ -5,6 +5,7 @@
import { parseFourVpsCredentials } from '@cfdm/shared/utils/api-credentials'
import { fourvpsRequest } from './client.js'
import { countryFromFourVpsFlag, parseFourVpsDcLocation } from './location.js'
export interface FourVpsServer {
id: number
@@ -126,6 +127,7 @@ function mapPresetToTariffItem(
dcName: string,
country: string,
cpuModel: string,
location = dcName,
): FourVpsTariffItem {
const diskMib = preset.disks?.[0]?.size_mib ?? (preset.rom ?? 0)
const diskGb = diskMib > 0 ? Math.round(diskMib / 1024) : preset.rom ?? 0
@@ -145,7 +147,7 @@ function mapPresetToTariffItem(
diskType: diskTag.toUpperCase(),
virtualization: 'KVM',
channel: preset.commentParsed?.eth ?? '',
location: dcName,
location,
country,
cpuModel,
orderAvailable: true,
@@ -187,13 +189,16 @@ export async function fetchTarifList(
const dcId = String(clusterInfo.id ?? dcKey)
const dcFromList = dcMap.get(Number(clusterInfo.id ?? dcKey))
const dcName = clusterInfo.dc_name ?? dcFromList?.dc_name ?? ''
const country = (clusterInfo.flag ?? dcFromList?.flag ?? '').toUpperCase()
const flag = clusterInfo.flag ?? dcFromList?.flag ?? ''
const { country: parsedCountry, city } = parseFourVpsDcLocation(dcName, flag)
const country = parsedCountry || countryFromFourVpsFlag(flag)
const location = city || dcName
const cpuModel = clusterInfo.cpu_name ?? dcFromList?.cpu_name ?? ''
const presets = cluster.presets ?? {}
for (const preset of Object.values(presets)) {
if (!preset?.id) continue
items.push(mapPresetToTariffItem(preset, dcId, dcName, country, cpuModel))
items.push(mapPresetToTariffItem(preset, dcId, dcName, country, cpuModel, location))
}
}
+11 -2
View File
@@ -113,10 +113,19 @@ describe('syncFromFourvps', () => {
expect(result.balance?.balance).toBe(77825)
const vps = getSqlite()
.prepare(`SELECT id, ip FROM vps WHERE providerAccountId = ?`)
.get('acc-4vps') as { id: string; ip: string }
.prepare(`SELECT id, ip, country, city, datacenter FROM vps WHERE providerAccountId = ?`)
.get('acc-4vps') as {
id: string
ip: string
country: string
city: string
datacenter: string
}
expect(vps.id).toBe('vps-4vps-acc-4vps-100')
expect(vps.ip).toBe('1.2.3.4')
expect(vps.country).toBe('ОАЭ')
expect(vps.city).toBe('')
expect(vps.datacenter).toBe('AE DC1')
const tariffs = getSqlite()
.prepare(`SELECT COUNT(*) as c FROM active_tariffs WHERE providerAccountId = ?`)