From 78f413324a3a692ec297983ecc82e23027565b3c Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 6 Nov 2026 22:32:50 +0700 Subject: [PATCH] =?UTF-8?q?fix(sync):=20=D0=BD=D0=B5=20=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=B0=D1=82=D1=8C=20=D1=82=D0=B0=D1=80=D0=B8=D1=84=D1=8B?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B5=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B9=D1=81-=D0=BB=D0=B8=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ошибка запроса тарифов превращалась в пустой список: active_tariffs удалялись, а следующий успешный синк объявлял все тарифы новыми. Пустой ответ теперь оставляет старый список, замена идёт в транзакции. Co-Authored-By: Claude Opus 5.5 --- apps/api/src/services/billmanager/sync.ts | 133 +++++++++++---------- apps/api/src/services/fourvps/sync.test.ts | 9 ++ apps/api/src/services/fourvps/sync.ts | 101 ++++++++-------- apps/api/src/services/ruvds/sync.ts | 101 ++++++++-------- apps/api/src/services/userapi/sync.ts | 101 ++++++++-------- apps/api/src/services/veesp/sync.ts | 101 ++++++++-------- 6 files changed, 285 insertions(+), 261 deletions(-) diff --git a/apps/api/src/services/billmanager/sync.ts b/apps/api/src/services/billmanager/sync.ts index 57186c4..8e029fb 100644 --- a/apps/api/src/services/billmanager/sync.ts +++ b/apps/api/src/services/billmanager/sync.ts @@ -3,7 +3,7 @@ */ import { eq } from 'drizzle-orm' -import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' +import { getCurrentSpaceId, getDb, getSqlite, schema } from '@cfdm/db' import { findSyncedVps } from '../providers/find-synced-vps.js' import type { BillmanagerSyncAccount } from './context.js' @@ -355,74 +355,77 @@ export async function syncFromBillmanager( 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) + // Fetch errors come back as [] — keep the old list instead of wiping it (and re-announcing all as new). + if (fetchTariffs && tariffItems.length > 0) { + getSqlite().transaction(() => { + 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)) - .all() - .map((r) => r.id), - ) - const syncedAt = new Date().toISOString() - db.delete(schema.activeTariffs) - .where(eq(schema.activeTariffs.providerAccountId, accountId)) - .run() + .run() - for (const t of tariffItems) { - const dcKey = t.datacenterKey ?? '' - const dcName = t.datacenterName ?? '' - const tariffId = dcKey - ? `tariff-bm-${accountId}-${t.externalId}-${dcKey}` - : `tariff-bm-${accountId}-${t.externalId}` - if (!existingTariffIds.has(tariffId)) { - newTariffs.push({ name: t.name || '', price: t.price || '', providerId }) + for (const t of tariffItems) { + const dcKey = t.datacenterKey ?? '' + const dcName = t.datacenterName ?? '' + const tariffId = dcKey + ? `tariff-bm-${accountId}-${t.externalId}-${dcKey}` + : `tariff-bm-${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 || 'SSD', + 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++ } - 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 || 'SSD', - 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 (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.insert(schema.tariffSyncOptions) - .values({ - spaceId: getCurrentSpaceId(), - providerAccountId: accountId, - datacenters, - periods, - syncedAt, - }) - .onConflictDoUpdate({ - target: schema.tariffSyncOptions.providerAccountId, - set: { datacenters, periods, syncedAt }, - }) - .run() - } + 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.insert(schema.tariffSyncOptions) + .values({ + spaceId: getCurrentSpaceId(), + providerAccountId: accountId, + datacenters, + periods, + syncedAt, + }) + .onConflictDoUpdate({ + target: schema.tariffSyncOptions.providerAccountId, + set: { datacenters, periods, syncedAt }, + }) + .run() + } + })() } if (!fetchVpsPayments) { diff --git a/apps/api/src/services/fourvps/sync.test.ts b/apps/api/src/services/fourvps/sync.test.ts index 85a2763..2b41f64 100644 --- a/apps/api/src/services/fourvps/sync.test.ts +++ b/apps/api/src/services/fourvps/sync.test.ts @@ -162,4 +162,13 @@ describe('syncFromFourvps', () => { const ids = (getSqlite().prepare(`SELECT id FROM vps ORDER BY id`).all() as { id: string }[]).map((r) => r.id) expect(ids).toEqual(['manual-12', 'vps-4vps-acc-4vps-100']) }) + it('keeps tariffs when the tariff fetch fails and does not re-announce them', async () => { + await syncFromFourvps(account) + vi.mocked(fetchTarifList).mockRejectedValueOnce(new Error('timeout')) + await syncFromFourvps(account) + const count = getSqlite().prepare(`SELECT COUNT(*) as c FROM active_tariffs`).get() as { c: number } + expect(count.c).toBe(1) + const third = await syncFromFourvps(account) + expect(third.newTariffs).toEqual([]) + }) }) diff --git a/apps/api/src/services/fourvps/sync.ts b/apps/api/src/services/fourvps/sync.ts index 7c60112..ae33f7e 100644 --- a/apps/api/src/services/fourvps/sync.ts +++ b/apps/api/src/services/fourvps/sync.ts @@ -3,7 +3,7 @@ */ import { eq } from 'drizzle-orm' -import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' +import { getCurrentSpaceId, getDb, getSqlite, schema } from '@cfdm/db' import { findSyncedVps } from '../providers/find-synced-vps.js' import type { FourvpsSyncAccount } from './context.js' @@ -223,56 +223,59 @@ export async function syncFromFourvps( 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) + // Fetch errors come back as [] — keep the old list instead of wiping it (and re-announcing all as new). + if (fetchTariffs && tariffItems.length > 0) { + getSqlite().transaction(() => { + 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)) - .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-4vps-${accountId}-${t.externalId}-${dcKey}` - : `tariff-4vps-${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++ - } + + for (const t of tariffItems) { + const dcKey = t.datacenterKey ?? '' + const dcName = t.datacenterName ?? '' + const tariffId = dcKey + ? `tariff-4vps-${accountId}-${t.externalId}-${dcKey}` + : `tariff-4vps-${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) { diff --git a/apps/api/src/services/ruvds/sync.ts b/apps/api/src/services/ruvds/sync.ts index 27e9e44..a58fb3d 100644 --- a/apps/api/src/services/ruvds/sync.ts +++ b/apps/api/src/services/ruvds/sync.ts @@ -3,7 +3,7 @@ */ import { eq } from 'drizzle-orm' -import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' +import { getCurrentSpaceId, getDb, getSqlite, schema } from '@cfdm/db' import { findSyncedVps } from '../providers/find-synced-vps.js' import type { RuvdsSyncAccount } from './context.js' @@ -286,56 +286,59 @@ export async function syncFromRuvds( 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) + // Fetch errors come back as [] — keep the old list instead of wiping it (and re-announcing all as new). + if (fetchTariffs && tariffItems.length > 0) { + getSqlite().transaction(() => { + 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)) - .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-ruvds-${accountId}-${t.externalId}-${dcKey}` - : `tariff-ruvds-${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++ - } + + for (const t of tariffItems) { + const dcKey = t.datacenterKey ?? '' + const dcName = t.datacenterName ?? '' + const tariffId = dcKey + ? `tariff-ruvds-${accountId}-${t.externalId}-${dcKey}` + : `tariff-ruvds-${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) { diff --git a/apps/api/src/services/userapi/sync.ts b/apps/api/src/services/userapi/sync.ts index 6017dc6..39ef475 100644 --- a/apps/api/src/services/userapi/sync.ts +++ b/apps/api/src/services/userapi/sync.ts @@ -3,7 +3,7 @@ */ import { eq } from 'drizzle-orm' -import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' +import { getCurrentSpaceId, getDb, getSqlite, schema } from '@cfdm/db' import { findSyncedVps } from '../providers/find-synced-vps.js' import type { UserApiSyncAccount } from './context.js' @@ -274,56 +274,59 @@ export async function syncFromUserApi( 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) + // Fetch errors come back as [] — keep the old list instead of wiping it (and re-announcing all as new). + if (fetchTariffs && tariffItems.length > 0) { + getSqlite().transaction(() => { + 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)) - .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-${idPrefix}-${accountId}-${t.externalId}-${dcKey}` - : `tariff-${idPrefix}-${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++ - } + + for (const t of tariffItems) { + const dcKey = t.datacenterKey ?? '' + const dcName = t.datacenterName ?? '' + const tariffId = dcKey + ? `tariff-${idPrefix}-${accountId}-${t.externalId}-${dcKey}` + : `tariff-${idPrefix}-${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) { diff --git a/apps/api/src/services/veesp/sync.ts b/apps/api/src/services/veesp/sync.ts index 64d11ec..4b8aecf 100644 --- a/apps/api/src/services/veesp/sync.ts +++ b/apps/api/src/services/veesp/sync.ts @@ -3,7 +3,7 @@ */ import { eq } from 'drizzle-orm' -import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' +import { getCurrentSpaceId, getDb, getSqlite, schema } from '@cfdm/db' import { findSyncedVps } from '../providers/find-synced-vps.js' import type { VeespSyncAccount } from './context.js' @@ -275,56 +275,59 @@ export async function syncFromVeesp( 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) + // Fetch errors come back as [] — keep the old list instead of wiping it (and re-announcing all as new). + if (fetchTariffs && tariffItems.length > 0) { + getSqlite().transaction(() => { + 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)) - .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++ - } + + 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) {