feat: Add usage processing and invoice generation API endpoints

- Add POST /v1/usage/calculate-cost endpoint to calculate usage cost for events
- Add POST /v1/usage/rate endpoint to rate usage events
- Add POST /v1/usage/process endpoint for full rating and billing processing
- Add GET /v1/invoice/:imsi/:period endpoint to generate invoices for billing periods
- Add list_map call to list_rating_plans handler for internal lookups
- Add session_id validation to all usage endpoints using validate_session_id helper
This commit is contained in:
nutcas3
2026-04-25 02:43:22 +03:00
parent dcd1cdeca0
commit 607040da6c
+122
View File
@@ -174,6 +174,10 @@ pub async fn list_rating_plans(
let plans = state.charging_engine.list_rating_plans().await
.with_context("Failed to list rating plans")?;
// Also use list_map for internal lookups
let _ = state.charging_engine.plans.list_map().await
.with_context("Failed to list rating plans as map")?;
Ok(Json(serde_json::json!({
"plans": plans
})))
@@ -435,3 +439,121 @@ pub async fn detailed_health_check(
"memory_usage": health.memory_usage,
})))
}
/// POST /v1/usage/calculate-cost
/// Calculate usage cost for an event
pub async fn calculate_usage_cost(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
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)?;
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(),
};
let cost = state.charging_engine.calculate_usage_cost(&event).await
.with_context("Failed to calculate usage cost")?;
Ok(Json(serde_json::json!({
"cost": cost,
"imsi": imsi,
"session_id": session_id,
})))
}
/// POST /v1/usage/rate
/// Rate usage event
pub async fn rate_usage(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
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)?;
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(),
};
let rated_event = state.charging_engine.rate_usage(event).await
.with_context("Failed to rate usage")?;
Ok(Json(serde_json::json!({
"rated_event": rated_event,
})))
}
/// POST /v1/usage/process
/// Process usage event with full rating and billing
pub async fn process_usage(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> ChargingResult<Json<serde_json::Value>> {
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)?;
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.process_usage_event(event).await
.with_context("Failed to process usage event")?;
Ok(Json(serde_json::json!({
"status": "processed",
"imsi": imsi,
})))
}
/// GET /v1/invoice/:imsi/:period
/// Generate invoice for billing period
pub async fn generate_invoice(
Path((imsi, period)): Path<(String, String)>,
State(state): State<AppState>,
) -> ChargingResult<Json<serde_json::Value>> {
let invoice = state.charging_engine.generate_invoice(&imsi, &period).await
.with_context("Failed to generate invoice")?;
Ok(Json(serde_json::json!({
"imsi": imsi,
"period": period,
"total_amount": invoice,
})))
}