mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-31 00:52:16 +00:00
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:
@@ -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()));
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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<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,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,
|
||||
|
||||
Reference in New Issue
Block a user