use crate::domain::{ entities::{ nota::Nota, resultado_analise::{ResultadoAnalise, ResultadoPreAnalise}, }, services::{ detector_duplicidade::duplicidades_por_serie, detector_sequencia::{LIMITE_FALTANTES, calcular_intervalo, detectar_faltantes}, }, }; use rust_decimal::Decimal; use std::collections::HashMap; /// Executa a pré-análise: calcula intervalos e duplicatas, sem expandir faltantes. /// O caller deve verificar se algum intervalo excede `LIMITE_FALTANTES` e, /// se sim, exibir confirmação ao usuário antes de chamar `expandir_analise`. pub fn pre_analisar(notas: &[Nota]) -> ResultadoPreAnalise { // Agrupar por série let mut por_serie: HashMap> = HashMap::new(); for nota in notas { por_serie.entry(nota.serie.clone()).or_default().push(nota); } let mut intervalos_por_serie = HashMap::new(); let mut soma_total = Decimal::ZERO; let mut soma_por_serie: HashMap = HashMap::new(); let mut total_por_serie: HashMap = HashMap::new(); for (serie, notas_serie) in &por_serie { // Somar valores for nota in notas_serie.iter() { if let Some(v) = nota.valor { soma_total += v; *soma_por_serie.entry(serie.clone()).or_insert(Decimal::ZERO) += v; } } *total_por_serie.entry(serie.clone()).or_insert(0) += notas_serie.len(); // Calcular intervalo de faltantes if let Some(intervalo) = calcular_intervalo(notas_serie) { intervalos_por_serie.insert(serie.clone(), intervalo); } } let duplicadas_por_serie = duplicidades_por_serie(notas); ResultadoPreAnalise { intervalos_por_serie, duplicadas_por_serie, soma_total, soma_por_serie, total_por_serie, } } /// Verifica se alguma série excede o limite de faltantes. /// Retorna lista de séries que precisam de confirmação. pub fn series_com_intervalo_excessivo(pre: &ResultadoPreAnalise) -> Vec<(String, u64)> { pre.intervalos_por_serie .iter() .filter(|(_, iv)| iv.excede_limite(LIMITE_FALTANTES)) .map(|(serie, iv)| (serie.clone(), iv.contagem_faltantes)) .collect() } /// Expande a pré-análise para o resultado completo, materializando a lista de faltantes. /// Deve ser chamado após confirmação do usuário (ou quando nenhum intervalo excede o limite). pub fn expandir_analise(pre: ResultadoPreAnalise, notas: &[Nota]) -> ResultadoAnalise { let mut por_serie: HashMap> = HashMap::new(); for nota in notas { por_serie.entry(nota.serie.clone()).or_default().push(nota); } let mut faltantes_por_serie = HashMap::new(); for (serie, notas_serie) in &por_serie { let faltantes = detectar_faltantes(notas_serie); faltantes_por_serie.insert(serie.clone(), faltantes); } ResultadoAnalise { faltantes_por_serie, duplicadas_por_serie: pre.duplicadas_por_serie, soma_total: pre.soma_total, soma_por_serie: pre.soma_por_serie, total_por_serie: pre.total_por_serie, } } #[cfg(test)] mod tests { use super::*; use crate::domain::entities::nota::Nota; use rust_decimal_macros::dec; /// Executa análise completa sem verificar limites (atalho para testes). fn executar_analise(notas: &[Nota]) -> ResultadoAnalise { let pre = pre_analisar(notas); expandir_analise(pre, notas) } fn nota(numero: u64, serie: &str, valor: Option) -> Nota { Nota::new(numero, serie.to_string(), valor, None) } #[test] fn analise_simples() { let notas = vec![ nota(1, "001", Some(dec!(100.00))), nota(2, "001", Some(dec!(200.00))), nota(4, "001", Some(dec!(50.00))), ]; let resultado = executar_analise(¬as); assert_eq!(resultado.faltantes_por_serie["001"], vec![3u64]); assert_eq!(resultado.soma_total, dec!(350.00)); } #[test] fn analise_multiplas_series() { let notas = vec![ nota(1, "001", None), nota(3, "001", None), nota(1, "002", None), nota(2, "002", None), ]; let resultado = executar_analise(¬as); assert_eq!(resultado.faltantes_por_serie["001"], vec![2u64]); assert!(resultado.faltantes_por_serie["002"].is_empty()); } #[test] fn pre_analise_detecta_intervalo_excessivo() { let notas = vec![nota(1, "001", None), nota(20_000, "001", None)]; let pre = pre_analisar(¬as); let excessivos = series_com_intervalo_excessivo(&pre); assert_eq!(excessivos.len(), 1); assert_eq!(excessivos[0].0, "001"); } }