This commit is contained in:
536
Cargo.lock
generated
536
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
5
lnvps_db/migrations/20250313140640_empty_country.sql
Normal file
5
lnvps_db/migrations/20250313140640_empty_country.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
alter table users
|
||||||
|
change column country_code country_code varchar (3);
|
||||||
|
-- assume country_code was not actually set until now
|
||||||
|
update users
|
||||||
|
set country_code = null;
|
@ -22,7 +22,7 @@ pub struct User {
|
|||||||
/// If user should be contacted via email for notifications
|
/// If user should be contacted via email for notifications
|
||||||
pub contact_email: bool,
|
pub contact_email: bool,
|
||||||
/// Users country
|
/// Users country
|
||||||
pub country_code: String,
|
pub country_code: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Clone, Debug, Default)]
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
|
@ -441,7 +441,8 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
|
|
||||||
let mut tx = self.db.begin().await?;
|
let mut tx = self.db.begin().await?;
|
||||||
|
|
||||||
sqlx::query("update vm_payment set is_paid = true where id = ?")
|
sqlx::query("update vm_payment set is_paid = true, external_data = ? where id = ?")
|
||||||
|
.bind(&vm_payment.external_data)
|
||||||
.bind(&vm_payment.id)
|
.bind(&vm_payment.id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -5,7 +5,8 @@ use anyhow::{anyhow, bail, Result};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use lnvps_db::{
|
use lnvps_db::{
|
||||||
LNVpsDb, PaymentMethod, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHostRegion, VmTemplate,
|
LNVpsDb, PaymentMethod, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate,
|
||||||
|
VmHostRegion, VmTemplate,
|
||||||
};
|
};
|
||||||
use nostr::util::hex;
|
use nostr::util::hex;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
@ -144,11 +145,12 @@ impl ApiTemplatesResponse {
|
|||||||
let rates = rates.list_rates().await?;
|
let rates = rates.list_rates().await?;
|
||||||
|
|
||||||
for template in &mut self.templates {
|
for template in &mut self.templates {
|
||||||
let list_price = CurrencyAmount(template.cost_plan.currency, template.cost_plan.amount);
|
let list_price =
|
||||||
|
CurrencyAmount::from_f32(template.cost_plan.currency, template.cost_plan.amount);
|
||||||
for alt_price in alt_prices(&rates, list_price) {
|
for alt_price in alt_prices(&rates, list_price) {
|
||||||
template.cost_plan.other_price.push(ApiPrice {
|
template.cost_plan.other_price.push(ApiPrice {
|
||||||
currency: alt_price.0,
|
currency: alt_price.0,
|
||||||
amount: alt_price.1,
|
amount: alt_price.value_f32(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,7 +254,7 @@ impl From<CurrencyAmount> for ApiPrice {
|
|||||||
fn from(value: CurrencyAmount) -> Self {
|
fn from(value: CurrencyAmount) -> Self {
|
||||||
Self {
|
Self {
|
||||||
currency: value.0,
|
currency: value.0,
|
||||||
amount: value.1,
|
amount: value.value_f32(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -402,7 +404,7 @@ pub struct AccountPatchRequest {
|
|||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub contact_nip17: bool,
|
pub contact_nip17: bool,
|
||||||
pub contact_email: bool,
|
pub contact_email: bool,
|
||||||
pub country_code: String,
|
pub country_code: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
|
@ -108,9 +108,11 @@ async fn v1_patch_account(
|
|||||||
user.email = req.email.clone();
|
user.email = req.email.clone();
|
||||||
user.contact_nip17 = req.contact_nip17;
|
user.contact_nip17 = req.contact_nip17;
|
||||||
user.contact_email = req.contact_email;
|
user.contact_email = req.contact_email;
|
||||||
user.country_code = CountryCode::for_alpha3(&req.country_code)?
|
user.country_code = req
|
||||||
.alpha3()
|
.country_code
|
||||||
.to_owned();
|
.as_ref()
|
||||||
|
.and_then(|c| CountryCode::for_alpha3(c).ok())
|
||||||
|
.map(|c| c.alpha3().to_string());
|
||||||
|
|
||||||
db.update_user(&user).await?;
|
db.update_user(&user).await?;
|
||||||
ApiData::ok(())
|
ApiData::ok(())
|
||||||
|
@ -60,11 +60,35 @@ impl Display for Ticker {
|
|||||||
pub struct TickerRate(pub Ticker, pub f32);
|
pub struct TickerRate(pub Ticker, pub f32);
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct CurrencyAmount(pub Currency, pub f32);
|
pub struct CurrencyAmount(pub Currency, u64);
|
||||||
|
|
||||||
impl CurrencyAmount {
|
impl CurrencyAmount {
|
||||||
|
const MILLI_SATS: f64 = 1.0e11;
|
||||||
|
|
||||||
pub fn from_u64(currency: Currency, amount: u64) -> Self {
|
pub fn from_u64(currency: Currency, amount: u64) -> Self {
|
||||||
CurrencyAmount(currency, amount as f32 / 100.0)
|
CurrencyAmount(currency, amount)
|
||||||
|
}
|
||||||
|
pub fn from_f32(currency: Currency, amount: f32) -> Self {
|
||||||
|
CurrencyAmount(
|
||||||
|
currency,
|
||||||
|
match currency {
|
||||||
|
Currency::EUR => (amount * 100.0) as u64, // cents
|
||||||
|
Currency::BTC => (amount as f64 * Self::MILLI_SATS) as u64, // milli-sats
|
||||||
|
Currency::USD => (amount * 100.0) as u64, // cents
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> u64 {
|
||||||
|
self.1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value_f32(&self) -> f32 {
|
||||||
|
match self.0 {
|
||||||
|
Currency::EUR => self.1 as f32 / 100.0,
|
||||||
|
Currency::BTC => (self.1 as f64 / Self::MILLI_SATS) as f32,
|
||||||
|
Currency::USD => self.1 as f32 / 100.0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,9 +104,15 @@ impl TickerRate {
|
|||||||
"Cant convert, currency doesnt match"
|
"Cant convert, currency doesnt match"
|
||||||
);
|
);
|
||||||
if source.0 == self.0 .0 {
|
if source.0 == self.0 .0 {
|
||||||
Ok(CurrencyAmount(self.0 .1, source.1 * self.1))
|
Ok(CurrencyAmount::from_f32(
|
||||||
|
self.0 .1,
|
||||||
|
source.value_f32() * self.1,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(CurrencyAmount(self.0 .0, source.1 / self.1))
|
Ok(CurrencyAmount::from_f32(
|
||||||
|
self.0 .0,
|
||||||
|
source.value_f32() / self.1,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,12 +207,14 @@ mod tests {
|
|||||||
let f = TickerRate(ticker, RATE);
|
let f = TickerRate(ticker, RATE);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
f.convert(CurrencyAmount(Currency::EUR, 5.0)).unwrap(),
|
f.convert(CurrencyAmount::from_f32(Currency::EUR, 5.0))
|
||||||
CurrencyAmount(Currency::BTC, 5.0 / RATE)
|
.unwrap(),
|
||||||
|
CurrencyAmount::from_f32(Currency::BTC, 5.0 / RATE)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
f.convert(CurrencyAmount(Currency::BTC, 0.001)).unwrap(),
|
f.convert(CurrencyAmount::from_f32(Currency::BTC, 0.001))
|
||||||
CurrencyAmount(Currency::EUR, RATE * 0.001)
|
.unwrap(),
|
||||||
|
CurrencyAmount::from_f32(Currency::EUR, RATE * 0.001)
|
||||||
);
|
);
|
||||||
assert!(!f.can_convert(Currency::USD));
|
assert!(!f.can_convert(Currency::USD));
|
||||||
assert!(f.can_convert(Currency::EUR));
|
assert!(f.can_convert(Currency::EUR));
|
||||||
|
@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct RevolutApi {
|
pub struct RevolutApi {
|
||||||
api: JsonApi,
|
api: JsonApi,
|
||||||
}
|
}
|
||||||
@ -63,6 +64,31 @@ impl RevolutApi {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_order(
|
||||||
|
&self,
|
||||||
|
amount: CurrencyAmount,
|
||||||
|
description: Option<String>,
|
||||||
|
) -> Result<RevolutOrder> {
|
||||||
|
self.api
|
||||||
|
.post(
|
||||||
|
"/api/orders",
|
||||||
|
CreateOrderRequest {
|
||||||
|
currency: amount.0.to_string(),
|
||||||
|
amount: match amount.0 {
|
||||||
|
Currency::BTC => bail!("Bitcoin amount not allowed for fiat payments"),
|
||||||
|
Currency::EUR => amount.value(),
|
||||||
|
Currency::USD => amount.value(),
|
||||||
|
},
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_order(&self, order_id: &str) -> Result<RevolutOrder> {
|
||||||
|
self.api.get(&format!("/api/orders/{}", order_id)).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FiatPaymentService for RevolutApi {
|
impl FiatPaymentService for RevolutApi {
|
||||||
@ -71,24 +97,10 @@ impl FiatPaymentService for RevolutApi {
|
|||||||
description: &str,
|
description: &str,
|
||||||
amount: CurrencyAmount,
|
amount: CurrencyAmount,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<FiatPaymentInfo>> + Send>> {
|
) -> Pin<Box<dyn Future<Output = Result<FiatPaymentInfo>> + Send>> {
|
||||||
let api = self.api.clone();
|
let s = self.clone();
|
||||||
let desc = description.to_string();
|
let desc = description.to_string();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let rsp: CreateOrderResponse = api
|
let rsp = s.create_order(amount, Some(desc)).await?;
|
||||||
.post(
|
|
||||||
"/api/orders",
|
|
||||||
CreateOrderRequest {
|
|
||||||
currency: amount.0.to_string(),
|
|
||||||
amount: match amount.0 {
|
|
||||||
Currency::BTC => bail!("Bitcoin amount not allowed for fiat payments"),
|
|
||||||
Currency::EUR => (amount.1 * 100.0).floor() as u64,
|
|
||||||
Currency::USD => (amount.1 * 100.0).floor() as u64,
|
|
||||||
},
|
|
||||||
description: Some(desc),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(FiatPaymentInfo {
|
Ok(FiatPaymentInfo {
|
||||||
raw_data: serde_json::to_string(&rsp)?,
|
raw_data: serde_json::to_string(&rsp)?,
|
||||||
external_id: rsp.id,
|
external_id: rsp.id,
|
||||||
@ -107,22 +119,106 @@ pub struct CreateOrderRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
pub struct CreateOrderResponse {
|
pub struct RevolutOrder {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub state: PaymentState,
|
pub state: RevolutOrderState,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
pub currency: String,
|
pub currency: String,
|
||||||
pub outstanding_amount: u64,
|
pub outstanding_amount: u64,
|
||||||
pub checkout_url: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub checkout_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub payments: Option<Vec<RevolutOrderPayment>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
pub struct RevolutOrderPayment {
|
||||||
|
pub id: String,
|
||||||
|
pub state: RevolutPaymentState,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub decline_reason: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bank_message: Option<String>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub token: Option<String>,
|
||||||
|
pub amount: u64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub currency: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub settled_amount: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub settled_currency: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub payment_method: Option<RevolutPaymentMethod>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub billing_address: Option<RevolutBillingAddress>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub risk_level: Option<RevolutRiskLevel>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
pub struct RevolutPaymentMethod {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub id: Option<String>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub kind: RevolutPaymentMethodType,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub card_brand: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub funding: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub card_country_code: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub card_bin: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub card_last_four: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub card_expiry: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cardholder_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum RevolutPaymentMethodType {
|
||||||
|
ApplePay,
|
||||||
|
Card,
|
||||||
|
GooglePay,
|
||||||
|
RevolutPayCard,
|
||||||
|
RevolutPayAccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum RevolutRiskLevel {
|
||||||
|
High,
|
||||||
|
Low
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
pub struct RevolutBillingAddress {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub street_line_1: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub street_line_2: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub region: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub city: Option<String>,
|
||||||
|
|
||||||
|
pub country_code: String,
|
||||||
|
pub postcode: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum PaymentState {
|
pub enum RevolutOrderState {
|
||||||
Pending,
|
Pending,
|
||||||
Processing,
|
Processing,
|
||||||
Authorised,
|
Authorised,
|
||||||
@ -131,6 +227,31 @@ pub enum PaymentState {
|
|||||||
Failed,
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum RevolutPaymentState {
|
||||||
|
Pending,
|
||||||
|
AuthenticationChallenge,
|
||||||
|
AuthenticationVerified,
|
||||||
|
AuthorisationStarted,
|
||||||
|
AuthorisationPassed,
|
||||||
|
Authorised,
|
||||||
|
CaptureStarted,
|
||||||
|
Captured,
|
||||||
|
RefundValidated,
|
||||||
|
RefundStarted,
|
||||||
|
CancellationStarted,
|
||||||
|
Declining,
|
||||||
|
Completing,
|
||||||
|
Cancelling,
|
||||||
|
Failing,
|
||||||
|
Completed,
|
||||||
|
Declined,
|
||||||
|
SoftDeclined,
|
||||||
|
Cancelled,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
pub struct RevolutWebhook {
|
pub struct RevolutWebhook {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -4,6 +4,7 @@ use crate::settings::RevolutConfig;
|
|||||||
use crate::worker::WorkJob;
|
use crate::worker::WorkJob;
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
|
use isocountry::CountryCode;
|
||||||
use lnvps_db::LNVpsDb;
|
use lnvps_db::LNVpsDb;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
@ -73,7 +74,30 @@ impl RevolutPaymentHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn try_complete_payment(&self, ext_id: &str) -> Result<()> {
|
async fn try_complete_payment(&self, ext_id: &str) -> Result<()> {
|
||||||
let p = self.db.get_vm_payment_by_ext_id(ext_id).await?;
|
let mut p = self.db.get_vm_payment_by_ext_id(ext_id).await?;
|
||||||
|
|
||||||
|
// save payment state json into external_data
|
||||||
|
// TODO: encrypt payment_data
|
||||||
|
let order = self.api.get_order(ext_id).await?;
|
||||||
|
p.external_data = serde_json::to_string(&order)?;
|
||||||
|
|
||||||
|
// check user country matches card country
|
||||||
|
if let Some(cc) = order
|
||||||
|
.payments
|
||||||
|
.and_then(|p| p.first().cloned())
|
||||||
|
.and_then(|p| p.payment_method)
|
||||||
|
.and_then(|p| p.card_country_code)
|
||||||
|
.and_then(|c| CountryCode::for_alpha2(&c).ok())
|
||||||
|
{
|
||||||
|
let vm = self.db.get_vm(p.vm_id).await?;
|
||||||
|
let mut user = self.db.get_user(vm.user_id).await?;
|
||||||
|
if user.country_code.is_none() {
|
||||||
|
// update user country code to match card country
|
||||||
|
user.country_code = Some(cc.alpha3().to_string());
|
||||||
|
self.db.update_user(&user).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.db.vm_payment_paid(&p).await?;
|
self.db.vm_payment_paid(&p).await?;
|
||||||
self.sender.send(WorkJob::CheckVm { vm_id: p.vm_id })?;
|
self.sender.send(WorkJob::CheckVm { vm_id: p.vm_id })?;
|
||||||
info!("VM payment {} for {}, paid", hex::encode(p.id), p.vm_id);
|
info!("VM payment {} for {}, paid", hex::encode(p.id), p.vm_id);
|
||||||
|
@ -122,7 +122,10 @@ impl PricingEngine {
|
|||||||
// custom templates are always 1-month intervals
|
// custom templates are always 1-month intervals
|
||||||
let time_value = (vm.expires.add(Months::new(1)) - vm.expires).num_seconds() as u64;
|
let time_value = (vm.expires.add(Months::new(1)) - vm.expires).num_seconds() as u64;
|
||||||
let (currency, amount, rate) = self
|
let (currency, amount, rate) = self
|
||||||
.get_amount_and_rate(CurrencyAmount(price.currency, price.total()), method)
|
.get_amount_and_rate(
|
||||||
|
CurrencyAmount::from_f32(price.currency, price.total()),
|
||||||
|
method,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(CostResult::New {
|
Ok(CostResult::New {
|
||||||
amount,
|
amount,
|
||||||
@ -136,13 +139,16 @@ impl PricingEngine {
|
|||||||
|
|
||||||
async fn get_tax_for_user(&self, user_id: u64, amount: u64) -> Result<u64> {
|
async fn get_tax_for_user(&self, user_id: u64, amount: u64) -> Result<u64> {
|
||||||
let user = self.db.get_user(user_id).await?;
|
let user = self.db.get_user(user_id).await?;
|
||||||
let cc = CountryCode::for_alpha3(&user.country_code).context("Invalid country code")?;
|
if let Some(cc) = user
|
||||||
|
.country_code
|
||||||
|
.and_then(|c| CountryCode::for_alpha3(&c).ok())
|
||||||
|
{
|
||||||
if let Some(c) = self.tax_rates.get(&cc) {
|
if let Some(c) = self.tax_rates.get(&cc) {
|
||||||
Ok((amount as f64 * (*c as f64 / 100f64)).floor() as u64)
|
return Ok((amount as f64 * (*c as f64 / 100f64)).floor() as u64);
|
||||||
} else {
|
|
||||||
Ok(0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_ticker(&self, currency: Currency) -> Result<TickerRate> {
|
async fn get_ticker(&self, currency: Currency) -> Result<TickerRate> {
|
||||||
let ticker = Ticker(Currency::BTC, currency);
|
let ticker = Ticker(Currency::BTC, currency);
|
||||||
@ -155,7 +161,7 @@ impl PricingEngine {
|
|||||||
|
|
||||||
async fn get_msats_amount(&self, amount: CurrencyAmount) -> Result<(u64, f32)> {
|
async fn get_msats_amount(&self, amount: CurrencyAmount) -> Result<(u64, f32)> {
|
||||||
let rate = self.get_ticker(amount.0).await?;
|
let rate = self.get_ticker(amount.0).await?;
|
||||||
let cost_btc = amount.1 / rate.1;
|
let cost_btc = amount.value_f32() / rate.1;
|
||||||
let cost_msats = (cost_btc as f64 * crate::BTC_SATS) as u64 * 1000;
|
let cost_msats = (cost_btc as f64 * crate::BTC_SATS) as u64 * 1000;
|
||||||
Ok((cost_msats, rate.1))
|
Ok((cost_msats, rate.1))
|
||||||
}
|
}
|
||||||
@ -185,7 +191,7 @@ impl PricingEngine {
|
|||||||
|
|
||||||
let currency = cost_plan.currency.parse().expect("Invalid currency");
|
let currency = cost_plan.currency.parse().expect("Invalid currency");
|
||||||
let (currency, amount, rate) = self
|
let (currency, amount, rate) = self
|
||||||
.get_amount_and_rate(CurrencyAmount(currency, cost_plan.amount), method)
|
.get_amount_and_rate(CurrencyAmount::from_f32(currency, cost_plan.amount), method)
|
||||||
.await?;
|
.await?;
|
||||||
let time_value = Self::next_template_expire(vm, &cost_plan);
|
let time_value = Self::next_template_expire(vm, &cost_plan);
|
||||||
Ok(CostResult::New {
|
Ok(CostResult::New {
|
||||||
@ -209,7 +215,7 @@ impl PricingEngine {
|
|||||||
(Currency::BTC, new_price.0, new_price.1)
|
(Currency::BTC, new_price.0, new_price.1)
|
||||||
}
|
}
|
||||||
(cur, PaymentMethod::Revolut) if cur != Currency::BTC => {
|
(cur, PaymentMethod::Revolut) if cur != Currency::BTC => {
|
||||||
(cur, (list_price.1 * 100.0).ceil() as u64, 0.01)
|
(cur, list_price.value(), 0.01)
|
||||||
}
|
}
|
||||||
(c, m) => bail!("Cannot create payment for method {} and currency {}", m, c),
|
(c, m) => bail!("Cannot create payment for method {} and currency {}", m, c),
|
||||||
})
|
})
|
||||||
@ -349,7 +355,7 @@ mod tests {
|
|||||||
email: None,
|
email: None,
|
||||||
contact_nip17: false,
|
contact_nip17: false,
|
||||||
contact_email: false,
|
contact_email: false,
|
||||||
country_code: "USA".to_string(),
|
country_code: Some("USA".to_string()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
u.insert(
|
u.insert(
|
||||||
@ -361,7 +367,7 @@ mod tests {
|
|||||||
email: None,
|
email: None,
|
||||||
contact_nip17: false,
|
contact_nip17: false,
|
||||||
contact_email: false,
|
contact_email: false,
|
||||||
country_code: "IRL".to_string(),
|
country_code: Some("IRL".to_string()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user