107 lines
2.7 KiB
Rust
107 lines
2.7 KiB
Rust
use std::sync::Arc;
|
|
|
|
use axum::extract::State;
|
|
use axum::http::StatusCode;
|
|
use axum::response::{IntoResponse, Response};
|
|
use axum::Json;
|
|
use serde::Serialize;
|
|
use tracing::warn;
|
|
|
|
use crate::pocketbase::get_superuser_token;
|
|
use crate::state::{AppState, SharedState};
|
|
|
|
/// Pricing tiers: (cumulative user cap, price in pence).
|
|
const TIERS: &[(u64, u64)] = &[
|
|
(1, 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 token = get_superuser_token(state).await?;
|
|
|
|
let pb_url = state.pocketbase_url.trim_end_matches('/');
|
|
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(shared): State<Arc<SharedState>>) -> Response {
|
|
let state = shared.load_state();
|
|
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()
|
|
}
|