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 <[email protected]>
This commit is contained in:
@@ -79,6 +79,8 @@ vps-tracker/
|
|||||||
|
|
||||||
Формат запроса: `?authinfo=user:pass&out=bjson&func=vds|payment|dashboard.info|vds.order`
|
Формат запроса: `?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
|
- **Движок:** `apps/api/src/services/notifications/` — rules, dedup, engine, channels
|
||||||
|
|||||||
@@ -10,11 +10,19 @@ export {
|
|||||||
fetchPayments,
|
fetchPayments,
|
||||||
fetchVdsOrderPricelist,
|
fetchVdsOrderPricelist,
|
||||||
fetchVdsOrderPricelistAllDatacenters,
|
fetchVdsOrderPricelistAllDatacenters,
|
||||||
|
mapVdsWithProfile,
|
||||||
|
mapPaymentWithProfile,
|
||||||
} from './operations.js'
|
} from './operations.js'
|
||||||
export { syncFromBillmanager } from './sync.js'
|
export { syncFromBillmanager } from './sync.js'
|
||||||
export { runBillmanagerAccountSync } from './sync-job.js'
|
export { runBillmanagerAccountSync } from './sync-job.js'
|
||||||
export { billmanagerAccountRowForSync, resolveBillmanagerApi } from './context.js'
|
export { billmanagerAccountRowForSync, resolveBillmanagerApi } from './context.js'
|
||||||
|
export {
|
||||||
|
resolveBillmanagerProfile,
|
||||||
|
DEFAULT_PROFILE,
|
||||||
|
mergeProfile,
|
||||||
|
} from './profiles/index.js'
|
||||||
export type { BillmanagerSyncAccount } from './context.js'
|
export type { BillmanagerSyncAccount } from './context.js'
|
||||||
|
export type { BillmanagerProfile, BillmanagerProfileOverrides } from './profiles/index.js'
|
||||||
export type {
|
export type {
|
||||||
SyncFromBillmanagerOptions,
|
SyncFromBillmanagerOptions,
|
||||||
SyncFromBillmanagerResult,
|
SyncFromBillmanagerResult,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* BILLmanager API operations — fetch VDS, payments, dashboard, tariffs
|
* BILLmanager API operations — fetch VDS, payments, dashboard, tariffs
|
||||||
|
* All funcs / extract keys come from resolveBillmanagerProfile(baseUrl).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { billmanagerRequest } from './client.js'
|
import { billmanagerRequest } from './client.js'
|
||||||
@@ -10,6 +11,11 @@ import {
|
|||||||
parseDatacenterName,
|
parseDatacenterName,
|
||||||
parseTariffDesc,
|
parseTariffDesc,
|
||||||
} from './parsers.js'
|
} from './parsers.js'
|
||||||
|
import {
|
||||||
|
resolveBillmanagerProfile,
|
||||||
|
type BillmanagerProfile,
|
||||||
|
} from './profiles/index.js'
|
||||||
|
import type { MappedPayment, MappedVps } from './mappers.js'
|
||||||
|
|
||||||
export interface DashboardInfo {
|
export interface DashboardInfo {
|
||||||
balance: number
|
balance: number
|
||||||
@@ -37,21 +43,60 @@ export interface TariffItem {
|
|||||||
country?: string
|
country?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchVds(baseUrl: string, authinfo: string): Promise<Record<string, string>[]> {
|
function profileFor(baseUrl: string): BillmanagerProfile {
|
||||||
const data = await billmanagerRequest(baseUrl, authinfo, 'vds')
|
return resolveBillmanagerProfile(baseUrl)
|
||||||
const elems = extractList(data, 'vds')
|
}
|
||||||
|
|
||||||
|
/** 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))
|
return elems.map((e) => elemToObject(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchDashboardInfo(
|
export async function fetchDashboardInfo(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
authinfo: string,
|
authinfo: string,
|
||||||
opts: { fallbackCurrency?: string | null } = {},
|
opts: { fallbackCurrency?: string | null; profile?: BillmanagerProfile } = {},
|
||||||
): Promise<DashboardInfo> {
|
): Promise<DashboardInfo> {
|
||||||
const data = await billmanagerRequest(baseUrl, authinfo, 'dashboard.info', {
|
const p = opts.profile ?? profileFor(baseUrl)
|
||||||
|
const data = await billmanagerRequest(
|
||||||
|
baseUrl,
|
||||||
|
authinfo,
|
||||||
|
p.funcs.dashboard,
|
||||||
|
p.requestParams?.dashboard ?? {
|
||||||
dashboard: 'info',
|
dashboard: 'info',
|
||||||
sfrom: 'ajax',
|
sfrom: 'ajax',
|
||||||
})
|
},
|
||||||
|
)
|
||||||
const elems =
|
const elems =
|
||||||
extractList(data, 'dashboard') ||
|
extractList(data, 'dashboard') ||
|
||||||
(Array.isArray(data.elem) ? (data.elem as unknown[]) : [])
|
(Array.isArray(data.elem) ? (data.elem as unknown[]) : [])
|
||||||
@@ -83,32 +128,48 @@ export async function fetchPayments(
|
|||||||
createdate?: string
|
createdate?: string
|
||||||
filter?: string
|
filter?: string
|
||||||
status?: string | number
|
status?: string | number
|
||||||
|
profile?: BillmanagerProfile
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<Record<string, string>[]> {
|
): 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.createdatestart) params.createdatestart = opts.createdatestart
|
||||||
if (opts.createdateend) params.createdateend = opts.createdateend
|
if (opts.createdateend) params.createdateend = opts.createdateend
|
||||||
if (opts.createdate === 'other') params.createdate = 'other'
|
if (opts.createdate === 'other') params.createdate = 'other'
|
||||||
if (opts.filter === 'on') params.filter = 'on'
|
if (opts.filter === 'on') params.filter = 'on'
|
||||||
if (opts.status != null) params.status = String(opts.status)
|
if (opts.status != null) params.status = String(opts.status)
|
||||||
const data = await billmanagerRequest(baseUrl, authinfo, 'payment', params)
|
const data = await billmanagerRequest(baseUrl, authinfo, p.funcs.payments, params)
|
||||||
const elems = extractList(data, 'payment')
|
const elems = extractList(data, p.extract.paymentsKey)
|
||||||
return elems.map((e) => elemToObject(e))
|
return elems.map((e) => elemToObject(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchVdsOrderPricelist(
|
export async function fetchVdsOrderPricelist(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
authinfo: 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> }> {
|
): 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 || '',
|
plid: opts.plid || '',
|
||||||
sfrom: 'ajax',
|
sfrom: 'ajax',
|
||||||
|
...p.requestParams?.orderPricelist,
|
||||||
}
|
}
|
||||||
if (opts.period) params.period = opts.period
|
if (opts.period) params.period = opts.period
|
||||||
if (opts.datacenter) params.datacenter = opts.datacenter
|
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 tariflist = extractTariflist(data)
|
||||||
const listNode = (data.list as Record<string, unknown>) ?? (data.doc as Record<string, unknown>)?.list
|
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(
|
export async function fetchVdsOrderPricelistAllDatacenters(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
authinfo: string,
|
authinfo: string,
|
||||||
|
profile?: BillmanagerProfile,
|
||||||
): Promise<{ tariffItems: TariffItem[]; slist: Record<string, unknown> }> {
|
): 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 slist = initial.slist || {}
|
||||||
const datacenters = Array.isArray(slist.datacenter) ? slist.datacenter : []
|
const datacenters = Array.isArray(slist.datacenter) ? slist.datacenter : []
|
||||||
|
|
||||||
@@ -161,7 +224,12 @@ export async function fetchVdsOrderPricelistAllDatacenters(
|
|||||||
const { country, location } = parseDatacenterName(dcName)
|
const { country, location } = parseDatacenterName(dcName)
|
||||||
|
|
||||||
const result =
|
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) {
|
for (const t of result.tariffItems) {
|
||||||
allTariffItems.push({
|
allTariffItems.push({
|
||||||
...t,
|
...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 type { BillmanagerSyncAccount } from './context.js'
|
||||||
import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance'
|
import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance'
|
||||||
import { mapPaymentToPayment, mapVdsToVps } from './mappers.js'
|
|
||||||
import {
|
import {
|
||||||
fetchDashboardInfo,
|
fetchDashboardInfo,
|
||||||
fetchPayments,
|
fetchPayments,
|
||||||
fetchVds,
|
fetchVds,
|
||||||
fetchVdsOrderPricelistAllDatacenters,
|
fetchVdsOrderPricelistAllDatacenters,
|
||||||
|
mapPaymentWithProfile,
|
||||||
|
mapVdsWithProfile,
|
||||||
type DashboardInfo,
|
type DashboardInfo,
|
||||||
type TariffItem,
|
type TariffItem,
|
||||||
} from './operations.js'
|
} from './operations.js'
|
||||||
|
import { resolveBillmanagerProfile } from './profiles/index.js'
|
||||||
|
|
||||||
export interface SyncFromBillmanagerOptions {
|
export interface SyncFromBillmanagerOptions {
|
||||||
skipTariffs?: boolean
|
skipTariffs?: boolean
|
||||||
@@ -69,6 +71,7 @@ export async function syncFromBillmanager(
|
|||||||
}
|
}
|
||||||
const authinfo = apiCredentials.trim()
|
const authinfo = apiCredentials.trim()
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
|
const profile = resolveBillmanagerProfile(apiBaseUrl)
|
||||||
|
|
||||||
const fetchVpsPayments = !skipVpsPayments
|
const fetchVpsPayments = !skipVpsPayments
|
||||||
const fetchTariffs = !skipTariffs
|
const fetchTariffs = !skipTariffs
|
||||||
@@ -76,16 +79,29 @@ export async function syncFromBillmanager(
|
|||||||
const fallbackCurrency = syncFallbackCurrency(account)
|
const fallbackCurrency = syncFallbackCurrency(account)
|
||||||
|
|
||||||
const [vdsItems, paymentItems, dashboardInfo, tariffResult] = await Promise.all([
|
const [vdsItems, paymentItems, dashboardInfo, tariffResult] = await Promise.all([
|
||||||
fetchVpsPayments ? fetchVds(apiBaseUrl, authinfo) : [],
|
fetchVpsPayments ? fetchVds(apiBaseUrl, authinfo, profile) : [],
|
||||||
fetchVpsPayments ? fetchPayments(apiBaseUrl, authinfo, {}) : [],
|
|
||||||
fetchVpsPayments
|
fetchVpsPayments
|
||||||
? fetchDashboardInfo(apiBaseUrl, authinfo, { fallbackCurrency }).catch(() => null)
|
? fetchPayments(apiBaseUrl, authinfo, { profile })
|
||||||
|
: [],
|
||||||
|
fetchVpsPayments
|
||||||
|
? fetchDashboardInfo(apiBaseUrl, authinfo, {
|
||||||
|
fallbackCurrency,
|
||||||
|
profile,
|
||||||
|
}).catch(() => null)
|
||||||
: null,
|
: null,
|
||||||
fetchTariffs
|
fetchTariffs
|
||||||
? fetchVdsOrderPricelistAllDatacenters(apiBaseUrl, authinfo).catch((err) => {
|
? fetchVdsOrderPricelistAllDatacenters(apiBaseUrl, authinfo, profile).catch(
|
||||||
console.warn('fetchVdsOrderPricelistAllDatacenters failed:', err instanceof Error ? err.message : err)
|
(err) => {
|
||||||
return { tariffItems: [] as TariffItem[], slist: {} as Record<string, unknown> }
|
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> },
|
: { tariffItems: [] as TariffItem[], slist: {} as Record<string, unknown> },
|
||||||
])
|
])
|
||||||
const { tariffItems = [], slist = {} } = tariffResult || {}
|
const { tariffItems = [], slist = {} } = tariffResult || {}
|
||||||
@@ -95,7 +111,7 @@ export async function syncFromBillmanager(
|
|||||||
|
|
||||||
if (fetchVpsPayments) {
|
if (fetchVpsPayments) {
|
||||||
for (const item of vdsItems) {
|
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 id = `vps-bm-${accountId}-${vps.externalId}`
|
||||||
const additionalIps = JSON.stringify(vps.additionalIps || [])
|
const additionalIps = JSON.stringify(vps.additionalIps || [])
|
||||||
const dailyRate = vps.dailyRate
|
const dailyRate = vps.dailyRate
|
||||||
@@ -229,7 +245,7 @@ export async function syncFromBillmanager(
|
|||||||
)
|
)
|
||||||
|
|
||||||
for (const item of paymentItems) {
|
for (const item of paymentItems) {
|
||||||
const payment = mapPaymentToPayment(item, accountId)
|
const payment = mapPaymentWithProfile(profile, item, accountId)
|
||||||
if (!payment || payment.amount <= 0) continue
|
if (!payment || payment.amount <= 0) continue
|
||||||
const note = payment.note
|
const note = payment.note
|
||||||
if (existingPayments.has(note)) continue
|
if (existingPayments.has(note)) continue
|
||||||
|
|||||||
Reference in New Issue
Block a user