# Fase 2 — a nona fatia: os convênios

Escolhida em 08/09/2026. O PDV aplica desconto de convênio desde a primeira
fatia, e a quarta já devolve `convenio` no recurso do cliente — mas **nada
listava os convênios**. Uma tela nova mostraria o convênio de um cliente sem
poder oferecer a lista para trocá-lo.

---

## O recorte

| endpoint | verbo | origem |
|---|---|---|
| `/adm/api/convenios` | `GET` | `indexAction` |
| `/adm/api/convenios` | `POST` | `novoAction` → `ConvenioService::insert` |
| `/adm/api/convenios/:id` | `GET` | `editarAction`, leitura |
| `/adm/api/convenios/:id` | `PUT` | `editarAction` → `update` |
| `/adm/api/convenios/:id/ativacao` | `POST` | `ativarRemover` |
| `/adm/api/convenios/:id/desativacao` | `POST` | idem |

**Fica de fora:**

| o quê | por quê |
|---|---|
| `historicoVendas` | é relatório: agrega venda por período, com o seu próprio recorte |
| `clientesConveniados` | lista de clientes filtrada — pertence ao recurso de clientes, com `convenio` como filtro |
| `unidadesConveniadas` | mesma coisa do lado das unidades; entra quando houver recurso de unidade |

---

## O ERP alterna o estado; a API tem dois endereços

```php
public function ativarRemover($convenio) {
    $convenio->setStatus($convenio->getStatus() ? false : true);
```

É um **interruptor**. E `POST` que alterna não é idempotente: quem reenvia por
rede ruim desfaz o que acabou de fazer — a lição que o check-in da agenda deu na
segunda fatia.

Como o serviço só sabe alternar, cada ação **confere o estado antes** e recusa
com `409` quando já está como se pede. Assim `POST .../ativacao` duas vezes ativa
uma vez, que é o que o endereço promete. Sem essa conferência, a segunda chamada
faria o **oposto** do nome dela.

---

## `contratoVencido` informa, não decide

A distinção é do sistema, não invenção da fatia: **o desconto da venda não
consulta esse sinalizador.** A vigência do convênio segue até que alguma parte
cancele; a data de contrato é referência comercial, não corte automático. O que
o campo responde é *"quais contratos vale revisitar"*.

O recurso devolve **os dois** — a data e o sinalizador — e nunca calcula um a
partir do outro. Calcular "vencido" na API faria ela e a tela discordarem sobre a
mesma linha.

**E hoje o sinalizador é `false` para todos.** Não é defeito desta fatia nem
achado novo: quem o mantém é a rotina `conveniosAtualizarVencimento`, e ela só
passa a rodar quando a linha de crontab do **D50** for aplicada em produção. A
medida na cópia de produção: **638 convênios, 18 com data de contrato — todas as
18 já passadas — e zero marcados**. O `CronJobService` já documenta isso, com o
mais antigo há 1.933 dias.

O recurso repete o estado como ele é. Maquiar aqui esconderia justamente o que a
linha de crontab vai consertar.

---

## Duas traduções na fronteira

**1. A porcentagem sai como texto decimal**, pelo mesmo motivo do dinheiro
(regra 7): 12,5% num JSON como número de ponto flutuante é um arredondamento
esperando acontecer — e esse número entra no cálculo da venda.

**2. A data vai nos dois sentidos.** `getContratoDataVencimento()` **sem
argumento devolve `d/m/Y`** — o mesmo tipo de getter que já mordeu na quarta
fatia (`Cliente::getDataNascimento()`) e na sexta (`getValorVenda()` mascarado).
E o *setter* espera `d/m/Y` também: mandar ISO direto cai num `createFromFormat`
que devolve `false` **sem lançar**. A fatia traduz nas duas pontas.

---

## Convênio sem unidade vale em todas

São **157 dos 638**. A lista vazia de unidades não significa "não vale em lugar
nenhum" — significa o contrário. Está escrito no `ConvenioJson` porque quem
consome a API não tem como deduzir isso do dado.

---

## O defeito que a leitura achou (D129)

```php
$dql .= " AND conuni.unidade = :unidade OR conuni.unidade IS NULL";
```

`A AND B OR C` é `(A AND B) OR C`: sem os parênteses o `OR` partia o `WHERE`
inteiro, e o ramo da direita entrava **ignorando o filtro de status**. Medido:
filtrar pela unidade com mais convênios devolvia **290 linhas; com os parênteses,
285** — as cinco a mais eram convênios **inativos** numa lista de ativos.

Corrigido antes de escrever a fatia, porque a listagem da API usa exatamente
essa consulta.

---

## 28/09/2026 -- a tela nova dos convênios

