portal_publico/portal_api/planos_saude/regras_empresa.py

279 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Regras de custeio especiais por empresa ("Regra especial da empresa" na tela
de nova importação/Cadastro de Regras) — ao contrário de
RegraCusteioPlanoSaude (banco de regras salvas, editável pelo usuário,
sempre no formato "por tipo de lançamento × titular/dependente"), as regras
aqui são cadastradas diretamente no código pelo desenvolvedor quando o
cliente repassa uma regra negociada com uma empresa específica que não se
encaixa nesse formato — tipicamente porque são calculadas por FAMÍLIA
(titular + todos os dependentes juntos), não por pessoa, ou porque o
critério de quem paga o quê não é um percentual/teto simples. Nunca
expostas para o usuário cadastrar pela tela; só "Selecionar regra" no
Cadastro de Regras, que lista as chaves já registradas aqui
(GET /api/importacoes-plano-saude/regras-empresa/).
Cada regra declara, além da função `aplica`, quais tipos de lançamento ela
cobre (`tipos_lancamento` — nem toda regra cobre os dois; a Tecnomyl abaixo
só cobre "mensalidade", coparticipação dela segue o custeio normal
configurado na importação) e qual estratégia de casamento ela exige
(`chave_casamento` — "nome" pra regras que dependem de agrupar uma família
inteira, como a Tecnomyl; "cpf" pra regras que decidem por pessoa, sem
precisar de família, como a Ottimizza abaixo). `aplica(linhas_e_valores,
tipo_lancamento)` recebe sempre os dois argumentos — o segundo permite que
uma mesma regra se comporte diferente por tipo de lançamento (ex.: a
Ottimizza abaixo aplica um critério pra mensalidade e outro, fixo, pra
coparticipação).
"""
from typing import Any, Callable, Dict, List, Tuple
from portal_api.planos_saude.leiaute_sistema import formata_valor_br
from portal_api.planos_saude.modelos import LinhaSistema
class RegraEmpresaIncompativelError(Exception):
"""Regra empresa selecionada não é compatível com esta importação
(tipo de lançamento não coberto pela regra, operadora com estratégia de
casamento diferente da que a regra exige, ou planilha padrão de uma
empresa diferente daquela pra qual a regra foi cadastrada) — views.py
devolve esta mensagem direto pro usuário, em vez do erro genérico de
"formato de arquivo não conforme"."""
def _eh_linha_titular(linha: Any) -> bool:
"""Mesmo critério de `LinhaSistema.eh_linha_titular()`, mas duck-typed —
as regras abaixo rodam tanto contra `LinhaSistema` (pipeline, na
criação da importação) quanto contra `ImportacaoPlanoSaudeLinha` (model
Django, no recálculo pós "Vincular pessoa" — ver
`views._recalcula_familia_regra_empresa`), que não tem esse método."""
return not (linha.nome_dependente or "").strip() and not (linha.cpf_dependente or "").strip()
def _aplica_teto_familia(linhas_e_valores: List[Tuple[Any, float]], teto: float) -> None:
"""Divide o teto de custeio da empresa (`teto`) entre as linhas de UMA
família (titular + dependentes) — decisão explícita do cliente:
DEPENDENTES TÊM PRIORIDADE no uso do teto (são cobertos primeiro, na
ordem em que aparecem — cada um recebe `min(seu valor, o que sobrou do
teto)`), e o TITULAR absorve por último o que sobrar do teto (o
"residual"). Se os dependentes sozinhos já consumirem o teto inteiro, o
titular fica com desconto integral (`valor_empresa=0`) e, se ainda
sobrar dependente sem cobrir depois disso, esse dependente também é
parcialmente descontado — ao contrário de uma divisão proporcional
(tentada numa primeira versão e revertida): o pedido do cliente é
"abater primeiro o valor dos dependentes", não repartir o teto
igualmente entre todos."""
dependentes = [(linha, valor) for linha, valor in linhas_e_valores if not _eh_linha_titular(linha)]
titulares = [(linha, valor) for linha, valor in linhas_e_valores if _eh_linha_titular(linha)]
teto_restante = teto
for linha, valor in dependentes + titulares:
valor = max(0.0, valor)
valor_empresa = round(min(valor, max(0.0, teto_restante)), 2)
valor_empregado = round(valor - valor_empresa, 2)
teto_restante = round(teto_restante - valor_empresa, 2)
linha.valor_empresa = formata_valor_br(valor_empresa)
linha.valor = formata_valor_br(valor_empregado)
def _regra_unimed_1778_tecnomyl(linhas_e_valores: List[Tuple[Any, float]], tipo_lancamento: str) -> None:
"""Tecnomyl (código 1778 na Unimed) — ajuda de custo de até R$ 661,61
por família (titular + dependentes juntos, não por pessoa), repassada
pelo cliente em 08/2026: família com mensalidade total acima do teto
tem o excedente descontado do empregado; igual ou abaixo do teto, a
empresa cobre 100% e o empregado não paga nada. Só cobre "mensalidade"
(ver `tipos_lancamento` no registro abaixo) — `tipo_lancamento` nunca
varia de fato aqui, mas o parâmetro é obrigatório pra toda regra."""
_aplica_teto_familia(linhas_e_valores, teto=661.61)
def _regra_amil_898_tecnomyl(linhas_e_valores: List[Tuple[Any, float]], tipo_lancamento: str) -> None:
"""Amil Odonto (código 898) na Tecnomyl, repassada pelo cliente em
09/2026: a empresa custeia 100% da mensalidade de titular e dependente
direto (cônjuge, filho(a)); agregados — avós, tios, sobrinhos, sogros e
similares — têm a mensalidade 100% descontada do empregado, no MESMO
valor (a regra é só sobre quem paga, não sobre um teto/percentual, por
isso cobre os dois planos do contrato — E200 a R$ 20,81/pessoa e
"DENTAL 200" a R$ 16,01/pessoa — sem precisar de nenhum valor fixo
aqui). Confirmado contra o arquivo real da competência 08/2026: todo
titular/dependente/agregado paga exatamente o mesmo valor por pessoa
dentro do mesmo plano, e a discriminação titular/dependente/agregado do
relatório (coluna "Tipo": T/D/A) bate exatamente com os exemplos do
cliente (avós, sogros, sobrinhos = agregado).
Ao contrário da Tecnomyl-Unimed (`_aplica_teto_familia`, um teto por
família), esta regra é por PESSOA, sem nenhuma interação entre membros
da família — usa `linha.tipo_pessoa` ('T'/'D'/'A', gravado pelo
matcher no momento do casamento a partir do Individuo.tipo original do
arquivo da operadora — ver `LinhaSistema.tipo_pessoa` — já que a
planilha padrão do sistema não distingue dependente de agregado por si
só). Uma linha sem `tipo_pessoa` preenchido (nunca passou pelo
casamento automático — ex.: incluída manualmente via "Adicionar linha")
cai no fallback "D": custeada pela empresa, o mesmo comportamento que
já valeria pra ela antes desta regra existir."""
for linha, valor in linhas_e_valores:
valor = max(0.0, valor)
tipo_pessoa = getattr(linha, "tipo_pessoa", "") or "D"
eh_agregado = tipo_pessoa == "A"
linha.valor_empresa = formata_valor_br(0.0 if eh_agregado else valor)
linha.valor = formata_valor_br(valor if eh_agregado else 0.0)
def _regra_sulamerica_5775_ottimizza(linhas_e_valores: List[Tuple[Any, float]], tipo_lancamento: str) -> None:
"""Ottimizza (código 1889) na SulAmérica (5775) — critério fixo, sem
depender de teto/percentual: a mensalidade do titular é 100% custeada
pela empresa e a do dependente é 100% descontada do empregado; toda
coparticipação (titular ou dependente) é 100% descontada do empregado.
Confirmado contra a planilha "Informações Plano de Saúde" real da
Ottimizza (competência 08/2026): toda linha de titular só vem com
"Benefício Mensalidade" preenchido, toda linha de dependente só vem com
"Desconto Mensalidade", e "Benefício Coparticipação" nunca aparece
preenchido em nenhuma linha (só "Desconto Coparticipação") — mesmo
critério, só que aplicado pelo Portal em vez de lido campo a campo da
planilha (ver `operadoras/sulamerica/saude.py`, que só soma os dois
valores de cada tipo e deixa a divisão pra esta regra)."""
custeada_pela_empresa = tipo_lancamento == "mensalidade"
for linha, valor in linhas_e_valores:
valor = max(0.0, valor)
eh_titular = custeada_pela_empresa and _eh_linha_titular(linha)
valor_empresa = valor if eh_titular else 0.0
valor_empregado = 0.0 if eh_titular else valor
linha.valor_empresa = formata_valor_br(valor_empresa)
linha.valor = formata_valor_br(valor_empregado)
# Pra cadastrar uma regra nova: escrever a função `_regra_...(linhas_e_valores,
# tipo_lancamento)` acima (ou reaproveitar `_aplica_teto_familia` se for só
# um teto por família) e registrar aqui. `codigos_empresa` é a tupla de
# códigos de empresa na planilha padrão pra os quais a regra foi negociada —
# usado só pra travar contra aplicar a regra errada numa planilha de outra
# empresa (ver `valida_regra_empresa` abaixo). É uma TUPLA, e não um código
# só, porque uma mesma condição negociada pode valer pra várias empresas do
# mesmo grupo econômico (caso do grupo Tecnomyl: a ajuda de custo da Unimed
# é a mesma na 1778 e na 1855) — nesse caso é a MESMA regra, não uma cópia
# por empresa. Só incluir um código aqui com confirmação de que as condições
# são idênticas: o valor errado aqui vira desconto errado na folha de alguém.
# `chave_casamento` é a estratégia que a
# OPERADORA precisa usar pra esta regra fazer sentido ("nome" pra regras que
# agrupam família — a função só recebe as linhas de UMA família por vez;
# "cpf" pra regras por pessoa, sem agrupamento — a função recebe TODAS as
# linhas casadas do tipo de uma vez). `tipos_lancamento` é a tupla de tipos
# que esta regra cobre — os demais tipos selecionados na importação seguem
# o custeio normal configurado (`custeio_por_tipo`), sem relação com a
# regra. `observacoes` é opcional (texto livre explicando a regra em
# português) — exibida só-leitura no topo da tela de revisão (ver
# "regra_empresa_observacoes" em ImportacaoPlanoSaudeDetailSerializer) pra o
# colaborador conferir a regra aplicada sem precisar abrir o código.
REGRAS_EMPRESA: Dict[str, dict] = {
"unimed_1778_tecnomyl": {
"label": "1778/1855/1872/1927 - Unimed (Tecnomyl)",
# Grupo Tecnomyl, todas com a MESMA ajuda de custo negociada
# (confirmado pelo usuário em 2026-09-22): 1778 Tecnomyl Brasil,
# 1855 H2O Innovation, 1872 GS3 Digital, 1927 YVY Agricultura
# Digital. Nomes conferidos no Questor antes de entrarem aqui.
"codigos_empresa": ("1778", "1855", "1872", "1927"),
"operadora": "unimed_saude",
"chave_casamento": "nome",
"tipos_lancamento": ("mensalidade",),
"aplica": _regra_unimed_1778_tecnomyl,
"observacoes": (
"O grupo Tecnomyl (empresas 1778, 1855, 1872 e 1927) oferece uma "
"ajuda de custo de até R$ 661,61 por família "
"(titular + dependentes juntos, independente da quantidade de "
"dependentes). Os dependentes são custeados primeiro; o titular "
"absorve o valor residual do teto, e o excedente (se houver) é "
"descontado do empregado. Coparticipação segue o custeio normal "
"configurado nesta importação, sem relação com esta regra."
),
},
"amil_898_tecnomyl": {
# Mesmo grupo (e mesmos códigos) da regra da Unimed acima —
# confirmado pelo usuário em 2026-09-22, logo depois da Unimed.
"label": "1778/1855/1872/1927 - Amil (Tecnomyl)",
"codigos_empresa": ("1778", "1855", "1872", "1927"),
"operadora": "amil_odonto_mensalidade_898",
"chave_casamento": "cpf",
"tipos_lancamento": ("mensalidade",),
"aplica": _regra_amil_898_tecnomyl,
"observacoes": (
"O grupo Tecnomyl (empresas 1778, 1855, 1872 e 1927) arca com o "
"valor integral da mensalidade de titular e "
"dependente direto (cônjuge, filho(a)). Agregados — avós, tios, "
"sobrinhos, sogros e similares — têm a mensalidade 100% "
"descontada do empregado, no mesmo valor por pessoa. Cobre os "
"planos do contrato (E200 e DENTAL 200), cada um no seu próprio "
"valor de mensalidade."
),
},
"sulamerica_5775_ottimizza": {
"label": "1889 - SulAmérica (5775)",
"codigos_empresa": ("1889",),
"operadora": "sulamerica_saude",
"chave_casamento": "cpf",
"tipos_lancamento": ("mensalidade", "coparticipacao"),
"aplica": _regra_sulamerica_5775_ottimizza,
"observacoes": (
"A Ottimizza tem um critério fixo na SulAmérica: a mensalidade do "
"titular é 100% custeada pela empresa, a do dependente é 100% "
"descontada do empregado, e toda coparticipação (titular ou "
"dependente) é 100% descontada do empregado."
),
},
}
def lista_regras_empresa() -> List[Dict[str, Any]]:
return [
{
"key": chave,
"label": dados["label"],
"observacoes": dados.get("observacoes", ""),
"tipos_lancamento": list(dados.get("tipos_lancamento", ("mensalidade",))),
}
for chave, dados in REGRAS_EMPRESA.items()
]
def valida_regra_empresa(
regra_empresa_key: str,
chave_casamento_por_tipo: Callable[[str], str],
linhas_sistema: List[LinhaSistema],
tipos_selecionados: List[str],
) -> Tuple[Callable[[List[Tuple[Any, float]], str], None], Tuple[str, ...]]:
"""Valida que a regra empresa escolhida pode ser aplicada nesta
importação e devolve `(aplica, tipos_cobertos)` já resolvidos —
`tipos_cobertos` é a interseção entre o que a regra cobre e o que foi
selecionado nesta importação (pode ser um subconjunto: uma regra que
cobre mensalidade+coparticipação numa importação que só selecionou
mensalidade só se aplica à mensalidade). Chamado por
pipeline.processa_importacao antes do casamento."""
regra = REGRAS_EMPRESA.get(regra_empresa_key)
if regra is None:
raise RegraEmpresaIncompativelError(f"Regra empresa desconhecida: {regra_empresa_key!r}.")
tipos_da_regra = regra.get("tipos_lancamento", ("mensalidade",))
tipos_cobertos = tuple(t for t in tipos_da_regra if t in tipos_selecionados)
if not tipos_cobertos:
raise RegraEmpresaIncompativelError(
f"A regra \"{regra['label']}\" cobre {'/'.join(tipos_da_regra)}, mas nenhum desses "
"tipos de importação está selecionado."
)
casamento_exigido = regra.get("chave_casamento", "nome")
for tipo in tipos_cobertos:
if chave_casamento_por_tipo(tipo) != casamento_exigido:
raise RegraEmpresaIncompativelError(
f"Esta regra especial exige casamento por {casamento_exigido!r} "
f"— esta operadora não é compatível para o tipo {tipo!r}."
)
# Basta a planilha ter linha de UMA das empresas cobertas — uma regra de
# grupo (ver `codigos_empresa` no registro acima) é usada numa importação
# de cada vez, então a planilha traz só a empresa daquela importação.
codigos_esperados = regra.get("codigos_empresa", ())
if codigos_esperados and not any(l.codigo_empresa in codigos_esperados for l in linhas_sistema):
esperados = " ou ".join(codigos_esperados)
raise RegraEmpresaIncompativelError(
f"A regra \"{regra['label']}\" foi cadastrada para a empresa código {esperados}, "
"mas a planilha padrão anexada não tem nenhuma linha com esse código."
)
return regra["aplica"], tipos_cobertos