feat: Add rating plan management, user blocking, and system stats API endpoints

- Add GET/POST /v1/rating-plans and GET/DELETE /v1/rating-plans/:id endpoints for rating plan CRUD operations
- Add POST /v1/block/:ip, POST /v1/unblock/:ip, and GET /v1/blocked/:ip endpoints for user blocking management
- Add GET /v1/stats endpoint to retrieve system statistics (accounts/sessions/blocked users/alerts/uptime)
- Add delete to routing imports and remove unused IntoResponse/warn imports
- Add RatingPlan
This commit is contained in:
nutcas3
2026-04-25 01:27:51 +03:00
parent 71ae1925f1
commit 1b5ee4ad4a
+152 -3
View File
@@ -1,11 +1,10 @@
use axum::{
extract::{Path, State},
response::IntoResponse,
routing::{get, post},
routing::{delete, get, post},
Json, Router,
};
use tower_http::cors::{Any, CorsLayer};
use tracing::{info, warn};
use tracing::info;
use crate::errors::{ChargingError, ChargingResult, validate_ip, validate_bytes, ErrorContext, log_error, validate_amount, validate_session_id};
use crate::models::*;
@@ -28,6 +27,14 @@ pub fn create_router(state: AppState) -> Router {
.route("/v1/credit/:ip/balance", get(get_balance))
.route("/v1/subscriber/:imsi", get(get_subscriber))
.route("/v1/usage", post(record_usage))
.route("/v1/rating-plans", get(list_rating_plans))
.route("/v1/rating-plans/:id", get(get_rating_plan))
.route("/v1/rating-plans", post(add_rating_plan))
.route("/v1/rating-plans/:id", delete(remove_rating_plan))
.route("/v1/block/:ip", post(block_user))
.route("/v1/unblock/:ip", post(unblock_user))
.route("/v1/blocked/:ip", get(is_user_blocked))
.route("/v1/stats", get(get_system_stats))
.route("/health", get(health_check))
.layer(cors)
.with_state(state)
@@ -196,3 +203,145 @@ pub async fn record_usage(
pub async fn health_check() -> ChargingResult<Json<HealthResponse>> {
Ok(Json(HealthResponse::default()))
}
/// GET /v1/rating-plans
/// List all rating plans
pub async fn list_rating_plans(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let plans = state.charging_engine.list_rating_plans().await
.with_context("Failed to list rating plans")?;
Ok(Json(serde_json::json!({
"plans": plans
})))
}
/// GET /v1/rating-plans/:id
/// Get a specific rating plan
pub async fn get_rating_plan(
Path(id): Path<String>,
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let plan = state.charging_engine.get_rating_plan(&id).await
.with_context("Failed to get rating plan")?;
match plan {
Some(p) => Ok(Json(serde_json::json!({
"id": p.plan_id,
"name": p.name,
"data_rate": p.data_rate,
"voice_rate": p.voice_rate,
"sms_rate": p.sms_rate,
"monthly_fee": p.monthly_fee,
"data_limit": p.data_limit,
"voice_limit": p.voice_limit,
"sms_limit": p.sms_limit,
}))),
None => Err(ChargingError::RatingPlanNotFound(id)),
}
}
/// POST /v1/rating-plans
/// Add a new rating plan
pub async fn add_rating_plan(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let plan = crate::charging::types::RatingPlan {
plan_id: req.get("plan_id").and_then(|v| v.as_str()).unwrap_or("default").to_string(),
name: req.get("name").and_then(|v| v.as_str()).unwrap_or("Default").to_string(),
data_rate: req.get("data_rate").and_then(|v| v.as_f64()).unwrap_or(0.001),
voice_rate: req.get("voice_rate").and_then(|v| v.as_f64()).unwrap_or(0.01),
sms_rate: req.get("sms_rate").and_then(|v| v.as_f64()).unwrap_or(0.1),
monthly_fee: req.get("monthly_fee").and_then(|v| v.as_f64()).unwrap_or(10.0),
data_limit: req.get("data_limit").and_then(|v| v.as_u64()).unwrap_or(1_000_000_000),
voice_limit: req.get("voice_limit").and_then(|v| v.as_u64()).unwrap_or(1000),
sms_limit: req.get("sms_limit").and_then(|v| v.as_u64()).unwrap_or(100),
};
state.charging_engine.add_rating_plan(plan.clone()).await
.with_context("Failed to add rating plan")?;
Ok(Json(serde_json::json!({
"status": "added",
"plan_id": plan.plan_id,
})))
}
/// DELETE /v1/rating-plans/:id
/// Remove a rating plan
pub async fn remove_rating_plan(
Path(id): Path<String>,
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let removed = state.charging_engine.remove_rating_plan(&id).await
.with_context("Failed to remove rating plan")?;
Ok(Json(serde_json::json!({
"status": if removed { "removed" } else { "not_found" },
"plan_id": id,
})))
}
/// POST /v1/block/:ip
/// Block a user
pub async fn block_user(
Path(ip): Path<String>,
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
state.charging_engine.block_user(&ip).await
.with_context("Failed to block user")?;
Ok(Json(serde_json::json!({
"status": "blocked",
"ip": ip,
})))
}
/// POST /v1/unblock/:ip
/// Unblock a user
pub async fn unblock_user(
Path(ip): Path<String>,
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
state.charging_engine.unblock_user(&ip).await
.with_context("Failed to unblock user")?;
Ok(Json(serde_json::json!({
"status": "unblocked",
"ip": ip,
})))
}
/// GET /v1/blocked/:ip
/// Check if user is blocked
pub async fn is_user_blocked(
Path(ip): Path<String>,
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let blocked = state.charging_engine.is_user_blocked(&ip).await
.with_context("Failed to check blocked status")?;
Ok(Json(serde_json::json!({
"ip": ip,
"blocked": blocked,
})))
}
/// GET /v1/stats
/// Get system statistics
pub async fn get_system_stats(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let stats = state.charging_engine.get_system_statistics().await
.with_context("Failed to get system statistics")?;
Ok(Json(serde_json::json!({
"total_accounts": stats.total_accounts,
"active_sessions": stats.active_sessions,
"blocked_users": stats.blocked_users,
"low_balance_alerts": stats.low_balance_alerts,
"uptime": stats.uptime,
})))
}