"Convênios", no menu de Configurações, deixou de abrir a tela antiga. A tela nova
lista os ativos, com busca por nome, situação e "vale na unidade" (as unidades do
`<select>` da tela antiga: todas as ativas ao administrador, a própria aos outros;
o filtro traz também os que valem em todas), e a página é de 20. A tabela diz o
desconto, **onde vale** ("todas" ou "N unidades"), os clientes, a última venda e o
contrato -- com o selo "vencido" só quando o servidor marca.

Cada convênio abre o cadastro: o desconto, **onde vale por extenso** ("Em todas as
unidades, inclusive nas que abrirem depois", ou a lista, com as inativas ligadas
marcadas -- e, se nenhuma está ativa, a frase de que hoje ele não dá desconto em
lugar nenhum), o contato, o contrato, o uso (os clientes ativos e a última venda) e
o histórico. As vendas com o convênio e os clientes conveniados ficam, por enquanto,
na tela antiga, com um link.

**Só o administrador altera** (o D255): cadastra, edita, ativa e desativa aqui
mesmo. Quem tem o privilégio de escrever e não é administrador lê, e a tela diz por
quê. **Desativar pergunta no próprio botão**, dizendo o alcance -- quantas unidades
ativas deixam de dar o desconto, e que as comandas já lançadas o mantêm --; ativar
não pergunta, como no ERP.

O formulário tem os campos da tela antiga e **"onde vale" como escolha obrigatória**:
dois rádios -- todas as unidades, inclusive as que abrirem depois, ou só as marcadas
--, e no cadastro nenhum vem marcado. As caixas são as unidades ativas mais as
inativas já ligadas (o D256), com um filtro e a contagem viva. A edição manda só o
que mudou, e confere só isso: o convênio antigo de nome longo salva o telefone.

### O recorte, de novo

| endpoint | verbo | o que mudou |
|---|---|---|
| `/adm/api/convenios` | `GET` | o filtro de unidade é uma das oferecidas (422 com o campo); `valeEmTodas` e `contratoVencimento` na linha |
| `/adm/api/convenios/unidades` | `GET` | **novo**: as unidades que a tela oferece, com a guarda de ler |
| `/adm/api/convenios` | `POST` | só o administrador; `unidades` obrigatória ("todas" ou a lista); nasce ativo (o D248) |
| `/adm/api/convenios/:id` | `GET` | `unidades` [{id, nome, ativa}], `valeEmTodas` e `totalClientes` |
| `/adm/api/convenios/:id` | `PUT` | só o administrador; o corpo vazio é 400; sem `unidades`, as ligações ficam (o D249) |
| `/adm/api/convenios/:id/ativacao` e `/desativacao` | `POST` | só o administrador; o estado pedido, e 409 no mesmo estado |

A regra do corpo mora em `RegraDoConvenio` (o D250), a da API e a do formulário do
ERP, na língua de cada um. O vencido é recalculado na gravação (o D251).

### As decisões que tomei, e o caminho de volta

Sete perguntas do desenho eram do dono, e eu as decidi para seguir -- todas
reversíveis, e registradas no roadmap:

1. Só o administrador escreve convênio (a medida: nenhum não administrador tinha).
2. O motivo do desconto na venda é cortado em 100 caracteres.
3. Sem filtro de contrato na tela até a rotina do D50 voltar a marcar o vencido.
4. As vendas e os clientes do convênio ficam na tela antiga, com um link.
5. Sem ordenação por coluna.
6. A porcentagem é um inteiro de 1 a 100.
7. "Todas as unidades" continua opção, mas sem padrão: a escolha é obrigatória.

E quatro desvios do desenho aprovado: o filtro "vale na unidade" abre em "qualquer
unidade" para todos, como a tela antiga; o "não desativar" e o alcance ficam no
corpo, e não no cabeçalho (o botão não anda sob o cursor entre os dois cliques); o
telefone não tem máscara; e a recusa da troca de estado aparece na região de status,
como nos pacotes.

### A verificação no navegador

No banco de contrato, com um administrador temporário (apagado depois, e o banco
remontado): a lista, o cadastro com os seis cartões, desativar e ativar (a pergunta
no botão, o alcance ligado a ele, o foco no botão do estado novo), e um convênio
cadastrado só numa unidade -- a 375 px e nos dois temas, sem rolagem lateral, e os
textos pequenos acima de 4,5:1 nos dois. Ela achou dois defeitos que os 1171 testes,
a revisão e os mutantes não acharam:

- **A busca desalinhada.** A nota do filtro de unidade morava dentro do campo, e a
  busca alinha os campos pelo pé: os dois `<select>` ficavam em alturas diferentes.
  A nota foi para a linha de baixo, com a frase inteira.
- **O foco no `body` durante a releitura** (o D261), nas três telas de lista com
  detalhe: voltar depois de desativar deixava o foco no vazio por todo o pedido --
  4 s no navegador. Medido de novo depois do conserto: o foco na linha durante a
  releitura, e no título quando ela saiu da lista.
