/** * Sync Veesp client area data into vps-tracker DB */ import { eq } from 'drizzle-orm' import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' import { findSyncedVps } from '../providers/find-synced-vps.js' import type { VeespSyncAccount } from './context.js' import { veespCredentialsString } from './context.js' import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance' import { mapInvoiceToPayment, mapVpsRecordToVps } from './mappers.js' import { fetchBalance, fetchInvoices, fetchTariffList, fetchVpsRecords, type VeespBalanceResult, } from './operations.js' export interface SyncFromVeespOptions { skipTariffs?: boolean skipVpsPayments?: boolean } export interface SyncSummary { added: { id: string; label: string }[] updated: { id: string; label: string; fields: string[] }[] paymentsAdded: number tariffsOnly?: boolean } export interface SyncFromVeespResult { vpsCount: number paymentsCount: number tariffsCount: number newTariffs: { name: string; price: string; providerId: string }[] balance: VeespBalanceResult | null syncSummary: SyncSummary } const SYNC_UPDATE_FIELDS = [ 'country', 'city', 'datacenter', 'os', 'notes', 'status', 'tariffType', 'currency', 'dailyRate', 'monthlyRate', 'paidUntil', ] as const const SYNC_SPEC_FIELDS = ['vcpu', 'ramGb', 'diskGb'] as const function normVal(v: unknown): string { if (v == null || v === '') return '' if (typeof v === 'number') return Number.isFinite(v) ? String(v) : '' return String(v) } export async function syncFromVeesp( account: VeespSyncAccount, opts: SyncFromVeespOptions = {}, ): Promise { const { skipTariffs = false, skipVpsPayments = false } = opts const { apiBaseUrl, providerId, id: accountId } = account const credentials = veespCredentialsString(account) if (!apiBaseUrl?.trim() || !account.apiLogin?.trim() || !account.apiPassword) { throw new Error('API URL and credentials are required') } const db = getDb() const fetchVpsData = !skipVpsPayments const fetchTariffs = !skipTariffs const [records, balanceInfo, tariffItems, invoices] = await Promise.all([ fetchVpsData ? fetchVpsRecords(apiBaseUrl, credentials) : [], fetchVpsData ? fetchBalance(apiBaseUrl, credentials, syncFallbackCurrency(account)).catch(() => null) : null, fetchTariffs ? fetchTariffList(apiBaseUrl, credentials, syncFallbackCurrency(account)).catch(() => []) : [], fetchVpsData ? fetchInvoices(apiBaseUrl, credentials).catch(() => []) : [], ]) const fallbackCurrency = syncFallbackCurrency(account, { balanceCurrency: balanceInfo?.currency, }) let vpsCount = 0 const syncSummary: SyncSummary = { added: [], updated: [], paymentsAdded: 0 } if (fetchVpsData) { for (const record of records) { const vps = mapVpsRecordToVps(record, providerId, accountId, fallbackCurrency) const id = `vps-veesp-${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 const existing = findSyncedVps(accountId, id, [`veesp-${vps.externalId}`, `veesp-${record.serviceId}`], vps.ip) if (existing) { let userOverrides: string[] = [] 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, vcpu: vps.vcpu, ramGb: vps.ramGb, diskGb: vps.diskGb, 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 as keyof typeof existing] as never } } for (const f of SYNC_SPEC_FIELDS) { if (userOverrides.includes(f)) { merged[f] = existing[f as keyof typeof existing] as never } } const compareFields = [ 'ip', 'ipv6', 'dns', ...SYNC_SPEC_FIELDS, ...SYNC_UPDATE_FIELDS, ] as const const changedFields = compareFields.filter( (f) => normVal(merged[f as keyof typeof merged]) !== normVal(existing[f as keyof typeof existing]), ) if (changedFields.length > 0) { const label = merged.dns || merged.ip || existing.id syncSummary.updated.push({ id: existing.id, label, fields: [...changedFields] }) } db.update(schema.vps) .set({ ip: merged.ip, ipv6: merged.ipv6, additionalIps: merged.additionalIps, dns: merged.dns, country: merged.country, city: merged.city, datacenter: merged.datacenter, os: merged.os, vcpu: merged.vcpu, ramGb: merged.ramGb, diskGb: merged.diskGb, status: merged.status, tariffType: merged.tariffType, currency: merged.currency, dailyRate: merged.dailyRate, monthlyRate: merged.monthlyRate, paidUntil: merged.paidUntil, notes: merged.notes, }) .where(eq(schema.vps.id, existing.id)) .run() } else { const label = vps.dns || vps.ip || id syncSummary.added.push({ id, label }) db.insert(schema.vps) .values({ spaceId: getCurrentSpaceId(), id, ip: vps.ip, ipv6: vps.ipv6, additionalIps, dns: vps.dns, providerId: vps.providerId, providerAccountId: vps.providerAccountId, country: vps.country, city: vps.city, datacenter: vps.datacenter, os: vps.os, vcpu: vps.vcpu, ramGb: vps.ramGb, diskGb: vps.diskGb, diskType: vps.diskType, virtualization: vps.virtualization, bandwidthTb: vps.bandwidthTb, sshPort: vps.sshPort, rootUser: vps.rootUser, purpose: vps.purpose, environment: vps.environment, project: vps.project, projectId: null, monitoringEnabled: vps.monitoringEnabled ? 1 : 0, backupEnabled: vps.backupEnabled ? 1 : 0, status: vps.status, tariffType: vps.tariffType, currency: vps.currency, dailyRate, monthlyRate, createdAt: vps.createdAt || new Date().toISOString().slice(0, 10), paidUntil, notes, userOverrides: '[]', }) .run() } vpsCount++ } } let paymentsCount = 0 if (fetchVpsData) { const existingPaymentRows = db .select({ note: schema.payments.note }) .from(schema.payments) .where(eq(schema.payments.providerAccountId, accountId)) .all() const existingPayments = new Set( existingPaymentRows.map((r) => r.note).filter((n): n is string => Boolean(n)), ) for (const item of invoices) { const payment = mapInvoiceToPayment(item, accountId, fallbackCurrency) if (!payment) continue const note = payment.note if (existingPayments.has(note)) continue const payId = `pay-veesp-${accountId}-${payment.externalId}` db.insert(schema.payments) .values({ spaceId: getCurrentSpaceId(), id: payId, type: payment.type, date: payment.date, amount: payment.amount, currency: payment.currency, providerAccountId: payment.providerAccountId, vpsId: payment.vpsId, note, }) .run() existingPayments.add(note) paymentsCount++ syncSummary.paymentsAdded += 1 } } if (fetchVpsData && balanceInfo) { db.update(schema.providerAccounts) .set({ balanceApi: balanceInfo.balance, balanceCurrency: balanceInfo.currency || fallbackCurrency, balanceUpdatedAt: new Date().toISOString(), enoughmoneyto: balanceInfo.enoughmoneyto || '', }) .where(eq(schema.providerAccounts.id, accountId)) .run() } let tariffsCount = 0 const newTariffs: { name: string; price: string; providerId: string }[] = [] if (fetchTariffs) { const existingTariffIds = new Set( db .select({ id: schema.activeTariffs.id }) .from(schema.activeTariffs) .where(eq(schema.activeTariffs.providerAccountId, accountId)) .all() .map((r) => r.id), ) const syncedAt = new Date().toISOString() db.delete(schema.activeTariffs) .where(eq(schema.activeTariffs.providerAccountId, accountId)) .run() for (const t of tariffItems) { const dcKey = t.datacenterKey ?? '' const dcName = t.datacenterName ?? '' const tariffId = dcKey ? `tariff-veesp-${accountId}-${t.externalId}-${dcKey}` : `tariff-veesp-${accountId}-${t.externalId}` if (!existingTariffIds.has(tariffId)) { newTariffs.push({ name: t.name || '', price: t.price || '', providerId }) } db.insert(schema.activeTariffs) .values({ spaceId: getCurrentSpaceId(), id: tariffId, providerAccountId: accountId, providerId, externalId: t.externalId, datacenterKey: dcKey, datacenterName: dcName, name: t.name || '', desc: t.desc || '', vcpu: t.vcpu || 0, ramGb: t.ramGb || 0, diskGb: t.diskGb || 0, diskType: t.diskType || 'NVMe', virtualization: t.virtualization || 'KVM', channel: t.channel || '', location: t.location || '', country: t.country || '', cpuModel: t.cpuModel || '', orderAvailable: t.orderAvailable ? 1 : 0, price: t.price || '', syncedAt, }) .run() tariffsCount++ } } if (!fetchVpsData) { syncSummary.tariffsOnly = true } return { vpsCount, paymentsCount, tariffsCount, newTariffs, balance: balanceInfo, syncSummary, } }