use std::sync::Arc; use common_contracts::{ dtos::{CompanyProfileDto, TimeSeriesFinancialDto}, messages::FetchCompanyDataCommand, workflow_harness::StandardFetchWorkflow, abstraction::DataProviderLogic, persistence_client::PersistenceClient, }; use crate::error::{Result, AppError}; use crate::state::AppState; pub struct YFinanceFetcher { state: Arc, } impl YFinanceFetcher { pub fn new(state: Arc) -> Self { Self { state } } } #[async_trait::async_trait] impl DataProviderLogic for YFinanceFetcher { fn provider_id(&self) -> &str { "yfinance" } async fn fetch_data(&self, symbol: &str) -> anyhow::Result<(CompanyProfileDto, Vec)> { // Check dynamic config (Single Source of Truth) let client = PersistenceClient::new(self.state.config.data_persistence_service_url.clone()); let config = client.get_data_sources_config().await?; let is_enabled = config.get("yfinance") .map(|c| c.enabled) .unwrap_or(false); if !is_enabled { return Err(anyhow::anyhow!("YFinance provider is disabled in dynamic config")); } // FIX: Use fully qualified call to avoid lifetime inference issue let (profile, financials) = self.state.yfinance_provider.fetch_all_data(symbol).await .map_err(|e| anyhow::anyhow!("YFinance API error: {}", e))?; Ok((profile, financials)) } } pub async fn handle_fetch_command( state: AppState, command: FetchCompanyDataCommand, _publisher: async_nats::Client, // Deprecated: harness creates its own scoped connection ) -> Result<()> { let state_arc = Arc::new(state); let fetcher = Arc::new(YFinanceFetcher::new(state_arc.clone())); // Use standard workflow // Note: YFinance worker signature was slightly different (no completion tx), // but StandardFetchWorkflow handles Option. StandardFetchWorkflow::run( state_arc, fetcher, command, None ).await.map_err(|e| AppError::Internal(e.to_string()))?; Ok(()) } #[cfg(test)] mod integration_tests { use super::*; use crate::config::AppConfig; use crate::state::AppState; use common_contracts::symbol_utils::{CanonicalSymbol, Market}; use common_contracts::config_models::{DataSourceConfig, DataSourceProvider}; use common_contracts::observability::{TaskProgress, ObservabilityTaskStatus}; use uuid::Uuid; use chrono::Utc; #[tokio::test] async fn test_yfinance_fetch_flow() { if std::env::var("NATS_ADDR").is_err() { println!("Skipping integration test (no environment)"); return; } let config = AppConfig::load().expect("Failed to load config"); let mut config_enabled = config.clone(); config_enabled.yfinance_enabled = true; let state = AppState::new(config_enabled); // 1. Enable YFinance in Persistence Service let persistence_client = PersistenceClient::new(config.data_persistence_service_url.clone()); let mut current_config = persistence_client.get_data_sources_config().await.unwrap_or_default(); current_config.insert("yfinance".to_string(), DataSourceConfig { provider: DataSourceProvider::Yfinance, enabled: true, api_key: None, api_url: None, }); persistence_client.update_data_sources_config(¤t_config).await .expect("Failed to enable YFinance in persistence"); // 2. Construct Command let request_id = Uuid::new_v4(); let cmd = FetchCompanyDataCommand { request_id, symbol: CanonicalSymbol::new("MSFT", &Market::US), market: "US".to_string(), template_id: Some("default".to_string()), }; // Init task state.tasks.insert(request_id, TaskProgress { request_id, task_name: "yfinance:MSFT".to_string(), status: ObservabilityTaskStatus::Queued, progress_percent: 0, details: "Init".to_string(), started_at: Utc::now() }); // 3. NATS (Dummy client for signature compatibility, though harness uses its own) let nats_client = async_nats::connect(&config.nats_addr).await .expect("Failed to connect to NATS"); // 4. Run let result = handle_fetch_command(state.clone(), cmd, nats_client).await; // 5. Assert assert!(result.is_ok(), "Worker execution failed: {:?}", result.err()); let task = state.tasks.get(&request_id).expect("Task should exist"); assert_eq!(task.status, ObservabilityTaskStatus::Completed); } }