feat: alt prices

This commit is contained in:
2025-03-10 15:09:50 +00:00
parent 3e9a850486
commit ddaed046dc
3 changed files with 100 additions and 49 deletions

View File

@ -4,7 +4,10 @@ use crate::status::VmState;
use anyhow::{anyhow, bail, Context, Result};
use chrono::{DateTime, Utc};
use ipnetwork::IpNetwork;
use lnvps_db::{LNVpsDb, Vm, VmCostPlan, VmCustomTemplate, VmHost, VmHostRegion, VmTemplate};
use lnvps_db::{
LNVpsDb, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost,
VmHostRegion, VmTemplate,
};
use nostr::util::hex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -141,16 +144,12 @@ impl ApiTemplatesResponse {
let rates = rates.list_rates().await?;
for mut template in &mut self.templates {
if let Some(list_price) = template.cost_plan.price.first() {
for alt_price in alt_prices(
&rates,
CurrencyAmount(list_price.currency, list_price.amount),
) {
template.cost_plan.price.push(ApiPrice {
currency: alt_price.0,
amount: alt_price.1,
});
}
let list_price = CurrencyAmount(template.cost_plan.currency, template.cost_plan.amount);
for alt_price in alt_prices(&rates, list_price) {
template.cost_plan.other_price.push(ApiPrice {
currency: alt_price.0,
amount: alt_price.1,
});
}
}
Ok(())
@ -170,6 +169,40 @@ pub struct ApiCustomTemplateParams {
pub disks: Vec<ApiCustomTemplateDiskParam>,
}
impl ApiCustomTemplateParams {
pub fn from(
pricing: &VmCustomPricing,
disks: &Vec<VmCustomPricingDisk>,
region: &VmHostRegion,
max_cpu: u16,
max_memory: u64,
max_disk: u64,
) -> Result<Self> {
const GB: u64 = 1024 * 1024 * 1024;
Ok(ApiCustomTemplateParams {
id: pricing.id,
name: pricing.name.clone(),
region: ApiVmHostRegion {
id: region.id,
name: region.name.clone(),
},
max_cpu,
min_cpu: 1,
min_memory: GB,
max_memory,
min_disk: GB * 5,
max_disk,
disks: disks
.iter()
.filter(|d| d.pricing_id == pricing.id)
.map(|d| ApiCustomTemplateDiskParam {
disk_type: d.kind.into(),
disk_interface: d.interface.into(),
})
.collect(),
})
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub struct ApiCustomTemplateDiskParam {
pub disk_type: DiskType,
@ -215,6 +248,15 @@ pub struct ApiPrice {
pub amount: f32,
}
impl From<CurrencyAmount> for ApiPrice {
fn from(value: CurrencyAmount) -> Self {
Self {
currency: value.0,
amount: value.1,
}
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ApiVmTemplate {
pub id: u64,

View File

@ -1,9 +1,10 @@
use crate::api::model::{
AccountPatchRequest, ApiPrice, ApiCustomTemplateDiskParam, ApiCustomTemplateParams,
ApiCustomVmOrder, ApiCustomVmRequest, ApiTemplatesResponse, ApiUserSshKey, ApiVmHostRegion,
AccountPatchRequest, ApiCustomTemplateDiskParam, ApiCustomTemplateParams, ApiCustomVmOrder,
ApiCustomVmRequest, ApiPrice, ApiTemplatesResponse, ApiUserSshKey, ApiVmHostRegion,
ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus, ApiVmTemplate, CreateSshKey,
CreateVmRequest, VMPatchRequest,
};
use crate::exchange::ExchangeRateService;
use crate::host::{get_host_client, FullVmInfo, TimeSeries, TimeSeriesData};
use crate::nip98::Nip98Auth;
use crate::provisioner::{HostCapacityService, LNVpsProvisioner, PricingEngine};
@ -266,7 +267,10 @@ async fn v1_list_vm_images(db: &State<Arc<dyn LNVpsDb>>) -> ApiResult<Vec<ApiVmO
/// List available VM templates (Offers)
#[openapi(tag = "VM")]
#[get("/api/v1/vm/templates")]
async fn v1_list_vm_templates(db: &State<Arc<dyn LNVpsDb>>) -> ApiResult<ApiTemplatesResponse> {
async fn v1_list_vm_templates(
db: &State<Arc<dyn LNVpsDb>>,
rates: &State<Arc<dyn ExchangeRateService>>,
) -> ApiResult<ApiTemplatesResponse> {
let hc = HostCapacityService::new((*db).clone());
let templates = hc.list_available_vm_templates().await?;
@ -322,49 +326,42 @@ async fn v1_list_vm_templates(db: &State<Arc<dyn LNVpsDb>>) -> ApiResult<ApiTemp
.flatten()
.collect();
ApiData::ok(ApiTemplatesResponse {
let mut rsp = ApiTemplatesResponse {
templates: ret,
custom_template: if custom_templates.is_empty() {
None
} else {
const GB: u64 = 1024 * 1024 * 1024;
let max_cpu = templates.iter().map(|t| t.cpu).max().unwrap_or(8);
let max_memory = templates.iter().map(|t| t.memory).max().unwrap_or(GB * 2);
let max_disk = templates
.iter()
.map(|t| t.disk_size)
.max()
.unwrap_or(GB * 5);
Some(
custom_templates
.into_iter()
.map(|t| ApiCustomTemplateParams {
id: t.id,
name: t.name,
region: regions
.get(&t.region_id)
.map(|r| ApiVmHostRegion {
id: r.id,
name: r.name.clone(),
})
.context("Region information missing in custom template")
.unwrap(),
max_cpu: templates.iter().map(|t| t.cpu).max().unwrap_or(8),
min_cpu: 1,
min_memory: GB,
max_memory: templates.iter().map(|t| t.memory).max().unwrap_or(GB * 2),
min_disk: GB * 5,
max_disk: templates
.iter()
.map(|t| t.disk_size)
.max()
.unwrap_or(GB * 5),
disks: custom_template_disks
.iter()
.filter(|d| d.pricing_id == t.id)
.map(|d| ApiCustomTemplateDiskParam {
disk_type: d.kind.into(),
disk_interface: d.interface.into(),
})
.collect(),
.filter_map(|t| {
let region = regions.get(&t.region_id)?;
Some(
ApiCustomTemplateParams::from(
&t,
&custom_template_disks,
region,
max_cpu,
max_memory,
max_disk,
)
.ok()?,
)
})
.collect(),
)
},
})
};
rsp.expand_pricing(rates).await?;
ApiData::ok(rsp)
}
/// Get a price for a custom order

View File

@ -4,7 +4,7 @@ use log::info;
use rocket::serde::Deserialize;
use schemars::JsonSchema;
use serde::Serialize;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use std::sync::Arc;
@ -91,11 +91,23 @@ pub trait ExchangeRateService: Send + Sync {
/// Get alternative prices based on a source price
pub fn alt_prices(rates: &Vec<TickerRate>, source: CurrencyAmount) -> Vec<CurrencyAmount> {
// TODO: return all alt prices by cross-converting all currencies
rates
let mut ret: Vec<CurrencyAmount> = rates
.iter()
.filter_map(|r| r.convert(source).ok())
.collect()
.collect();
let mut ret2 = vec![];
for y in rates.iter() {
for x in ret.iter() {
if let Ok(r1) = y.convert(x.clone()) {
if r1.0 != source.0 {
ret2.push(r1);
}
}
}
}
ret.append(&mut ret2);
ret
}
#[derive(Clone, Default)]