""" 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