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, price_pence: u64, slots: u64, } #[derive(Serialize)] pub struct PricingResponse { licensed_count: u64, current_price_pence: u64, tiers: Vec, } /// 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 { 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>) -> 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() }