feat: generate invoices
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
closes #29
This commit is contained in:
72
Cargo.lock
generated
72
Cargo.lock
generated
@ -1000,7 +1000,7 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"regex",
|
||||
]
|
||||
|
||||
@ -1014,7 +1014,7 @@ dependencies = [
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1266,7 +1266,7 @@ checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"rustversion",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
@ -1700,7 +1700,7 @@ dependencies = [
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.61.0",
|
||||
]
|
||||
@ -1969,7 +1969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
@ -2142,7 +2142,8 @@ dependencies = [
|
||||
"lnurl-rs",
|
||||
"lnvps_common",
|
||||
"lnvps_db",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"mustache",
|
||||
"native-tls",
|
||||
"nostr",
|
||||
"nostr-sdk",
|
||||
@ -2194,7 +2195,7 @@ dependencies = [
|
||||
"hex",
|
||||
"lnvps_common",
|
||||
"lnvps_db",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"rocket",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -2211,6 +2212,15 @@ dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
|
||||
dependencies = [
|
||||
"log 0.4.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
@ -2320,6 +2330,16 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
||||
|
||||
[[package]]
|
||||
name = "mustache"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51956ef1c5d20a1384524d91e616fb44dfc7d8f249bf696d49c97dd3289ecab5"
|
||||
dependencies = [
|
||||
"log 0.3.9",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
@ -2327,7 +2347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
@ -2513,7 +2533,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a64853d7ab065474e87696f7601cee817d200e86c42e04004e005cb3e20c3c5"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -2938,7 +2958,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"heck",
|
||||
"itertools",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"multimap",
|
||||
"once_cell",
|
||||
"petgraph",
|
||||
@ -3166,7 +3186,7 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
@ -3228,7 +3248,7 @@ dependencies = [
|
||||
"figment",
|
||||
"futures",
|
||||
"indexmap 2.8.0",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"memchr",
|
||||
"multer",
|
||||
"num_cpus",
|
||||
@ -3280,7 +3300,7 @@ dependencies = [
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.32",
|
||||
"indexmap 2.8.0",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"memchr",
|
||||
"pear",
|
||||
"percent-encoding",
|
||||
@ -3301,7 +3321,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "074297bec35db2fc7ebb6ade6a955b5566de66f83d9af5b5602a350a71bdef43"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"okapi",
|
||||
"rocket",
|
||||
"rocket_okapi_codegen",
|
||||
@ -3402,7 +3422,7 @@ version = "0.21.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"ring",
|
||||
"rustls-webpki 0.101.7",
|
||||
"sct",
|
||||
@ -3814,7 +3834,7 @@ dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
"hashlink",
|
||||
"indexmap 2.8.0",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
@ -3893,7 +3913,7 @@ dependencies = [
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"itoa",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"md-5",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
@ -3933,7 +3953,7 @@ dependencies = [
|
||||
"hmac",
|
||||
"home",
|
||||
"itoa",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"md-5",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
@ -3964,7 +3984,7 @@ dependencies = [
|
||||
"futures-intrusive",
|
||||
"futures-util",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_urlencoded",
|
||||
@ -4388,7 +4408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
@ -4402,7 +4422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"rustls 0.23.25",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@ -4554,7 +4574,7 @@ version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
@ -4587,7 +4607,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
@ -4633,7 +4653,7 @@ dependencies = [
|
||||
"data-encoding",
|
||||
"http 1.3.1",
|
||||
"httparse",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"native-tls",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
@ -4652,7 +4672,7 @@ dependencies = [
|
||||
"data-encoding",
|
||||
"http 1.3.1",
|
||||
"httparse",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"rand 0.9.0",
|
||||
"rustls 0.23.25",
|
||||
"rustls-pki-types",
|
||||
@ -4885,7 +4905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"log 0.4.27",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
|
@ -57,6 +57,7 @@ lettre = { version = "0.11.10", features = ["tokio1-native-tls"] }
|
||||
ws = { package = "rocket_ws", version = "0.1.1" }
|
||||
native-tls = "0.2.12"
|
||||
lnurl-rs = { version = "0.9.0", default-features = false }
|
||||
mustache = "0.9.0"
|
||||
|
||||
futures = "0.3.31"
|
||||
isocountry = "0.3.2"
|
||||
|
159
lnvps_api/invoice.html
Normal file
159
lnvps_api/invoice.html
Normal file
@ -0,0 +1,159 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{payment.id}}</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-family: "Source Code Pro", monospace;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
.page {
|
||||
margin-left: 4rem;
|
||||
margin-right: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 3rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.billing {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.4em 0.1em;
|
||||
}
|
||||
|
||||
.total {
|
||||
text-align: end;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 0.5em 0.2em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
LNVPS
|
||||
<img height="48" width="48" src="https://lnvps.net/logo.jpg" alt="logo"/>
|
||||
</div>
|
||||
<hr/>
|
||||
<h2>Invoice</h2>
|
||||
<div class="flex-col">
|
||||
<div>
|
||||
<b>ID:</b>
|
||||
{{payment.id}}
|
||||
</div>
|
||||
<div>
|
||||
<b>Date:</b>
|
||||
{{payment.created}}
|
||||
</div>
|
||||
<div>
|
||||
<b>Status:</b>
|
||||
{{#payment.is_paid}}Paid{{/payment.is_paid}}
|
||||
{{^payment.is_paid}}Unpaid{{/payment.is_paid}}
|
||||
</div>
|
||||
<div>
|
||||
<b>Nostr Pubkey:</b>
|
||||
{{npub}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="billing">
|
||||
<div class="flex-col">
|
||||
<h2>Bill To:</h2>
|
||||
<div>{{user.name}}</div>
|
||||
<div>{{user.address_1}}</div>
|
||||
<div>{{user.address_2}}</div>
|
||||
<div>{{user.city}}</div>
|
||||
<div>{{user.state}}</div>
|
||||
<div>{{user.postcode}}</div>
|
||||
<div>{{user.country}}</div>
|
||||
<div>{{user.tax_id}}</div>
|
||||
</div>
|
||||
{{#company}}
|
||||
<div class="flex-col">
|
||||
<h2> </h2>
|
||||
<div>{{company.name}}</div>
|
||||
<div>{{company.address_1}}</div>
|
||||
<div>{{company.address_2}}</div>
|
||||
<div>{{company.city}}</div>
|
||||
<div>{{company.state}}</div>
|
||||
<div>{{company.postcode}}</div>
|
||||
<div>{{company.country}}</div>
|
||||
<div>{{company.tax_id}}</div>
|
||||
</div>
|
||||
{{/company}}
|
||||
</div>
|
||||
<hr/>
|
||||
<h2>Details:</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Currency</th>
|
||||
<th>Gross</th>
|
||||
<th>Taxes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
VM Renewal #{{vm.id}}
|
||||
- {{vm.template.name}}
|
||||
- {{vm.image.distribution}} {{vm.image.version}}
|
||||
- {{payment.time}} seconds
|
||||
</td>
|
||||
<td>{{payment.currency}}</td>
|
||||
<td>{{payment.amount}}</td>
|
||||
<td>{{payment.tax}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="4" class="total">
|
||||
Total: {{total}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<b>
|
||||
All BTC amounts are in milli-satoshis and all fiat amounts are in cents.
|
||||
</b>
|
||||
<hr/>
|
||||
<small>
|
||||
(c) {{year}} LNVPS.net - Generated at {{current_date}}
|
||||
</small>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -425,6 +425,24 @@ pub struct AccountPatchRequest {
|
||||
pub tax_id: Option<String>,
|
||||
}
|
||||
|
||||
impl From<lnvps_db::User> for AccountPatchRequest {
|
||||
fn from(user: lnvps_db::User) -> Self {
|
||||
AccountPatchRequest {
|
||||
email: user.email,
|
||||
contact_nip17: user.contact_nip17,
|
||||
contact_email: user.contact_email,
|
||||
country_code: user.country_code,
|
||||
name: user.billing_name,
|
||||
address_1: user.billing_address_1,
|
||||
address_2: user.billing_address_2,
|
||||
state: user.billing_state,
|
||||
city: user.billing_city,
|
||||
postcode: user.billing_postcode,
|
||||
tax_id: user.billing_tax_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateVmRequest {
|
||||
pub template_id: u64,
|
||||
@ -574,3 +592,45 @@ impl From<PaymentMethod> for ApiPaymentMethod {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApiCompany {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub country_code: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub address_1: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub address_2: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub state: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub city: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub postcode: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tax_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone: Option<String>,
|
||||
}
|
||||
|
||||
impl From<lnvps_db::Company> for ApiCompany {
|
||||
fn from(value: lnvps_db::Company) -> Self {
|
||||
Self {
|
||||
email: value.email,
|
||||
country_code: value.country_code,
|
||||
name: value.name,
|
||||
id: value.id,
|
||||
address_1: value.address_1,
|
||||
address_2: value.address_2,
|
||||
state: value.state,
|
||||
city: value.city,
|
||||
postcode: value.postcode,
|
||||
tax_id: value.tax_id,
|
||||
phone: value.phone,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::api::model::{
|
||||
AccountPatchRequest, ApiCustomTemplateParams, ApiCustomVmOrder, ApiCustomVmRequest,
|
||||
AccountPatchRequest, ApiCompany, ApiCustomTemplateParams, ApiCustomVmOrder, ApiCustomVmRequest,
|
||||
ApiPaymentInfo, ApiPaymentMethod, ApiPrice, ApiTemplatesResponse, ApiUserSshKey,
|
||||
ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus, ApiVmTemplate, CreateSshKey,
|
||||
CreateVmRequest, VMPatchRequest,
|
||||
@ -12,6 +12,7 @@ use crate::settings::Settings;
|
||||
use crate::status::{VmState, VmStateCache};
|
||||
use crate::worker::WorkJob;
|
||||
use anyhow::{bail, Result};
|
||||
use chrono::{DateTime, Datelike, Utc};
|
||||
use futures::future::join_all;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use isocountry::CountryCode;
|
||||
@ -22,7 +23,8 @@ use lnvps_db::{
|
||||
};
|
||||
use log::{error, info};
|
||||
use nostr::util::hex;
|
||||
use nostr::Url;
|
||||
use nostr::{ToBech32, Url};
|
||||
use rocket::http::ContentType;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{get, patch, post, routes, Responder, Route, State};
|
||||
use rocket_okapi::gen::OpenApiGenerator;
|
||||
@ -34,6 +36,7 @@ use serde::{Deserialize, Serialize};
|
||||
use ssh_key::PublicKey;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Display;
|
||||
use std::io::{BufWriter, Cursor};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{Sender, UnboundedSender};
|
||||
@ -71,7 +74,8 @@ pub fn routes() -> Vec<Route> {
|
||||
routes.append(&mut routes![
|
||||
v1_terminal_proxy,
|
||||
v1_lnurlp,
|
||||
v1_renew_vm_lnurlp
|
||||
v1_renew_vm_lnurlp,
|
||||
v1_get_payment_invoice
|
||||
]);
|
||||
|
||||
routes
|
||||
@ -156,19 +160,7 @@ async fn v1_get_account(
|
||||
let uid = db.upsert_user(&pubkey).await?;
|
||||
let user = db.get_user(uid).await?;
|
||||
|
||||
ApiData::ok(AccountPatchRequest {
|
||||
email: user.email,
|
||||
contact_nip17: user.contact_nip17,
|
||||
contact_email: user.contact_email,
|
||||
country_code: user.country_code,
|
||||
name: user.billing_name,
|
||||
address_1: user.billing_address_1,
|
||||
address_2: user.billing_address_2,
|
||||
state: user.billing_state,
|
||||
city: user.billing_city,
|
||||
postcode: user.billing_postcode,
|
||||
tax_id: user.billing_tax_id,
|
||||
})
|
||||
ApiData::ok(user.into())
|
||||
}
|
||||
|
||||
async fn vm_to_status(
|
||||
@ -894,6 +886,99 @@ async fn v1_get_payment(
|
||||
ApiData::ok(payment.into())
|
||||
}
|
||||
|
||||
/// Print payment invoice
|
||||
#[get("/api/v1/payment/<id>/invoice?<auth>")]
|
||||
async fn v1_get_payment_invoice(
|
||||
db: &State<Arc<dyn LNVpsDb>>,
|
||||
id: &str,
|
||||
auth: &str,
|
||||
) -> Result<(ContentType, Vec<u8>), &'static str> {
|
||||
let auth = Nip98Auth::from_base64(auth).map_err(|e| "Missing or invalid auth param")?;
|
||||
if auth
|
||||
.check(&format!("/api/v1/payment/{id}/invoice"), "GET")
|
||||
.is_err()
|
||||
{
|
||||
return Err("Invalid auth event");
|
||||
}
|
||||
let pubkey = auth.event.pubkey.to_bytes();
|
||||
let uid = db.upsert_user(&pubkey).await.map_err(|_| "Insert failed")?;
|
||||
let id = if let Ok(i) = hex::decode(id) {
|
||||
i
|
||||
} else {
|
||||
return Err("Invalid payment id");
|
||||
};
|
||||
|
||||
let payment = db
|
||||
.get_vm_payment(&id)
|
||||
.await
|
||||
.map_err(|_| "Payment not found")?;
|
||||
let vm = db.get_vm(payment.vm_id).await.map_err(|_| "VM not found")?;
|
||||
if vm.user_id != uid {
|
||||
return Err("VM does not belong to you");
|
||||
}
|
||||
|
||||
if !payment.is_paid {
|
||||
return Err("Payment is not paid, can't generate invoice");
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PaymentInfo {
|
||||
year: i32,
|
||||
current_date: DateTime<Utc>,
|
||||
vm: ApiVmStatus,
|
||||
payment: ApiVmPayment,
|
||||
user: AccountPatchRequest,
|
||||
npub: String,
|
||||
total: u64,
|
||||
company: Option<ApiCompany>,
|
||||
}
|
||||
|
||||
let host = db
|
||||
.get_host(vm.host_id)
|
||||
.await
|
||||
.map_err(|_| "Host not found")?;
|
||||
let region = db
|
||||
.get_host_region(host.region_id)
|
||||
.await
|
||||
.map_err(|_| "Region not found")?;
|
||||
let company = if let Some(c) = region.company_id {
|
||||
Some(db.get_company(c).await.map_err(|_| "Company not found")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let user = db.get_user(uid).await.map_err(|_| "User not found")?;
|
||||
#[cfg(debug_assertions)]
|
||||
let template =
|
||||
mustache::compile_path("lnvps_api/invoice.html").map_err(|_| "Invalid template")?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
let template = mustache::compile_str(include_str!("../../invoice.html"))
|
||||
.map_err(|_| "Invalid template")?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut html = Cursor::new(Vec::new());
|
||||
template
|
||||
.render(
|
||||
&mut html,
|
||||
&PaymentInfo {
|
||||
year: now.year(),
|
||||
current_date: now,
|
||||
vm: vm_to_status(db, vm, None)
|
||||
.await
|
||||
.map_err(|_| "Failed to get VM state")?,
|
||||
total: payment.amount + payment.tax,
|
||||
payment: payment.into(),
|
||||
npub: nostr::PublicKey::from_slice(&user.pubkey)
|
||||
.map_err(|_| "Invalid pubkey")?
|
||||
.to_bech32()
|
||||
.unwrap(),
|
||||
user: user.into(),
|
||||
company: company.map(|c| c.into()),
|
||||
},
|
||||
)
|
||||
.map_err(|_| "Failed to generate invoice")?;
|
||||
Ok((ContentType::HTML, html.into_inner()))
|
||||
}
|
||||
|
||||
/// List payment history of a VM
|
||||
#[openapi(tag = "VM")]
|
||||
#[get("/api/v1/vm/<id>/payments")]
|
||||
|
@ -10,12 +10,7 @@ use crate::status::{VmRunningState, VmState};
|
||||
use anyhow::{anyhow, bail, ensure, Context};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use fedimint_tonic_lnd::tonic::codegen::tokio_stream::Stream;
|
||||
use lnvps_db::{
|
||||
async_trait, AccessPolicy, DiskInterface, DiskType, IpRange, IpRangeAllocationMode,
|
||||
LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, OsDistribution, User, UserSshKey, Vm,
|
||||
VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate,
|
||||
VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
||||
};
|
||||
use lnvps_db::{async_trait, AccessPolicy, Company, DiskInterface, DiskType, IpRange, IpRangeAllocationMode, LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, OsDistribution, User, UserSshKey, Vm, VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Add;
|
||||
use std::pin::Pin;
|
||||
@ -108,6 +103,7 @@ impl Default for MockDb {
|
||||
id: 1,
|
||||
name: "Mock".to_string(),
|
||||
enabled: true,
|
||||
company_id: None,
|
||||
},
|
||||
);
|
||||
let mut ip_ranges = HashMap::new();
|
||||
@ -695,6 +691,10 @@ impl LNVpsDb for MockDb {
|
||||
.cloned()
|
||||
.context("no access policy")?)
|
||||
}
|
||||
|
||||
async fn get_company(&self, company_id: u64) -> anyhow::Result<Company> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
19
lnvps_db/migrations/20250501162308_company_info.sql
Normal file
19
lnvps_db/migrations/20250501162308_company_info.sql
Normal file
@ -0,0 +1,19 @@
|
||||
-- Add migration script here
|
||||
create table company
|
||||
(
|
||||
id integer unsigned not null auto_increment primary key,
|
||||
created timestamp not null default current_timestamp,
|
||||
name varchar(100) not null,
|
||||
email varchar(100) not null,
|
||||
phone varchar(100),
|
||||
address_1 varchar(200),
|
||||
address_2 varchar(200),
|
||||
city varchar(100),
|
||||
state varchar(100),
|
||||
postcode varchar(50),
|
||||
country_code varchar(3),
|
||||
tax_id varchar(50)
|
||||
);
|
||||
alter table vm_host_region
|
||||
add column company_id integer unsigned,
|
||||
add constraint fk_host_region_company foreign key (company_id) references company (id);
|
@ -172,6 +172,9 @@ pub trait LNVpsDb: LNVPSNostrDb + Send + Sync {
|
||||
|
||||
/// Get access policy
|
||||
async fn get_access_policy(&self, access_policy_id: u64) -> Result<AccessPolicy>;
|
||||
|
||||
/// Get company
|
||||
async fn get_company(&self, company_id: u64) -> Result<Company>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "nostr-domain")]
|
||||
|
@ -71,6 +71,7 @@ pub struct VmHostRegion {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub enabled: bool,
|
||||
pub company_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Clone, Debug, Default)]
|
||||
@ -524,3 +525,19 @@ pub struct NostrDomainHandle {
|
||||
pub pubkey: Vec<u8>,
|
||||
pub relays: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Clone, Debug, Default)]
|
||||
pub struct Company {
|
||||
pub id: u64,
|
||||
pub created: DateTime<Utc>,
|
||||
pub name: String,
|
||||
pub address_1: Option<String>,
|
||||
pub address_2: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub country_code: Option<String>,
|
||||
pub tax_id: Option<String>,
|
||||
pub postcode: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
|
@ -1,8 +1,4 @@
|
||||
use crate::{
|
||||
AccessPolicy, IpRange, LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, Router, User,
|
||||
UserSshKey, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost,
|
||||
VmHostDisk, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
||||
};
|
||||
use crate::{AccessPolicy, Company, IpRange, LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, Router, User, UserSshKey, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate};
|
||||
use anyhow::{bail, Error, Result};
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{Executor, MySqlPool, Row};
|
||||
@ -562,6 +558,14 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
}
|
||||
|
||||
async fn get_company(&self, company_id: u64) -> Result<Company> {
|
||||
sqlx::query_as("select * from company where id=?")
|
||||
.bind(company_id)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nostr-domain")]
|
||||
|
Reference in New Issue
Block a user