mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-15 01:04:03 +00:00
Exchange rate providers are now pluggable via the module Registry. The four built-in drivers (currency_converter, currency_freak, currency_layer, open_exchange_rate) move from a static config array into App\\Providers\\DriverRegistryProvider, which calls Registry::registerExchangeRateDriver() for each during app boot with metadata the frontend needs: label (i18n key), website (help-text URL), and config_fields (schema for driver-specific driver_config JSON).
The Currency Converter's server-type selector and dedicated URL field — previously hardcoded in ExchangeRateProviderModal.vue — are now just another config_fields entry with a visible_when rule that shows the URL input only when type=DEDICATED. Any module that wants to ship a custom driver gets the same treatment for free: declare config_fields in the registration, and the host app's modal renders them automatically.
ExchangeRateDriverFactory::make() falls back to Registry::driverMeta() when a name isn't in the local built-in map, and availableDrivers() merges both sources. ConfigController handles the exchange_rate_drivers key specially by mapping Registry::allDrivers('exchange_rate') to enriched option objects, so the config-file route still works for every other key. The static exchange_rate_drivers + currency_converter_servers arrays in config/invoiceshelf.php are deleted.
Unit tests cover the new Registry::register/flushDrivers, the factory merging built-ins with Registry-contributed drivers, and the factory rejecting unknown names. A feature test exercises the end-to-end /api/v1/config?key=exchange_rate_drivers response shape.
NOTE: this commit depends on invoiceshelf/modules package commit e44d951 which adds the Registry driver API. The package needs to be released and pinned in composer.json before a fresh composer install on this commit will work.
157 lines
4.8 KiB
TypeScript
157 lines
4.8 KiB
TypeScript
import { client } from '../client'
|
|
import { API } from '../endpoints'
|
|
import type { ExchangeRateProvider, Currency } from '@/scripts/types/domain/currency'
|
|
import type { ApiResponse, ListParams } from '@/scripts/types/api'
|
|
|
|
export interface CreateExchangeRateProviderPayload {
|
|
driver: string
|
|
key: string
|
|
active?: boolean
|
|
currencies?: string[]
|
|
driver_config?: Record<string, string>
|
|
}
|
|
|
|
// Normalized response types (what callers receive)
|
|
export interface ExchangeRateResponse {
|
|
exchangeRate: number | null
|
|
}
|
|
|
|
export interface ActiveProviderResponse {
|
|
hasActiveProvider: boolean
|
|
}
|
|
|
|
export interface SupportedCurrenciesResponse {
|
|
supportedCurrencies: string[]
|
|
}
|
|
|
|
export interface UsedCurrenciesResponse {
|
|
activeUsedCurrencies: string[]
|
|
allUsedCurrencies: string[]
|
|
}
|
|
|
|
export interface BulkCurrenciesResponse {
|
|
currencies: Array<Currency & { exchange_rate: number | null }>
|
|
}
|
|
|
|
export interface BulkUpdatePayload {
|
|
currencies: Array<{
|
|
id: number
|
|
exchange_rate: number
|
|
}>
|
|
}
|
|
|
|
export interface DriverConfigFieldOption {
|
|
label: string
|
|
value: string
|
|
}
|
|
|
|
export interface DriverConfigField {
|
|
key: string
|
|
type: 'text' | 'select'
|
|
label: string
|
|
options?: DriverConfigFieldOption[]
|
|
default?: string
|
|
visible_when?: Record<string, string>
|
|
}
|
|
|
|
export interface ExchangeRateDriverOption {
|
|
value: string
|
|
label: string
|
|
website?: string
|
|
config_fields?: DriverConfigField[]
|
|
}
|
|
|
|
export interface ConfigDriversResponse {
|
|
exchange_rate_drivers: ExchangeRateDriverOption[]
|
|
}
|
|
|
|
export interface SupportedCurrenciesParams {
|
|
driver: string
|
|
key: string
|
|
driver_config?: Record<string, string>
|
|
}
|
|
|
|
export const exchangeRateService = {
|
|
// Providers CRUD
|
|
async listProviders(params?: ListParams): Promise<ApiResponse<ExchangeRateProvider[]>> {
|
|
const { data } = await client.get(API.EXCHANGE_RATE_PROVIDERS, { params })
|
|
return data
|
|
},
|
|
|
|
async getProvider(id: number): Promise<ApiResponse<ExchangeRateProvider>> {
|
|
const { data } = await client.get(`${API.EXCHANGE_RATE_PROVIDERS}/${id}`)
|
|
return data
|
|
},
|
|
|
|
async createProvider(payload: CreateExchangeRateProviderPayload): Promise<ApiResponse<ExchangeRateProvider>> {
|
|
const { data } = await client.post(API.EXCHANGE_RATE_PROVIDERS, payload)
|
|
return data
|
|
},
|
|
|
|
async updateProvider(
|
|
id: number,
|
|
payload: Partial<CreateExchangeRateProviderPayload>,
|
|
): Promise<ApiResponse<ExchangeRateProvider>> {
|
|
const { data } = await client.put(`${API.EXCHANGE_RATE_PROVIDERS}/${id}`, payload)
|
|
return data
|
|
},
|
|
|
|
async deleteProvider(id: number): Promise<{ success: boolean }> {
|
|
const { data } = await client.delete(`${API.EXCHANGE_RATE_PROVIDERS}/${id}`)
|
|
return data
|
|
},
|
|
|
|
// Exchange Rates
|
|
// Backend returns { exchangeRate: [number] } or { error: string }
|
|
async getRate(currencyId: number): Promise<ExchangeRateResponse> {
|
|
const { data } = await client.get(`${API.CURRENCIES}/${currencyId}/exchange-rate`)
|
|
const raw = data as Record<string, unknown>
|
|
|
|
if (raw.exchangeRate && Array.isArray(raw.exchangeRate)) {
|
|
return { exchangeRate: Number(raw.exchangeRate[0]) ?? null }
|
|
}
|
|
|
|
return { exchangeRate: null }
|
|
},
|
|
|
|
// Backend returns { success: true, message: "provider_active" } or { error: "no_active_provider" }
|
|
async getActiveProvider(currencyId: number): Promise<ActiveProviderResponse> {
|
|
const { data } = await client.get(`${API.CURRENCIES}/${currencyId}/active-provider`)
|
|
const raw = data as Record<string, unknown>
|
|
|
|
return { hasActiveProvider: raw.success === true }
|
|
},
|
|
|
|
// Currency lists
|
|
async getSupportedCurrencies(params: SupportedCurrenciesParams): Promise<SupportedCurrenciesResponse> {
|
|
const { data } = await client.get(API.SUPPORTED_CURRENCIES, { params })
|
|
return data
|
|
},
|
|
|
|
// Backend returns { activeUsedCurrencies: string[], allUsedCurrencies: string[] }
|
|
async getUsedCurrencies(params?: { provider_id?: number }): Promise<UsedCurrenciesResponse> {
|
|
const { data } = await client.get(API.USED_CURRENCIES, { params })
|
|
return data
|
|
},
|
|
|
|
async getBulkCurrencies(): Promise<BulkCurrenciesResponse> {
|
|
const { data } = await client.get(API.CURRENCIES_USED)
|
|
return data
|
|
},
|
|
|
|
async bulkUpdateExchangeRate(payload: BulkUpdatePayload): Promise<{ success: boolean }> {
|
|
const { data } = await client.post(API.CURRENCIES_BULK_UPDATE, payload)
|
|
return data
|
|
},
|
|
|
|
// Config
|
|
// Backend returns { exchange_rate_drivers: ExchangeRateDriverOption[] } where each option
|
|
// includes the metadata needed to render a driver-specific config form (label, website,
|
|
// config_fields). The list is built dynamically from the module Registry, so module-
|
|
// contributed drivers appear here automatically.
|
|
async getDrivers(): Promise<ConfigDriversResponse> {
|
|
const { data } = await client.get(API.CONFIG, { params: { key: 'exchange_rate_drivers' } })
|
|
return data
|
|
},
|
|
}
|