222 lines
9.0 KiB
Python
222 lines
9.0 KiB
Python
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}]}}
|
||
) == ""
|