fix(sync): не стирать тарифы при ошибке загрузки прайс-листа

Ошибка запроса тарифов превращалась в пустой список: active_tariffs
удалялись, а следующий успешный синк объявлял все тарифы новыми.
Пустой ответ теперь оставляет старый список, замена идёт в транзакции.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-11-06 22:32:50 +07:00
1 parent 01a771407d
commit 78f413324a
6 files changed
+285 -261

No files matched your search

+68 -65
View File
@@ -3,7 +3,7 @@
*/ */
import { eq } from 'drizzle-orm' 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 { findSyncedVps } from '../providers/find-synced-vps.js'
import type { BillmanagerSyncAccount } from './context.js' import type { BillmanagerSyncAccount } from './context.js'
@@ -355,74 +355,77 @@ export async function syncFromBillmanager(
let tariffsCount = 0 let tariffsCount = 0
const newTariffs: { name: string; price: string; providerId: string }[] = [] const newTariffs: { name: string; price: string; providerId: string }[] = []
if (fetchTariffs) { // Fetch errors come back as [] — keep the old list instead of wiping it (and re-announcing all as new).
const existingTariffIds = new Set( if (fetchTariffs && tariffItems.length > 0) {
db getSqlite().transaction(() => {
.select({ id: schema.activeTariffs.id }) const existingTariffIds = new Set(
.from(schema.activeTariffs) 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)) .where(eq(schema.activeTariffs.providerAccountId, accountId))
.all() .run()
.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) { for (const t of tariffItems) {
const dcKey = t.datacenterKey ?? '' const dcKey = t.datacenterKey ?? ''
const dcName = t.datacenterName ?? '' const dcName = t.datacenterName ?? ''
const tariffId = dcKey const tariffId = dcKey
? `tariff-bm-${accountId}-${t.externalId}-${dcKey}` ? `tariff-bm-${accountId}-${t.externalId}-${dcKey}`
: `tariff-bm-${accountId}-${t.externalId}` : `tariff-bm-${accountId}-${t.externalId}`
if (!existingTariffIds.has(tariffId)) { if (!existingTariffIds.has(tariffId)) {
newTariffs.push({ name: t.name || '', price: t.price || '', providerId }) 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) { if (Object.keys(slist).length > 0) {
const datacenters = Array.isArray(slist.datacenter) ? JSON.stringify(slist.datacenter) : '[]' const datacenters = Array.isArray(slist.datacenter) ? JSON.stringify(slist.datacenter) : '[]'
const periods = Array.isArray(slist.period) ? JSON.stringify(slist.period) : '[]' const periods = Array.isArray(slist.period) ? JSON.stringify(slist.period) : '[]'
db.insert(schema.tariffSyncOptions) db.insert(schema.tariffSyncOptions)
.values({ .values({
spaceId: getCurrentSpaceId(), spaceId: getCurrentSpaceId(),
providerAccountId: accountId, providerAccountId: accountId,
datacenters, datacenters,
periods, periods,
syncedAt, syncedAt,
}) })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: schema.tariffSyncOptions.providerAccountId, target: schema.tariffSyncOptions.providerAccountId,
set: { datacenters, periods, syncedAt }, set: { datacenters, periods, syncedAt },
}) })
.run() .run()
} }
})()
} }
if (!fetchVpsPayments) { if (!fetchVpsPayments) {
@@ -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) 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']) 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([])
})
}) })
+52 -49
View File
@@ -3,7 +3,7 @@
*/ */
import { eq } from 'drizzle-orm' 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 { findSyncedVps } from '../providers/find-synced-vps.js'
import type { FourvpsSyncAccount } from './context.js' import type { FourvpsSyncAccount } from './context.js'
@@ -223,56 +223,59 @@ export async function syncFromFourvps(
let tariffsCount = 0 let tariffsCount = 0
const newTariffs: { name: string; price: string; providerId: string }[] = [] const newTariffs: { name: string; price: string; providerId: string }[] = []
if (fetchTariffs) { // Fetch errors come back as [] — keep the old list instead of wiping it (and re-announcing all as new).
const existingTariffIds = new Set( if (fetchTariffs && tariffItems.length > 0) {
db getSqlite().transaction(() => {
.select({ id: schema.activeTariffs.id }) const existingTariffIds = new Set(
.from(schema.activeTariffs) 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)) .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() .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) { if (!fetchVpsData) {
+52 -49
View File
@@ -3,7 +3,7 @@
*/ */
import { eq } from 'drizzle-orm' 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 { findSyncedVps } from '../providers/find-synced-vps.js'
import type { RuvdsSyncAccount } from './context.js' import type { RuvdsSyncAccount } from './context.js'
@@ -286,56 +286,59 @@ export async function syncFromRuvds(
let tariffsCount = 0 let tariffsCount = 0
const newTariffs: { name: string; price: string; providerId: string }[] = [] const newTariffs: { name: string; price: string; providerId: string }[] = []
if (fetchTariffs) { // Fetch errors come back as [] — keep the old list instead of wiping it (and re-announcing all as new).
const existingTariffIds = new Set( if (fetchTariffs && tariffItems.length > 0) {
db getSqlite().transaction(() => {
.select({ id: schema.activeTariffs.id }) const existingTariffIds = new Set(
.from(schema.activeTariffs) 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)) .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() .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) { if (!fetchVpsData) {
+52 -49
View File
@@ -3,7 +3,7 @@
*/ */
import { eq } from 'drizzle-orm' 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 { findSyncedVps } from '../providers/find-synced-vps.js'
import type { UserApiSyncAccount } from './context.js' import type { UserApiSyncAccount } from './context.js'
@@ -274,56 +274,59 @@ export async function syncFromUserApi(
let tariffsCount = 0 let tariffsCount = 0
const newTariffs: { name: string; price: string; providerId: string }[] = [] const newTariffs: { name: string; price: string; providerId: string }[] = []
if (fetchTariffs) { // Fetch errors come back as [] — keep the old list instead of wiping it (and re-announcing all as new).
const existingTariffIds = new Set( if (fetchTariffs && tariffItems.length > 0) {
db getSqlite().transaction(() => {
.select({ id: schema.activeTariffs.id }) const existingTariffIds = new Set(
.from(schema.activeTariffs) 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)) .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() .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) { if (!fetchVpsData) {
+52 -49
View File
@@ -3,7 +3,7 @@
*/ */
import { eq } from 'drizzle-orm' 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 { findSyncedVps } from '../providers/find-synced-vps.js'
import type { VeespSyncAccount } from './context.js' import type { VeespSyncAccount } from './context.js'
@@ -275,56 +275,59 @@ export async function syncFromVeesp(
let tariffsCount = 0 let tariffsCount = 0
const newTariffs: { name: string; price: string; providerId: string }[] = [] const newTariffs: { name: string; price: string; providerId: string }[] = []
if (fetchTariffs) { // Fetch errors come back as [] — keep the old list instead of wiping it (and re-announcing all as new).
const existingTariffIds = new Set( if (fetchTariffs && tariffItems.length > 0) {
db getSqlite().transaction(() => {
.select({ id: schema.activeTariffs.id }) const existingTariffIds = new Set(
.from(schema.activeTariffs) 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)) .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() .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) { if (!fetchVpsData) {