feat: Add list_rating_plans error handling, dead_code attributes, and refactor monitoring/models

- Add error mapping to list_rating_plans in engine.rs
- Add dead_code attributes to get/list_map/deactivate methods in rating_plans_repo and row_to_plan function
- Add list method to RatingPlansRepo returning Vec<RatingPlan>
- Add dead_code attribute to validate_imsi in helpers
- Add UsageBlocked and ConfigurationError variants to ChargingError with Display/IntoResponse implementations
- Add Default
This commit is contained in:
nutcas3
2026-04-25 02:14:10 +03:00
parent 8b86e55226
commit 1ee156af75
10 changed files with 55 additions and 28 deletions
-6
View File
@@ -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<crate::charging::ChargingEngine>,
}
/// POST /v1/credit/:ip/check
/// Check if user has enough credit for data usage
pub async fn check_credit(
@@ -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)]
@@ -90,5 +90,6 @@ impl ChargingEngine {
#[allow(dead_code)]
pub async fn list_rating_plans(&self) -> ChargingResult<Vec<RatingPlan>> {
self.plans.list().await
.map_err(|e| crate::errors::ChargingError::RedisOperation(e.to_string()))
}
}
@@ -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<Option<RatingPlan>> {
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<HashMap<String, RatingPlan>> {
let rows = sqlx::query(
@@ -139,6 +141,27 @@ impl RatingPlansRepo {
Ok(out)
}
/// List all active plans.
pub async fn list(&self) -> Result<Vec<RatingPlan>, 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<bool> {
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"),
@@ -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()));
+7
View File
@@ -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),
};
+9
View File
@@ -43,3 +43,12 @@ pub struct HealthResponse {
pub status: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl Default for HealthResponse {
fn default() -> Self {
Self {
status: "OK".to_string(),
timestamp: chrono::Utc::now(),
}
}
}
@@ -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<String> = redis::AsyncCommands::get(&mut conn, "last_error").await.unwrap_or(None);
Ok(last_error)
}
async fn get_uptime(&self) -> ChargingResult<u64> {
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)]
+9 -5
View File
@@ -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, &current_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<String> = redis::AsyncCommands::keys(&mut conn, "account:*").await.unwrap_or_default();
let total_accounts: u64 = account_keys.len() as u64;
let block_keys: Vec<String> = redis::AsyncCommands::keys(&mut conn, "block:*").await.unwrap_or_default();
let blocked_users: u64 = block_keys.len() as u64;
let alert_keys: Vec<String> = 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<u64> {
pub async fn get_uptime(&self) -> ChargingResult<u64> {
// Calculate actual uptime since startup
match self.startup_time.elapsed() {
Ok(duration) => Ok(duration.as_secs()),
+1 -4
View File
@@ -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,