feat(api, web): интеграция хостера 4VPS.SU — синк VPS, баланса и тарифов
Docker / build (push) Has been cancelled

Добавлен адаптер 4vps, обобщён pipeline синхронизации через ProviderAdapter и обновлён UI для Panel ID и API Key.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-29 01:13:50 +07:00
co-authored by Cursor
parent 05e7bf829e
commit 9be50aa03b
33 changed files with 1564 additions and 174 deletions
+4 -3
View File
@@ -5,7 +5,8 @@ import type {
BalanceLedgerRow,
} from '@/types/entities'
import { accountBalanceApi } from '@/lib/account'
import { accountBillmanagerUiReady } from '@/lib/billmanager'
import { isSyncApiType } from '@cfdm/shared/contracts/provider'
import { accountSyncUiReady } from '@/lib/provider-sync'
import {
accountHasApiLedgerMismatch,
getStaleSyncAccountIds,
@@ -40,7 +41,7 @@ export function getAccountHealthFlags(
const provider = ctx.providers.find((p) => p.id === account.providerId)
const flags: AccountHealthFlag[] = []
if (provider?.apiType === 'billmanager' && !account.apiCredentialsSet) {
if (isSyncApiType(provider?.apiType) && !account.apiCredentialsSet) {
flags.push('no-creds')
}
@@ -99,5 +100,5 @@ export function countLowBalanceAccounts(
export function isAccountSyncable(account: ProviderAccount, providers: Provider[]): boolean {
const provider = providers.find((p) => p.id === account.providerId)
return accountBillmanagerUiReady(account, provider)
return accountSyncUiReady(account, provider)
}
+2 -2
View File
@@ -101,10 +101,10 @@ export const api = {
`/api/sync/${encodeURIComponent(accountId)}/balance`,
),
testConnection: (apiBaseUrl: string, apiCredentials: string) =>
testConnection: (apiBaseUrl: string, apiCredentials: string, apiType?: string) =>
fetchApi('/api/sync/test-connection', {
method: 'POST',
body: JSON.stringify({ apiBaseUrl, apiCredentials }),
body: JSON.stringify({ apiBaseUrl, apiCredentials, apiType }),
}),
fetchSyncStatus: () => fetchApi('/api/sync/status'),
+12 -45
View File
@@ -1,45 +1,12 @@
import type { ProviderAccount, Provider } from '@/types/entities'
export function providerByIdMap(providers: Provider[]): Map<string, Provider> {
return new Map(providers.map((p) => [p.id, p]))
}
export function billmanagerSyncableAccounts(
providerAccounts: ProviderAccount[],
providers: Provider[],
): ProviderAccount[] {
const pmap = providerByIdMap(providers)
return providerAccounts.filter((a) => {
const p = pmap.get(a.providerId)
return p?.apiType === 'billmanager' && Boolean((p.apiBaseUrl || '').trim()) && a.apiCredentialsSet
})
}
export function accountBillmanagerUiReady(
account: ProviderAccount,
provider?: Provider | null,
): boolean {
return (
provider?.apiType === 'billmanager' &&
Boolean((provider.apiBaseUrl || '').trim()) &&
Boolean(account.apiCredentialsSet)
)
}
export function accountUsesBillmanagerBalanceApi(
account: ProviderAccount,
provider?: Provider | null,
): boolean {
return provider?.apiType === 'billmanager' && account.balance_api != null
}
export function accountSelectLabel(
account: ProviderAccount,
providerById: Map<string, Provider>,
scopedProviderId?: string,
): string {
const name = account.name?.trim() || '—'
if (scopedProviderId) return name
const providerName = providerById.get(account.providerId)?.name ?? '—'
return `${providerName} / ${name}`
}
export {
providerByIdMap,
syncableAccounts,
billmanagerSyncableAccounts,
accountSyncUiReady,
accountBillmanagerUiReady,
accountUsesApiBalance,
accountUsesBillmanagerBalanceApi,
accountSelectLabel,
isSyncApiType,
accountCredentialLabels,
} from './provider-sync'
+77
View File
@@ -0,0 +1,77 @@
import { isSyncApiType } from '@cfdm/shared/contracts/provider'
import type { ProviderAccount, Provider } from '@/types/entities'
export { isSyncApiType }
export function providerByIdMap(providers: Provider[]): Map<string, Provider> {
return new Map(providers.map((p) => [p.id, p]))
}
export function syncableAccounts(
providerAccounts: ProviderAccount[],
providers: Provider[],
): ProviderAccount[] {
const pmap = providerByIdMap(providers)
return providerAccounts.filter((a) => {
const p = pmap.get(a.providerId)
return isSyncApiType(p?.apiType) && Boolean((p?.apiBaseUrl || '').trim()) && a.apiCredentialsSet
})
}
/** @deprecated Используйте syncableAccounts */
export const billmanagerSyncableAccounts = syncableAccounts
export function accountSyncUiReady(
account: ProviderAccount,
provider?: Provider | null,
): boolean {
return (
isSyncApiType(provider?.apiType) &&
Boolean((provider?.apiBaseUrl || '').trim()) &&
Boolean(account.apiCredentialsSet)
)
}
/** @deprecated Используйте accountSyncUiReady */
export const accountBillmanagerUiReady = accountSyncUiReady
export function accountUsesApiBalance(
account: ProviderAccount,
provider?: Provider | null,
): boolean {
return isSyncApiType(provider?.apiType) && account.balance_api != null
}
/** @deprecated Используйте accountUsesApiBalance */
export const accountUsesBillmanagerBalanceApi = accountUsesApiBalance
export function accountSelectLabel(
account: ProviderAccount,
providerById: Map<string, Provider>,
scopedProviderId?: string,
): string {
const name = account.name?.trim() || '—'
if (scopedProviderId) return name
const providerName = providerById.get(account.providerId)?.name ?? '—'
return `${providerName} / ${name}`
}
export function accountCredentialLabels(apiType?: string | null): {
loginLabel: string
passwordLabel: string
loginPlaceholder: string
} {
if (String(apiType).toLowerCase() === '4vps') {
return {
loginLabel: 'Panel ID',
passwordLabel: 'API Key',
loginPlaceholder: '1',
}
}
return {
loginLabel: 'Логин API',
passwordLabel: 'Пароль API',
loginPlaceholder: '',
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { z } from 'zod'
import { apiTypeSchema as sharedApiTypeSchema } from '@cfdm/shared/contracts/provider'
import { billingModeSchema as sharedBillingModeSchema } from '@cfdm/shared/contracts/provider-account'
import { customFieldsSchema } from '@cfdm/shared/contracts/custom-fields'
@@ -12,7 +13,7 @@ export const paymentTypeSchema = z.enum([
'monthly_debit',
])
export const ledgerDirectionSchema = z.enum(['credit', 'debit'])
export const apiTypeSchema = z.enum(['billmanager', 'none'])
export const apiTypeSchema = sharedApiTypeSchema
export const providerSchema = z.object({
id: z.string().min(1).optional(),