feat(billmanager): унифицированные профили хостеров (waicore vds.vps)
Docker / build (push) Failing after 20s
Docker / build (push) Failing after 20s
Overrides по apiBaseUrl: DEFAULT + partial merge; Waicore использует func=vds.vps. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -79,6 +79,8 @@ vps-tracker/
|
||||
|
||||
Формат запроса: `?authinfo=user:pass&out=bjson&func=vds|payment|dashboard.info|vds.order`
|
||||
|
||||
**Профили хостеров (overrides):** `apps/api/src/services/billmanager/profiles/` — матч по `apiBaseUrl`, новый хостер = один файл overrides + запись в registry. HOWTO: [`profiles/README.md`](apps/api/src/services/billmanager/profiles/README.md).
|
||||
|
||||
## Уведомления
|
||||
|
||||
- **Движок:** `apps/api/src/services/notifications/` — rules, dedup, engine, channels
|
||||
|
||||
@@ -10,11 +10,19 @@ export {
|
||||
fetchPayments,
|
||||
fetchVdsOrderPricelist,
|
||||
fetchVdsOrderPricelistAllDatacenters,
|
||||
mapVdsWithProfile,
|
||||
mapPaymentWithProfile,
|
||||
} from './operations.js'
|
||||
export { syncFromBillmanager } from './sync.js'
|
||||
export { runBillmanagerAccountSync } from './sync-job.js'
|
||||
export { billmanagerAccountRowForSync, resolveBillmanagerApi } from './context.js'
|
||||
export {
|
||||
resolveBillmanagerProfile,
|
||||
DEFAULT_PROFILE,
|
||||
mergeProfile,
|
||||
} from './profiles/index.js'
|
||||
export type { BillmanagerSyncAccount } from './context.js'
|
||||
export type { BillmanagerProfile, BillmanagerProfileOverrides } from './profiles/index.js'
|
||||
export type {
|
||||
SyncFromBillmanagerOptions,
|
||||
SyncFromBillmanagerResult,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* BILLmanager API operations — fetch VDS, payments, dashboard, tariffs
|
||||
* All funcs / extract keys come from resolveBillmanagerProfile(baseUrl).
|
||||
*/
|
||||
|
||||
import { billmanagerRequest } from './client.js'
|
||||
@@ -10,6 +11,11 @@ import {
|
||||
parseDatacenterName,
|
||||
parseTariffDesc,
|
||||
} from './parsers.js'
|
||||
import {
|
||||
resolveBillmanagerProfile,
|
||||
type BillmanagerProfile,
|
||||
} from './profiles/index.js'
|
||||
import type { MappedPayment, MappedVps } from './mappers.js'
|
||||
|
||||
export interface DashboardInfo {
|
||||
balance: number
|
||||
@@ -37,21 +43,60 @@ export interface TariffItem {
|
||||
country?: string
|
||||
}
|
||||
|
||||
export async function fetchVds(baseUrl: string, authinfo: string): Promise<Record<string, string>[]> {
|
||||
const data = await billmanagerRequest(baseUrl, authinfo, 'vds')
|
||||
const elems = extractList(data, 'vds')
|
||||
function profileFor(baseUrl: string): BillmanagerProfile {
|
||||
return resolveBillmanagerProfile(baseUrl)
|
||||
}
|
||||
|
||||
/** Map raw VDS elem through profile.map.vds (+ optional enrichVds). */
|
||||
export function mapVdsWithProfile(
|
||||
profile: BillmanagerProfile,
|
||||
item: Record<string, string>,
|
||||
providerId: string,
|
||||
accountId: string,
|
||||
): MappedVps {
|
||||
const mapped = profile.map.vds(item, providerId, accountId)
|
||||
return profile.map.enrichVds ? profile.map.enrichVds(item, mapped) : mapped
|
||||
}
|
||||
|
||||
export function mapPaymentWithProfile(
|
||||
profile: BillmanagerProfile,
|
||||
item: Record<string, string>,
|
||||
accountId: string,
|
||||
): MappedPayment | null {
|
||||
return profile.map.payment(item, accountId)
|
||||
}
|
||||
|
||||
export async function fetchVds(
|
||||
baseUrl: string,
|
||||
authinfo: string,
|
||||
profile?: BillmanagerProfile,
|
||||
): Promise<Record<string, string>[]> {
|
||||
const p = profile ?? profileFor(baseUrl)
|
||||
const data = await billmanagerRequest(
|
||||
baseUrl,
|
||||
authinfo,
|
||||
p.funcs.listVds,
|
||||
p.requestParams?.listVds,
|
||||
)
|
||||
const elems = extractList(data, p.extract.listVdsKey)
|
||||
return elems.map((e) => elemToObject(e))
|
||||
}
|
||||
|
||||
export async function fetchDashboardInfo(
|
||||
baseUrl: string,
|
||||
authinfo: string,
|
||||
opts: { fallbackCurrency?: string | null } = {},
|
||||
opts: { fallbackCurrency?: string | null; profile?: BillmanagerProfile } = {},
|
||||
): Promise<DashboardInfo> {
|
||||
const data = await billmanagerRequest(baseUrl, authinfo, 'dashboard.info', {
|
||||
dashboard: 'info',
|
||||
sfrom: 'ajax',
|
||||
})
|
||||
const p = opts.profile ?? profileFor(baseUrl)
|
||||
const data = await billmanagerRequest(
|
||||
baseUrl,
|
||||
authinfo,
|
||||
p.funcs.dashboard,
|
||||
p.requestParams?.dashboard ?? {
|
||||
dashboard: 'info',
|
||||
sfrom: 'ajax',
|
||||
},
|
||||
)
|
||||
const elems =
|
||||
extractList(data, 'dashboard') ||
|
||||
(Array.isArray(data.elem) ? (data.elem as unknown[]) : [])
|
||||
@@ -83,32 +128,48 @@ export async function fetchPayments(
|
||||
createdate?: string
|
||||
filter?: string
|
||||
status?: string | number
|
||||
profile?: BillmanagerProfile
|
||||
} = {},
|
||||
): Promise<Record<string, string>[]> {
|
||||
const params: Record<string, string> = {}
|
||||
const p = opts.profile ?? profileFor(baseUrl)
|
||||
const params: Record<string, string | number> = {
|
||||
...p.requestParams?.payments,
|
||||
}
|
||||
if (opts.createdatestart) params.createdatestart = opts.createdatestart
|
||||
if (opts.createdateend) params.createdateend = opts.createdateend
|
||||
if (opts.createdate === 'other') params.createdate = 'other'
|
||||
if (opts.filter === 'on') params.filter = 'on'
|
||||
if (opts.status != null) params.status = String(opts.status)
|
||||
const data = await billmanagerRequest(baseUrl, authinfo, 'payment', params)
|
||||
const elems = extractList(data, 'payment')
|
||||
const data = await billmanagerRequest(baseUrl, authinfo, p.funcs.payments, params)
|
||||
const elems = extractList(data, p.extract.paymentsKey)
|
||||
return elems.map((e) => elemToObject(e))
|
||||
}
|
||||
|
||||
export async function fetchVdsOrderPricelist(
|
||||
baseUrl: string,
|
||||
authinfo: string,
|
||||
opts: { plid?: string; period?: string; datacenter?: string } = {},
|
||||
opts: {
|
||||
plid?: string
|
||||
period?: string
|
||||
datacenter?: string
|
||||
profile?: BillmanagerProfile
|
||||
} = {},
|
||||
): Promise<{ tariffItems: TariffItem[]; slist: Record<string, unknown> }> {
|
||||
const params: Record<string, string> = {
|
||||
const p = opts.profile ?? profileFor(baseUrl)
|
||||
const params: Record<string, string | number> = {
|
||||
plid: opts.plid || '',
|
||||
sfrom: 'ajax',
|
||||
...p.requestParams?.orderPricelist,
|
||||
}
|
||||
if (opts.period) params.period = opts.period
|
||||
if (opts.datacenter) params.datacenter = opts.datacenter
|
||||
|
||||
const data = await billmanagerRequest(baseUrl, authinfo, 'vds.order', params)
|
||||
const data = await billmanagerRequest(
|
||||
baseUrl,
|
||||
authinfo,
|
||||
p.funcs.orderPricelist,
|
||||
params,
|
||||
)
|
||||
|
||||
const tariflist = extractTariflist(data)
|
||||
const listNode = (data.list as Record<string, unknown>) ?? (data.doc as Record<string, unknown>)?.list
|
||||
@@ -143,8 +204,10 @@ export async function fetchVdsOrderPricelist(
|
||||
export async function fetchVdsOrderPricelistAllDatacenters(
|
||||
baseUrl: string,
|
||||
authinfo: string,
|
||||
profile?: BillmanagerProfile,
|
||||
): Promise<{ tariffItems: TariffItem[]; slist: Record<string, unknown> }> {
|
||||
const initial = await fetchVdsOrderPricelist(baseUrl, authinfo)
|
||||
const p = profile ?? profileFor(baseUrl)
|
||||
const initial = await fetchVdsOrderPricelist(baseUrl, authinfo, { profile: p })
|
||||
const slist = initial.slist || {}
|
||||
const datacenters = Array.isArray(slist.datacenter) ? slist.datacenter : []
|
||||
|
||||
@@ -161,7 +224,12 @@ export async function fetchVdsOrderPricelistAllDatacenters(
|
||||
const { country, location } = parseDatacenterName(dcName)
|
||||
|
||||
const result =
|
||||
i === 0 ? initial : await fetchVdsOrderPricelist(baseUrl, authinfo, { datacenter: dcKey })
|
||||
i === 0
|
||||
? initial
|
||||
: await fetchVdsOrderPricelist(baseUrl, authinfo, {
|
||||
datacenter: dcKey,
|
||||
profile: p,
|
||||
})
|
||||
for (const t of result.tariffItems) {
|
||||
allTariffItems.push({
|
||||
...t,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { describe, expect, it, vi, afterEach } from 'vitest'
|
||||
|
||||
import { elemToObject, extractList } from './parsers.js'
|
||||
import { mapVdsToVps } from './mappers.js'
|
||||
import {
|
||||
DEFAULT_PROFILE,
|
||||
mergeProfile,
|
||||
resolveBillmanagerProfile,
|
||||
waicoreOverrides,
|
||||
} from './profiles/index.js'
|
||||
import { fetchVds, mapVdsWithProfile } from './operations.js'
|
||||
|
||||
/** Minimal Waicore-style bjson (no credentials / addon noise). */
|
||||
const WAICORE_VDS_FIXTURE = {
|
||||
func: 'vds.vps',
|
||||
elem: [
|
||||
{
|
||||
id: '87173',
|
||||
ip: '212.192.246.214',
|
||||
domain: 'instance87173.waicore.network',
|
||||
expiredate: '2027-01-21',
|
||||
real_expiredate: '2027-01-21',
|
||||
ostempl: 'Ubuntu 24.04',
|
||||
datacentername: '[DE] Франкфурт | Промо',
|
||||
pricelist: '[DE] RP-1',
|
||||
cost: '1.80 € / Месяц',
|
||||
item_cost: '10.8000',
|
||||
currency_str: '€',
|
||||
createdate: '2025-07-15',
|
||||
item_status_orig: '2',
|
||||
item_status: 'Активен',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('billmanager profiles', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('resolve: waicore hostname → waicore profile', () => {
|
||||
const p = resolveBillmanagerProfile('https://my.waicore.com/')
|
||||
expect(p.id).toBe('waicore')
|
||||
expect(p.funcs.listVds).toBe('vds.vps')
|
||||
expect(p.funcs.payments).toBe('payment')
|
||||
expect(p.funcs.dashboard).toBe('dashboard.info')
|
||||
})
|
||||
|
||||
it('resolve: keyword in URL → waicore', () => {
|
||||
expect(resolveBillmanagerProfile('https://panel.example/waicore-proxy').id).toBe(
|
||||
'waicore',
|
||||
)
|
||||
})
|
||||
|
||||
it('resolve: unknown hoster → default', () => {
|
||||
const p = resolveBillmanagerProfile('https://bill.hoster.ru/')
|
||||
expect(p.id).toBe('default')
|
||||
expect(p.funcs.listVds).toBe('vds')
|
||||
})
|
||||
|
||||
it('merge: only overrides listVds func', () => {
|
||||
const merged = mergeProfile(DEFAULT_PROFILE, waicoreOverrides)
|
||||
expect(merged.funcs.listVds).toBe('vds.vps')
|
||||
expect(merged.funcs.payments).toBe(DEFAULT_PROFILE.funcs.payments)
|
||||
expect(merged.extract.listVdsKey).toBe(DEFAULT_PROFILE.extract.listVdsKey)
|
||||
expect(merged.map.vds).toBe(DEFAULT_PROFILE.map.vds)
|
||||
})
|
||||
|
||||
it('Waicore fixture elem → MappedVps fields', () => {
|
||||
const profile = resolveBillmanagerProfile('https://my.waicore.com/')
|
||||
const elems = extractList(WAICORE_VDS_FIXTURE, profile.extract.listVdsKey)
|
||||
expect(elems).toHaveLength(1)
|
||||
const item = elemToObject(elems[0]!)
|
||||
const vps = mapVdsWithProfile(profile, item, 'prov-1', 'acc-1')
|
||||
expect(vps.externalId).toBe('87173')
|
||||
expect(vps.ip).toBe('212.192.246.214')
|
||||
expect(vps.dns).toBe('instance87173.waicore.network')
|
||||
expect(vps.paidUntil).toBe('2027-01-21')
|
||||
expect(vps.os).toBe('Ubuntu 24.04')
|
||||
expect(vps.status).toBe('active')
|
||||
})
|
||||
|
||||
it('fetchVds for waicore URL uses func=vds.vps', async () => {
|
||||
const calls: string[] = []
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input)
|
||||
calls.push(url)
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => WAICORE_VDS_FIXTURE,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const items = await fetchVds('https://my.waicore.com/', 'user:pass')
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]).toContain('func=vds.vps')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]?.id).toBe('87173')
|
||||
})
|
||||
|
||||
it('fetchVds for default URL uses func=vds', async () => {
|
||||
const calls: string[] = []
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input)
|
||||
calls.push(url)
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ elem: [] }),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
await fetchVds('https://bill.example.com/', 'user:pass')
|
||||
expect(calls[0]).toContain('func=vds')
|
||||
expect(calls[0]).not.toContain('func=vds.vps')
|
||||
})
|
||||
|
||||
it('default mapVds still works without profile enrich', () => {
|
||||
const mapped = mapVdsToVps(
|
||||
{
|
||||
id: '1',
|
||||
ip: '1.2.3.4',
|
||||
domain: '',
|
||||
expiredate: '2026-12-01',
|
||||
item_status_orig: '2',
|
||||
},
|
||||
'p',
|
||||
'a',
|
||||
)
|
||||
expect(mapped.paidUntil).toBe('2026-12-01')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
# BILLmanager hoster profiles
|
||||
|
||||
Унифицированные переопределители для исключений из стандартного ISPsystem API.
|
||||
|
||||
## Как добавить профиль (5 минут)
|
||||
|
||||
1. Создай `profiles/<id>.ts` с **только расхождениями**:
|
||||
|
||||
```ts
|
||||
import type { BillmanagerProfileOverrides } from './types.js'
|
||||
|
||||
export const myHosterOverrides: BillmanagerProfileOverrides = {
|
||||
id: 'myhoster',
|
||||
match: {
|
||||
hostnames: ['myhoster.com'],
|
||||
keywords: ['myhoster'],
|
||||
},
|
||||
funcs: {
|
||||
listVds: 'vds.custom', // если отличается от 'vds'
|
||||
},
|
||||
// map: { enrichVds: (item, mapped) => ({ ...mapped, country: 'DE' }) },
|
||||
// requestParams: { listVds: { p_cnt: 1000 } },
|
||||
}
|
||||
```
|
||||
|
||||
2. Зарегистрируй в [`registry.ts`](./registry.ts) — массив `PROFILE_OVERRIDES` (порядок = приоритет матча).
|
||||
|
||||
3. Добавь fixture + тест в [`profiles.test.ts`](../profiles.test.ts).
|
||||
|
||||
4. Новый hook (редко) — расширь `BillmanagerProfile` в [`types.ts`](./types.ts) и значение в [`default.ts`](./default.ts).
|
||||
|
||||
## Контракт
|
||||
|
||||
| Поле | Назначение |
|
||||
|------|------------|
|
||||
| `match.hostnames` | substring hostname |
|
||||
| `match.keywords` | substring всего URL |
|
||||
| `funcs.*` | `func=` для list / payment / dashboard / order |
|
||||
| `extract.*` | ключ для `extractList` |
|
||||
| `map.vds` / `map.payment` | полный маппер |
|
||||
| `map.enrichVds` | пост-обработка после default map |
|
||||
| `requestParams.*` | доп. query params |
|
||||
|
||||
`resolveBillmanagerProfile(apiBaseUrl)` → `merge(DEFAULT, override)` или `DEFAULT`.
|
||||
|
||||
Sync и operations **не** содержат `if (hoster)` — только профиль.
|
||||
@@ -0,0 +1,28 @@
|
||||
import { mapPaymentToPayment, mapVdsToVps } from '../mappers.js'
|
||||
import type { BillmanagerProfile } from './types.js'
|
||||
|
||||
/** Standard ISPsystem BILLmanager 6 behaviour. */
|
||||
export const DEFAULT_PROFILE: BillmanagerProfile = {
|
||||
id: 'default',
|
||||
match: {},
|
||||
funcs: {
|
||||
listVds: 'vds',
|
||||
payments: 'payment',
|
||||
dashboard: 'dashboard.info',
|
||||
orderPricelist: 'vds.order',
|
||||
},
|
||||
extract: {
|
||||
listVdsKey: 'vds',
|
||||
paymentsKey: 'payment',
|
||||
},
|
||||
map: {
|
||||
vds: mapVdsToVps,
|
||||
payment: mapPaymentToPayment,
|
||||
},
|
||||
requestParams: {
|
||||
dashboard: {
|
||||
dashboard: 'info',
|
||||
sfrom: 'ajax',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export { DEFAULT_PROFILE } from './default.js'
|
||||
export { mergeProfile } from './merge.js'
|
||||
export { PROFILE_OVERRIDES, resolveBillmanagerProfile } from './registry.js'
|
||||
export type {
|
||||
BillmanagerExtract,
|
||||
BillmanagerFuncs,
|
||||
BillmanagerMap,
|
||||
BillmanagerMatch,
|
||||
BillmanagerProfile,
|
||||
BillmanagerProfileOverrides,
|
||||
BillmanagerRequestParams,
|
||||
} from './types.js'
|
||||
export { waicoreOverrides } from './waicore.js'
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { BillmanagerProfile, BillmanagerProfileOverrides } from './types.js'
|
||||
|
||||
/**
|
||||
* Merge DEFAULT profile with hoster overrides.
|
||||
* Nested funcs/extract/map are shallow-merged; requestParams replaced per-key.
|
||||
*/
|
||||
export function mergeProfile(
|
||||
defaults: BillmanagerProfile,
|
||||
overrides: BillmanagerProfileOverrides,
|
||||
): BillmanagerProfile {
|
||||
return {
|
||||
id: overrides.id,
|
||||
match: {
|
||||
...defaults.match,
|
||||
...overrides.match,
|
||||
},
|
||||
funcs: {
|
||||
...defaults.funcs,
|
||||
...overrides.funcs,
|
||||
},
|
||||
extract: {
|
||||
...defaults.extract,
|
||||
...overrides.extract,
|
||||
},
|
||||
map: {
|
||||
...defaults.map,
|
||||
...overrides.map,
|
||||
},
|
||||
requestParams: {
|
||||
...defaults.requestParams,
|
||||
...overrides.requestParams,
|
||||
listVds: {
|
||||
...defaults.requestParams?.listVds,
|
||||
...overrides.requestParams?.listVds,
|
||||
},
|
||||
payments: {
|
||||
...defaults.requestParams?.payments,
|
||||
...overrides.requestParams?.payments,
|
||||
},
|
||||
dashboard: {
|
||||
...defaults.requestParams?.dashboard,
|
||||
...overrides.requestParams?.dashboard,
|
||||
},
|
||||
orderPricelist: {
|
||||
...defaults.requestParams?.orderPricelist,
|
||||
...overrides.requestParams?.orderPricelist,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { DEFAULT_PROFILE } from './default.js'
|
||||
import { mergeProfile } from './merge.js'
|
||||
import type { BillmanagerProfile, BillmanagerProfileOverrides } from './types.js'
|
||||
import { waicoreOverrides } from './waicore.js'
|
||||
|
||||
/**
|
||||
* Hoster override list — first match wins.
|
||||
* Add new hosters here after creating profiles/<id>.ts.
|
||||
*/
|
||||
export const PROFILE_OVERRIDES: BillmanagerProfileOverrides[] = [waicoreOverrides]
|
||||
|
||||
function matchesUrl(url: string, override: BillmanagerProfileOverrides): boolean {
|
||||
const match = override.match
|
||||
if (!match) return false
|
||||
|
||||
let hostname = ''
|
||||
try {
|
||||
hostname = new URL(url).hostname.toLowerCase()
|
||||
} catch {
|
||||
hostname = ''
|
||||
}
|
||||
const haystack = url.toLowerCase()
|
||||
|
||||
if (match.hostnames?.some((h) => hostname.includes(h.toLowerCase()))) {
|
||||
return true
|
||||
}
|
||||
if (match.keywords?.some((k) => haystack.includes(k.toLowerCase()))) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Resolve profile for apiBaseUrl: first matching override merged onto DEFAULT, else DEFAULT. */
|
||||
export function resolveBillmanagerProfile(apiBaseUrl: string): BillmanagerProfile {
|
||||
const url = String(apiBaseUrl || '').trim()
|
||||
if (!url) return DEFAULT_PROFILE
|
||||
|
||||
for (const override of PROFILE_OVERRIDES) {
|
||||
if (matchesUrl(url, override)) {
|
||||
return mergeProfile(DEFAULT_PROFILE, override)
|
||||
}
|
||||
}
|
||||
return DEFAULT_PROFILE
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Unified BILLmanager hoster profile contract.
|
||||
* DEFAULT fills all hooks; hoster files declare only overrides.
|
||||
*/
|
||||
|
||||
import type { MappedPayment, MappedVps } from '../mappers.js'
|
||||
|
||||
export type BillmanagerMatch = {
|
||||
/** Hostname substring match (e.g. waicore.com) */
|
||||
hostnames?: string[]
|
||||
/** Full URL lowercase substring (e.g. waicore) */
|
||||
keywords?: string[]
|
||||
}
|
||||
|
||||
export type BillmanagerFuncs = {
|
||||
listVds: string
|
||||
payments: string
|
||||
dashboard: string
|
||||
orderPricelist: string
|
||||
}
|
||||
|
||||
export type BillmanagerExtract = {
|
||||
listVdsKey: string
|
||||
paymentsKey: string
|
||||
}
|
||||
|
||||
export type BillmanagerMap = {
|
||||
vds: (
|
||||
item: Record<string, string>,
|
||||
providerId: string,
|
||||
accountId: string,
|
||||
) => MappedVps
|
||||
payment: (
|
||||
item: Record<string, string>,
|
||||
accountId: string,
|
||||
) => MappedPayment | null
|
||||
/** Optional post-process after map.vds (specs / geo / etc.) */
|
||||
enrichVds?: (
|
||||
item: Record<string, string>,
|
||||
mapped: MappedVps,
|
||||
) => MappedVps
|
||||
}
|
||||
|
||||
export type BillmanagerRequestParams = {
|
||||
listVds?: Record<string, string | number>
|
||||
payments?: Record<string, string | number>
|
||||
dashboard?: Record<string, string | number>
|
||||
orderPricelist?: Record<string, string | number>
|
||||
}
|
||||
|
||||
export type BillmanagerProfile = {
|
||||
id: string
|
||||
match: BillmanagerMatch
|
||||
funcs: BillmanagerFuncs
|
||||
extract: BillmanagerExtract
|
||||
map: BillmanagerMap
|
||||
requestParams?: BillmanagerRequestParams
|
||||
}
|
||||
|
||||
/** Deep-partial for hoster override files (only divergences). */
|
||||
export type BillmanagerProfileOverrides = {
|
||||
id: string
|
||||
match?: BillmanagerMatch
|
||||
funcs?: Partial<BillmanagerFuncs>
|
||||
extract?: Partial<BillmanagerExtract>
|
||||
map?: Partial<BillmanagerMap>
|
||||
requestParams?: BillmanagerRequestParams
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { BillmanagerProfileOverrides } from './types.js'
|
||||
|
||||
/**
|
||||
* Waicore (my.waicore.com) — list VPS via func=vds.vps instead of vds.
|
||||
* Response uses top-level elem[] (covered by extractList).
|
||||
*/
|
||||
export const waicoreOverrides: BillmanagerProfileOverrides = {
|
||||
id: 'waicore',
|
||||
match: {
|
||||
hostnames: ['waicore.com', 'waicore.network'],
|
||||
keywords: ['waicore'],
|
||||
},
|
||||
funcs: {
|
||||
listVds: 'vds.vps',
|
||||
},
|
||||
}
|
||||
@@ -7,15 +7,17 @@ import { getDb, schema } from '@cfdm/db'
|
||||
|
||||
import type { BillmanagerSyncAccount } from './context.js'
|
||||
import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance'
|
||||
import { mapPaymentToPayment, mapVdsToVps } from './mappers.js'
|
||||
import {
|
||||
fetchDashboardInfo,
|
||||
fetchPayments,
|
||||
fetchVds,
|
||||
fetchVdsOrderPricelistAllDatacenters,
|
||||
mapPaymentWithProfile,
|
||||
mapVdsWithProfile,
|
||||
type DashboardInfo,
|
||||
type TariffItem,
|
||||
} from './operations.js'
|
||||
import { resolveBillmanagerProfile } from './profiles/index.js'
|
||||
|
||||
export interface SyncFromBillmanagerOptions {
|
||||
skipTariffs?: boolean
|
||||
@@ -69,6 +71,7 @@ export async function syncFromBillmanager(
|
||||
}
|
||||
const authinfo = apiCredentials.trim()
|
||||
const db = getDb()
|
||||
const profile = resolveBillmanagerProfile(apiBaseUrl)
|
||||
|
||||
const fetchVpsPayments = !skipVpsPayments
|
||||
const fetchTariffs = !skipTariffs
|
||||
@@ -76,16 +79,29 @@ export async function syncFromBillmanager(
|
||||
const fallbackCurrency = syncFallbackCurrency(account)
|
||||
|
||||
const [vdsItems, paymentItems, dashboardInfo, tariffResult] = await Promise.all([
|
||||
fetchVpsPayments ? fetchVds(apiBaseUrl, authinfo) : [],
|
||||
fetchVpsPayments ? fetchPayments(apiBaseUrl, authinfo, {}) : [],
|
||||
fetchVpsPayments ? fetchVds(apiBaseUrl, authinfo, profile) : [],
|
||||
fetchVpsPayments
|
||||
? fetchDashboardInfo(apiBaseUrl, authinfo, { fallbackCurrency }).catch(() => null)
|
||||
? fetchPayments(apiBaseUrl, authinfo, { profile })
|
||||
: [],
|
||||
fetchVpsPayments
|
||||
? fetchDashboardInfo(apiBaseUrl, authinfo, {
|
||||
fallbackCurrency,
|
||||
profile,
|
||||
}).catch(() => null)
|
||||
: null,
|
||||
fetchTariffs
|
||||
? fetchVdsOrderPricelistAllDatacenters(apiBaseUrl, authinfo).catch((err) => {
|
||||
console.warn('fetchVdsOrderPricelistAllDatacenters failed:', err instanceof Error ? err.message : err)
|
||||
return { tariffItems: [] as TariffItem[], slist: {} as Record<string, unknown> }
|
||||
})
|
||||
? fetchVdsOrderPricelistAllDatacenters(apiBaseUrl, authinfo, profile).catch(
|
||||
(err) => {
|
||||
console.warn(
|
||||
'fetchVdsOrderPricelistAllDatacenters failed:',
|
||||
err instanceof Error ? err.message : err,
|
||||
)
|
||||
return {
|
||||
tariffItems: [] as TariffItem[],
|
||||
slist: {} as Record<string, unknown>,
|
||||
}
|
||||
},
|
||||
)
|
||||
: { tariffItems: [] as TariffItem[], slist: {} as Record<string, unknown> },
|
||||
])
|
||||
const { tariffItems = [], slist = {} } = tariffResult || {}
|
||||
@@ -95,7 +111,7 @@ export async function syncFromBillmanager(
|
||||
|
||||
if (fetchVpsPayments) {
|
||||
for (const item of vdsItems) {
|
||||
const vps = mapVdsToVps(item, providerId, accountId)
|
||||
const vps = mapVdsWithProfile(profile, item, providerId, accountId)
|
||||
const id = `vps-bm-${accountId}-${vps.externalId}`
|
||||
const additionalIps = JSON.stringify(vps.additionalIps || [])
|
||||
const dailyRate = vps.dailyRate
|
||||
@@ -229,7 +245,7 @@ export async function syncFromBillmanager(
|
||||
)
|
||||
|
||||
for (const item of paymentItems) {
|
||||
const payment = mapPaymentToPayment(item, accountId)
|
||||
const payment = mapPaymentWithProfile(profile, item, accountId)
|
||||
if (!payment || payment.amount <= 0) continue
|
||||
const note = payment.note
|
||||
if (existingPayments.has(note)) continue
|
||||
|
||||
Reference in New Issue
Block a user