from app.domain.contas.parsers.invoice_to_text import get_vas_product_names, to_text def _build_lines(text: str) -> list[str]: return text.splitlines() def test_sva_renderiza_acao_por_item() -> None: """SVA: header "Itens Eventuais | cobrados a parte" (a seção "SVA Detalhe Total" é impressa sob esse rótulo); estrategico -> 'falar sobre'; avulso -> 'cancelar'.""" data = { "11999990000": { "SVA Detalhe Total": [ {"desc": "Aluguel de Filme 1", "value": 19.90, "period": "02/05/26"}, { "desc": "YouTube Premium Mensal", "value": 24.90, "period": "02/05/26", "estrategico": True, }, ], } } lines = _build_lines(to_text(data)) # header "cobrados a parte", sem "| ação=... | avulso" assert "Itens Eventuais | cobrados a parte" in lines assert "SVA Detalhe Total" not in to_text(data) avulso = next(l for l in lines if "Aluguel de Filme 1" in l) estrategico = next(l for l in lines if "YouTube Premium Mensal" in l) assert avulso.endswith("| ação=cancelar") # Item estratégico no SVA: ação "falar sobre" + marcador "type=estrategico". assert "| ação=falar sobre" in estrategico assert estrategico.endswith("| type=estrategico") def test_itens_eventuais_cobrados_a_parte_acao_por_item() -> None: """Itens Eventuais: header "cobrados a parte" + ação por item (avulso -> 'cancelar'; estratégico -> 'falar sobre' + 'type=estrategico').""" data = { "11999990000": { "Itens Eventuais": [ {"desc": "Serviço Avulso X", "value": 9.90, "period": "02/05/26"}, { "desc": "Max Mensal", "value": 34.90, "period": "02/05/26", "estrategico": True, }, ], } } lines = _build_lines(to_text(data)) assert "Itens Eventuais | cobrados a parte" in lines assert "Itens Eventuais | ação=cancelar | avulso" not in lines avulso = next(l for l in lines if "Serviço Avulso X" in l) estrategico = next(l for l in lines if "Max Mensal" in l) assert avulso.endswith("| ação=cancelar") assert "| ação=falar sobre" in estrategico assert estrategico.endswith("| type=estrategico") def test_sva_e_itens_eventuais_mesclam_num_unico_cabecalho() -> None: """Linha com as DUAS seções ("SVA Detalhe Total" e "Itens Eventuais"): um único cabeçalho "Itens Eventuais", na posição da primeira delas, com os itens das duas embaixo. A ação por item continua vindo da seção de ORIGEM.""" data = { "11999990000": { "SVA Detalhe Total": [ {"desc": "Tamboro Mensal", "value": 14.99, "period": "01/11/25"}, ], "Mensalidades Adicionais": [{"desc": "Plugin 5G Plus", "value": 5.00}], "Itens Eventuais": [ {"desc": "Serviço Avulso X", "value": 9.90, "period": "02/05/26"}, {"desc": "CRÉDITO: PAGAMENTO", "value": -20.00}, ], } } full = to_text(data) lines = _build_lines(full) assert lines.count("Itens Eventuais | cobrados a parte") == 1 assert "SVA Detalhe Total" not in full # Posição: o bloco mesclado fica onde a PRIMEIRA das duas seções aparecia, # antes de "Mensalidades Adicionais". assert [l for l in lines if not l[:1].isspace() and l] == [ "LINHA 11999990000", "Itens Eventuais | cobrados a parte", "Mensalidades Adicionais", "multiplas_linhas = false", ] assert " Tamboro Mensal | valor=14.99 | data=01/11/25 | ação=cancelar" in lines assert " Serviço Avulso X | valor=9.90 | data=02/05/26 | ação=cancelar" in lines assert " CRÉDITO: PAGAMENTO | valor=-20.00 | ação=cancelar" in lines # No modo enxuto o cabeçalho mesclado é só o rótulo (sem "| cobrados a parte"). lean_lines = _build_lines(to_text(data, lean=True)) assert lean_lines.count("Itens Eventuais") == 1 assert " Tamboro Mensal | valor=14.99 | data=01/11/25" in lean_lines assert " Serviço Avulso X | valor=9.90 | data=02/05/26" in lean_lines def test_lean_enxuga_para_o_intent() -> None: """``lean=True`` (formato do intent classifier): mantém LINHA/seção, o NOME dos itens e ``valor``/``data`` por item (para desambiguar cobranças duplicadas); descarta Fatura Resumo, Planos, Outros Valores e os demais campos por-item (ação/type/franquia/consumo). Item ``Incluído`` e período em faixa não geram valor/data.""" data = { "Fatura Resumo": [{"desc": "total geral", "value": 207.43}], "1199999999": { "Planos": {"TIM Black A 8.0": {"valor_final": 104.99}}, "Outros Valores": [{"desc": "juros: (VENC 10/11)", "value": 0.26}], "SVA Detalhe Total": [ {"desc": "Tamboro Mensal", "value": 14.99, "period": "01/11/25"}, {"desc": "Youtube Premium", "value": 10.0, "estrategico": True}, ], "Serviços Bundle Inclusos": [ {"desc": "Minutos Locais e DDD com 41", "value": "Incluído", "period": "19/05 a 18/06", "franchise": "Ilimitado", "consumption": "43m30s"}, ], }, } lean = to_text(data, lean=True) lines = _build_lines(lean) # Estrutura + nome + valor/data por item (data só p/ período de data única). assert "LINHA 1199999999" in lines assert " Tamboro Mensal | valor=14.99 | data=01/11/25" in lines # Estratégico sem período: valor sim, data não. assert " Youtube Premium | valor=10.00" in lines # Bundle "Incluído" com período em faixa: só o nome (sem valor/data). assert " Minutos Locais e DDD com 41" in lines assert "multiplas_linhas = false" in lines # Cabeçalho de seção é só o nome (sem "| ação=… | classe") no modo enxuto. assert "Benefícios do Plano" in lines assert "ação=" not in lean assert "| bundle" not in lean # valor/data por item agora PRESENTES (distinguem cobranças duplicadas). assert "valor=14.99" in lean assert "data=01/11/25" in lean # "Incluído" nunca vira valor=; período em faixa nunca vira data=. assert "valor=Incluído" not in lean assert "data=19/05" not in lean # Descartado: resumo, planos, outros valores e os demais campos por-item. assert "Fatura Resumo" not in lean assert "Planos" not in lean assert "Outros Valores" not in lean assert "TIM Black A 8.0" not in lean assert "type=estrategico" not in lean assert "franquia=" not in lean assert "consumo=" not in lean # O formato completo (default) SEGUE com tudo — orquestrador inalterado. full = to_text(data) assert "Fatura Resumo" in full assert "Planos" in full assert "valor=14.99" in full def test_get_vas_product_names_ordem_e_filtra_nao_vas() -> None: """Só VAS, na ordem avulso -> estrategico -> bundle; plano e não-VAS ficam de fora. `classe` é a fonte; item com só `estrategico=True` (seção sem classe) cai no fallback estratégico.""" data = { "Fatura Resumo": [{"desc": "total geral", "value": 100.0}], "vocalized_msisdn": {"11999990000": "um um ..."}, "11999990000": { "Planos": {"TIM Black A 8.0": {"valor_final": 100.0}}, "SVA Detalhe Total": [ {"desc": "Focus Mensal", "value": 14.99, "classe": "avulso"}, {"desc": "Netflix", "value": 39.9, "classe": "estrategico", "estrategico": True}, ], "Itens Eventuais": [ {"desc": "Tamboro Mensal", "value": 9.9, "classe": "avulso"}, ], "Serviços Bundle Inclusos": [ {"desc": "HBO Bundle", "classe": "bundle"}, ], # seção sem `classe`, só a flag -> fallback estratégico. "Cobranças de Terceiros": [ {"desc": "Globoplay", "value": 19.9, "estrategico": True}, ], "Descontos": [{"desc": "Desconto Fidelidade", "value": -10.0}], }, } result = get_vas_product_names(data) assert result == "Focus Mensal, Tamboro Mensal, Netflix, Globoplay, HBO Bundle" # plano e seção não-VAS não entram. assert "TIM Black" not in result assert "Desconto" not in result def test_get_vas_product_names_deduplica_entre_linhas() -> None: """Mesmo produto em duas linhas (MSISDN) aparece 1×, na primeira ocorrência.""" data = { "11999990000": {"SVA Detalhe Total": [{"desc": "Focus Mensal", "classe": "avulso"}]}, "11888880000": {"SVA Detalhe Total": [{"desc": "Focus Mensal", "classe": "avulso"}]}, } assert get_vas_product_names(data) == "Focus Mensal" def test_get_vas_product_names_sem_vas_retorna_vazio() -> None: assert get_vas_product_names({}) == "" assert get_vas_product_names( {"11999990000": {"Descontos": [{"desc": "X", "value": -1.0}]}} ) == ""