340 lines
11 KiB
Rust
340 lines
11 KiB
Rust
use axum::{extract::State, Json};
|
|
use chrono::Utc;
|
|
use std::collections::HashMap;
|
|
use tracing::{info, warn};
|
|
use uuid::Uuid;
|
|
|
|
use crate::{AppState, models::*};
|
|
|
|
pub async fn generate_report(
|
|
State(state): State<AppState>,
|
|
Json(req): Json<ReportRequest>,
|
|
) -> Json<ReportResult> {
|
|
let cache_key = format!("{}:{:?}", req.tenant_id, req.report_type);
|
|
|
|
if let Some(cached) = state.report_cache.get(&cache_key) {
|
|
info!("Cache hit for report: {}", cache_key);
|
|
return Json(cached.clone());
|
|
}
|
|
|
|
info!("Generating report: {:?} for tenant: {}", req.report_type, req.tenant_id);
|
|
|
|
let result = match req.report_type {
|
|
ReportType::FinancialSummary => generate_financial_summary(&req).await,
|
|
ReportType::SalesPipeline => generate_sales_pipeline(&req).await,
|
|
ReportType::CustomerAnalytics => generate_customer_analytics(&req).await,
|
|
ReportType::RevenueForecast => generate_revenue_forecast(&req).await,
|
|
ReportType::ExpenseBreakdown => generate_expense_breakdown(&req).await,
|
|
ReportType::CashflowProjection => generate_cashflow_projection(&req).await,
|
|
ReportType::MrrAnalysis => generate_mrr_analysis(&req).await,
|
|
ReportType::ChurnAnalysis => generate_churn_analysis(&req).await,
|
|
ReportType::Custom(ref name) => generate_custom_report(&req, name).await,
|
|
};
|
|
|
|
state.report_cache.insert(cache_key, result.clone());
|
|
Json(result)
|
|
}
|
|
|
|
pub async fn analytics_query(
|
|
State(_state): State<AppState>,
|
|
Json(req): Json<AnalyticsQuery>,
|
|
) -> Json<AnalyticsResult> {
|
|
let start = std::time::Instant::now();
|
|
|
|
info!("Analytics query: {} for tenant: {}", req.query_type, req.tenant_id);
|
|
|
|
let rows: Vec<HashMap<String, serde_json::Value>> = match req.query_type.as_str() {
|
|
"revenue_by_month" => vec![
|
|
{
|
|
let mut row = HashMap::new();
|
|
row.insert("month".to_string(), serde_json::json!("2026-07"));
|
|
row.insert("revenue".to_string(), serde_json::json!(12450.0));
|
|
row.insert("expenses".to_string(), serde_json::json!(8230.0));
|
|
row
|
|
},
|
|
{
|
|
let mut row = HashMap::new();
|
|
row.insert("month".to_string(), serde_json::json!("2026-06"));
|
|
row.insert("revenue".to_string(), serde_json::json!(11520.0));
|
|
row.insert("expenses".to_string(), serde_json::json!(8480.0));
|
|
row
|
|
},
|
|
],
|
|
"sales_pipeline" => vec![
|
|
{
|
|
let mut row = HashMap::new();
|
|
row.insert("stage".to_string(), serde_json::json!("qualified"));
|
|
row.insert("count".to_string(), serde_json::json!(12));
|
|
row.insert("value".to_string(), serde_json::json!(145000.0));
|
|
row
|
|
},
|
|
{
|
|
let mut row = HashMap::new();
|
|
row.insert("stage".to_string(), serde_json::json!("proposal"));
|
|
row.insert("count".to_string(), serde_json::json!(5));
|
|
row.insert("value".to_string(), serde_json::json!(89000.0));
|
|
row
|
|
},
|
|
],
|
|
_ => vec![],
|
|
};
|
|
|
|
let mut totals = HashMap::new();
|
|
totals.insert("total_revenue".to_string(), 23970.0);
|
|
totals.insert("total_expenses".to_string(), 16710.0);
|
|
|
|
Json(AnalyticsResult {
|
|
query_id: Uuid::new_v4().to_string(),
|
|
rows,
|
|
totals,
|
|
execution_time_ms: start.elapsed().as_millis() as u64,
|
|
})
|
|
}
|
|
|
|
pub async fn batch_analytics(
|
|
State(state): State<AppState>,
|
|
Json(req): Json<BatchRequest>,
|
|
) -> Json<Vec<AnalyticsResult>> {
|
|
info!("Batch analytics: {} queries for tenant: {}", req.queries.len(), req.tenant_id);
|
|
|
|
let results: Vec<AnalyticsResult> = req.queries
|
|
.into_iter()
|
|
.map(|q| {
|
|
let start = std::time::Instant::now();
|
|
AnalyticsResult {
|
|
query_id: Uuid::new_v4().to_string(),
|
|
rows: vec![],
|
|
totals: HashMap::new(),
|
|
execution_time_ms: start.elapsed().as_millis() as u64,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Json(results)
|
|
}
|
|
|
|
pub async fn dashboard_summary(
|
|
State(_state): State<AppState>,
|
|
) -> Json<DashboardSummary> {
|
|
Json(DashboardSummary {
|
|
tenant_id: "default".to_string(),
|
|
generated_at: Utc::now(),
|
|
finance: FinanceKPIs {
|
|
revenue_mtd: 12450.0,
|
|
expenses_mtd: 8230.0,
|
|
cash_balance: 45230.0,
|
|
runway_months: 14.5,
|
|
outstanding_invoices: 12340.0,
|
|
tax_due_days: Some(7),
|
|
},
|
|
sales: SalesKPIs {
|
|
mrr: 8450.0,
|
|
arr: 101400.0,
|
|
pipeline_value: 340000.0,
|
|
win_rate: 0.34,
|
|
avg_deal_size: 15500.0,
|
|
sales_cycle_days: 45.0,
|
|
},
|
|
marketing: MarketingKPIs {
|
|
active_campaigns: 3,
|
|
impressions_30d: 45230,
|
|
ctr: 0.058,
|
|
brand_mentions: 23,
|
|
ai_spend: 1247.0,
|
|
},
|
|
hr: HRKPIs {
|
|
headcount: 12,
|
|
open_positions: 3,
|
|
avg_time_to_hire_days: 28.0,
|
|
retention_rate: 0.92,
|
|
},
|
|
})
|
|
}
|
|
|
|
async fn generate_financial_summary(req: &ReportRequest) -> ReportResult {
|
|
let mut key_metrics = HashMap::new();
|
|
key_metrics.insert("total_revenue".to_string(), 12450.0);
|
|
key_metrics.insert("total_expenses".to_string(), 8230.0);
|
|
key_metrics.insert("net_profit".to_string(), 4220.0);
|
|
key_metrics.insert("profit_margin".to_string(), 0.34);
|
|
|
|
ReportResult {
|
|
report_type: "financial_summary".to_string(),
|
|
tenant_id: req.tenant_id.clone(),
|
|
generated_at: Utc::now(),
|
|
data: serde_json::json!({
|
|
"revenue_by_category": {
|
|
"subscriptions": 8450.0,
|
|
"services": 2800.0,
|
|
"other": 1200.0,
|
|
},
|
|
"expenses_by_category": {
|
|
"infrastructure": 3200.0,
|
|
"personnel": 3800.0,
|
|
"marketing": 1230.0,
|
|
},
|
|
}),
|
|
summary: ReportSummary {
|
|
total_records: 156,
|
|
period_days: 30,
|
|
key_metrics,
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn generate_sales_pipeline(req: &ReportRequest) -> ReportResult {
|
|
let mut key_metrics = HashMap::new();
|
|
key_metrics.insert("pipeline_value".to_string(), 340000.0);
|
|
key_metrics.insert("win_rate".to_string(), 0.34);
|
|
key_metrics.insert("avg_deal_size".to_string(), 15500.0);
|
|
|
|
ReportResult {
|
|
report_type: "sales_pipeline".to_string(),
|
|
tenant_id: req.tenant_id.clone(),
|
|
generated_at: Utc::now(),
|
|
data: serde_json::json!({
|
|
"stages": [
|
|
{"name": "lead", "count": 45, "value": 0.0},
|
|
{"name": "qualified", "count": 12, "value": 145000.0},
|
|
{"name": "proposal", "count": 5, "value": 89000.0},
|
|
{"name": "negotiation", "count": 3, "value": 67000.0},
|
|
{"name": "closed_won", "count": 8, "value": 124000.0},
|
|
],
|
|
}),
|
|
summary: ReportSummary {
|
|
total_records: 73,
|
|
period_days: 30,
|
|
key_metrics,
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn generate_customer_analytics(_req: &ReportRequest) -> ReportResult {
|
|
ReportResult {
|
|
report_type: "customer_analytics".to_string(),
|
|
tenant_id: _req.tenant_id.clone(),
|
|
generated_at: Utc::now(),
|
|
data: serde_json::json!({}),
|
|
summary: ReportSummary {
|
|
total_records: 0,
|
|
period_days: 30,
|
|
key_metrics: HashMap::new(),
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn generate_revenue_forecast(_req: &ReportRequest) -> ReportResult {
|
|
ReportResult {
|
|
report_type: "revenue_forecast".to_string(),
|
|
tenant_id: _req.tenant_id.clone(),
|
|
generated_at: Utc::now(),
|
|
data: serde_json::json!({
|
|
"forecast_3m": 38500.0,
|
|
"forecast_6m": 82000.0,
|
|
"forecast_12m": 185000.0,
|
|
}),
|
|
summary: ReportSummary {
|
|
total_records: 12,
|
|
period_days: 365,
|
|
key_metrics: HashMap::new(),
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn generate_expense_breakdown(_req: &ReportRequest) -> ReportResult {
|
|
ReportResult {
|
|
report_type: "expense_breakdown".to_string(),
|
|
tenant_id: _req.tenant_id.clone(),
|
|
generated_at: Utc::now(),
|
|
data: serde_json::json!({}),
|
|
summary: ReportSummary {
|
|
total_records: 0,
|
|
period_days: 30,
|
|
key_metrics: HashMap::new(),
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn generate_cashflow_projection(_req: &ReportRequest) -> ReportResult {
|
|
ReportResult {
|
|
report_type: "cashflow_projection".to_string(),
|
|
tenant_id: _req.tenant_id.clone(),
|
|
generated_at: Utc::now(),
|
|
data: serde_json::json!({
|
|
"projected_balance_3m": 52000.0,
|
|
"projected_balance_6m": 61000.0,
|
|
"burn_rate_monthly": 8230.0,
|
|
}),
|
|
summary: ReportSummary {
|
|
total_records: 6,
|
|
period_days: 180,
|
|
key_metrics: HashMap::new(),
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn generate_mrr_analysis(_req: &ReportRequest) -> ReportResult {
|
|
let mut key_metrics = HashMap::new();
|
|
key_metrics.insert("mrr".to_string(), 8450.0);
|
|
key_metrics.insert("arr".to_string(), 101400.0);
|
|
key_metrics.insert("mrr_growth_rate".to_string(), 0.08);
|
|
|
|
ReportResult {
|
|
report_type: "mrr_analysis".to_string(),
|
|
tenant_id: _req.tenant_id.clone(),
|
|
generated_at: Utc::now(),
|
|
data: serde_json::json!({
|
|
"mrr_by_plan": {
|
|
"starter": 1200.0,
|
|
"professional": 4800.0,
|
|
"enterprise": 2450.0,
|
|
},
|
|
}),
|
|
summary: ReportSummary {
|
|
total_records: 48,
|
|
period_days: 30,
|
|
key_metrics,
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn generate_churn_analysis(_req: &ReportRequest) -> ReportResult {
|
|
let mut key_metrics = HashMap::new();
|
|
key_metrics.insert("churn_rate".to_string(), 0.05);
|
|
key_metrics.insert("retention_rate".to_string(), 0.95);
|
|
|
|
ReportResult {
|
|
report_type: "churn_analysis".to_string(),
|
|
tenant_id: _req.tenant_id.clone(),
|
|
generated_at: Utc::now(),
|
|
data: serde_json::json!({
|
|
"churn_by_reason": {
|
|
"price": 2,
|
|
"features": 1,
|
|
"competitor": 0,
|
|
"other": 1,
|
|
},
|
|
}),
|
|
summary: ReportSummary {
|
|
total_records: 48,
|
|
period_days: 90,
|
|
key_metrics,
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn generate_custom_report(req: &ReportRequest, name: &str) -> ReportResult {
|
|
warn!("Custom report '{}' not fully implemented", name);
|
|
ReportResult {
|
|
report_type: format!("custom:{}", name),
|
|
tenant_id: req.tenant_id.clone(),
|
|
generated_at: Utc::now(),
|
|
data: serde_json::json!({"message": "Custom report template"}),
|
|
summary: ReportSummary {
|
|
total_records: 0,
|
|
period_days: 30,
|
|
key_metrics: HashMap::new(),
|
|
},
|
|
}
|
|
}
|