# Changelog — Importação de Plano de Saúde > Histórico específico desta aplicação, extraído de `plano.md`. É a aplicação com mais rodadas do projeto — cada operadora nova, cada regra de custeio e cada bug de parser tem sua própria entrada abaixo. > > **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`. ### Rodada 36 — Importação de Plano de Saúde (Utilitários) Primeira aplicação de "Utilitários" (os placeholders "Conversor de Arquivos"/"Calculadora Fiscal" foram removidos do menu nesta mesma rodada). Importa o relatório de faturamento de uma operadora de plano de saúde/odontológico (Amil, Unimed) e gera o arquivo de lançamento no leiaute do Questor, mais um relatório de auditoria do que não pôde ser lançado automaticamente. A lógica de negócio veio de um pipeline já testado (`projects/importacao-planos-saude.skill` + protótipo em `projects/project/`), portado quase 1:1 pra dentro do Django em `portal_api/planos_saude/` (pacote Python puro, sem ORM). Decisões principais: permissão de **toggle único** (quem tem acesso faz o fluxo inteiro — criar, revisar, gerar); histórico completo (cada importação fica salva, diferente de um fluxo descartável); regra de custeio (empresa/empregado/específica, com limite de valor e/ou percentual) configurável por tipo de lançamento e tipo de beneficiário na tela, em vez da regra fixa do pipeline original; resolução manual de auditoria por nome (confirmação humana item por item quando o casamento automático falha, nunca fuzzy matching); pré-validação de cada arquivo anexado antes do envio final, reaproveitando o mesmo parser Python que o `create()` usaria. Migrações `0021_importacaoplanosaude_importacaoplanosaudeauditoria_and_more` e `0022_importacaoplanosaudeauditoria_resolucao_manual`. Detalhe completo em `CLAUDE.md` desta pasta. ### Rodada 47 — Bug: `SuspiciousFileOperation` ao anexar arquivo com nome muito longo Testando em produção real, upload de um arquivo da operadora com nome de arquivo original bem longo (ex.: "LEIAUTE_IMPORTACAO_DESPESAS_MEDICAS_EMP_92_PRESCINOTTI_CIA_LTDA...OPER_5060_UNIMED_DO_ESTADO_DO_PARANA_-_FEDERACAO_ESTADUAL_DA.CSV") deu 400 com `django.core.exceptions.SuspiciousFileOperation: Storage can not find an available filename ... Please make sure that the corresponding file field allows sufficient "max_length"`. Causa: `ImportacaoPlanoSaude.planilha_padrao`/`arquivo_operadora` (`FileField`) não tinham `max_length` explícito — o padrão do Django é 100, insuficiente pra `upload_to="planos_saude/planilha_padrao/"` (ou `.../operadora/`) somado a um nome de arquivo original longo (nome de arquivo real do cliente, fora do controle do Portal) + o sufixo que a storage acrescenta pra evitar colisão. Corrigido definindo `max_length=255` nos dois campos (`portal_api/models.py`, migração `0036_alter_importacaoplanosaude_arquivo_operadora_and_more`, aplicada no ambiente local). Mesmo cuidado vale pra qualquer `FileField`/`ImageField` novo que aceite nome de arquivo originado fora do Portal (upload do usuário) — o padrão de 100 caracteres do Django é apertado demais pra nomes de arquivo reais de operadoras/clientes. ### Rodada 48 — Banco de regras de custeio salvas Pedido do usuário, testando a importação da empresa 92 (Unimed): em vez de exportar/importar um arquivo `.json` com a regra de custeio preenchida (mecanismo puramente client-side, sem persistência — nada guardado no banco, sem nome, sem observação), ele queria um banco de regras de verdade: salvar a configuração usada como "092 - Unimed", escolhê-la numa lista em importações futuras, poder editá-la depois e anexar uma observação livre (ex.: "Empresa não desconta plano do empregado XX"). O que mudou: - **Model novo** `RegraCusteioPlanoSaude` (migração `0037_regracusteioplanosaude`) — `nome`/`operadora`/`tipos_lancamento`/`custeio_por_tipo` (mesmo formato dos campos homônimos de `ImportacaoPlanoSaude`) + `observacoes` (texto livre) + `criado_por`/`criado_em`/`atualizado_em`. Lista compartilhada, sem "dono", mesma permissão de toggle único da ferramenta (`PermissaoApp("utilitarios", "importacao-plano-saude")`). - **`RegraCusteioPlanoSaudeViewSet`** (CRUD completo, GET/POST/PATCH/DELETE) registrado em `/api/regras-custeio-plano-saude/`, seguindo o mesmo padrão de `IndicadorPercentualTipoViewSet` (`perform_create` grava `criado_por`). - **Validação de custeio extraída pra uma função compartilhada** (`_monta_regra_custeio()`, `serializers.py`) — antes só existia dentro de `ImportacaoPlanoSaudeCreateSerializer._valida_regra_especifica()`; extraída pra módulo-level e reaproveitada por `RegraCusteioPlanoSaudeSerializer.validate()`, pra não duplicar a regra de negócio (parsing BR, faixa 0–100 do percentual, "ao menos limite ou percentual") em dois serializers que podiam divergir com o tempo. `ImportacaoPlanoSaudeCreateSerializer` foi refatorado pra chamar essa mesma função — comportamento idêntico, validado com teste manual comparando a saída antes/depois do refactor. - **Round-trip float↔texto BR**: uma regra salva volta do `GET` com `limite_valor`/`percentual` já como `float` (formato final persistido), mas a validação de entrada só entende texto BR (`"150,00"`). `_valor_custeio_para_texto_br()` normaliza um float de volta pra BR (via `formata_valor_br`, já existente em `leiaute_sistema.py`) antes de repassar pro parser — sem isso, reenviar uma regra sem editar o custeio (ex.: só corrigindo o nome) corromperia o valor (`"150.0"` seria lido como 15000 por `parse_valor_br`, que remove pontos como separador de milhar). Validado via shell: criar uma regra, pegar `validated_data` de volta e revalidar como se fosse um update sem mudanças reproduz exatamente o mesmo resultado. - **Frontend** (`importacao-plano-saude.js`/`.html`/`.css`): a seção "Regra de custeio" do formulário de Nova Importação trocou os botões "Exportar regra"/"Importar regra" por um `` virou combobox pesquisável Com o banco de regras salvas crescendo (8 regras já cadastradas pelo usuário entre as 5 operadoras), o `` (`#ips-regra-search`) que funciona tanto como campo de busca quanto como "display" do valor selecionado, com uma lista flutuante (`#ips-regra-combo-list`, `position:absolute` abaixo do input) que filtra pelas regras cujo nome contém o texto digitado (case-insensitive) — abre no foco (mostrando todas) e a cada tecla digitada; fecha ao clicar fora (listener de `click` no `document`, checando `!ipsRegraCombo.contains(event.target)`) ou ao escolher um item. Mesmo espírito de busca+lista já usado em "Vincular pessoa", só que aqui o campo de busca dobra como o "valor exibido" no lugar de uma `