Files
vps-tracker/apps/api/adapters/billmanager/sync.js
T
DenozordecandCursor 6fbd1a9113 refactor(repo): переход на pnpm monorepo с shadcn/ui и Fastify+Drizzle
Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom

Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)

Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo

Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 13:42:05 +07:00

256 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Sync BILLmanager data into vps-tracker DB
*/
import { fetchVds, fetchPayments, fetchDashboardInfo, fetchVdsOrderPricelistAllDatacenters } from './operations.js'
import { mapVdsToVps, mapPaymentToPayment } from './mappers.js'
/**
* Sync BILLmanager data into vps-tracker DB
* @param {object} account - provider_account с apiCredentials и apiBaseUrl (URL с хостера или уже подставленный)
* @param {object} db - getDb() wrapper
* @param {object} [opts] - { paymentDaysBack, skipTariffs, skipVpsPayments }
* @returns {{ vpsCount: number, paymentsCount: number, tariffsCount: number, balance?: object }}
*/
export async function syncFromBillmanager(account, db, opts = {}) {
const { skipTariffs = false, skipVpsPayments = false } = opts
const { apiBaseUrl, apiCredentials, providerId, id: accountId } = account
if (!apiBaseUrl?.trim() || !apiCredentials?.trim()) {
throw new Error('API URL and credentials are required')
}
const authinfo = apiCredentials.trim()
const fetchVpsPayments = !skipVpsPayments
const fetchTariffs = !skipTariffs
const [vdsItems, paymentItems, dashboardInfo, tariffResult] = await Promise.all([
fetchVpsPayments ? fetchVds(apiBaseUrl, authinfo) : [],
fetchVpsPayments ? fetchPayments(apiBaseUrl, authinfo, {}) : [],
fetchVpsPayments ? fetchDashboardInfo(apiBaseUrl, authinfo, { fallbackCurrency: account.currency }).catch(() => null) : null,
fetchTariffs ? fetchVdsOrderPricelistAllDatacenters(apiBaseUrl, authinfo).catch((err) => {
console.warn('fetchVdsOrderPricelistAllDatacenters failed:', err.message)
return { tariffItems: [], slist: {} }
}) : { tariffItems: [], slist: {} },
])
const { tariffItems = [], slist = {} } = tariffResult || {}
let vpsCount = 0
/** @type {{ added: { id: string, label: string }[], updated: { id: string, label: string, fields: string[] }[], paymentsAdded: number }} */
const syncSummary = { added: [], updated: [], paymentsAdded: 0 }
if (fetchVpsPayments) {
const vpsInsertSql = `INSERT INTO vps (id, ip, ipv6, additionalIps, dns, providerId, providerAccountId, country, city, datacenter, os, vcpu, ramGb, diskGb, diskType, virtualization, bandwidthTb, sshPort, rootUser, purpose, environment, project, projectId, monitoringEnabled, backupEnabled, status, tariffType, currency, dailyRate, monthlyRate, createdAt, paidUntil, notes, userOverrides)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
const vpsUpdateSql = `UPDATE vps SET ip=?, ipv6=?, additionalIps=?, dns=?, country=?, city=?, datacenter=?, os=?, status=?, tariffType=?, currency=?, dailyRate=?, monthlyRate=?, paidUntil=?, notes=?
WHERE id=?`
const SYNC_UPDATE_FIELDS = ['country', 'city', 'datacenter', 'os', 'notes', 'status', 'tariffType', 'currency', 'dailyRate', 'monthlyRate', 'paidUntil']
const normVal = (v) => {
if (v == null || v === '') return ''
if (typeof v === 'number') return Number.isFinite(v) ? String(v) : ''
return String(v)
}
for (const item of vdsItems) {
const vps = mapVdsToVps(item, providerId, accountId)
const id = `vps-bm-${accountId}-${vps.externalId}`
const additionalIps = JSON.stringify(vps.additionalIps || [])
const dailyRate = vps.dailyRate
const monthlyRate = vps.monthlyRate
const paidUntil = vps.paidUntil || ''
const notes = vps.notes ? `${vps.notes} [bm-${vps.externalId}]` : `bm-${vps.externalId}`
const existing = db.prepare('SELECT * FROM vps WHERE providerAccountId = ? AND (ip = ? OR notes LIKE ?)').get(accountId, vps.ip, `%bm-${vps.externalId}%`)
if (existing) {
let userOverrides = []
try {
userOverrides = existing.userOverrides ? JSON.parse(existing.userOverrides) : []
} catch {
userOverrides = []
}
const merged = {
ip: vps.ip,
ipv6: vps.ipv6,
additionalIps,
dns: vps.dns,
country: vps.country,
city: vps.city,
datacenter: vps.datacenter,
os: vps.os,
status: vps.status,
tariffType: vps.tariffType,
currency: vps.currency,
dailyRate,
monthlyRate,
paidUntil,
notes,
}
for (const f of SYNC_UPDATE_FIELDS) {
if (userOverrides.includes(f)) {
merged[f] = existing[f]
}
}
const compareFields = ['ip', 'ipv6', 'dns', ...SYNC_UPDATE_FIELDS]
const changedFields = compareFields.filter(
(f) => normVal(merged[f]) !== normVal(existing[f]),
)
if (changedFields.length > 0) {
const label = merged.dns || merged.ip || existing.id
syncSummary.updated.push({ id: existing.id, label, fields: changedFields })
}
db.run(vpsUpdateSql,
merged.ip,
merged.ipv6,
merged.additionalIps,
merged.dns,
merged.country,
merged.city,
merged.datacenter,
merged.os,
merged.status,
merged.tariffType,
merged.currency,
merged.dailyRate,
merged.monthlyRate,
merged.paidUntil,
merged.notes,
existing.id,
)
} else {
const label = vps.dns || vps.ip || id
syncSummary.added.push({ id, label })
db.run(vpsInsertSql,
id,
vps.ip,
vps.ipv6,
additionalIps,
vps.dns,
vps.providerId,
vps.providerAccountId,
vps.country,
vps.city,
vps.datacenter,
vps.os,
vps.vcpu,
vps.ramGb,
vps.diskGb,
vps.diskType,
vps.virtualization,
vps.bandwidthTb,
vps.sshPort,
vps.rootUser,
vps.purpose,
vps.environment,
vps.project,
null,
vps.monitoringEnabled ? 1 : 0,
vps.backupEnabled ? 1 : 0,
vps.status,
vps.tariffType,
vps.currency,
dailyRate,
monthlyRate,
vps.createdAt,
paidUntil,
notes,
'[]',
)
}
vpsCount++
}
}
let paymentsCount = 0
if (fetchVpsPayments) {
const existingPayments = new Set(
db.prepare('SELECT note FROM payments WHERE providerAccountId = ?').all(accountId).map((r) => r.note),
)
const paymentInsertSql = `INSERT INTO payments (id, type, date, amount, currency, providerAccountId, vpsId, note)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
for (const item of paymentItems) {
const payment = mapPaymentToPayment(item, accountId)
if (!payment || payment.amount <= 0) continue
const note = payment.note
if (existingPayments.has(note)) continue
const id = `pay-bm-${accountId}-${payment.externalId}`
db.run(paymentInsertSql, id, payment.type, payment.date, payment.amount, payment.currency, payment.providerAccountId, payment.vpsId, note)
existingPayments.add(note)
paymentsCount++
syncSummary.paymentsAdded += 1
}
}
if (fetchVpsPayments && dashboardInfo) {
db.run(
'UPDATE provider_accounts SET balance_api=?, balance_currency=?, balance_updated_at=?, enoughmoneyto=? WHERE id=?',
dashboardInfo.balance,
dashboardInfo.currency || 'RUB',
new Date().toISOString(),
dashboardInfo.enoughmoneyto || '',
accountId,
)
}
let tariffsCount = 0
let newTariffs = []
if (fetchTariffs) {
const existingTariffIds = new Set(
db.prepare('SELECT id FROM active_tariffs WHERE providerAccountId = ?').all(accountId).map((r) => r.id),
)
const syncedAt = new Date().toISOString()
db.run('DELETE FROM active_tariffs WHERE providerAccountId = ?', accountId)
const tariffInsertSql = `INSERT INTO active_tariffs (id, providerAccountId, providerId, externalId, datacenterKey, datacenterName, name, desc, vcpu, ramGb, diskGb, diskType, virtualization, channel, location, country, cpuModel, orderAvailable, price, syncedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
for (const t of tariffItems) {
const dcKey = t.datacenterKey ?? ''
const dcName = t.datacenterName ?? ''
const id = dcKey ? `tariff-bm-${accountId}-${t.externalId}-${dcKey}` : `tariff-bm-${accountId}-${t.externalId}`
if (!existingTariffIds.has(id)) {
newTariffs.push({ name: t.name || '', price: t.price || '', providerId })
}
db.run(tariffInsertSql,
id,
accountId,
providerId,
t.externalId,
dcKey,
dcName,
t.name || '',
t.desc || '',
t.vcpu || 0,
t.ramGb || 0,
t.diskGb || 0,
t.diskType || 'SSD',
t.virtualization || 'KVM',
t.channel || '',
t.location || '',
t.country || '',
t.cpuModel || '',
t.orderAvailable ? 1 : 0,
t.price || '',
syncedAt,
)
tariffsCount++
}
if (Object.keys(slist).length > 0) {
const datacenters = Array.isArray(slist.datacenter) ? JSON.stringify(slist.datacenter) : '[]'
const periods = Array.isArray(slist.period) ? JSON.stringify(slist.period) : '[]'
db.run(
`INSERT OR REPLACE INTO tariff_sync_options (providerAccountId, datacenters, periods, syncedAt) VALUES (?, ?, ?, ?)`,
accountId,
datacenters,
periods,
syncedAt,
)
}
}
if (!fetchVpsPayments) {
syncSummary.tariffsOnly = true
}
return { vpsCount, paymentsCount, tariffsCount, newTariffs, balance: dashboardInfo, syncSummary }
}