132 lines
7.0 KiB
Python
132 lines
7.0 KiB
Python
"""
|
||
Regras de custeio especiais por empresa ("Regra empresa" na tela de nova
|
||
importação, terceiro item de "Tipo de importação" ao lado de Mensalidade/
|
||
Coparticipação) — 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. Nunca expostas para o usuário cadastrar pela tela; só
|
||
"Selecionar regra" na tela de nova importação, que lista as chaves já
|
||
registradas aqui (GET /api/importacoes-plano-saude/regras-empresa/).
|
||
|
||
Só se aplica ao tipo de lançamento "mensalidade" — coparticipação sempre
|
||
segue o custeio normal configurado na tela (por isso REGRAS_EMPRESA não
|
||
tem nada de coparticipação), e é por isso que "Regra empresa" e
|
||
"Mensalidade" são mutuamente exclusivos no formulário: as duas são formas
|
||
alternativas de configurar o MESMO tipo de lançamento "mensalidade".
|
||
"""
|
||
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
|
||
(operadora sem casamento por nome, 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 —
|
||
`_aplica_teto_familia` roda 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]]) -> 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."""
|
||
_aplica_teto_familia(linhas_e_valores, teto=661.61)
|
||
|
||
|
||
# Pra cadastrar uma regra nova: escrever a função `_regra_...(linhas_e_valores)`
|
||
# acima (ou reaproveitar `_aplica_teto_familia` se for só um teto por família)
|
||
# e registrar aqui. `codigo_empresa` é o código da empresa na planilha padrão
|
||
# pra qual a regra foi negociada — usado só pra travar contra aplicar a regra
|
||
# errada numa planilha de outra empresa (ver `valida_regra_empresa` abaixo).
|
||
# `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 - Unimed (Tecnomyl)",
|
||
"codigo_empresa": "1778",
|
||
"operadora": "unimed_saude",
|
||
"aplica": _regra_unimed_1778_tecnomyl,
|
||
"observacoes": (
|
||
"A Tecnomyl 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."
|
||
),
|
||
},
|
||
}
|
||
|
||
|
||
def lista_regras_empresa() -> List[Dict[str, str]]:
|
||
return [
|
||
{"key": chave, "label": dados["label"], "observacoes": dados.get("observacoes", "")}
|
||
for chave, dados in REGRAS_EMPRESA.items()
|
||
]
|
||
|
||
|
||
def valida_regra_empresa(
|
||
regra_empresa_key: str, chave_casamento: str, linhas_sistema: List[LinhaSistema]
|
||
) -> Callable[[List[Tuple[Any, float]]], None]:
|
||
"""Valida que a regra empresa escolhida pode ser aplicada nesta
|
||
importação e devolve a função de aplicação já resolvida — chamado por
|
||
pipeline.processa_importacao antes de rodar o casamento de mensalidade."""
|
||
regra = REGRAS_EMPRESA.get(regra_empresa_key)
|
||
if regra is None:
|
||
raise RegraEmpresaIncompativelError(f"Regra empresa desconhecida: {regra_empresa_key!r}.")
|
||
if chave_casamento != "nome":
|
||
raise RegraEmpresaIncompativelError(
|
||
"Esta operadora não é compatível com 'Regra empresa' (requer casamento por nome)."
|
||
)
|
||
codigo_esperado = regra.get("codigo_empresa")
|
||
if codigo_esperado and not any(l.codigo_empresa == codigo_esperado for l in linhas_sistema):
|
||
raise RegraEmpresaIncompativelError(
|
||
f"A regra \"{regra['label']}\" foi cadastrada para a empresa código {codigo_esperado}, "
|
||
"mas a planilha padrão anexada não tem nenhuma linha com esse código."
|
||
)
|
||
return regra["aplica"]
|