# Campos que parecem regra e são referência

Levantamento de 28/08/2026, feito com `scripts/campos-regra-ou-referencia.php` e
verificado à mão.

---

## A pergunta

Veio de `convenios.contrato_vencido`: um campo cujo nome se lê como controle de
validade e que, na prática, só marca "passou da data de referência". A vigência
do convênio segue até que alguma parte cancele (**D50**), e nada no caminho do
dinheiro consulta o sinalizador.

**Quantos outros campos deste sistema são assim?**

A pergunta importa porque um campo desses é armadilha para quem reconstruir a
tela: parece controle esquecido, e "consertá-lo" — ligá-lo à decisão — muda a
regra do negócio com aparência de correção de bug.

---

## A resposta

**Um.** E **dois fósseis**, que são outra coisa — o segundo só apareceu quando a
ferramenta parou de contar comentário como leitura.

| | Campos | |
|---|---:|---|
| **DECIDE** | 19 | lidos em serviço — regra de verdade |
| **FILTRA** | 10 | citados num `WHERE`; **nove são regra**, um não |
| **CONFERIR** | 8 | lidos fora das camadas usuais |
| **REFERÊNCIA** | 2 | texto exibido — `vendas.cancelado_motivo`, `usuarios.visivel_pdv` |
| **NUNCA LIDO** | **2** | fósseis — `vendas.comanda_temp` (D51) e `clientes_creditos.pontuacao_validade` (D53) |

Dos 10 que aparecem em `WHERE`, li quem chama cada consulta. Nove filtram em
métodos que **decidem** — `selecionarCuponsAtivosCliente`, `validarVencimento`,
`selecionarUsuariosAgenda`, `selecionarNpsNaoContabilizados`. **Só
`contrato_vencido` é filtrado apenas em `listagemIndex` (relatório) e em
`selecionarConvenios` (a própria rotina se alimentando).**

Dois que pareciam candidatos e não são, verificados um a um:

- **`clientes.bloqueio_agendamento_app`** aparecia só numa listagem de blacklist.
  É aplicado: `AplicativoController:698` recusa o agendamento.
- **`cupons_clientes.usado`** e **`cupons.data_limite`** saíram como referência na
  primeira rodada. São aplicados — no `WHERE` da consulta que o aplicativo usa
  para listar cupons disponíveis. A regra existe, escrita em SQL em vez de PHP.

---

## O fóssil: `vendas.comanda_temp`

Nada no sistema o lê. Nada o escreve — a não ser o construtor da entidade, que
grava `false` em **toda venda criada**. E 13.508 vendas o têm marcado, todas
entre 2018-06-06 e 2020-03-21.

### Não existe `vendas.comanda` — a comanda mora no `status`

Procurando o correlato, o schema inteiro tem três colunas com "comanda":

| Coluna | Situação |
|---|---|
| `agendas.add_comanda` | **viva** — lida em serviço |
| `agendas_exclusoes.add_comanda` | **viva** |
| `vendas.comanda_temp` | fóssil |

Não há tabela de comandas nem coluna `vendas.comanda`. **A comanda é uma venda,
e o estado dela está codificado em `vendas.status`:**

| `status` | Significa |
|---|---|
| `NULL` | venda temporária |
| `3` | comanda aberta |
| `1` | venda concluída |
| `0` | cancelada |

A substituição está escrita no código, com comentário e tudo — `VendaService`:

```php
$venda->setStatus(null); //null = venda temp
```

O conceito "venda temporária" saiu do campo dedicado e passou a ser um valor de
`status`. O nome sobreviveu no método que os consulta:
`selecionarVendaTemporariaPorUsuario()`, que hoje filtra `status IS NULL`.

### A linha do tempo, medida

A rotina `comandasCancelar` cancela comanda aberta há mais de dois dias, e roda
**desde 2017 até hoje**:

| Ano | Canceladas com o sinalizador | Sem |
|---|---:|---:|
| 2017 | 0 | 26 |
| 2018 | **4.642** | 56 |
| 2019 | **5.272** | 155 |
| 2020 | 1.804 | 2.310 |
| 2021 a 2026 | **0** | 18.375 |

A rotina é anterior ao sinalizador e sobreviveu a ele. Durante a janela de
2018–2019 o sinalizador marcava **quase toda** comanda cancelada — 98,8% em
2018. Em março de 2020 parou, e as comandas seguiram sendo criadas e canceladas
exatamente como antes.

Ou seja: `comanda_temp` **não era o mecanismo da comanda**. Era uma marcação
paralela, que durou 21 meses e foi substituída pela codificação no `status`.

### O que ela marcava tem nome na operação

> Uma comanda temporária é a que foi aberta — o cliente esteve na unidade — e não
> foi fechada, por falta de pagamento. O caso comum é o caixa abrir uma comanda
> nova e finalizar essa, deixando a primeira órfã.

A **comanda fantasma**. E os motivos de cancelamento gravados à mão confirmam:
além dos 11.718 automáticos, há **243 "duplicado"** e **159 "abriu duas vezes"**.

Isso muda o peso do achado. O campo não é só um fóssil a remover: ele carregava
uma distinção que o sistema **perdeu** — e que continua fazendo falta. Está em
**D52**, com os números: cerca de 1.200 comandas por ano são canceladas sem ser
fantasma, com serviço lançado e sem nenhuma venda do cliente em três dias.

O código que a escrevia não está no repositório — o histórico começa na
importação inicial, então não há o que datar por lá. O que sobrou é o
construtor, que grava `false` em toda venda nova.

### O que ele custa hoje

**Um índice de 91 MB**, o maior da tabela `vendas` — maior que a própria chave
primária, de 49 MB:

