From 71ae1925f1929adf4bed4c1df6c1f235fbdcedec Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Sat, 25 Apr 2026 01:22:12 +0300 Subject: [PATCH] feat: Add subscriber account and usage recording API endpoints with validation - Add GET /v1/subscriber/:imsi endpoint to retrieve subscriber account details - Add POST /v1/usage endpoint to record usage events with session validation - Remove unused validate_imsi import and dummy validation calls from check_credit - Add validate_amount call for bytes_used in deduct_credit - Add balance retrieval logging and fix IP clone in get_balance response - Add IMSI/session_id validation and error handling for missing fields in record_usage - Return subscriber --- apps/charging-engine/src/api.rs | 74 ++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/apps/charging-engine/src/api.rs b/apps/charging-engine/src/api.rs index a550d9b..ceb7d3b 100644 --- a/apps/charging-engine/src/api.rs +++ b/apps/charging-engine/src/api.rs @@ -7,7 +7,7 @@ use axum::{ use tower_http::cors::{Any, CorsLayer}; use tracing::{info, warn}; -use crate::errors::{ChargingError, ChargingResult, validate_ip, validate_bytes, ErrorContext, log_error, validate_amount, validate_imsi, validate_session_id}; +use crate::errors::{ChargingError, ChargingResult, validate_ip, validate_bytes, ErrorContext, log_error, validate_amount, validate_session_id}; use crate::models::*; #[derive(Clone)] @@ -26,6 +26,8 @@ pub fn create_router(state: AppState) -> Router { .route("/v1/credit/:ip/deduct", post(deduct_credit)) .route("/v1/credit/:ip/add", post(add_credit)) .route("/v1/credit/:ip/balance", get(get_balance)) + .route("/v1/subscriber/:imsi", get(get_subscriber)) + .route("/v1/usage", post(record_usage)) .route("/health", get(health_check)) .layer(cors) .with_state(state) @@ -41,9 +43,6 @@ pub async fn check_credit( // Validate input validate_ip(&ip)?; validate_bytes(req.bytes_requested)?; - // Use validation functions to ensure they're not removed - let _ = validate_imsi(&ip); - let _ = validate_session_id(&ip); let allowed = state.charging_engine.check_credit(&ip, req.bytes_requested).await .with_context("Failed to check credit") @@ -76,6 +75,7 @@ pub async fn deduct_credit( // Validate input 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")?; @@ -122,13 +122,75 @@ pub async fn get_balance( 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: 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)?; + + // Create and record usage event + 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> {