Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/e2e/api-config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
db: "mysql://root:root@localhost:3377/lnvps"
lightning:
lnd:
url: "https://localhost:10009"
cert: "/tmp/e2e-lnd/tls.cert"
macaroon: "/tmp/e2e-lnd/data/chain/bitcoin/regtest/admin.macaroon"
# Payment providers (LND Lightning + on-chain) are seeded into the
# payment_method_config table by scripts/run-e2e.sh, not configured here.
delete-after: 3
public-url: "http://localhost:8000"
read-only: true
Expand Down
1 change: 1 addition & 0 deletions API_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Changed

- **Payment providers are now configured exclusively in the database** (issue #182) — the Lightning node, on-chain provider and Revolut service used at startup are now constructed from the per-company `payment_method_config` rows (via `PaymentMethodFactory`). The YAML `lightning:` / `revolut:` config sections have been **removed**, along with the one-time YAML→DB seed migration and the `Settings::get_node`/`get_onchain`/`get_revolut` helpers. Providers must be configured via the admin API (`POST/PATCH /api/admin/v1/payment_method_configs`); startup now fails with a clear error if no enabled Lightning/on-chain config exists for the default company. The on-chain provider now honours the DB `min_confirmations`, `address_type` and `account` fields (previously hardcoded to 1 / p2wkh / default account). The runtime still resolves a single global provider set from the default (first) company; true per-company provider routing remains future work. **Operators must ensure their `payment_method_config` rows are seeded before upgrading** (deployments that ran the previous seed migration already are). No API surface change.
- **Custom VM price quote now returns converted prices** — `POST /api/v1/vm/custom-template/price` now returns `{ currency, amount, other_price }`, where `other_price` lists the same quote converted into the other supported currencies (matching the template listing's `cost_plan.other_price`). Previously it returned only the single base-currency `{ currency, amount }`; existing clients reading those two fields are unaffected.
- **Currency conversion (`alt_prices`) de-duplicates per target currency** — the price-conversion helper behind every `other_price` list (templates, custom pricing quote, IP-space pricing) no longer emits duplicate entries for the same currency. When both a direct fiat FX rate (e.g. `EUR/USD`) and the BTC round-trip (`EUR→BTC→USD`) exist, it now keeps a single entry and **prefers the direct fiat FX rate** (ECB is more accurate for fiat↔fiat than hopping through BTC). Single-currency deployments (no direct fiat FX rates) are unchanged.
- **Renewals are now bounded by a maximum prepay window** — a renewal is rejected (with a clear error) once it would push a subscription's expiry beyond `now + max_prepay_days`. This bounds both an oversized single `?intervals=N` request and repeated back-to-back renewals (once expiry is ~the window out, further renewals are rejected until real time passes). Affects `POST /api/v1/vm/{id}/renew` and `POST /api/v1/subscriptions/{id}/renew`. The limit is configured per company (`max_prepay_days` on `POST`/`PATCH /api/admin/v1/companies`, also returned on company info; `0` inherits the global default) with a global fallback (`max-prepay-days` in service config, default 365 days). It composes with host sunsetting — the effective ceiling is the earlier of the sunset date and the prepay window. The effective window is also surfaced to clients on the user-facing VM status (`GET /api/v1/vm/{id}` and `GET /api/v1/vms`) as `max_prepay_days`, so the UI can cap the renewal interval selector to what the server will accept.
Expand Down
9 changes: 4 additions & 5 deletions lnvps_api/config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
db: "mysql://root:root@localhost:3376/lnvps"
lightning:
lnd:
url: "https://127.0.0.1:10003"
cert: "/home/kieran/.polar/networks/1/volumes/lnd/alice/tls.cert"
macaroon: "/home/kieran/.polar/networks/1/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon"
# Payment providers (Lightning node, on-chain wallet, Revolut, ...) are no
# longer configured here. They are stored in the database `payment_method_config`
# table (per company) and managed via the admin API
# (POST/PATCH /api/admin/v1/payment_method_configs).
delete-after: 3
# Global default maximum prepay/renewal window in days. A renewal is rejected
# once it would push a subscription's expiry beyond now + this many days. Used
Expand Down
57 changes: 55 additions & 2 deletions lnvps_api/src/bin/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use clap::{Parser, ValueEnum};
use config::{Config, File};
use lnvps_api::data_migration::run_data_migrations;
use lnvps_api::dvm::start_dvms;
use lnvps_api::payment_factory::PaymentMethodFactory;
use lnvps_api::payments::listen_all_payments;
use lnvps_api::settings::Settings;
use lnvps_api::worker::Worker;
Expand Down Expand Up @@ -162,8 +163,59 @@ async fn main() -> Result<(), Error> {
};

let exchange = make_exchange_service(&settings.redis);
let node = settings.get_node().await?;
let onchain = settings.get_onchain().await?;

// Payment providers are sourced exclusively from the database
// `payment_method_config` rows (per-company config). There is no YAML
// fallback: providers must be configured in the database (via the admin
// API), otherwise startup fails.
//
// NOTE: the runtime still uses a single *global* node/on-chain provider
// selected from the default (first) company. True per-company provider
// routing is tracked as future work (see issue #182); the data model
// already carries `company_id` end-to-end, but the settlement handlers and
// SubscriptionHandler resolve one provider set for the whole process.
let factory = PaymentMethodFactory::new(db.clone());
let default_company_id = db
.list_companies()
.await?
.into_iter()
.next()
.map(|c| c.id)
.ok_or_else(|| {
Error::msg("No companies found in database; cannot resolve payment providers")
})?;

let node = factory
.get_lightning_node_for_company(default_company_id)
.await?
.ok_or_else(|| {
Error::msg(format!(
"No enabled Lightning payment_method_config for company {default_company_id}; \
configure one via the admin API"
))
})?;
info!("Using Lightning node from payment_method_config (company {default_company_id})");

#[cfg(feature = "onchain")]
let onchain = factory
.get_onchain_provider_for_company(default_company_id)
.await?
.ok_or_else(|| {
Error::msg(format!(
"No enabled on-chain payment_method_config for company {default_company_id}; \
configure one via the admin API"
))
})?;
#[cfg(feature = "onchain")]
info!("Using on-chain provider from payment_method_config (company {default_company_id})");

// The global Revolut service (if any) used by SubscriptionHandler for
// saved-card / off-session charges. Absent when no company has an enabled
// Revolut config.
let revolut = factory
.get_revolut_for_company(default_company_id)
.await
.unwrap_or(None);

// Optional IP -> country geolocation for VAT place-of-supply evidence.
let geoip: Option<Arc<dyn CountryResolver>> = match &settings.geoip_database {
Expand Down Expand Up @@ -225,6 +277,7 @@ async fn main() -> Result<(), Error> {
db.clone(),
node.clone(),
onchain.clone(),
revolut.clone(),
exchange.clone(),
vat.clone(),
work_commander.clone(),
Expand Down
8 changes: 0 additions & 8 deletions lnvps_api/src/data_migration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use crate::data_migration::email_hash_backfill::EmailHashBackfillMigration;
use crate::data_migration::encryption_migration::EncryptionDataMigration;
use crate::data_migration::ip6_init::Ip6InitDataMigration;
use crate::data_migration::orphaned_custom_templates::OrphanedCustomTemplatesMigration;
use crate::data_migration::payment_method_config::PaymentMethodConfigMigration;
use crate::data_migration::purge_never_paid_deleted_vms::PurgeNeverPaidDeletedVmsMigration;
use crate::data_migration::ssh_key_migration::SshKeyMigration;
use crate::provisioner::VmProvisioner;
Expand All @@ -22,7 +21,6 @@ mod email_hash_backfill;
mod encryption_migration;
mod ip6_init;
mod orphaned_custom_templates;
mod payment_method_config;
mod purge_never_paid_deleted_vms;
mod ssh_key_migration;

Expand Down Expand Up @@ -57,12 +55,6 @@ pub async fn run_data_migrations(

migrations.push(Box::new(ArpRefFixerDataMigration::new(db.clone())));

// Migrate payment method config from YAML to database
migrations.push(Box::new(PaymentMethodConfigMigration::new(
db.clone(),
settings.clone(),
)));

// Migrate SSH key from proxmox config to database
migrations.push(Box::new(SshKeyMigration::new(db.clone(), settings.clone())));

Expand Down
Loading
Loading