```
vendas_idx_vendas_data_status (inquilino_id, data_criacao, status, comanda_temp)
```

A última coluna é o fóssil, e **nenhuma consulta do sistema cita `comandaTemp`**.
O índice provavelmente serve pelas três primeiras colunas; a quarta é peso
morto — em cada uma das 2,2 milhões de linhas, e em cada escrita nova.

*(Os contadores de uso do PostgreSQL aqui não valem: a base foi carregada há
poucas horas. O que vale é o tamanho, que é estrutural.)*

### Um detalhe que a migração acertou

A entidade declara `nullable=true`; o MySQL diz `NOT NULL`. O gerador de DDL
seguiu **o MySQL**, e o PostgreSQL ficou `boolean NOT NULL` — fiel à origem. A
divergência nunca apareceu porque o construtor sempre grava `false`.

### O que fazer

**Não durante o corte.** O portão do corte é "o comportamento não mudou"; mexer
no schema no mesmo movimento torna esse portão ilegível.

**Depois, e nesta ordem:** confirmar com quem sabe o que a "comanda temporária"
de 2018–2020 significava — cinco anos de silêncio é forte indício, não prova —,
então remover a coluna e reconstruir o índice sem ela. Ver **D51**.

---

## Como foi medido

A assinatura procurada:

> o campo é **escrito** em algum lugar, e **não é lido** em nenhum lugar que
> decida

Ler acontece por dois caminhos e os dois contam: chamada de acessor no PHP, e
citação em DQL. Onde se lê é o que classifica — serviço decide, projeção de
consulta exibe, `WHERE` aplica.

### As quatro vezes em que a ferramenta estava errada

Vale registrar porque cada correção mudou a resposta, e porque a mesma armadilha
espera quem repetir o levantamento.

**1. O filtro era palpite meu.** A primeira versão selecionava candidatos por
pedaço de nome — `vencid`, `bloquei`, `ativo`. Achou 16 campos num sistema com
77 colunas booleanas. Booleano **é** sinalizador por definição; o critério passou
a ser o tipo, e os candidatos foram de 16 para 80.

**2. Um nível de acessor não bastava.** Sete campos — `unidades.convenio_segunda`
a `convenio_domingo` — saíram como NUNCA LIDO. São lidos, por uma cadeia de três
degraus:

```
$this->convenioSegunda
  <- getConvenioSegunda()
    <- getConvenioDiasSemana()        (o agregado, que só CHAMA)
      <- VendaService:101             (o desconto de convênio)
```

O agregado não lê a propriedade: chama o getter que lê. **A ferramenta declarou
mortos sete campos que estão no caminho da decisão do desconto.** Corrigido com
fecho transitivo dentro da entidade.

**3. Faltava uma camada, e era a que decide.** `unidades.agenda_primeiro_nome`
saiu como NUNCA LIDO. É lido em `Permissions/SessaoAcl.php`, que põe o valor na
sessão para a agenda decidir se mostra o nome completo do cliente. Eu enumerava
quatro pastas — `Service`, `Repository`, `Controller`, views — e `Permissions`
não era nenhuma. **Enumerar o que se conhece deixa de fora o que não se lembrou,
e o silêncio parece ausência.** Passou a varrer tudo, com uma camada `outros`
que aparece no relatório em vez de sumir.

**4. Contava COMENTÁRIO como leitura.** `convenios.contrato_vencido` chegou a
sair como DECIDE — lido em serviço — e as duas "leituras" eram os meus próprios
comentários, citando `getContratoVencido()` em prosa para explicar por que **não**
se deve lê-lo ali. A documentação do defeito virou prova de que ele não existe.

É a mesma armadilha que mordeu `scripts/conferir-agendamento.php`, onde a frase
"esta rotina não está agendada" fazia a rotina passar por agendada. **Toda
ferramenta que procura um nome em código precisa tirar os comentários antes de
procurar** — e vale registrar como padrão, porque mordeu duas vezes no mesmo dia.

Corrigir isso **revelou um segundo fóssil**:
`clientes_creditos.pontuacao_validade`, com 103.849 linhas e nenhuma leitura. A
única citação estava dentro de um método inteiramente comentado. Ver **D53**.

**5. "Lido no repositório" era vago demais.** Citação na projeção (`SELECT ...`)
é exibição; citação num `WHERE` é aplicação. Sem essa distinção,
`cupons_clientes.usado` — que impede oferecer cupom já gasto — foi classificado
como referência.

### O limite, dito na frente

**41 dos 80 candidatos ficaram fora**, por terem nome de propriedade repetido em
várias entidades. `getStatus()` de um Convênio e de um Produto são a mesma
string, e resolvê-la por texto seria inventar precisão. O maior grupo é `status`,
em 30 tabelas — e esse é obviamente lido em toda parte.

O relatório **lista quais ficaram de fora**, em vez de só contá-los.

---

## O que isso decide para as Fases 2 e 3

**A boa notícia é o tamanho da lista.** O sistema tem um único campo com a forma
de regra e a função de referência, e ele já está registrado (**D50**). Não há uma
safra de armadilhas esperando a reconstrução.

**As duas ações que ficam:**

1. **`contrato_vencido`** — agendar a rotina, renomear na API e no schema novos
   (`contrato_a_renovar`), documentar a regra junto do campo. Ver **D50**.
2. **`comanda_temp`** — confirmar o que significava, remover a coluna e
   reconstruir o índice de 91 MB sem ela. Depois do corte, não durante. Ver
   **D51**.

**E uma prática, que vale mais que as duas:** ao levar um campo booleano para a
API nova, o nome deve dizer o que ele *é*, não o que alguém supõe que ele
*controle*. `contrato_vencido` custou uma investigação inteira porque o nome
prometia uma regra que o sistema nunca teve.
