forked from coracle/caravel
Add plan model
This commit is contained in:
@@ -27,6 +27,19 @@ Notes:
|
||||
- Adds CORS middleware based on `origins`
|
||||
- Calls `axum::serve` with a listener
|
||||
|
||||
--- Plan routes
|
||||
|
||||
## `async fn list_plans(...) -> Response`
|
||||
|
||||
- Serves `GET /plans`
|
||||
- Return `data` is a list of plan structs from `Repo::list_plans`
|
||||
|
||||
## `async fn get_plan(...) -> Response`
|
||||
|
||||
- Serves `GET /plans/:id`
|
||||
- Return `data` is a single plan struct matching `id`
|
||||
- If plan does not exist, return `404` with `code=not-found`
|
||||
|
||||
--- Tenant routes
|
||||
|
||||
## `async fn list_tenants(...) -> Response`
|
||||
@@ -100,6 +113,13 @@ Notes:
|
||||
- If user is a tenant, `tenant` query parameter is not ok; authenticated `pubkey` is used
|
||||
- Return `data` is a list of invoice structs from `repo.list_invoices`
|
||||
|
||||
## `async fn update_tenant_billing(...) -> Response`
|
||||
|
||||
- Serves `PUT /tenants/:pubkey/billing`
|
||||
- Authorizes admin or matching tenant
|
||||
- Updates tenant billing NWC URL using `repo.update_tenant_nwc_url`
|
||||
- Return `data` is the submitted billing payload
|
||||
|
||||
# Utility functions
|
||||
|
||||
## `extract_auth_pubkey(headers: &HeaderMap, method: &Method, uri: &Uri) -> Result<String>`
|
||||
|
||||
@@ -27,6 +27,23 @@ Activity is an audit log of all actions performed by a user or a worker process.
|
||||
- `resource_type` is a string identifying the resource type being modified.
|
||||
- `resource_id` is a string identifying the resource id being modified.
|
||||
|
||||
# Plan
|
||||
|
||||
A plan represents a rate charged for relays at a given feature/usage limit. Plans aren't saved to the database, but are simply hardcoded. However, they are exposed through the API so they can be used as a single source of truth.
|
||||
|
||||
- `id` - the plan slug
|
||||
- `name` - the plan name
|
||||
- `sats` - the plan't cost per month
|
||||
- `members` - the max number of members a relay can have before needing to upgrade. If empty, membership is not limited.
|
||||
- `blossom` - whether blossom media hosting is available on this plan
|
||||
- `livekit` - whether livekit audio/video calls are available on this plan
|
||||
|
||||
There are three plans available:
|
||||
|
||||
- `free` - 0 sats/mo, up to 10 members, no blossom/livekit
|
||||
- `basic` - 10k sats/mo, up to 100 members, includes blossom/livekit
|
||||
- `growth` - 50k sats/mo, unlimited members, includes blossom/livekit
|
||||
|
||||
# Tenant
|
||||
|
||||
Tenants are customers of the service, identified by a nostr `pubkey`. Public metadata like name etc are pulled from the nostr network. They also have associated billing information.
|
||||
|
||||
@@ -48,6 +48,11 @@ Notes:
|
||||
- Updates tenant `nwc_url`
|
||||
- Logs activity as `(update_tenant_nwc_url, tenant_id)`
|
||||
|
||||
## `pub fn list_plans() -> Vec<Plan>`
|
||||
|
||||
- Returns the hardcoded relay plans used by the system (`free`, `basic`, `growth`)
|
||||
- This is the source of truth for plan metadata exposed via API
|
||||
|
||||
## `pub fn list_relays(&self, tenant_id: Option<&str>) -> Result<Vec<Relay>>`
|
||||
|
||||
- Returns all matching relays
|
||||
@@ -120,3 +125,8 @@ Notes:
|
||||
## `pub fn list_activity(&self, since: &i64, tenant: Option<&str>) -> Result<Vec<Activity>>`
|
||||
|
||||
- Returns all activity occuring after `since` matching `tenant`
|
||||
|
||||
## `pub fn get_relay_plan_sats(&self, plan: &str) -> Result<i64>`
|
||||
|
||||
- Returns the monthly sats amount for a given plan id
|
||||
- Uses `list_plans()` data for consistent pricing logic across API and billing
|
||||
|
||||
@@ -76,6 +76,8 @@ impl Api {
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/plans", get(list_plans))
|
||||
.route("/plans/:id", get(get_plan))
|
||||
.route("/tenants", get(list_tenants).post(create_tenant))
|
||||
.route("/tenants/:pubkey", get(get_tenant))
|
||||
.route("/tenants/:pubkey/billing", put(update_tenant_billing))
|
||||
@@ -323,6 +325,36 @@ async fn list_tenants(
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_plans(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
method: Method,
|
||||
uri: Uri,
|
||||
) -> Response {
|
||||
if let Err(e) = extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
|
||||
return auth_fail_response(e);
|
||||
}
|
||||
|
||||
ok(StatusCode::OK, Repo::list_plans())
|
||||
}
|
||||
|
||||
async fn get_plan(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
method: Method,
|
||||
uri: Uri,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
if let Err(e) = extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
|
||||
return auth_fail_response(e);
|
||||
}
|
||||
|
||||
match Repo::list_plans().into_iter().find(|p| p.id == id) {
|
||||
Some(plan) => ok(StatusCode::OK, plan),
|
||||
None => err(StatusCode::NOT_FOUND, "not-found", "plan not found"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_tenant(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
|
||||
@@ -125,7 +125,7 @@ impl Billing {
|
||||
if hours <= 0 {
|
||||
continue;
|
||||
}
|
||||
let plan_monthly = self.repo.get_relay_plan_amount_sats(&relay.plan).await?;
|
||||
let plan_monthly = self.repo.get_relay_plan_sats(&relay.plan).await?;
|
||||
if plan_monthly <= 0 {
|
||||
continue;
|
||||
}
|
||||
@@ -228,7 +228,7 @@ impl Billing {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn make_bolt11(&self, amount_sats: i64) -> Result<String> {
|
||||
async fn make_bolt11(&self, sats: i64) -> Result<String> {
|
||||
if self.nwc_url.trim().is_empty() {
|
||||
anyhow::bail!("NWC_URL not configured")
|
||||
}
|
||||
@@ -236,7 +236,7 @@ impl Billing {
|
||||
let uri = nostr_sdk::nips::nip47::NostrWalletConnectURI::parse(&self.nwc_url)?;
|
||||
let req = nostr_sdk::nips::nip47::Request::make_invoice(
|
||||
nostr_sdk::nips::nip47::MakeInvoiceRequest {
|
||||
amount: (amount_sats as u64) * 1_000,
|
||||
amount: (sats as u64) * 1_000,
|
||||
description: Some("Caravel relay invoice".to_string()),
|
||||
description_hash: None,
|
||||
expiry: None,
|
||||
|
||||
@@ -10,6 +10,16 @@ pub struct Activity {
|
||||
pub resource_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Plan {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub sats: i64,
|
||||
pub members: Option<i64>,
|
||||
pub blossom: bool,
|
||||
pub livekit: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Tenant {
|
||||
pub pubkey: String,
|
||||
|
||||
+36
-8
@@ -7,7 +7,7 @@ use sqlx::{
|
||||
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
|
||||
};
|
||||
|
||||
use crate::models::{Activity, Invoice, InvoiceItem, Relay, Tenant};
|
||||
use crate::models::{Activity, Invoice, InvoiceItem, Plan, Relay, Tenant};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Repo {
|
||||
@@ -502,15 +502,43 @@ impl Repo {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn get_relay_plan_amount_sats(&self, plan: &str) -> Result<i64> {
|
||||
let sats = match plan {
|
||||
"free" => 0,
|
||||
"basic" => 10_000,
|
||||
"growth" => 50_000,
|
||||
_ => 0,
|
||||
};
|
||||
pub async fn get_relay_plan_sats(&self, plan: &str) -> Result<i64> {
|
||||
let sats = Self::list_plans()
|
||||
.into_iter()
|
||||
.find(|p| p.id == plan)
|
||||
.map(|p| p.sats)
|
||||
.unwrap_or(0);
|
||||
Ok(sats)
|
||||
}
|
||||
|
||||
pub fn list_plans() -> Vec<Plan> {
|
||||
vec![
|
||||
Plan {
|
||||
id: "free".to_string(),
|
||||
name: "Free".to_string(),
|
||||
sats: 0,
|
||||
members: Some(10),
|
||||
blossom: false,
|
||||
livekit: false,
|
||||
},
|
||||
Plan {
|
||||
id: "basic".to_string(),
|
||||
name: "Basic".to_string(),
|
||||
sats: 10_000,
|
||||
members: Some(100),
|
||||
blossom: true,
|
||||
livekit: true,
|
||||
},
|
||||
Plan {
|
||||
id: "growth".to_string(),
|
||||
name: "Growth".to_string(),
|
||||
sats: 50_000,
|
||||
members: None,
|
||||
blossom: true,
|
||||
livekit: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_sqlite_url(url: &str) -> String {
|
||||
|
||||
Reference in New Issue
Block a user