From b7a4a10333962f5f1cf165f38a991efdfae5ef9d Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 20 Jul 2026 00:30:15 +0700 Subject: [PATCH] =?UTF-8?q?feat(billmanager):=20=D0=BF=D1=80=D0=BE=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D0=B8=20ihor,=20cloudrix,=20servhost,=20landvps,?= =?UTF-8?q?=20datacheap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Гео из datacentername, specs через vds.order/vds.edit; парсер ДЦ/городов РФ. Co-authored-by: Cursor --- apps/api/src/services/billmanager/parsers.ts | 23 +- .../src/services/billmanager/profiles.test.ts | 351 ++++++++++++++++++ .../services/billmanager/profiles/README.md | 5 + .../services/billmanager/profiles/cloudrix.ts | 57 +++ .../billmanager/profiles/datacheap.ts | 56 +++ .../src/services/billmanager/profiles/ihor.ts | 42 +++ .../services/billmanager/profiles/index.ts | 5 + .../services/billmanager/profiles/landvps.ts | 56 +++ .../services/billmanager/profiles/registry.ts | 10 + .../services/billmanager/profiles/servhost.ts | 54 +++ 10 files changed, 658 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/services/billmanager/profiles/cloudrix.ts create mode 100644 apps/api/src/services/billmanager/profiles/datacheap.ts create mode 100644 apps/api/src/services/billmanager/profiles/ihor.ts create mode 100644 apps/api/src/services/billmanager/profiles/landvps.ts create mode 100644 apps/api/src/services/billmanager/profiles/servhost.ts diff --git a/apps/api/src/services/billmanager/parsers.ts b/apps/api/src/services/billmanager/parsers.ts index 0b756fa..001ca4f 100644 --- a/apps/api/src/services/billmanager/parsers.ts +++ b/apps/api/src/services/billmanager/parsers.ts @@ -207,7 +207,28 @@ export function parseDatacenterName(dcName: unknown): { country: string; locatio } } - if (/ММТС|Adman|Москва/i.test(s)) { + // «Москва DC3», «ДЦ Москва», «Новосибирск Xeon» — город РФ (+ опц. префикс ДЦ / суффикс) + // Без \b: в JS word-boundary не срабатывает на кириллице без флага u. + const RU_CITY_CANON: Record = { + москва: 'Москва', + 'санкт-петербург': 'Санкт-Петербург', + спб: 'Санкт-Петербург', + новосибирск: 'Новосибирск', + екатеринбург: 'Екатеринбург', + казань: 'Казань', + самара: 'Самара', + краснодар: 'Краснодар', + } + const cityHaystack = s.replace(/^ДЦ\s+/i, '').trim() + const ruCityMatch = cityHaystack.match( + /^(Москва|Санкт-Петербург|СПб|Новосибирск|Екатеринбург|Казань|Самара|Краснодар)(?:\s|$)/i, + ) + if (ruCityMatch) { + const key = ruCityMatch[1]!.toLowerCase() + return { country: 'Россия', location: RU_CITY_CANON[key] || ruCityMatch[1]! } + } + + if (/ММТС|Adman/i.test(s)) { return { country: 'Россия', location: s } } if (/Европа/i.test(s)) { diff --git a/apps/api/src/services/billmanager/profiles.test.ts b/apps/api/src/services/billmanager/profiles.test.ts index 9c90098..c428b14 100644 --- a/apps/api/src/services/billmanager/profiles.test.ts +++ b/apps/api/src/services/billmanager/profiles.test.ts @@ -95,6 +95,138 @@ const FIRSTBYTE_VDS_FIXTURE = { ], } +/** Minimal ihor-style bjson (`datacentername`: «Москва DC3»). */ +const IHOR_VDS_FIXTURE = { + func: 'vds', + elem: [ + { + id: '2927366', + ip: '185.105.116.43', + domain: 'vds2927366.my-ihor.ru', + expiredate: '2026-08-08', + real_expiredate: '2026-08-08', + datacenter: '1', + datacentername: 'Москва DC3', + ostempl: 'Ubuntu-26.04-amd64', + pricelist: 'KVM SSD Start (1 СPU/768 MB RAM/7 GB SSD)', + pricelist_id: '7818', + intname: 'SSD_Start', + cost: '100.00 RUB / Месяц', + item_cost: '100.0000', + currency_str: 'RUB', + createdate: '2026-03-08', + item_status_orig: '2', + item_status: 'Активен', + }, + ], +} + +/** Minimal Cloudrix-style bjson — promo plan without CPU/RAM in pricelist name. */ +const CLOUDRIX_VDS_FIXTURE = { + func: 'vds', + elem: [ + { + id: '8226', + ip: '95.85.240.37', + domain: 'msk.shz.su', + expiredate: '2026-08-18', + real_expiredate: '2026-08-18', + datacenter: '1', + datacentername: 'Россия, Москва', + ipv6_subnet: '2a12:bec4:1280:7e::/64', + ostempl: 'Alma Linux 10', + pricelist: 'SPROMO-1', + pricelist_id: '1', + intname: 'spromo', + cost: '160.00 RUB / Месяц', + item_cost: '160.0000', + currency_str: 'RUB', + createdate: '2026-05-18', + item_status_orig: '2', + item_status: 'Активен', + }, + ], +} + +/** Minimal serv.host bjson — named Xeon plan, city+CPU line in datacentername. */ +const SERVHOST_VDS_FIXTURE = { + func: 'vds', + elem: [ + { + id: '621601', + ip: '178.250.186.111', + domain: 'shatskiy.denis.serv.host', + expiredate: '2027-06-16', + real_expiredate: '2027-06-16', + datacenter: '5', + datacentername: 'Новосибирск Xeon', + ostempl: 'Ubuntu 24.04', + pricelist: 'Тариф «Xeon 2667V2-1»', + pricelist_id: '502', + intname: 'Тариф «Xeon 2667V2-1»', + cost: '127.50 RUB / Месяц', + item_cost: '1530.0000', + currency_str: 'RUB', + createdate: '2026-02-16', + item_status_orig: '2', + item_status: 'Активен', + }, + ], +} + +/** Minimal LandVPS bjson — `ДЦ Москва`, tariff code MSK-0, ipv6_subnet. */ +const LANDVPS_VDS_FIXTURE = { + func: 'vds', + elem: [ + { + id: '196380', + ip: '77.105.133.124', + domain: '196380.landvps.online', + expiredate: '2026-08-21', + real_expiredate: '2026-08-21', + datacenter: '3', + datacentername: 'ДЦ Москва', + ipv6_subnet: '2a12:b3c0:1:b6::/64', + ostempl: 'Ubuntu 24.04', + pricelist: 'MSK-0', + pricelist_id: '19', + intname: 'RUS-0', + cost: '90.00 RUB / Месяц', + item_cost: '270.0000', + currency_str: 'RUB', + createdate: '2026-04-21', + item_status_orig: '2', + item_status: 'Активен', + }, + ], +} + +/** Minimal Datacheap bjson — country-only DC, NVMe plan without CPU/RAM in name. */ +const DATACHEAP_VDS_FIXTURE = { + func: 'vds', + elem: [ + { + id: '189678', + ip: '84.252.100.102', + domain: 'dc.msk.rt.shx.su', + expiredate: '2026-08-21', + real_expiredate: '2026-08-21', + datacenter: '8', + datacentername: 'Россия', + ostempl: 'Ubuntu 24.04', + pricelist: 'VPS Start NVMe unlimited (RU)', + pricelist_id: '2489', + intname: 'NVMe unlim', + cost: '203.70 RUB / Месяц', + item_cost: '611.1000', + currency_str: 'RUB', + createdate: '2026-01-21', + item_status_orig: '2', + item_status: 'Активен', + }, + ], +} + describe('billmanager profiles', () => { afterEach(() => { vi.unstubAllGlobals() @@ -122,6 +254,13 @@ describe('billmanager profiles', () => { expect(p.map.enrichVds).toBeTypeOf('function') }) + it('resolve: ihor hostname → ihor profile', () => { + const p = resolveBillmanagerProfile('https://bill.my-ihor.ru/') + expect(p.id).toBe('ihor') + expect(p.funcs.listVds).toBe('vds') + expect(p.map.enrichVds).toBeTypeOf('function') + }) + it('resolve: unknown hoster → default', () => { const p = resolveBillmanagerProfile('https://bill.hoster.ru/') expect(p.id).toBe('default') @@ -143,6 +282,218 @@ describe('billmanager profiles', () => { }) }) + it('parseDatacenterName: ihor «Москва DC3»', () => { + expect(parseDatacenterName('Москва DC3')).toEqual({ + country: 'Россия', + location: 'Москва', + }) + }) + + it('ihor fixture → country/city from datacentername', () => { + const profile = resolveBillmanagerProfile('https://my-ihor.ru/') + const elems = extractList(IHOR_VDS_FIXTURE, profile.extract.listVdsKey) + expect(elems).toHaveLength(1) + const vps = mapVdsWithProfile( + profile, + elemToObject(elems[0]!), + 'prov-ihor', + 'acc-ihor', + ) + expect(vps.externalId).toBe('2927366') + expect(vps.ip).toBe('185.105.116.43') + expect(vps.country).toBe('Россия') + expect(vps.city).toBe('Москва') + expect(vps.datacenter).toBe('Москва DC3') + expect(vps.os).toBe('Ubuntu-26.04-amd64') + expect(vps.paidUntil).toBe('2026-08-08') + expect(vps.status).toBe('active') + }) + + it('resolve: cloudrix hostname → cloudrix profile', () => { + const p = resolveBillmanagerProfile('https://bill.cloudrix.ru/') + expect(p.id).toBe('cloudrix') + expect(p.funcs.listVds).toBe('vds') + expect(p.map.enrichVds).toBeTypeOf('function') + expect(p.options?.fetchVdsEditForSpecs).toBe(true) + }) + + it('Cloudrix fixture → geo, ipv6; specs from vds.order by pricelist_id', () => { + const profile = resolveBillmanagerProfile('https://cloudrix.ru/') + const elems = extractList(CLOUDRIX_VDS_FIXTURE, profile.extract.listVdsKey) + expect(elems).toHaveLength(1) + const item = elemToObject(elems[0]!) + let vps = mapVdsWithProfile(profile, item, 'prov-cr', 'acc-cr') + expect(vps.externalId).toBe('8226') + expect(vps.country).toBe('Россия') + expect(vps.city).toBe('Москва') + expect(vps.datacenter).toBe('Россия, Москва') + expect(vps.ipv6).toBe('2a12:bec4:1280:7e::/64') + expect(vps.os).toBe('Alma Linux 10') + expect(vps.vcpu).toBe(0) + expect(vps.ramGb).toBe(0) + expect(vps.diskGb).toBe(0) + + vps = enrichMappedVpsFromTariffs(item, vps, [ + { + externalId: '1', + name: 'SPROMO-1', + desc: '1 CPU / 1 GB RAM / 15 GB NVMe', + vcpu: 1, + ramGb: 1, + diskGb: 15, + diskType: 'NVMe', + virtualization: 'KVM', + }, + ]) + expect(vps.vcpu).toBe(1) + expect(vps.ramGb).toBe(1) + expect(vps.diskGb).toBe(15) + expect(vps.diskType).toBe('NVMe') + expect(vps.virtualization).toBe('KVM') + }) + + it('resolve: serv.host → servhost profile', () => { + const p = resolveBillmanagerProfile('https://bill.serv.host/') + expect(p.id).toBe('servhost') + expect(p.funcs.listVds).toBe('vds') + expect(p.options?.fetchVdsEditForSpecs).toBe(true) + }) + + it('parseDatacenterName: serv.host «Новосибирск Xeon»', () => { + expect(parseDatacenterName('Новосибирск Xeon')).toEqual({ + country: 'Россия', + location: 'Новосибирск', + }) + }) + + it('serv.host fixture → geo; specs from vds.order by pricelist_id', () => { + const profile = resolveBillmanagerProfile('https://my.serv.host/') + const elems = extractList(SERVHOST_VDS_FIXTURE, profile.extract.listVdsKey) + expect(elems).toHaveLength(1) + const item = elemToObject(elems[0]!) + let vps = mapVdsWithProfile(profile, item, 'prov-sh', 'acc-sh') + expect(vps.externalId).toBe('621601') + expect(vps.country).toBe('Россия') + expect(vps.city).toBe('Новосибирск') + expect(vps.datacenter).toBe('Новосибирск Xeon') + expect(vps.os).toBe('Ubuntu 24.04') + expect(vps.vcpu).toBe(0) + expect(vps.diskGb).toBe(0) + + vps = enrichMappedVpsFromTariffs(item, vps, [ + { + externalId: '502', + name: 'Тариф «Xeon 2667V2-1»', + desc: '2 CPU / 2 GB RAM / 40 GB NVMe', + vcpu: 2, + ramGb: 2, + diskGb: 40, + diskType: 'NVMe', + virtualization: 'KVM', + }, + ]) + expect(vps.vcpu).toBe(2) + expect(vps.ramGb).toBe(2) + expect(vps.diskGb).toBe(40) + expect(vps.virtualization).toBe('KVM') + }) + + it('resolve: landvps.online → landvps profile', () => { + const p = resolveBillmanagerProfile('https://bill.landvps.online/') + expect(p.id).toBe('landvps') + expect(p.funcs.listVds).toBe('vds') + expect(p.options?.fetchVdsEditForSpecs).toBe(true) + }) + + it('parseDatacenterName: LandVPS «ДЦ Москва»', () => { + expect(parseDatacenterName('ДЦ Москва')).toEqual({ + country: 'Россия', + location: 'Москва', + }) + }) + + it('LandVPS fixture → geo, ipv6; specs from vds.order by pricelist_id', () => { + const profile = resolveBillmanagerProfile('https://my.landvps.online/') + const elems = extractList(LANDVPS_VDS_FIXTURE, profile.extract.listVdsKey) + expect(elems).toHaveLength(1) + const item = elemToObject(elems[0]!) + let vps = mapVdsWithProfile(profile, item, 'prov-lv', 'acc-lv') + expect(vps.externalId).toBe('196380') + expect(vps.country).toBe('Россия') + expect(vps.city).toBe('Москва') + expect(vps.datacenter).toBe('ДЦ Москва') + expect(vps.ipv6).toBe('2a12:b3c0:1:b6::/64') + expect(vps.os).toBe('Ubuntu 24.04') + expect(vps.paidUntil).toBe('2026-08-21') + expect(vps.vcpu).toBe(0) + expect(vps.diskGb).toBe(0) + + vps = enrichMappedVpsFromTariffs(item, vps, [ + { + externalId: '19', + name: 'MSK-0', + desc: '1 CPU / 1 GB RAM / 20 GB NVMe', + vcpu: 1, + ramGb: 1, + diskGb: 20, + diskType: 'NVMe', + virtualization: 'KVM', + }, + ]) + expect(vps.vcpu).toBe(1) + expect(vps.ramGb).toBe(1) + expect(vps.diskGb).toBe(20) + expect(vps.virtualization).toBe('KVM') + }) + + it('resolve: datacheap.ru → datacheap profile', () => { + const p = resolveBillmanagerProfile('https://bill.datacheap.ru/') + expect(p.id).toBe('datacheap') + expect(p.funcs.listVds).toBe('vds') + expect(p.options?.fetchVdsEditForSpecs).toBe(true) + }) + + it('parseDatacenterName: Datacheap «Россия»', () => { + expect(parseDatacenterName('Россия')).toEqual({ + country: 'Россия', + location: '', + }) + }) + + it('Datacheap fixture → country; specs from vds.order by pricelist_id', () => { + const profile = resolveBillmanagerProfile('https://my.datacheap.ru/') + const elems = extractList(DATACHEAP_VDS_FIXTURE, profile.extract.listVdsKey) + expect(elems).toHaveLength(1) + const item = elemToObject(elems[0]!) + let vps = mapVdsWithProfile(profile, item, 'prov-dc', 'acc-dc') + expect(vps.externalId).toBe('189678') + expect(vps.country).toBe('Россия') + expect(vps.city).toBe('') + expect(vps.datacenter).toBe('Россия') + expect(vps.os).toBe('Ubuntu 24.04') + expect(vps.paidUntil).toBe('2026-08-21') + expect(vps.vcpu).toBe(0) + expect(vps.diskGb).toBe(0) + + vps = enrichMappedVpsFromTariffs(item, vps, [ + { + externalId: '2489', + name: 'VPS Start NVMe unlimited (RU)', + desc: '1 CPU / 1 GB RAM / 15 GB NVMe', + vcpu: 1, + ramGb: 1, + diskGb: 15, + diskType: 'NVMe', + virtualization: 'KVM', + }, + ]) + expect(vps.vcpu).toBe(1) + expect(vps.ramGb).toBe(1) + expect(vps.diskGb).toBe(15) + expect(vps.diskType).toBe('NVMe') + expect(vps.virtualization).toBe('KVM') + }) + it('Waicore fixture elem → MappedVps fields + geo from datacentername', () => { const profile = resolveBillmanagerProfile('https://my.waicore.com/') const elems = extractList(WAICORE_VDS_FIXTURE, profile.extract.listVdsKey) diff --git a/apps/api/src/services/billmanager/profiles/README.md b/apps/api/src/services/billmanager/profiles/README.md index 924cadc..01559a4 100644 --- a/apps/api/src/services/billmanager/profiles/README.md +++ b/apps/api/src/services/billmanager/profiles/README.md @@ -51,3 +51,8 @@ Sync и operations **не** содержат `if (hoster)` — только пр |----|-------|-------------------| | `waicore` | `waicore.com` / keyword | `funcs.listVds = vds.vps`; geo из `datacentername` (`[DE] Город \| …` / `Страна, Город`) | | `firstbyte` | `firstbyte.ru`, `firstbyte.club`, `1byte.ru` | `enrichVds`: страна/город, daily из `billdaily`/`Ежедневное списание`, `dailyRate=cost/30`; `enrichVdsBatch`: общий баланс ÷ сумма daily → один `paidUntil`; specs из `vds.order` / `vds.edit` | +| `ihor` | `my-ihor.ru`, `ihor.ru` / keyword | `enrichVds`: geo из `datacentername` (`Москва DC3` → Россия / Москва) | +| `cloudrix` | `cloudrix.ru` / keyword | `enrichVds`: geo (`Россия, Москва`) + `ipv6_subnet`; specs через `vds.order` / `vds.edit` (`fetchVdsEditForSpecs`) | +| `servhost` | `serv.host` | `enrichVds`: geo (`Новосибирск Xeon` → Россия / Новосибирск); specs через `vds.order` / `vds.edit` | +| `landvps` | `landvps.online` / keyword | `enrichVds`: geo (`ДЦ Москва` → Россия / Москва) + `ipv6_subnet`; specs через `vds.order` / `vds.edit` | +| `datacheap` | `datacheap.ru` / keyword | `enrichVds`: geo (`Россия`); specs через `vds.order` / `vds.edit` | diff --git a/apps/api/src/services/billmanager/profiles/cloudrix.ts b/apps/api/src/services/billmanager/profiles/cloudrix.ts new file mode 100644 index 0000000..582432b --- /dev/null +++ b/apps/api/src/services/billmanager/profiles/cloudrix.ts @@ -0,0 +1,57 @@ +import { parseDatacenterName } from '../parsers.js' +import type { MappedVps } from '../mappers.js' +import type { + BillmanagerMapOverrides, + BillmanagerProfileOverrides, +} from './types.js' + +/** + * Geo + IPv6 from Cloudrix list row. + * - `datacentername`: `Россия, Москва` → Россия / Москва + * - `ipv6_subnet`: `2a12:…::/64` + * + * Hardware (vcpu/ram/disk): list has only `pricelist` like SPROMO-1 — + * filled in sync via vds.order match and `vds.edit` (fetchVdsEditForSpecs). + */ +export function enrichCloudrixVds( + item: Record, + mapped: MappedVps, +): MappedVps { + const dcRaw = (item.datacentername || item.datacenter || mapped.datacenter || '').trim() + const { country, location } = dcRaw + ? parseDatacenterName(dcRaw) + : { country: '', location: '' } + + const ipv6 = (item.ipv6_subnet || item.ipv6 || mapped.ipv6 || '').trim() + + return { + ...mapped, + country: country || mapped.country, + city: location || mapped.city, + datacenter: dcRaw || mapped.datacenter, + ipv6: ipv6 || mapped.ipv6, + // Promo / named plans rarely encode disk type — prefer NVMe when unknown + diskType: mapped.diskGb ? mapped.diskType : mapped.diskType || 'NVMe', + virtualization: mapped.virtualization || 'KVM', + } +} + +const cloudrixMap: BillmanagerMapOverrides = { + enrichVds: enrichCloudrixVds, +} + +/** + * Cloudrix — standard func=vds; geo from datacentername; + * specs via vds.order + vds.edit (no CPU/RAM in list pricelist name). + */ +export const cloudrixOverrides: BillmanagerProfileOverrides = { + id: 'cloudrix', + match: { + hostnames: ['cloudrix.ru', 'cloudrix.space'], + keywords: ['cloudrix'], + }, + map: cloudrixMap, + options: { + fetchVdsEditForSpecs: true, + }, +} diff --git a/apps/api/src/services/billmanager/profiles/datacheap.ts b/apps/api/src/services/billmanager/profiles/datacheap.ts new file mode 100644 index 0000000..6d43484 --- /dev/null +++ b/apps/api/src/services/billmanager/profiles/datacheap.ts @@ -0,0 +1,56 @@ +import { parseDatacenterName } from '../parsers.js' +import type { MappedVps } from '../mappers.js' +import type { + BillmanagerMapOverrides, + BillmanagerProfileOverrides, +} from './types.js' + +/** + * Geo from Datacheap list row. + * - `datacentername`: `Россия` (country only; city empty) + * - `ipv6_subnet` when present + * + * Hardware: `pricelist` like `VPS Start NVMe unlimited (RU)` has no CPU/RAM — + * filled via vds.order + vds.edit (fetchVdsEditForSpecs). + */ +export function enrichDatacheapVds( + item: Record, + mapped: MappedVps, +): MappedVps { + const dcRaw = (item.datacentername || item.datacenter || mapped.datacenter || '').trim() + const { country, location } = dcRaw + ? parseDatacenterName(dcRaw) + : { country: '', location: '' } + + const ipv6 = (item.ipv6_subnet || item.ipv6 || mapped.ipv6 || '').trim() + + return { + ...mapped, + country: country || mapped.country, + city: location || mapped.city, + datacenter: dcRaw || mapped.datacenter, + ipv6: ipv6 || mapped.ipv6, + diskType: mapped.diskGb ? mapped.diskType : mapped.diskType || 'NVMe', + virtualization: mapped.virtualization || 'KVM', + } +} + +const datacheapMap: BillmanagerMapOverrides = { + enrichVds: enrichDatacheapVds, +} + +/** + * Datacheap — standard func=vds; geo from datacentername; + * specs via vds.order + vds.edit. + */ +export const datacheapOverrides: BillmanagerProfileOverrides = { + id: 'datacheap', + match: { + hostnames: ['datacheap.ru', 'datacheap.com'], + keywords: ['datacheap'], + }, + map: datacheapMap, + options: { + fetchVdsEditForSpecs: true, + }, +} diff --git a/apps/api/src/services/billmanager/profiles/ihor.ts b/apps/api/src/services/billmanager/profiles/ihor.ts new file mode 100644 index 0000000..3aa3717 --- /dev/null +++ b/apps/api/src/services/billmanager/profiles/ihor.ts @@ -0,0 +1,42 @@ +import { parseDatacenterName } from '../parsers.js' +import type { MappedVps } from '../mappers.js' +import type { + BillmanagerMapOverrides, + BillmanagerProfileOverrides, +} from './types.js' + +/** + * Geo from ihor `datacentername`, e.g. `Москва DC3` → Россия / Москва. + */ +export function enrichIhorVds( + item: Record, + mapped: MappedVps, +): MappedVps { + const dcRaw = (item.datacentername || item.datacenter || mapped.datacenter || '').trim() + if (!dcRaw) return mapped + + const { country, location } = parseDatacenterName(dcRaw) + return { + ...mapped, + country: country || mapped.country, + city: location || mapped.city, + datacenter: dcRaw || mapped.datacenter, + } +} + +const ihorMap: BillmanagerMapOverrides = { + enrichVds: enrichIhorVds, +} + +/** + * ihor (my-ihor.ru) — standard func=vds. + * Geo from datacentername via enrichVds. + */ +export const ihorOverrides: BillmanagerProfileOverrides = { + id: 'ihor', + match: { + hostnames: ['my-ihor.ru', 'ihor.ru'], + keywords: ['ihor'], + }, + map: ihorMap, +} diff --git a/apps/api/src/services/billmanager/profiles/index.ts b/apps/api/src/services/billmanager/profiles/index.ts index 7b31da0..bd8787b 100644 --- a/apps/api/src/services/billmanager/profiles/index.ts +++ b/apps/api/src/services/billmanager/profiles/index.ts @@ -11,4 +11,9 @@ export type { BillmanagerProfileOverrides, BillmanagerRequestParams, } from './types.js' +export { enrichCloudrixVds, cloudrixOverrides } from './cloudrix.js' +export { enrichDatacheapVds, datacheapOverrides } from './datacheap.js' +export { enrichIhorVds, ihorOverrides } from './ihor.js' +export { enrichLandvpsVds, landvpsOverrides } from './landvps.js' +export { enrichServhostVds, servhostOverrides } from './servhost.js' export { waicoreOverrides } from './waicore.js' diff --git a/apps/api/src/services/billmanager/profiles/landvps.ts b/apps/api/src/services/billmanager/profiles/landvps.ts new file mode 100644 index 0000000..e0e5f71 --- /dev/null +++ b/apps/api/src/services/billmanager/profiles/landvps.ts @@ -0,0 +1,56 @@ +import { parseDatacenterName } from '../parsers.js' +import type { MappedVps } from '../mappers.js' +import type { + BillmanagerMapOverrides, + BillmanagerProfileOverrides, +} from './types.js' + +/** + * Geo + IPv6 from LandVPS list row. + * - `datacentername`: `ДЦ Москва` → Россия / Москва + * - `ipv6_subnet` + * + * Hardware: `pricelist` like `MSK-0` has no CPU/RAM — + * filled via vds.order + vds.edit (fetchVdsEditForSpecs). + */ +export function enrichLandvpsVds( + item: Record, + mapped: MappedVps, +): MappedVps { + const dcRaw = (item.datacentername || item.datacenter || mapped.datacenter || '').trim() + const { country, location } = dcRaw + ? parseDatacenterName(dcRaw) + : { country: '', location: '' } + + const ipv6 = (item.ipv6_subnet || item.ipv6 || mapped.ipv6 || '').trim() + + return { + ...mapped, + country: country || mapped.country, + city: location || mapped.city, + datacenter: dcRaw || mapped.datacenter, + ipv6: ipv6 || mapped.ipv6, + diskType: mapped.diskGb ? mapped.diskType : mapped.diskType || 'NVMe', + virtualization: mapped.virtualization || 'KVM', + } +} + +const landvpsMap: BillmanagerMapOverrides = { + enrichVds: enrichLandvpsVds, +} + +/** + * LandVPS — standard func=vds; geo from datacentername; + * specs via vds.order + vds.edit. + */ +export const landvpsOverrides: BillmanagerProfileOverrides = { + id: 'landvps', + match: { + hostnames: ['landvps.online', 'landvps.ru', 'landvps.com'], + keywords: ['landvps'], + }, + map: landvpsMap, + options: { + fetchVdsEditForSpecs: true, + }, +} diff --git a/apps/api/src/services/billmanager/profiles/registry.ts b/apps/api/src/services/billmanager/profiles/registry.ts index 72ce240..e87e41f 100644 --- a/apps/api/src/services/billmanager/profiles/registry.ts +++ b/apps/api/src/services/billmanager/profiles/registry.ts @@ -1,6 +1,11 @@ import { DEFAULT_PROFILE } from './default.js' +import { cloudrixOverrides } from './cloudrix.js' +import { datacheapOverrides } from './datacheap.js' import { firstbyteOverrides } from './firstbyte.js' +import { ihorOverrides } from './ihor.js' +import { landvpsOverrides } from './landvps.js' import { mergeProfile } from './merge.js' +import { servhostOverrides } from './servhost.js' import type { BillmanagerProfile, BillmanagerProfileOverrides } from './types.js' import { waicoreOverrides } from './waicore.js' @@ -11,6 +16,11 @@ import { waicoreOverrides } from './waicore.js' export const PROFILE_OVERRIDES: BillmanagerProfileOverrides[] = [ waicoreOverrides, firstbyteOverrides, + ihorOverrides, + cloudrixOverrides, + servhostOverrides, + landvpsOverrides, + datacheapOverrides, ] function matchesUrl(url: string, override: BillmanagerProfileOverrides): boolean { diff --git a/apps/api/src/services/billmanager/profiles/servhost.ts b/apps/api/src/services/billmanager/profiles/servhost.ts new file mode 100644 index 0000000..3559a23 --- /dev/null +++ b/apps/api/src/services/billmanager/profiles/servhost.ts @@ -0,0 +1,54 @@ +import { parseDatacenterName } from '../parsers.js' +import type { MappedVps } from '../mappers.js' +import type { + BillmanagerMapOverrides, + BillmanagerProfileOverrides, +} from './types.js' + +/** + * Geo from serv.host `datacentername`, e.g. `Новосибирск Xeon` → Россия / Новосибирск. + * + * Hardware: list `pricelist` like `Тариф «Xeon 2667V2-1»` has no CPU/RAM/disk — + * filled via vds.order + vds.edit (fetchVdsEditForSpecs). + */ +export function enrichServhostVds( + item: Record, + mapped: MappedVps, +): MappedVps { + const dcRaw = (item.datacentername || item.datacenter || mapped.datacenter || '').trim() + const { country, location } = dcRaw + ? parseDatacenterName(dcRaw) + : { country: '', location: '' } + + const ipv6 = (item.ipv6_subnet || item.ipv6 || mapped.ipv6 || '').trim() + + return { + ...mapped, + country: country || mapped.country, + city: location || mapped.city, + datacenter: dcRaw || mapped.datacenter, + ipv6: ipv6 || mapped.ipv6, + diskType: mapped.diskGb ? mapped.diskType : mapped.diskType || 'NVMe', + virtualization: mapped.virtualization || 'KVM', + } +} + +const servhostMap: BillmanagerMapOverrides = { + enrichVds: enrichServhostVds, +} + +/** + * serv.host — standard func=vds; geo from datacentername; + * specs via vds.order + vds.edit. + */ +export const servhostOverrides: BillmanagerProfileOverrides = { + id: 'servhost', + match: { + hostnames: ['serv.host'], + keywords: ['serv.host'], + }, + map: servhostMap, + options: { + fetchVdsEditForSpecs: true, + }, +}