use crate::domain::{ entities::{ chave_serie::ChaveSerie, 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 (serie, documento_tipo) let mut por_chave: HashMap> = HashMap::new(); for nota in notas { let chave = ChaveSerie::new(nota.serie.clone(), nota.documento_tipo.clone()); por_chave.entry(chave).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 (chave, notas_grupo) in &por_chave { // Somar valores for nota in notas_grupo.iter() { if let Some(v) = nota.valor { soma_total += v; *soma_por_serie.entry(chave.clone()).or_insert(Decimal::ZERO) += v; } } *total_por_serie.entry(chave.clone()).or_insert(0) += notas_grupo.len(); // Calcular intervalo de faltantes if let Some(intervalo) = calcular_intervalo(notas_grupo) { intervalos_por_serie.insert(chave.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 algum grupo excede o limite de faltantes. /// Retorna lista de chaves que precisam de confirmação. pub fn series_com_intervalo_excessivo(pre: &ResultadoPreAnalise) -> Vec<(ChaveSerie, u64)> { pre.intervalos_por_serie .iter() .filter(|(_, iv)| iv.excede_limite(LIMITE_FALTANTES)) .map(|(chave, iv)| (chave.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_chave: HashMap> = HashMap::new(); for nota in notas { let chave = ChaveSerie::new(nota.serie.clone(), nota.documento_tipo.clone()); por_chave.entry(chave).or_default().push(nota); } let mut faltantes_por_serie = HashMap::new(); for (chave, notas_grupo) in &por_chave { let faltantes = detectar_faltantes(notas_grupo); faltantes_por_serie.insert(chave.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(), None, valor, None) } fn nota_com_tipo(numero: u64, serie: &str, tipo: &str) -> Nota { Nota::new(numero, serie.to_string(), Some(tipo.to_string()), None, 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); let chave = ChaveSerie::new("001".to_string(), None); assert_eq!(resultado.faltantes_por_serie[&chave], 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); let chave001 = ChaveSerie::new("001".to_string(), None); let chave002 = ChaveSerie::new("002".to_string(), None); assert_eq!(resultado.faltantes_por_serie[&chave001], vec![2u64]); assert!(resultado.faltantes_por_serie[&chave002].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, ChaveSerie::new("001".to_string(), None)); } #[test] fn analise_nfe_nfce_mesma_serie_grupos_separados() { // NFE série 001: 1, 3 → faltante 2 // NFCE série 001: 2, 5 → faltante 3 e 4 let notas = vec![ nota_com_tipo(1, "001", "NFE"), nota_com_tipo(3, "001", "NFE"), nota_com_tipo(2, "001", "NFCE"), nota_com_tipo(5, "001", "NFCE"), ]; let resultado = executar_analise(¬as); let chave_nfe = ChaveSerie::new("001".to_string(), Some("NFE".to_string())); let chave_nfce = ChaveSerie::new("001".to_string(), Some("NFCE".to_string())); assert_eq!(resultado.faltantes_por_serie[&chave_nfe], vec![2u64]); assert_eq!(resultado.faltantes_por_serie[&chave_nfce], vec![3u64, 4u64]); } }