use axum::{ extract::{Path, State}, http::StatusCode, routing::{get, post}, Json, Router, }; use serde_json::json; use std::sync::Arc; use uuid::Uuid; use crate::{ auth::{auth_middleware, create_jwt}, db, models::*, }; pub struct AppState { pub pool: sqlx::PgPool, pub jwt_secret: String, } pub fn create_router(state: Arc) -> Router { let public_routes = Router::new() .route("/health", get(health_check)) .route("/login", post(login)); let protected_routes = Router::new() .route("/accounts", get(list_accounts).post(create_account_handler)) .route("/accounts/:id", get(get_account_handler)) .route("/journal-entries", get(list_journal_entries).post(create_journal_entry_handler)) .route("/journal-entries/:id", get(get_journal_entry_handler)) .route("/trial-balance", get(get_trial_balance_legacy)) .route_layer(axum::middleware::from_fn(auth_middleware)); let api_routes = Router::new() .nest("/api/v1", protected_routes); // Legacy /api/ledger routes for Ouroboros UI compatibility let ledger_routes = Router::new() .route("/journal", get(list_journal_entries_legacy)) .route("/trial-balance", get(get_trial_balance_legacy)) .route_layer(axum::middleware::from_fn(auth_middleware)); Router::new() .merge(public_routes) .merge(api_routes) .nest("/api/ledger", ledger_routes) .with_state(state) } async fn health_check() -> Json { Json(json!({"status": "ok"})) } async fn login( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { // Simplified auth - in production, verify password hash against database if req.password != "password" { return Err(( StatusCode::UNAUTHORIZED, Json(json!({"error": "Invalid credentials"})), )); } let token = create_jwt(&state.jwt_secret, &req.username).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": format!("Failed to create token: {}", e)})), ) })?; Ok(Json(LoginResponse { token })) } async fn list_accounts( State(state): State>, ) -> Result>, (StatusCode, Json)> { let accounts = db::get_accounts(&state.pool).await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": format!("Database error: {}", e)})), ) })?; Ok(Json(accounts)) } async fn create_account_handler( State(state): State>, Json(req): Json, ) -> Result<(StatusCode, Json), (StatusCode, Json)> { let account = db::create_account(&state.pool, &req).await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": format!("Database error: {}", e)})), ) })?; Ok((StatusCode::CREATED, Json(account))) } async fn get_account_handler( State(state): State>, Path(id): Path, ) -> Result, (StatusCode, Json)> { let account = db::get_account_by_id(&state.pool, id).await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": format!("Database error: {}", e)})), ) })?; match account { Some(account) => Ok(Json(account)), None => Err(( StatusCode::NOT_FOUND, Json(json!({"error": "Account not found"})), )), } } async fn list_journal_entries( State(state): State>, ) -> Result>, (StatusCode, Json)> { let entries = db::get_journal_entries(&state.pool).await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": format!("Database error: {}", e)})), ) })?; Ok(Json(entries)) } async fn create_journal_entry_handler( State(state): State>, Json(req): Json, ) -> Result<(StatusCode, Json), (StatusCode, Json)> { // Validate that debits equal credits let total_debit: f64 = req .lines .iter() .filter_map(|l| l.debit) .fold(0.0, |acc, val| acc + val); let total_credit: f64 = req .lines .iter() .filter_map(|l| l.credit) .fold(0.0, |acc, val| acc + val); if (total_debit - total_credit).abs() > 0.01 { return Err(( StatusCode::BAD_REQUEST, Json(json!({"error": format!("Total debits ({:.2}) must equal total credits ({:.2})", total_debit, total_credit)})), )); } let entry = db::create_journal_entry(&state.pool, &req, "system") .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": format!("Database error: {}", e)})), ) })?; Ok((StatusCode::CREATED, Json(entry))) } async fn get_journal_entry_handler( State(state): State>, Path(id): Path, ) -> Result, (StatusCode, Json)> { let entry = db::get_journal_entry_by_id(&state.pool, id).await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": format!("Database error: {}", e)})), ) })?; match entry { Some(entry) => Ok(Json(entry)), None => Err(( StatusCode::NOT_FOUND, Json(json!({"error": "Journal entry not found"})), )), } } async fn get_trial_balance_handler( State(state): State>, ) -> Result>, (StatusCode, Json)> { let rows = db::get_trial_balance(&state.pool).await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": format!("Database error: {}", e)})), ) })?; Ok(Json(rows)) } // Legacy handler for /api/ledger/journal - returns format compatible with Ouroboros UI async fn list_journal_entries_legacy( State(state): State>, ) -> Result, (StatusCode, Json)> { let entries = db::get_journal_entries(&state.pool).await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": format!("Database error: {}", e)})), ) })?; // Format compatible with Ouroboros UI let formatted_entries: Vec = entries .into_iter() .map(|e| { json!({ "entry_number": e.entry_number, "entry_date": e.entry_date, "description": e.description, "reference": e.entry_number, "rader": [] }) }) .collect(); Ok(Json(json!({ "ok": true, "entries": formatted_entries }))) } // Legacy handler for /api/ledger/trial-balance - returns format compatible with Ouroboros UI async fn get_trial_balance_legacy( State(state): State>, ) -> Result, (StatusCode, Json)> { let rows = db::get_trial_balance(&state.pool).await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": format!("Database error: {}", e)})), ) })?; // Format compatible with Ouroboros UI let accounts: Vec = rows .into_iter() .map(|r| { json!({ "account_number": r.account_code, "account_name": r.account_name, "balance": r.balance.unwrap_or(0.0) }) }) .collect(); Ok(Json(json!({ "ok": true, "accounts": accounts }))) }