feat: Add charging engine with Redis-based credit management and usage tracking

- Add ChargingEngine struct with Redis client, rating plans, and sync interval
- Add SubscriberAccount model with balance, data/voice/SMS limits and usage tracking
- Add UsageEvent model for billing events with cost calculation
- Add RatingPlan model with data/voice/SMS rates and limits
- Add AccountStatus enum (Active, Suspended, Terminated, Blocked)
- Add UsageType enum (Data, Voice, SMS)
- Add sync_usage_data for
This commit is contained in:
nutcas3
2026-04-14 15:53:14 +03:00
parent cf92bd0ab4
commit f10d180bf4
4 changed files with 866 additions and 199 deletions
+26 -199
View File
@@ -1,23 +1,17 @@
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use redis::AsyncCommands;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer};
use tracing::{info, warn};
mod api;
mod charging;
mod errors;
mod models;
#[derive(Clone)]
struct AppState {
redis: redis::Client,
}
use anyhow::Result;
use std::sync::Arc;
use tracing::info;
use api::create_router;
use charging::ChargingEngine;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
async fn main() -> Result<()> {
// Initialize tracing
tracing_subscriber::fmt()
.with_target(false)
@@ -27,33 +21,26 @@ async fn main() -> anyhow::Result<()> {
// Load environment variables
dotenv::dotenv().ok();
// Connect to Redis
// Initialize charging engine
let redis_url = std::env::var("REDIS_URI").unwrap_or_else(|_| "redis://127.0.0.1/".to_string());
let redis_client = redis::Client::open(redis_url)?;
let sync_interval = std::env::var("SYNC_INTERVAL")
.unwrap_or_else(|_| "1".to_string())
.parse::<u64>()
.unwrap_or(1);
let charging_engine = Arc::new(ChargingEngine::new(&redis_url, sync_interval)?);
// Test connection
let mut conn = redis_client.get_multiplexed_async_connection().await?;
let _: () = redis::cmd("PING").query_async(&mut conn).await?;
// Test Redis connection
charging_engine.test_connection().await?;
info!("Connected to Redis successfully");
let state = Arc::new(AppState {
redis: redis_client,
});
// Create application state
let state = api::AppState {
charging_engine,
};
// Build HTTP API with CORS
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
let app = Router::new()
.route("/v1/credit/:ip/check", post(check_credit))
.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("/health", get(health_check))
.layer(cors)
.with_state(state);
// Create router
let app = create_router(state);
// Start server
let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string());
@@ -66,163 +53,3 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}
#[derive(Deserialize)]
struct CreditCheckRequest {
bytes_requested: u64,
}
#[derive(Serialize)]
struct CreditCheckResponse {
allowed: bool,
remaining_bytes: i64,
}
/// POST /v1/credit/:ip/check
/// Check if user has enough credit for data usage
async fn check_credit(
Path(ip): Path<String>,
State(state): State<Arc<AppState>>,
Json(req): Json<CreditCheckRequest>,
) -> impl IntoResponse {
let mut conn = match state.redis.get_multiplexed_async_connection().await {
Ok(c) => c,
Err(e) => {
warn!("Redis connection failed: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(CreditCheckResponse {
allowed: false,
remaining_bytes: 0,
}),
);
}
};
let key = format!("credit:{}", ip);
// Get current credit balance
let credit: i64 = conn.get(&key).await.unwrap_or(0);
let allowed = credit >= req.bytes_requested as i64;
info!(
"Credit check for {}: {} bytes requested, {} bytes available, allowed: {}",
ip, req.bytes_requested, credit, allowed
);
(
StatusCode::OK,
Json(CreditCheckResponse {
allowed,
remaining_bytes: credit,
}),
)
}
#[derive(Deserialize)]
struct DeductRequest {
bytes_used: u64,
}
/// POST /v1/credit/:ip/deduct
/// Deduct bytes from user's credit balance
async fn deduct_credit(
Path(ip): Path<String>,
State(state): State<Arc<AppState>>,
Json(req): Json<DeductRequest>,
) -> impl IntoResponse {
let mut conn = match state.redis.get_multiplexed_async_connection().await {
Ok(c) => c,
Err(e) => {
warn!("Redis connection failed: {}", e);
return StatusCode::INTERNAL_SERVER_ERROR;
}
};
let key = format!("credit:{}", ip);
// Atomically subtract bytes
let new_balance: i64 = conn.decr(&key, req.bytes_used).await.unwrap_or(0);
info!(
"User {} deducted {} bytes, remaining: {}",
ip, req.bytes_used, new_balance
);
StatusCode::OK
}
#[derive(Deserialize)]
struct AddCreditRequest {
bytes_to_add: u64,
}
/// POST /v1/credit/:ip/add
/// Add bytes to user's credit balance
async fn add_credit(
Path(ip): Path<String>,
State(state): State<Arc<AppState>>,
Json(req): Json<AddCreditRequest>,
) -> impl IntoResponse {
let mut conn = match state.redis.get_multiplexed_async_connection().await {
Ok(c) => c,
Err(e) => {
warn!("Redis connection failed: {}", e);
return StatusCode::INTERNAL_SERVER_ERROR;
}
};
let key = format!("credit:{}", ip);
// Atomically add bytes
let new_balance: i64 = conn.incr(&key, req.bytes_to_add).await.unwrap_or(0);
info!(
"User {} added {} bytes, new balance: {}",
ip, req.bytes_to_add, new_balance
);
StatusCode::OK
}
#[derive(Serialize)]
struct BalanceResponse {
ip: String,
balance_bytes: i64,
}
/// GET /v1/credit/:ip/balance
/// Get current credit balance
async fn get_balance(
Path(ip): Path<String>,
State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
let mut conn = match state.redis.get_multiplexed_async_connection().await {
Ok(c) => c,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(BalanceResponse {
ip: ip.clone(),
balance_bytes: 0,
}),
);
}
};
let key = format!("credit:{}", ip);
let balance: i64 = conn.get(&key).await.unwrap_or(0);
(
StatusCode::OK,
Json(BalanceResponse {
ip,
balance_bytes: balance,
}),
)
}
async fn health_check() -> &'static str {
"OK"
}