use axum::{ extract::State, http::StatusCode, routing::{get, post}, Json, Router, }; use reqwest::Client; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::net::SocketAddr; use std::sync::Arc; use tower_http::cors::{Any, CorsLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; pub mod lead_requests; #[derive(Clone)] pub struct AppState { pub pool: PgPool, pub http_client: reqwest::Client, pub ollama_base_url: String, pub ollama_model: String, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct Lead { pub id: uuid::Uuid, pub title: String, pub description: String, pub location: String, pub profession_key: String, pub status: String, pub created_at: chrono::DateTime, } #[derive(Debug, Deserialize)] pub struct CreateLead { pub title: String, pub description: String, pub location: String, pub profession_key: String, } async fn list_leads(State(state): State>) -> Result>, StatusCode> { let leads = sqlx::query_as::<_, Lead>( "SELECT id, title, description, location, profession_key, status, created_at FROM leads ORDER BY created_at DESC" ) .fetch_all(&state.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(leads)) } async fn create_lead( State(state): State>, Json(payload): Json, ) -> Result, StatusCode> { let lead = sqlx::query_as::<_, Lead>( r#" INSERT INTO leads (title, description, location, profession_key) VALUES ($1, $2, $3, $4) RETURNING id, title, description, location, profession_key, status, created_at "# ) .bind(&payload.title) .bind(&payload.description) .bind(&payload.location) .bind(&payload.profession_key) .fetch_one(&state.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(lead)) } async fn get_lead( State(state): State>, axum::extract::Path(id): axum::extract::Path, ) -> Result, StatusCode> { let lead = sqlx::query_as::<_, Lead>( "SELECT id, title, description, location, profession_key, status, created_at FROM leads WHERE id = $1" ) .bind(id) .fetch_optional(&state.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::NOT_FOUND)?; Ok(Json(lead)) } async fn health() -> &'static str { "Leads Service OK" } #[tokio::main] async fn main() { tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), )) .with(tracing_subscriber::fmt::layer()) .init(); let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); let pool = sqlx::postgres::PgPoolOptions::new() .max_connections(10) .connect(&database_url) .await .expect("Failed to connect to database"); tracing::info!("Connected to database"); let state = Arc::new(AppState { pool, http_client: Client::new(), ollama_base_url: std::env::var("OLLAMA_BASE_URL") .unwrap_or_else(|_| "http://ollama.nxtgauge-ai.svc.cluster.local:11434".to_string()), ollama_model: std::env::var("OLLAMA_CHAT_MODEL") .unwrap_or_else(|_| "gemma3:270m".to_string()), }); let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any); let app = Router::new() .route("/health", get(health)) .route("/leads", get(list_leads)) .route("/leads", post(create_lead)) .route("/leads/{id}", get(get_lead)) .nest("/api/lead-requests", lead_requests::router()) .layer(cors) .with_state(state); let port: u16 = std::env::var("PORT") .unwrap_or_else(|_| "9118".to_string()) .parse() .expect("PORT must be a valid u16"); let addr = SocketAddr::from(([0, 0, 0, 0], port)); tracing::info!("Leads service listening on {}", addr); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); }