feat: Remove handlers module and relocate 25+ API handler functions

- Delete handlers.rs module containing credit management handlers (check_credit, deduct_credit, add_credit, get_balance)
- Remove subscriber account handlers (get_subscriber, update_subscriber)
- Remove usage event handlers (record_usage, calculate_usage_cost, rate_usage, process_usage)
- Remove rating plan handlers (list_rating_plans, get_rating_plan, add_rating_plan, remove_rating_plan)
- Remove user blocking handlers (block_user, unblock_user,
This commit is contained in:
nutcas3
2026-04-25 02:52:27 +03:00
parent 69c54c4a35
commit c37ba6c8f2
10 changed files with 624 additions and 559 deletions
-559
View File
@@ -1,559 +0,0 @@
use axum::{
extract::{Path, State},
Json,
};
use tracing::info;
use crate::errors::{ChargingError, ChargingResult, ErrorContext, log_error, validate_ip, validate_bytes, validate_amount, validate_session_id};
use crate::models::*;
/// POST /v1/credit/:ip/check
/// Check if user has enough credit for data usage
pub async fn check_credit(
Path(ip): Path<String>,
State(state): State<AppState>,
Json(req): Json<CreditCheckRequest>,
) -> ChargingResult<Json<CreditCheckResponse>> {
validate_ip(&ip)?;
validate_bytes(req.bytes_requested)?;
let allowed = state.charging_engine.check_credit(&ip, req.bytes_requested).await
.with_context("Failed to check credit")
.map_err(|e| {
log_error(&e);
e
})?;
let remaining = state.charging_engine.get_balance(&ip).await
.with_context("Failed to get balance")?;
info!(
"Credit check for {}: {} bytes requested, {} bytes available, allowed: {}",
ip, req.bytes_requested, remaining, allowed
);
Ok(Json(CreditCheckResponse {
allowed,
remaining_bytes: remaining as i64,
}))
}
/// POST /v1/credit/:ip/deduct
/// Deduct bytes from user's credit balance
pub async fn deduct_credit(
Path(ip): Path<String>,
State(state): State<AppState>,
Json(req): Json<DeductRequest>,
) -> ChargingResult<()> {
validate_ip(&ip)?;
validate_bytes(req.bytes_used)?;
validate_amount(req.bytes_used as f64)?;
let new_balance = state.charging_engine.deduct_credit(&ip, req.bytes_used).await
.with_context("Failed to deduct credit")?;
info!(
"User {} deducted {} bytes, remaining: {}",
ip, req.bytes_used, new_balance
);
Ok(())
}
/// POST /v1/credit/:ip/add
/// Add bytes to user's credit balance
pub async fn add_credit(
Path(ip): Path<String>,
State(state): State<AppState>,
Json(req): Json<AddCreditRequest>,
) -> ChargingResult<()> {
validate_ip(&ip)?;
validate_bytes(req.bytes_to_add)?;
validate_amount(req.bytes_to_add as f64)?;
let new_balance = state.charging_engine.add_credit(&ip, req.bytes_to_add).await
.with_context("Failed to add credit")?;
info!(
"User {} added {} bytes, new balance: {}",
ip, req.bytes_to_add, new_balance
);
Ok(())
}
/// GET /v1/credit/:ip/balance
/// Get current credit balance
pub async fn get_balance(
Path(ip): Path<String>,
State(state): State<AppState>,
) -> ChargingResult<Json<BalanceResponse>> {
validate_ip(&ip)?;
let balance = state.charging_engine.get_balance(&ip).await
.with_context("Failed to get balance")?;
info!("Retrieved balance for IP: {}", ip);
Ok(Json(BalanceResponse {
ip: ip.clone(),
balance_bytes: balance as i64,
}))
}
/// GET /v1/subscriber/:imsi
/// Get subscriber account by IMSI
pub async fn get_subscriber(
Path(imsi): Path<String>,
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let account = state.charging_engine.get_subscriber_account(&imsi).await
.with_context("Failed to get subscriber account")?;
match account {
Some(acc) => Ok(Json(serde_json::json!({
"imsi": acc.imsi,
"balance": acc.balance,
"data_used": acc.data_used,
"data_limit": acc.data_limit,
"voice_used": acc.voice_used,
"voice_limit": acc.voice_limit,
"sms_used": acc.sms_used,
"sms_limit": acc.sms_limit,
}))),
None => Err(ChargingError::SubscriberNotFound(imsi)),
}
}
/// POST /v1/usage
/// Record usage event
pub async fn record_usage(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let imsi = req.get("imsi").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing IMSI".to_string())
})?;
let session_id = req.get("session_id").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing session_id".to_string())
})?;
validate_session_id(session_id)?;
let event = crate::charging::types::UsageEvent {
imsi: imsi.to_string(),
session_id: session_id.to_string(),
volume: req.get("volume").and_then(|v| v.as_u64()).unwrap_or(0),
cost: req.get("cost").and_then(|v| v.as_f64()).unwrap_or(0.0),
rate: req.get("rate").and_then(|v| v.as_f64()).unwrap_or(0.0),
usage_type: crate::charging::types::UsageType::Data,
timestamp: chrono::Utc::now(),
};
state.charging_engine.record_usage_event(&event).await
.with_context("Failed to record usage event")?;
Ok(Json(serde_json::json!({
"status": "recorded",
"imsi": imsi,
"session_id": session_id,
})))
}
/// GET /health
/// Health check endpoint
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")?;
// Also use list_map for internal lookups
let _ = state.charging_engine.plans.list_map().await
.with_context("Failed to list rating plans as map")?;
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,
})))
}
/// PUT /v1/subscriber/:imsi
/// Update subscriber account
pub async fn update_subscriber(
Path(imsi): Path<String>,
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let account = crate::charging::types::SubscriberAccount {
imsi: imsi.clone(),
balance: req.get("balance").and_then(|v| v.as_i64()).unwrap_or(0),
data_used: req.get("data_used").and_then(|v| v.as_u64()).unwrap_or(0),
data_limit: req.get("data_limit").and_then(|v| v.as_u64()).unwrap_or(1_000_000_000),
voice_used: req.get("voice_used").and_then(|v| v.as_u64()).unwrap_or(0),
voice_limit: req.get("voice_limit").and_then(|v| v.as_u64()).unwrap_or(1000),
sms_used: req.get("sms_used").and_then(|v| v.as_u64()).unwrap_or(0),
sms_limit: req.get("sms_limit").and_then(|v| v.as_u64()).unwrap_or(100),
status: crate::charging::types::AccountStatus::Active,
last_updated: chrono::Utc::now(),
};
state.charging_engine.update_subscriber_account(&account).await
.with_context("Failed to update subscriber account")?;
Ok(Json(serde_json::json!({
"status": "updated",
"imsi": imsi,
})))
}
/// GET /v1/metrics
/// Get performance metrics
pub async fn get_performance_metrics(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let metrics = state.charging_engine.get_performance_metrics().await
.with_context("Failed to get performance metrics")?;
Ok(Json(serde_json::json!({
"connected_clients": metrics.connected_clients,
"used_memory": metrics.used_memory,
"total_commands_processed": metrics.total_commands_processed,
"requests_per_second": metrics.requests_per_second,
"average_response_time": metrics.average_response_time,
})))
}
/// POST /v1/engine/start
/// Start the engine
pub async fn engine_start(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
state.charging_engine.start().await
.with_context("Failed to start engine")?;
Ok(Json(serde_json::json!({
"status": "started",
})))
}
/// POST /v1/engine/stop
/// Stop the engine
pub async fn engine_stop(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
state.charging_engine.stop().await
.with_context("Failed to stop engine")?;
Ok(Json(serde_json::json!({
"status": "stopped",
})))
}
/// GET /v1/engine/uptime
/// Get engine uptime
pub async fn engine_uptime(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let uptime = state.charging_engine.uptime();
Ok(Json(serde_json::json!({
"uptime_seconds": uptime.as_secs(),
})))
}
/// GET /v1/errors
/// Get error statistics
pub async fn get_error_stats(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let errors = state.charging_engine.get_error_statistics().await
.with_context("Failed to get error statistics")?;
Ok(Json(serde_json::json!({
"total_errors": errors.total_errors,
"error_types": errors.error_types,
"last_error": errors.last_error,
})))
}
/// POST /v1/sync/start
/// Start background sync
pub async fn start_sync(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
state.charging_engine.start_background_sync().await
.with_context("Failed to start background sync")?;
Ok(Json(serde_json::json!({
"status": "sync_started",
})))
}
/// GET /v1/health/detailed
/// Get detailed health check
pub async fn detailed_health_check(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let health = state.charging_engine.health_check().await
.with_context("Failed to get health check")?;
Ok(Json(serde_json::json!({
"redis_connected": health.redis_connected,
"active_sync": health.active_sync,
"last_sync": health.last_sync,
"memory_usage": health.memory_usage,
})))
}
/// POST /v1/usage/calculate-cost
/// Calculate usage cost for an event
pub async fn calculate_usage_cost(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let imsi = req.get("imsi").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing IMSI".to_string())
})?;
let session_id = req.get("session_id").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing session_id".to_string())
})?;
validate_session_id(session_id)?;
let event = crate::charging::types::UsageEvent {
imsi: imsi.to_string(),
session_id: session_id.to_string(),
volume: req.get("volume").and_then(|v| v.as_u64()).unwrap_or(0),
cost: req.get("cost").and_then(|v| v.as_f64()).unwrap_or(0.0),
rate: req.get("rate").and_then(|v| v.as_f64()).unwrap_or(0.0),
usage_type: crate::charging::types::UsageType::Data,
timestamp: chrono::Utc::now(),
};
let cost = state.charging_engine.calculate_usage_cost(&event).await
.with_context("Failed to calculate usage cost")?;
Ok(Json(serde_json::json!({
"cost": cost,
"imsi": imsi,
"session_id": session_id,
})))
}
/// POST /v1/usage/rate
/// Rate usage event
pub async fn rate_usage(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let imsi = req.get("imsi").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing IMSI".to_string())
})?;
let session_id = req.get("session_id").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing session_id".to_string())
})?;
validate_session_id(session_id)?;
let event = crate::charging::types::UsageEvent {
imsi: imsi.to_string(),
session_id: session_id.to_string(),
volume: req.get("volume").and_then(|v| v.as_u64()).unwrap_or(0),
cost: req.get("cost").and_then(|v| v.as_f64()).unwrap_or(0.0),
rate: req.get("rate").and_then(|v| v.as_f64()).unwrap_or(0.0),
usage_type: crate::charging::types::UsageType::Data,
timestamp: chrono::Utc::now(),
};
let rated_event = state.charging_engine.rate_usage(event).await
.with_context("Failed to rate usage")?;
Ok(Json(serde_json::json!({
"rated_event": rated_event,
})))
}
/// POST /v1/usage/process
/// Process usage event with full rating and billing
pub async fn process_usage(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let imsi = req.get("imsi").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing IMSI".to_string())
})?;
let session_id = req.get("session_id").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing session_id".to_string())
})?;
validate_session_id(session_id)?;
let event = crate::charging::types::UsageEvent {
imsi: imsi.to_string(),
session_id: session_id.to_string(),
volume: req.get("volume").and_then(|v| v.as_u64()).unwrap_or(0),
cost: req.get("cost").and_then(|v| v.as_f64()).unwrap_or(0.0),
rate: req.get("rate").and_then(|v| v.as_f64()).unwrap_or(0.0),
usage_type: crate::charging::types::UsageType::Data,
timestamp: chrono::Utc::now(),
};
state.charging_engine.process_usage_event(event).await
.with_context("Failed to process usage event")?;
Ok(Json(serde_json::json!({
"status": "processed",
"imsi": imsi,
})))
}
/// GET /v1/invoice/:imsi/:period
/// Generate invoice for billing period
pub async fn generate_invoice(
Path((imsi, period)): Path<(String, String)>,
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let invoice = state.charging_engine.generate_invoice(&imsi, &period).await
.with_context("Failed to generate invoice")?;
Ok(Json(serde_json::json!({
"imsi": imsi,
"period": period,
"total_amount": invoice,
})))
}
@@ -0,0 +1,52 @@
use axum::{
extract::{Path, State},
Json,
};
use crate::errors::{ChargingResult, ErrorContext};
use crate::models::AppState;
/// 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,
})))
}
+102
View File
@@ -0,0 +1,102 @@
use axum::{
extract::{Path, State},
Json,
};
use tracing::info;
use crate::errors::{ChargingResult, ErrorContext, log_error, validate_ip, validate_bytes, validate_amount};
use crate::models::*;
/// POST /v1/credit/:ip/check
/// Check if user has enough credit for data usage
pub async fn check_credit(
Path(ip): Path<String>,
State(state): State<AppState>,
Json(req): Json<CreditCheckRequest>,
) -> ChargingResult<Json<CreditCheckResponse>> {
validate_ip(&ip)?;
validate_bytes(req.bytes_requested)?;
let allowed = state.charging_engine.check_credit(&ip, req.bytes_requested).await
.with_context("Failed to check credit")
.map_err(|e| {
log_error(&e);
e
})?;
let remaining = state.charging_engine.get_balance(&ip).await
.with_context("Failed to get balance")?;
info!(
"Credit check for {}: {} bytes requested, {} bytes available, allowed: {}",
ip, req.bytes_requested, remaining, allowed
);
Ok(Json(CreditCheckResponse {
allowed,
remaining_bytes: remaining as i64,
}))
}
/// POST /v1/credit/:ip/deduct
/// Deduct bytes from user's credit balance
pub async fn deduct_credit(
Path(ip): Path<String>,
State(state): State<AppState>,
Json(req): Json<DeductRequest>,
) -> ChargingResult<()> {
validate_ip(&ip)?;
validate_bytes(req.bytes_used)?;
validate_amount(req.bytes_used as f64)?;
let new_balance = state.charging_engine.deduct_credit(&ip, req.bytes_used).await
.with_context("Failed to deduct credit")?;
info!(
"User {} deducted {} bytes, remaining: {}",
ip, req.bytes_used, new_balance
);
Ok(())
}
/// POST /v1/credit/:ip/add
/// Add bytes to user's credit balance
pub async fn add_credit(
Path(ip): Path<String>,
State(state): State<AppState>,
Json(req): Json<AddCreditRequest>,
) -> ChargingResult<()> {
validate_ip(&ip)?;
validate_bytes(req.bytes_to_add)?;
validate_amount(req.bytes_to_add as f64)?;
let new_balance = state.charging_engine.add_credit(&ip, req.bytes_to_add).await
.with_context("Failed to add credit")?;
info!(
"User {} added {} bytes, new balance: {}",
ip, req.bytes_to_add, new_balance
);
Ok(())
}
/// GET /v1/credit/:ip/balance
/// Get current credit balance
pub async fn get_balance(
Path(ip): Path<String>,
State(state): State<AppState>,
) -> ChargingResult<Json<BalanceResponse>> {
validate_ip(&ip)?;
let balance = state.charging_engine.get_balance(&ip).await
.with_context("Failed to get balance")?;
info!("Retrieved balance for IP: {}", ip);
Ok(Json(BalanceResponse {
ip: ip.clone(),
balance_bytes: balance as i64,
}))
}
@@ -0,0 +1,45 @@
use axum::{
extract::{State},
Json,
};
use crate::errors::{ChargingResult, ErrorContext};
use crate::models::AppState;
/// POST /v1/engine/start
/// Start the engine
pub async fn engine_start(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
state.charging_engine.start().await
.with_context("Failed to start engine")?;
Ok(Json(serde_json::json!({
"status": "started",
})))
}
/// POST /v1/engine/stop
/// Stop the engine
pub async fn engine_stop(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
state.charging_engine.stop().await
.with_context("Failed to stop engine")?;
Ok(Json(serde_json::json!({
"status": "stopped",
})))
}
/// GET /v1/engine/uptime
/// Get engine uptime
pub async fn engine_uptime(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let uptime = state.charging_engine.uptime();
Ok(Json(serde_json::json!({
"uptime_seconds": uptime.as_secs(),
})))
}
@@ -0,0 +1,10 @@
use axum::Json;
use crate::errors::ChargingResult;
use crate::models::HealthResponse;
/// GET /health
/// Health check endpoint
pub async fn health_check() -> ChargingResult<Json<HealthResponse>> {
Ok(Json(HealthResponse::default()))
}
+19
View File
@@ -0,0 +1,19 @@
// Handler modules
pub mod block;
pub mod credit;
pub mod engine;
pub mod health;
pub mod monitoring;
pub mod rating;
pub mod subscriber;
pub mod usage;
// Re-export all handlers for convenience
pub use block::{block_user, is_user_blocked, unblock_user};
pub use credit::{add_credit, check_credit, deduct_credit, get_balance};
pub use engine::{engine_start, engine_stop, engine_uptime};
pub use health::health_check;
pub use monitoring::{detailed_health_check, get_error_stats, get_performance_metrics, get_system_stats, start_sync};
pub use rating::{add_rating_plan, get_rating_plan, list_rating_plans, remove_rating_plan};
pub use subscriber::{get_subscriber, update_subscriber};
pub use usage::{calculate_usage_cost, generate_invoice, process_usage, rate_usage, record_usage};
@@ -0,0 +1,85 @@
use axum::{
extract::{State},
Json,
};
use crate::errors::{ChargingResult, ErrorContext};
use crate::models::AppState;
/// 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,
})))
}
/// GET /v1/metrics
/// Get performance metrics
pub async fn get_performance_metrics(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let metrics = state.charging_engine.get_performance_metrics().await
.with_context("Failed to get performance metrics")?;
Ok(Json(serde_json::json!({
"connected_clients": metrics.connected_clients,
"used_memory": metrics.used_memory,
"total_commands_processed": metrics.total_commands_processed,
"requests_per_second": metrics.requests_per_second,
"average_response_time": metrics.average_response_time,
})))
}
/// GET /v1/errors
/// Get error statistics
pub async fn get_error_stats(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let errors = state.charging_engine.get_error_statistics().await
.with_context("Failed to get error statistics")?;
Ok(Json(serde_json::json!({
"total_errors": errors.total_errors,
"error_types": errors.error_types,
"last_error": errors.last_error,
})))
}
/// POST /v1/sync/start
/// Start background sync
pub async fn start_sync(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
state.charging_engine.start_background_sync().await
.with_context("Failed to start background sync")?;
Ok(Json(serde_json::json!({
"status": "sync_started",
})))
}
/// GET /v1/health/detailed
/// Get detailed health check
pub async fn detailed_health_check(
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let health = state.charging_engine.health_check().await
.with_context("Failed to get health check")?;
Ok(Json(serde_json::json!({
"redis_connected": health.redis_connected,
"active_sync": health.active_sync,
"last_sync": health.last_sync,
"memory_usage": health.memory_usage,
})))
}
@@ -0,0 +1,91 @@
use axum::{
extract::{Path, State},
Json,
};
use crate::errors::{ChargingError, ChargingResult, ErrorContext};
use crate::models::AppState;
/// 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")?;
// Also use list_map for internal lookups
let _ = state.charging_engine.plans.list_map().await
.with_context("Failed to list rating plans as map")?;
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,
})))
}
@@ -0,0 +1,60 @@
use axum::{
extract::{Path, State},
Json,
};
use crate::errors::{ChargingError, ChargingResult, ErrorContext};
use crate::models::AppState;
/// GET /v1/subscriber/:imsi
/// Get subscriber account by IMSI
pub async fn get_subscriber(
Path(imsi): Path<String>,
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let account = state.charging_engine.get_subscriber_account(&imsi).await
.with_context("Failed to get subscriber account")?;
match account {
Some(acc) => Ok(Json(serde_json::json!({
"imsi": acc.imsi,
"balance": acc.balance,
"data_used": acc.data_used,
"data_limit": acc.data_limit,
"voice_used": acc.voice_used,
"voice_limit": acc.voice_limit,
"sms_used": acc.sms_used,
"sms_limit": acc.sms_limit,
}))),
None => Err(ChargingError::SubscriberNotFound(imsi)),
}
}
/// PUT /v1/subscriber/:imsi
/// Update subscriber account
pub async fn update_subscriber(
Path(imsi): Path<String>,
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let account = crate::charging::types::SubscriberAccount {
imsi: imsi.clone(),
balance: req.get("balance").and_then(|v| v.as_i64()).unwrap_or(0),
data_used: req.get("data_used").and_then(|v| v.as_u64()).unwrap_or(0),
data_limit: req.get("data_limit").and_then(|v| v.as_u64()).unwrap_or(1_000_000_000),
voice_used: req.get("voice_used").and_then(|v| v.as_u64()).unwrap_or(0),
voice_limit: req.get("voice_limit").and_then(|v| v.as_u64()).unwrap_or(1000),
sms_used: req.get("sms_used").and_then(|v| v.as_u64()).unwrap_or(0),
sms_limit: req.get("sms_limit").and_then(|v| v.as_u64()).unwrap_or(100),
status: crate::charging::types::AccountStatus::Active,
last_updated: chrono::Utc::now(),
};
state.charging_engine.update_subscriber_account(&account).await
.with_context("Failed to update subscriber account")?;
Ok(Json(serde_json::json!({
"status": "updated",
"imsi": imsi,
})))
}
+160
View File
@@ -0,0 +1,160 @@
use axum::{
extract::{Path, State},
Json,
};
use crate::errors::{ChargingError, ChargingResult, ErrorContext, validate_session_id};
use crate::models::AppState;
/// POST /v1/usage
/// Record usage event
pub async fn record_usage(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let imsi = req.get("imsi").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing IMSI".to_string())
})?;
let session_id = req.get("session_id").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing session_id".to_string())
})?;
validate_session_id(session_id)?;
let event = crate::charging::types::UsageEvent {
imsi: imsi.to_string(),
session_id: session_id.to_string(),
volume: req.get("volume").and_then(|v| v.as_u64()).unwrap_or(0),
cost: req.get("cost").and_then(|v| v.as_f64()).unwrap_or(0.0),
rate: req.get("rate").and_then(|v| v.as_f64()).unwrap_or(0.0),
usage_type: crate::charging::types::UsageType::Data,
timestamp: chrono::Utc::now(),
};
state.charging_engine.record_usage_event(&event).await
.with_context("Failed to record usage event")?;
Ok(Json(serde_json::json!({
"status": "recorded",
"imsi": imsi,
"session_id": session_id,
})))
}
/// POST /v1/usage/calculate-cost
/// Calculate usage cost for an event
pub async fn calculate_usage_cost(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let imsi = req.get("imsi").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing IMSI".to_string())
})?;
let session_id = req.get("session_id").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing session_id".to_string())
})?;
validate_session_id(session_id)?;
let event = crate::charging::types::UsageEvent {
imsi: imsi.to_string(),
session_id: session_id.to_string(),
volume: req.get("volume").and_then(|v| v.as_u64()).unwrap_or(0),
cost: req.get("cost").and_then(|v| v.as_f64()).unwrap_or(0.0),
rate: req.get("rate").and_then(|v| v.as_f64()).unwrap_or(0.0),
usage_type: crate::charging::types::UsageType::Data,
timestamp: chrono::Utc::now(),
};
let cost = state.charging_engine.calculate_usage_cost(&event).await
.with_context("Failed to calculate usage cost")?;
Ok(Json(serde_json::json!({
"cost": cost,
"imsi": imsi,
"session_id": session_id,
})))
}
/// POST /v1/usage/rate
/// Rate usage event
pub async fn rate_usage(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let imsi = req.get("imsi").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing IMSI".to_string())
})?;
let session_id = req.get("session_id").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing session_id".to_string())
})?;
validate_session_id(session_id)?;
let event = crate::charging::types::UsageEvent {
imsi: imsi.to_string(),
session_id: session_id.to_string(),
volume: req.get("volume").and_then(|v| v.as_u64()).unwrap_or(0),
cost: req.get("cost").and_then(|v| v.as_f64()).unwrap_or(0.0),
rate: req.get("rate").and_then(|v| v.as_f64()).unwrap_or(0.0),
usage_type: crate::charging::types::UsageType::Data,
timestamp: chrono::Utc::now(),
};
let rated_event = state.charging_engine.rate_usage(event).await
.with_context("Failed to rate usage")?;
Ok(Json(serde_json::json!({
"rated_event": rated_event,
})))
}
/// POST /v1/usage/process
/// Process usage event with full rating and billing
pub async fn process_usage(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
let imsi = req.get("imsi").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing IMSI".to_string())
})?;
let session_id = req.get("session_id").and_then(|v| v.as_str()).ok_or_else(|| {
ChargingError::InvalidInput("Missing session_id".to_string())
})?;
validate_session_id(session_id)?;
let event = crate::charging::types::UsageEvent {
imsi: imsi.to_string(),
session_id: session_id.to_string(),
volume: req.get("volume").and_then(|v| v.as_u64()).unwrap_or(0),
cost: req.get("cost").and_then(|v| v.as_f64()).unwrap_or(0.0),
rate: req.get("rate").and_then(|v| v.as_f64()).unwrap_or(0.0),
usage_type: crate::charging::types::UsageType::Data,
timestamp: chrono::Utc::now(),
};
state.charging_engine.process_usage_event(event).await
.with_context("Failed to process usage event")?;
Ok(Json(serde_json::json!({
"status": "processed",
"imsi": imsi,
})))
}
/// GET /v1/invoice/:imsi/:period
/// Generate invoice for billing period
pub async fn generate_invoice(
Path((imsi, period)): Path<(String, String)>,
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let invoice = state.charging_engine.generate_invoice(&imsi, &period).await
.with_context("Failed to generate invoice")?;
Ok(Json(serde_json::json!({
"imsi": imsi,
"period": period,
"total_amount": invoice,
})))
}