lmao
This commit is contained in:
parent
03445188ea
commit
524580eb25
102 changed files with 36625 additions and 1295 deletions
105
server-rs/src/routes/pricing.rs
Normal file
105
server-rs/src/routes/pricing.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::pocketbase::auth_superuser;
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Pricing tiers: (cumulative user cap, price in pence).
|
||||
const TIERS: &[(u64, u64)] = &[
|
||||
(10, 0), // First 10 users: free
|
||||
(20, 1000), // Next 10: £10
|
||||
(45, 2500), // Next 25: £25
|
||||
(95, 5000), // Next 50: £50
|
||||
];
|
||||
const FINAL_PRICE_PENCE: u64 = 10000; // £100 after 95
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Tier {
|
||||
up_to: Option<u64>,
|
||||
price_pence: u64,
|
||||
slots: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PricingResponse {
|
||||
licensed_count: u64,
|
||||
current_price_pence: u64,
|
||||
tiers: Vec<Tier>,
|
||||
}
|
||||
|
||||
/// Determine the price (in pence) for the next user given `count` existing licensed users.
|
||||
pub fn price_for_count(count: u64) -> u64 {
|
||||
for &(cap, price) in TIERS {
|
||||
if count < cap {
|
||||
return price;
|
||||
}
|
||||
}
|
||||
FINAL_PRICE_PENCE
|
||||
}
|
||||
|
||||
/// Count users with subscription="licensed" in PocketBase.
|
||||
pub async fn count_licensed_users(state: &AppState) -> anyhow::Result<u64> {
|
||||
let pb_url = state.pocketbase_url.trim_end_matches('/');
|
||||
let token = auth_superuser(&state.http_client, pb_url, &state.pocketbase_admin_email, &state.pocketbase_admin_password).await?;
|
||||
|
||||
let filter = "subscription=\"licensed\"";
|
||||
let url = format!(
|
||||
"{pb_url}/api/collections/users/records?filter={}&perPage=1",
|
||||
urlencoding::encode(filter)
|
||||
);
|
||||
|
||||
let resp = state
|
||||
.http_client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("PocketBase returned {}", resp.status());
|
||||
}
|
||||
|
||||
let body: serde_json::Value = resp.json().await?;
|
||||
let total = body["totalItems"].as_u64().unwrap_or(0);
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
pub async fn get_pricing(state: Arc<AppState>) -> Response {
|
||||
let count = match count_licensed_users(&state).await {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
warn!("Failed to count licensed users: {err}");
|
||||
return StatusCode::SERVICE_UNAVAILABLE.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let current_price = price_for_count(count);
|
||||
|
||||
let mut tiers = Vec::new();
|
||||
let mut prev_cap = 0u64;
|
||||
for &(cap, price) in TIERS {
|
||||
tiers.push(Tier {
|
||||
up_to: Some(cap),
|
||||
price_pence: price,
|
||||
slots: cap - prev_cap,
|
||||
});
|
||||
prev_cap = cap;
|
||||
}
|
||||
tiers.push(Tier {
|
||||
up_to: None,
|
||||
price_pence: FINAL_PRICE_PENCE,
|
||||
slots: 0,
|
||||
});
|
||||
|
||||
Json(PricingResponse {
|
||||
licensed_count: count,
|
||||
current_price_pence: current_price,
|
||||
tiers,
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue