Projeto do Agent Contas ORACLE

This commit is contained in:
2026-08-19 09:35:50 -03:00
commit 950a2bcd33
1366 changed files with 177217 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
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}]}}
) == ""