""" Modelos de dados usados por todo o pipeline de importação de plano de saúde, independente da operadora. Porta quase 1:1 de projects/project/core/modelos.py (ver projects/importacao-planos-saude.skill para as regras de negócio por trás destas decisões) — a única diferença é `LinhaSistema` não guardar mais `_linha_original` (não é usado fora do CLI original) e passar a ter `id` (preenchido pela view a partir da ImportacaoPlanoSaudeLinha ao editar uma importação já existente). """ import unicodedata from dataclasses import dataclass, field from typing import Optional def normaliza_cpf(cpf: str) -> str: """Remove pontuação, deixando só os dígitos. Usado como chave de casamento.""" return "".join(ch for ch in cpf if ch.isdigit()) def normaliza_nome(nome: str) -> str: """ Normaliza um nome para comparação: maiúsculas, sem acento, sem espaços duplicados/nas pontas. Usado como chave de casamento quando a operadora não manda CPF (ex: Unimed) — por isso é estrito por design: 'SCHAEFER' e 'SCHAFER' são tratados como NOMES DIFERENTES. Divergências de grafia devem cair em auditoria, nunca ser resolvidas por aproximação automática. """ sem_acento = unicodedata.normalize("NFKD", nome or "") sem_acento = "".join(c for c in sem_acento if not unicodedata.combining(c)) return " ".join(sem_acento.upper().split()) @dataclass class Lancamento: """Uma linha crua extraída do arquivo da operadora (uma rubrica/serviço).""" numero_beneficiario: str # identificador do indivíduo no arquivo da operadora nome: str cpf: str # pode vir vazio quando a operadora não informa CPF (ex: Unimed) tipo: str # 'T' = Titular, 'D' = Dependente, 'A' = Agregado rubrica: str valor: float # já convertido para float, com sinal (negativo = desconto/devolução) tipo_lancamento: str = "mensalidade" # 'mensalidade' ou 'coparticipacao' dependencia: Optional[str] = None # ex: "Conjuge", "Filho(a)", "Pai/Mãe" numero_titular: Optional[str] = None # id do titular da família no arquivo da operadora @dataclass class Individuo: """ Um beneficiário (titular ou dependente) já com o valor do mês agregado PARA UM tipo de lançamento (mensalidade OU coparticipação). Uma mesma pessoa pode gerar dois objetos Individuo no mesmo mês — um de cada tipo — quando a operadora manda os dois juntos (ex: Unimed). """ numero_beneficiario: str nome: str cpf: str tipo: str tipo_lancamento: str = "mensalidade" valor_total: float = 0.0 rubricas: list = field(default_factory=list) # histórico de rubricas somadas numero_titular: Optional[str] = None # para casamento por família (chave_casamento='nome') @property def cpf_normalizado(self) -> str: return normaliza_cpf(self.cpf) @property def nome_normalizado(self) -> str: return normaliza_nome(self.nome) @dataclass class LinhaSistema: """Uma linha da planilha padrão do sistema (leiaute de importação).""" codigo_empresa: str nome_func: str cpf_func: str codigo_out_emp: str data_inicial: str nome_dependente: str cpf_dependente: str valor_empresa: str valor: str descricao: str def cpf_func_normalizado(self) -> str: return normaliza_cpf(self.cpf_func) def cpf_dependente_normalizado(self) -> str: return normaliza_cpf(self.cpf_dependente) if self.cpf_dependente else "" def nome_func_normalizado(self) -> str: return normaliza_nome(self.nome_func) def nome_dependente_normalizado(self) -> str: return normaliza_nome(self.nome_dependente) if self.nome_dependente else "" def eh_linha_titular(self) -> bool: """Linha 'do titular': dependente vazio.""" return not self.cpf_dependente.strip() and not self.nome_dependente.strip() @dataclass class ItemAuditoria: motivo: str # 'VALOR_NEGATIVO' | 'NAO_CADASTRADO' | 'NOME_DIVERGENTE' | 'TIPO_INVALIDO' numero_beneficiario: str nome: str cpf: str tipo: str valor: float tipo_lancamento: str = "" detalhe: str = "" @dataclass class VinculoNome: """Representação "pura" (sem ORM) de um VinculoNomeOperadora salvo (portal_api.models) — só os campos que matcher.py precisa pra resolver automaticamente um nome divergente já confirmado antes, sem esse pacote (deliberadamente sem ORM) depender do Django. Montado por ImportacaoPlanoSaudeViewSet.create() (views.py) a partir de uma consulta real ao banco, e passado adiante até `_casa_por_nome`.""" id: int nome_func_destino: str nome_dependente_destino: str @dataclass class VinculoAplicado: """Registra que um Individuo foi casado com uma LinhaSistema através de um VinculoNome já salvo (nome divergente, resolvido automaticamente), não por nome exato — usado por views.py pra gerar um ImportacaoPlanoSaudeAlteracao (tipo "vinculo_automatico") depois que as linhas forem persistidas (ainda não têm `id` no momento em que matcher.py roda).""" indice_linha: int # índice de `linha` dentro de linhas_sistema, pra esse tipo_lancamento tipo_lancamento: str vinculo_id: int nome_arquivo_operadora: str