138 lines
4.7 KiB
Rust
138 lines
4.7 KiB
Rust
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<AppState>,
|
|
}
|
|
|
|
impl YFinanceFetcher {
|
|
pub fn new(state: Arc<AppState>) -> 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<TimeSeriesFinancialDto>)> {
|
|
// 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<Sender>.
|
|
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);
|
|
}
|
|
}
|