diff --git a/apps/charging-engine/src/api.rs b/apps/charging-engine/src/api.rs index 0bf65c6..d7d55af 100644 --- a/apps/charging-engine/src/api.rs +++ b/apps/charging-engine/src/api.rs @@ -1,6 +1,5 @@ use axum::{ extract::{Path, State}, - response::IntoResponse, Json, }; use tracing::info; @@ -8,11 +7,6 @@ use tracing::info; use crate::errors::{ChargingError, ChargingResult, ErrorContext, log_error, validate_ip, validate_bytes, validate_amount, validate_session_id}; use crate::models::*; -#[derive(Clone)] -pub struct AppState { - pub charging_engine: std::sync::Arc, -} - /// POST /v1/credit/:ip/check /// Check if user has enough credit for data usage pub async fn check_credit( diff --git a/apps/charging-engine/src/charging/credit_management.rs b/apps/charging-engine/src/charging/credit_management.rs index a855eb3..e79c30c 100644 --- a/apps/charging-engine/src/charging/credit_management.rs +++ b/apps/charging-engine/src/charging/credit_management.rs @@ -2,7 +2,7 @@ use redis::AsyncCommands; use tracing::{info, warn, debug}; use super::types::{SubscriberAccount, UsageEvent}; -use crate::errors::{ChargingError, ChargingResult, ErrorContext}; +use crate::errors::{ChargingError, ChargingResult}; impl crate::charging::ChargingEngine { #[allow(dead_code)] diff --git a/apps/charging-engine/src/charging/engine.rs b/apps/charging-engine/src/charging/engine.rs index b58380f..7e9a585 100644 --- a/apps/charging-engine/src/charging/engine.rs +++ b/apps/charging-engine/src/charging/engine.rs @@ -90,5 +90,6 @@ impl ChargingEngine { #[allow(dead_code)] pub async fn list_rating_plans(&self) -> ChargingResult> { self.plans.list().await + .map_err(|e| crate::errors::ChargingError::RedisOperation(e.to_string())) } } diff --git a/apps/charging-engine/src/charging/rating_plans_repo.rs b/apps/charging-engine/src/charging/rating_plans_repo.rs index 7141d3c..2c58a35 100644 --- a/apps/charging-engine/src/charging/rating_plans_repo.rs +++ b/apps/charging-engine/src/charging/rating_plans_repo.rs @@ -99,6 +99,7 @@ impl RatingPlansRepo { Ok(()) } + #[allow(dead_code)] /// Fetch a single plan by id. pub async fn get(&self, plan_id: &str) -> ChargingResult> { let row = sqlx::query( @@ -117,6 +118,7 @@ impl RatingPlansRepo { Ok(row.map(row_to_plan)) } + #[allow(dead_code)] /// Fetch all active plans as a HashMap for fast lookup. pub async fn list_map(&self) -> ChargingResult> { let rows = sqlx::query( @@ -139,6 +141,27 @@ impl RatingPlansRepo { Ok(out) } + /// List all active plans. + pub async fn list(&self) -> Result, sqlx::Error> { + let rows = sqlx::query( + r#" + SELECT plan_id, name, data_rate, voice_rate, sms_rate, + monthly_fee, data_limit, voice_limit, sms_limit + FROM rating_plans + WHERE is_active = TRUE + "#, + ) + .fetch_all(&self.pool) + .await?; + + let plans = rows + .into_iter() + .map(|row| row_to_plan(row)) + .collect(); + + Ok(plans) + } + /// Insert or update a plan. pub async fn upsert(&self, plan: &RatingPlan) -> ChargingResult<()> { sqlx::query( @@ -175,6 +198,7 @@ impl RatingPlansRepo { Ok(()) } + #[allow(dead_code)] /// Soft-delete a plan by marking it inactive. pub async fn deactivate(&self, plan_id: &str) -> ChargingResult { let result = sqlx::query( @@ -189,6 +213,7 @@ impl RatingPlansRepo { } /// Map a Postgres row to a RatingPlan. Column order must match the SELECT. +#[allow(dead_code)] fn row_to_plan(row: sqlx::postgres::PgRow) -> RatingPlan { RatingPlan { plan_id: row.get("plan_id"), diff --git a/apps/charging-engine/src/errors/helpers.rs b/apps/charging-engine/src/errors/helpers.rs index c29980b..f26621f 100644 --- a/apps/charging-engine/src/errors/helpers.rs +++ b/apps/charging-engine/src/errors/helpers.rs @@ -68,6 +68,7 @@ pub fn validate_bytes(bytes: u64) -> ChargingResult<()> { Ok(()) } +#[allow(dead_code)] pub fn validate_imsi(imsi: &str) -> ChargingResult<()> { if imsi.is_empty() { return Err(ChargingError::InvalidInput("IMSI cannot be empty".to_string())); diff --git a/apps/charging-engine/src/errors/types.rs b/apps/charging-engine/src/errors/types.rs index 17421d2..7b3acfb 100644 --- a/apps/charging-engine/src/errors/types.rs +++ b/apps/charging-engine/src/errors/types.rs @@ -10,8 +10,11 @@ pub enum ChargingError { SubscriberNotFound(String), RatingPlanNotFound(String), InsufficientCredit { available: u64, requested: u64 }, + UsageBlocked(String), InvalidInput(String), SerializationError(String), + #[allow(dead_code)] + ConfigurationError(String), InternalError(String), } @@ -25,8 +28,10 @@ impl fmt::Display for ChargingError { ChargingError::InsufficientCredit { available, requested } => { write!(f, "Insufficient credit: available={}, requested={}", available, requested) } + ChargingError::UsageBlocked(reason) => write!(f, "Usage blocked: {}", reason), ChargingError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), ChargingError::SerializationError(msg) => write!(f, "Serialization error: {}", msg), + ChargingError::ConfigurationError(msg) => write!(f, "Configuration error: {}", msg), ChargingError::InternalError(msg) => write!(f, "Internal error: {}", msg), } } @@ -44,8 +49,10 @@ impl IntoResponse for ChargingError { ChargingError::InsufficientCredit { available, requested } => { (StatusCode::PAYMENT_REQUIRED, format!("Insufficient credit: available={}, requested={}", available, requested)) } + ChargingError::UsageBlocked(reason) => (StatusCode::FORBIDDEN, reason), ChargingError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg), ChargingError::SerializationError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), + ChargingError::ConfigurationError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), ChargingError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), }; diff --git a/apps/charging-engine/src/models.rs b/apps/charging-engine/src/models.rs index 013039e..ec92ac5 100644 --- a/apps/charging-engine/src/models.rs +++ b/apps/charging-engine/src/models.rs @@ -43,3 +43,12 @@ pub struct HealthResponse { pub status: String, pub timestamp: chrono::DateTime, } + +impl Default for HealthResponse { + fn default() -> Self { + Self { + status: "OK".to_string(), + timestamp: chrono::Utc::now(), + } + } +} diff --git a/apps/charging-engine/src/monitoring/monitoring.rs b/apps/charging-engine/src/monitoring/monitoring.rs index 34d2609..4cf1d12 100644 --- a/apps/charging-engine/src/monitoring/monitoring.rs +++ b/apps/charging-engine/src/monitoring/monitoring.rs @@ -1,8 +1,7 @@ use tracing::{info, debug}; use std::time::SystemTime; -use crate::errors::{ChargingError, ChargingResult, ErrorContext}; -use super::types::{SystemStats, HealthStatus}; +use crate::errors::ChargingResult; #[allow(dead_code)] impl crate::charging::ChargingEngine { @@ -130,16 +129,6 @@ impl crate::charging::ChargingEngine { let last_error: Option = redis::AsyncCommands::get(&mut conn, "last_error").await.unwrap_or(None); Ok(last_error) } - - async fn get_uptime(&self) -> ChargingResult { - match self.startup_time.elapsed() { - Ok(duration) => Ok(duration.as_secs()), - Err(_) => Ok(std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs()) - } - } } #[allow(dead_code)] diff --git a/apps/charging-engine/src/monitoring/sync.rs b/apps/charging-engine/src/monitoring/sync.rs index 4dddbb8..c0cf950 100644 --- a/apps/charging-engine/src/monitoring/sync.rs +++ b/apps/charging-engine/src/monitoring/sync.rs @@ -1,5 +1,6 @@ use tokio::time::interval; use tracing::{info, warn, error, debug}; +use chrono::Datelike; use crate::errors::{ChargingResult, log_error}; use crate::monitoring::types::{SystemStats, HealthStatus}; @@ -144,7 +145,7 @@ impl crate::charging::ChargingEngine { let processed = self.apply_monthly_fees().await?; // Update last fee timestamp - let _: () = redis::AsyncCommands::set(&mut conn, last_fee_key, current_month).await.unwrap_or(()); + let _: () = redis::AsyncCommands::set(&mut conn, last_fee_key, ¤t_month).await.unwrap_or(()); info!("Applied monthly fees to {} accounts for {}", processed, current_month); Ok(()) @@ -155,9 +156,12 @@ impl crate::charging::ChargingEngine { .map_err(|e| crate::errors::ChargingError::RedisConnection(e.to_string()))?; let active_sessions: u64 = redis::AsyncCommands::get(&mut conn, "stats:active_sessions").await.unwrap_or(0); - let total_accounts: u64 = redis::AsyncCommands::keys(&mut conn, "account:*").await.unwrap_or_default().len() as u64; - let blocked_users: u64 = redis::AsyncCommands::keys(&mut conn, "block:*").await.unwrap_or_default().len() as u64; - let low_balance_alerts: u64 = redis::AsyncCommands::keys(&mut conn, "alert:low_balance:*").await.unwrap_or_default().len() as u64; + let account_keys: Vec = redis::AsyncCommands::keys(&mut conn, "account:*").await.unwrap_or_default(); + let total_accounts: u64 = account_keys.len() as u64; + let block_keys: Vec = redis::AsyncCommands::keys(&mut conn, "block:*").await.unwrap_or_default(); + let blocked_users: u64 = block_keys.len() as u64; + let alert_keys: Vec = redis::AsyncCommands::keys(&mut conn, "alert:low_balance:*").await.unwrap_or_default(); + let low_balance_alerts: u64 = alert_keys.len() as u64; let stats = SystemStats { active_sessions, @@ -170,7 +174,7 @@ impl crate::charging::ChargingEngine { Ok(stats) } - async fn get_uptime(&self) -> ChargingResult { + pub async fn get_uptime(&self) -> ChargingResult { // Calculate actual uptime since startup match self.startup_time.elapsed() { Ok(duration) => Ok(duration.as_secs()), diff --git a/apps/charging-engine/src/routes.rs b/apps/charging-engine/src/routes.rs index 3b258fd..8f9dcd4 100644 --- a/apps/charging-engine/src/routes.rs +++ b/apps/charging-engine/src/routes.rs @@ -1,11 +1,8 @@ use axum::{ - extract::{Path, State}, - response::IntoResponse, routing::{delete, get, post, put}, - Json, Router, + Router, }; use tower_http::cors::{Any, CorsLayer}; -use tracing::info; use crate::api::{ add_credit, add_rating_plan, block_user, check_credit, deduct_credit, detailed_health_check,