diff --git a/apps/charging-engine/src/handlers.rs b/apps/charging-engine/src/handlers.rs deleted file mode 100644 index 0c4dec3..0000000 --- a/apps/charging-engine/src/handlers.rs +++ /dev/null @@ -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, - State(state): State, - Json(req): Json, -) -> ChargingResult> { - 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, - State(state): State, - Json(req): Json, -) -> 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, - State(state): State, - Json(req): Json, -) -> 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, - State(state): State, -) -> ChargingResult> { - 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, - State(state): State, -) -> ChargingResult> { - 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, - Json(req): Json, -) -> ChargingResult> { - 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> { - Ok(Json(HealthResponse::default())) -} - -/// GET /v1/rating-plans -/// List all rating plans -pub async fn list_rating_plans( - State(state): State, -) -> ChargingResult> { - 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, - State(state): State, -) -> ChargingResult> { - 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, - Json(req): Json, -) -> ChargingResult> { - 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, - State(state): State, -) -> ChargingResult> { - 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, - State(state): State, -) -> ChargingResult> { - 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, - State(state): State, -) -> ChargingResult> { - 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, - State(state): State, -) -> ChargingResult> { - 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, -) -> ChargingResult> { - 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, - State(state): State, - Json(req): Json, -) -> ChargingResult> { - 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, -) -> ChargingResult> { - 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, -) -> ChargingResult> { - 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, -) -> ChargingResult> { - 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, -) -> ChargingResult> { - 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, -) -> ChargingResult> { - 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, -) -> ChargingResult> { - 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, -) -> ChargingResult> { - 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, - Json(req): Json, -) -> ChargingResult> { - 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, - Json(req): Json, -) -> ChargingResult> { - 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, - Json(req): Json, -) -> ChargingResult> { - 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, -) -> ChargingResult> { - 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, - }))) -} diff --git a/apps/charging-engine/src/handlers/block.rs b/apps/charging-engine/src/handlers/block.rs new file mode 100644 index 0000000..ad6ff62 --- /dev/null +++ b/apps/charging-engine/src/handlers/block.rs @@ -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, + State(state): State, +) -> ChargingResult> { + 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, + State(state): State, +) -> ChargingResult> { + 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, + State(state): State, +) -> ChargingResult> { + 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, + }))) +} diff --git a/apps/charging-engine/src/handlers/credit.rs b/apps/charging-engine/src/handlers/credit.rs new file mode 100644 index 0000000..6ef0622 --- /dev/null +++ b/apps/charging-engine/src/handlers/credit.rs @@ -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, + State(state): State, + Json(req): Json, +) -> ChargingResult> { + 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, + State(state): State, + Json(req): Json, +) -> 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, + State(state): State, + Json(req): Json, +) -> 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, + State(state): State, +) -> ChargingResult> { + 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, + })) +} diff --git a/apps/charging-engine/src/handlers/engine.rs b/apps/charging-engine/src/handlers/engine.rs new file mode 100644 index 0000000..fe634a5 --- /dev/null +++ b/apps/charging-engine/src/handlers/engine.rs @@ -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, +) -> ChargingResult> { + 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, +) -> ChargingResult> { + 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, +) -> ChargingResult> { + let uptime = state.charging_engine.uptime(); + + Ok(Json(serde_json::json!({ + "uptime_seconds": uptime.as_secs(), + }))) +} diff --git a/apps/charging-engine/src/handlers/health.rs b/apps/charging-engine/src/handlers/health.rs new file mode 100644 index 0000000..b02bd4c --- /dev/null +++ b/apps/charging-engine/src/handlers/health.rs @@ -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> { + Ok(Json(HealthResponse::default())) +} diff --git a/apps/charging-engine/src/handlers/mod.rs b/apps/charging-engine/src/handlers/mod.rs new file mode 100644 index 0000000..3f2d8aa --- /dev/null +++ b/apps/charging-engine/src/handlers/mod.rs @@ -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}; diff --git a/apps/charging-engine/src/handlers/monitoring.rs b/apps/charging-engine/src/handlers/monitoring.rs new file mode 100644 index 0000000..462b3d5 --- /dev/null +++ b/apps/charging-engine/src/handlers/monitoring.rs @@ -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, +) -> ChargingResult> { + 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, +) -> ChargingResult> { + 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, +) -> ChargingResult> { + 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, +) -> ChargingResult> { + 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, +) -> ChargingResult> { + 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, + }))) +} diff --git a/apps/charging-engine/src/handlers/rating.rs b/apps/charging-engine/src/handlers/rating.rs new file mode 100644 index 0000000..f8d9a3c --- /dev/null +++ b/apps/charging-engine/src/handlers/rating.rs @@ -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, +) -> ChargingResult> { + 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, + State(state): State, +) -> ChargingResult> { + 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, + Json(req): Json, +) -> ChargingResult> { + 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, + State(state): State, +) -> ChargingResult> { + 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, + }))) +} diff --git a/apps/charging-engine/src/handlers/subscriber.rs b/apps/charging-engine/src/handlers/subscriber.rs new file mode 100644 index 0000000..d217dc0 --- /dev/null +++ b/apps/charging-engine/src/handlers/subscriber.rs @@ -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, + State(state): State, +) -> ChargingResult> { + 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, + State(state): State, + Json(req): Json, +) -> ChargingResult> { + 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, + }))) +} diff --git a/apps/charging-engine/src/handlers/usage.rs b/apps/charging-engine/src/handlers/usage.rs new file mode 100644 index 0000000..b2f3f5c --- /dev/null +++ b/apps/charging-engine/src/handlers/usage.rs @@ -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, + Json(req): Json, +) -> ChargingResult> { + 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, + Json(req): Json, +) -> ChargingResult> { + 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, + Json(req): Json, +) -> ChargingResult> { + 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, + Json(req): Json, +) -> ChargingResult> { + 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, +) -> ChargingResult> { + 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, + }))) +}