# Sorteios — o desenho da tela nova

*02/10/2026. Só li o repositório e rodei SELECT agregado no `franquiabv_dev` (cópia anonimizada; relógio do banco em 02/10/2026 04:03). Nenhum arquivo do repositório foi tocado; no rascunho, um arquivo auxiliar de consulta foi criado e apagado, e fica só este. As consultas estão no apêndice (C1 a C12). "Fixture" é id ≥ 90001000.*

Pedido do dono: "irei dormir, continue de forma autonoma até a exaustão (antes: continue de forma autonoma a criação das telas faltantes. quando necessario abra a perguntas/decisões necessárias)".

## Resumo

- **Sorteios é uma tela de rede, usada só por administrador, e está parada.** São 4 sorteios reais, todos em outubro, na semana do Dia das Crianças (12/10): um em 2023, dois em 2024 e um em 2025, com 7.214 cupons de 5.002 clientes (C3). Ninguém tem o privilégio (C6). Os 10 administradores entram assim mesmo (C7; `Acl.php:30`).
- **Migrar já está decidido** (`roadmap.md:2117`; `funcionalidades-sem-uso.md:61-65`): os 5.002 clientes veem os sorteios no aplicativo. **A tela não depende de decisão nenhuma para ser feita.** Dependem de decisão três partes dela: o cadastro (F3), o aviso ao ganhador (depende do WhatsApp) e a planilha (depende da LGPD).
- **O "Dia das Crianças" de 2025 (#4) espera o sorteio há 352 dias.** 2.996 clientes o veem como "Aguardando Sorteio" no aplicativo, e 2.994 deles têm venda ativa. O prêmio não está no sistema: a descrição está vazia (C1). É a pergunta P1.
- **Estamos em outubro.** Nos três anos, o sorteio começou entre 01 e 07/10. Se houver um em 2026, ele já devia ter começado. Um sorteio criado depois do início perde as vendas anteriores sem aviso: 728 no #1 e 115 no #3 (C9). É a pergunta P2, a única com prazo.

## 0. O que conferi no levantamento, e o que mudou

Medi de novo os números (C1 a C12) e li cada linha citada. O levantamento confere, com quatro correções e cinco achados novos.

**Correções:**

1. **"Vendas ativas" era contagem de cupons.** A coluna do levantamento conta os **cupons** de venda ativa (2.321, 1.377 e 3.475). As **vendas ativas distintas** são 2.320, 1.376 e 3.473 (C2). Em cada sorteio há venda ativa com cupom em dobro: uma no #1, uma no #3 e duas no #4.
2. **Clientes do #4:** 2.996 têm algum cupom, e **2.994 têm venda ativa** (C2). A diferença são os 2 do DS-8 (C10).
3. **"Data inválida dá 500" vale só para lixo.** Uma data que não existe, como 31/02, **vira 03/03 sem aviso**: `DateTime::createFromFormat('d/m/Y')` transborda em vez de devolver `false` (`DataHora.php:39-45`).
4. **Clientes no programa:** 25.453 de 242.586 sem as fixtures, e 25.461 de 242.597 com elas (C11).

**Achados novos:**

5. **O #3 também perdeu vendas por ter sido criado tarde.** Foi criado às 17:38 do primeiro dia (07/10/2024). As 115 vendas ativas de clientes do programa feitas antes disso não têm cupom; das 75 feitas depois, 58 têm (C9). O #4 foi criado na véspera e não perdeu nenhuma (C1).
6. **O prêmio do #4 não está no sistema:** a descrição tem 0 caracteres (C1).
7. **A tabela `sorteios` é `utf8mb3`** (`mysql.sql:1513`). Um emoji no nome dá erro no MySQL, que roda com `STRICT_TRANS_TABLES` (C12), e passa no PostgreSQL (`postgres/schema.sql:975`). O mesmo nome dá resultados diferentes em cada banco.
8. **O nome do ganhador também sai cru.** Sai assim na lista (`index.phtml:108`) e na mensagem de sucesso, que monta HTML com o nome do cliente (`SorteiosController.php:164`, `:217`). É da família do D203.
9. **Consertar o DS-8 muda o contrato do aplicativo.** As duas fixtures de sorteio só têm cupom de venda cancelada (`fixtures.sql:848-853` e `:1305-1310` no HEAD; 7 linhas abaixo na árvore de trabalho, que outro agente está alterando). Os snapshots congelam `"cupons": ["number"]` (`tests/Contrato/snapshots/sorteios-selecionar-cupons-autenticado.json` e `v4-clientes-sorteios-autenticado.json`). Com o filtro, a rota antiga deixa de mandar a chave `cupons`, porque ela só nasce dentro do laço (`AplicativoController.php:222-226`), e a v4 passa a mandar `[]`.

E uma nota menor: o comentário de `SorteioVendaRepository.php:32-35` diz que a consulta não filtra pelo sorteio, mas ela filtra desde o D94 (`:25`).

## 1. A tela nova

### O que ela faz

"Sorteios", em Marketing (`menu-do-erp.ts:235-239`), passa a abrir a tela nova no lugar da `TelaNaoMigrada` (`Console.tsx:902`). Segue o molde dos convênios e dos cupons.

**A lista** tem busca por nome e por situação, com 20 por página. O tamanho da página vem do envelope, como na comanda (`f3-desenho-da-interface.md`, seção 6). Cada linha mostra:

- o nome e a janela ("01/10 a 15/10/2025");
- um selo de situação;
- as **vendas**, contadas como vendas ativas distintas (a tela antiga contava cupons: 3.502 no #4), e os clientes;
- o ganhador, quando houver (por exemplo, "venda 1.234.567 · MG – Ubá – Centro").

O servidor calcula os selos com o relógio dele, como faz com o `encerrada` dos cupons (`Adm/CuponsController.php:84-100`):

| selo | quando | o que a tela diz |
|---|---|---|
| `agendado` | `ativo`, e hoje é antes do início | começa em dd/mm |
| `recebendo` | `ativo`, e hoje está dentro da janela (o último dia conta, `SorteioRepository.php:61`) | recebendo cupons até dd/mm |
| `falta sortear` | `aguardandoSorteio`, ou `ativo` com o fim no passado | encerrado há N dias, falta sortear |
| `sorteado` | `finalizado` | sorteado em dd/mm |
| `cancelado` | `cancelado` | — |

Sem filtro, a lista esconde os cancelados, como hoje (`SorteioRepository.php:23-25`).

**O sorteio aberto** mostra:

- **o cadastro:** o nome, a descrição ("sem descrição do prêmio" quando está vazia, como no #4), a janela e a data de criação. Se o sorteio foi criado depois do início, aparece o aviso: "criado 3 dias depois do início: as vendas de 01 a 04/10 não ganharam cupom".
- **os números:** cupons, vendas ativas, clientes e unidades. À parte, os que não contam: cupom em dobro e cupom de venda cancelada. Junto vem a tabela por unidade, com até 34 linhas.
- **o ganhador:** a venda, o dia, a unidade da venda, o cliente, como foi sorteado (pelo sistema ou informado) e por quem.
- **"como funciona"**, por extenso:
  - ganha cupom toda venda finalizada **em qualquer unidade**, de cliente que não é avulso e está no programa de fidelidade, enquanto o sorteio está no ar (`VendaPagamentoService.php:249-257`, `:349-364`; `SorteioRepository.php:58-65`). A comanda nova também dá cupom (`Adm/ComandasController.php:1070`);
  - cada venda é um cupom, então quem compra mais tem mais chances. É isso que o aplicativo mostra ao cliente como "cupons" (`SorteioVendaRepository.php:91-101`);
  - o aplicativo mostra o sorteio a quem tem cupom, com a situação, até 10 sorteios por cliente, sem os cancelados (`SorteioVendaRepository.php:54-72`).

**As ações** aparecem só para quem escreve e é administrador:

- **Sortear** (o "Sorteio Interno"): vale para o sorteio em `falta sortear` que tenha venda ativa. A pergunta, no próprio botão, diz entre quantas vendas e quantos clientes será o sorteio. O duplo clique não confirma: é o mesmo cuidado do "remover" dos pacotes (`roadmap.md:7700-7702`).
- **Informar o ganhador** (a "Plataforma de Terceiros"): recebe o código da venda sorteada fora do sistema. O servidor confere se ela tem cupom neste sorteio e se está ativa.
- **Cancelar**: só quando a `RegraDoSorteio` deixa, isto é, sem cupom de venda ativa (`RegraDoSorteio.php:21-36`). A pergunta fica no botão.
- **A planilha dos participantes**, a do "Exportar": uma linha por venda ativa. O formato espera a resposta da P6.
- **Cadastrar e alterar** ficam para a fatia F3, depois das respostas P2, P3 e P5.

### Quem vê e quem escreve

- **Vê** quem tem `marketing/sorteios/ler`, que é a porta do menu (`layout.phtml:260`). Hoje são os 10 administradores, de 3 unidades, e mais ninguém (C6, C7).
- **Escreve** quem tem `marketing/sorteios/escrever` **e é administrador**. É o molde dos convênios (`Adm/ConveniosController.php:46`, `:302`; `Console.tsx:349-355`), porque o sorteio alcança a rede inteira: o #3 recebeu cupons de 33 unidades (C2).
  - Medi antes: a funcionalidade 52 tem 0 linhas em `privilegios` e 0 em `privilegios_usuarios` (C6). **A guarda não tira acesso de ninguém.**
  - Quem tiver o privilégio sem ser administrador só lê, e a tela diz por quê.
- **O contato do ganhador** (telefone e e-mail) aparece só para quem escreve, como o preço de compra no catálogo e os emitidos nos cupons.

### O que entra e o que sai, em relação à tela antiga

| | tela antiga (`index.phtml`, `SorteiosController.php`) | tela nova |
|---|---|---|
| abrir a lista | **muda o estado** de todo sorteio vencido da rede (`:51`; `SorteioService.php:118-129`) | só lê; a situação é calculada |
| coluna de participação | "Nº Vendas" conta cupons, inclusive os repetidos e os cancelados (`SorteioRepository.php:12`; `index.phtml:113`) | vendas ativas distintas e clientes |
| o sorteio aberto | não existe | números, unidades, ganhador, avisos e "como funciona" |
| editar | link para uma ação que não existe (`index.phtml:94`, `:124`; D105) | F3: nome e descrição até o sorteio; as datas, só sem cupom |
| sortear | só o `aguardandoSorteio`, que só existe se alguém abriu a lista (`:203`, `:256`) | também o `ativo` vencido, sob trava; 409 se outra pessoa já sorteou |
| exportar | o botão aparece a quem só lê, mas a ação exige escrever (`index.phtml:115-119`; `:136`); uma linha por cupom, inclusive de venda cancelada (`SorteioRepository.php:47-56`) | só a quem escreve; uma linha por venda ativa |
| avisar o ganhador | põe na fila um WhatsApp da unidade ou um push do aplicativo (`:227-246`) | espera a P4; até lá, a tela mostra o contato |
| cadastrar | grava o POST cru (`:76`) | F3, com a regra |
| unidade | não existe (`Entity/Sorteio.php:10-104`) | continua não existindo; a tela diz "vale para a rede inteira" (P3) |

**Ficam de fora:** ordenação por coluna, filtro por unidade (o sorteio não tem unidade) e o "encerrar sem sorteio", que só entra se a P1 pedir. **A tela antiga continua no ar**, acessível pelo endereço, como nas outras telas migradas.

## 2. O servidor

### A API não existe

Não há `/adm/api/sorteios`. O módulo só tem a rota dos cupons (`ISMarketing/config/module.config.php:23-160`) e a rota genérica do ERP (`:162-189`). Existem as duas rotas do aplicativo, congeladas pelo contrato:

- `GET /api/sorteios/selecionarCupons/:id` (`AplicativoController.php:184-233`);
- `GET /api/v4/clientes/:id/sorteios` (`V4/ClientesController.php:255-293`).

### Os recursos, no molde do `ApiAdmController`

- **O controller** é o `ISMarketing/src/Controller/Adm/SorteiosController.php`, com o módulo `marketing` e a funcionalidade `sorteios`. Sem a funcionalidade, a guarda não protege nada (D123).
- **A forma da resposta** fica em `ISMarketing/src/Api/SorteioJson.php`, e **a regra** na `RegraDoSorteio`.
- **A barreira de origem** vem da classe base (`ApiAdmController.php:86-123`).

| endpoint | verbo | guarda | o que faz |
|---|---|---|---|
| `/adm/api/sorteios?situacao=&nome=&pagina=` | GET | ler | a lista, com a situação, as vendas ativas distintas, os clientes e o ganhador (venda e unidade); não grava nada |
| `/adm/api/sorteios/:id` | GET | ler | o sorteio aberto: os números, a tabela por unidade, `criadoDepoisDoInicio` e o ganhador; o contato, só a quem escreve |
| `/adm/api/sorteios/:id/sorteio` | POST | escrever + administrador | sorteia entre as vendas ativas distintas, sem reposição (D95), **sob trava**; 409 se o sorteio não está em `falta sortear` ou já foi sorteado; 422 se não há venda que possa ganhar |
| `/adm/api/sorteios/:id/ganhador` | POST | escrever + administrador | recebe `{venda: inteiro}`; 422 para texto, para venda sem cupom neste sorteio (D94) ou cancelada; 409 como acima |
| `/adm/api/sorteios/:id/cancelamento` | POST | escrever + administrador | aplica a `RegraDoSorteio`, sob a mesma trava; 409 se já está cancelado ou sorteado; 422 se tem cupom de venda ativa |
| `/adm/api/sorteios/:id/participantes` | GET | escrever + administrador | uma linha por venda ativa; o formato espera a P6 |
| `/adm/api/sorteios` | POST | escrever + administrador | F3: o corpo passa pela regra |
| `/adm/api/sorteios/:id` | PUT | escrever + administrador | F3: nome e descrição até o sorteio ou o cancelamento; as datas, só sem cupom; `PUT {}` é 400 |

O 409 e o 422 seguem a convenção das ativações (`Adm/CuponsController.php:33-44`). As chaves que a API não altera (`status`, `ganhador`, `dataSorteio`, `tipoSorteio`, `usuarioSorteio`, `id`) dão 422 com o nome da chave, como no D267.

**A situação é calculada num lugar só:** `RegraDoSorteio::situacao(status, inicio, fim, hoje)`, uma função pura. Ela é lida pela API, pela lista antiga e pelo **rótulo das duas rotas do aplicativo** (DS-1).

### Os consertos que vêm antes: débitos novos, com número a definir

Aqui eles se chamam DS-1 a DS-12. O número D3xx sai no registro. O último débito commitado é o D322; a árvore de trabalho já tem um D323, de outro agente.

**DS-1 — O sorteio vencido só muda de estado num GET da lista antiga, e o aplicativo depende disso.**

- **O que acontece:** abrir a lista chama `validarVencimento` (`SorteiosController.php:51`), que grava `aguardandoSorteio` em todo sorteio `ativo` vencido da rede (`SorteioService.php:118-129`; `SorteioRepository.php:67-74`).
- **Os detalhes:**
  - o `$unidadeId` passado é ignorado, porque o método do repositório não tem esse parâmetro (`:67`);
  - a mudança não grava `data_alteracao` (`SorteioService.php:121-126`), e no #4 a coluna está vazia (C1);
  - nenhuma rotina faz essa mudança: o `CronJobService.php` não cita sorteio;
  - as duas finalizações exigem o estado já mudado (`SorteiosController.php:203`, `:256`).
- **O risco:** com a tela nova no lugar da lista, um sorteio que terminar fica `ativo` para sempre. Ninguém consegue sorteá-lo, e o aplicativo mostra "Ativo".
- **Conserto:**
  - a `situacao` passa a ser calculada;
  - o sortear aceita o sorteio vencido;
  - as duas rotas do aplicativo tiram o rótulo da mesma regra. O `ativo` vencido sai como "Aguardando Sorteio", o mesmo texto que a lista antiga produziria.
- **O contrato não muda:** a forma é a mesma (`status: string`), as fixtures são `finalizado` e nenhum sorteio está `ativo` hoje (C1).
- A lista antiga continua mudando o estado, agora gravando a data. É o precedente dos cupons (`roadmap.md:7868-7870`).
- **Feito em 07/11/2026 (a fatia 112, o D334 CORRIGIDO): a lista antiga deixou de gravar.** O `indexAction` não chama mais o `validarVencimento` (que saiu do serviço e do repositório, com o `passarAAguardando` do D559); o filtro de "Status", a coluna e o "Realizar Sorteio" da lista antiga leem a `RegraDoSorteio` (`entraNoFiltroDaListaAntiga`, `rotuloDaListaAntiga`, `faltaSortearNaListaAntiga`): o `ativo` vencido aparece em "Aguardando Sorteio", com o rótulo e o botão, e fica `ativo` no banco até ser sorteado ou cancelado. Provado no `TelasDosSorteiosDoErpTest`, nos dois bancos.

**DS-2 — A coluna e a planilha contam cupons, e não vendas.**

- **A coluna:** "Nº Vendas" é um `COUNT(ven.id)` sobre os cupons (`SorteioRepository.php:12`; `index.phtml:113`). No #4 ela mostra 3.502, para 3.473 vendas ativas (C2).
- **A planilha:** lista cada cupom sem olhar a venda (`SorteioRepository.php:47-56`).
  - As 4 vendas com cupom em dobro (C4) têm chance dupla no sorteio feito fora do sistema.
  - As 41 linhas de venda cancelada (27 no #4, C2) são vendas que o "informar ganhador" recusa depois (`SorteiosController.php:270-271`).
- **O sorteio interno** conta cada venda uma vez só (`SorteioVendaRepository.php:44-52`).
- **Conserto:** uma linha por venda ativa nos dois lugares, para que a coluna e a planilha contem igual.

**DS-3 — O cupom em dobro.**

- **A causa:** `salvarSorteios` cria o cupom sem verificar se a venda já tem um neste sorteio (`VendaPagamentoService.php:349-364`). Não há índice único em (`sorteio`, `venda`) em nenhum dos bancos (`mysql.sql:1523-1534`; `postgres/schema.sql:989-997`).
- **Medido:** 4 vendas têm dois cupons, gravados com 5 s, 582 s, 635 s e 1.419 s de intervalo (C4). É o padrão do clique repetido do D321.
- **O que já está fechado:** o D316 impede o clique repetido no PDV antigo. A corrida de dois cliques continua aberta (pergunta 2 do D321).
- **A corrida fechou em 15/10/2026** (a resposta 85 do dono, a opção (a); a fatia 5c): a primeira escrita do finalizar é o `UPDATE` condicional da venda que só um de dois pedidos vence (`VendaRepository::fecharParaOFinalizar`), e `salvarSorteios` só roda no que venceu -- a venda não se finaliza duas vezes, nem no mesmo instante. A conferência antes de criar (o "conserto barato") fica como rede, e o índice único como está abaixo. **Para a fatia 26:** o caso "a venda finalizada duas vezes ganha um cupom só" já não se monta finalizando duas vezes (o segundo é recusado antes de tudo); monte o cupom que já existe com um `INSERT` e finalize uma vez.
- **Conserto barato:** verificar antes de criar.
- **O índice único** fica para a migração final, depois de limpar os 4 pares. O sorteio interno já conta esses pares uma vez só, então a limpeza não muda nenhum resultado.

**DS-4 — O cadastro grava o POST cru, sem limite de tamanho e sem regra de data.**

- **O POST cru:** `novoAction` grava `$request->getPost()`, e não o que o formulário filtrou (`SorteiosController.php:76`). O construtor preenche a entidade com todas as chaves recebidas (`Entity/Sorteio.php:106-111`):
  - `data_sorteio` vira "agora" (`:195-198`);
  - `tipo_sorteio` aceita qualquer texto;
  - `ganhador` em texto quebra o `flush`;
  - o `id` do campo oculto (`Form/Sorteio.php:24`) também entra na entidade. Não medi o efeito disso no `persist`.
- **O filtro:** só tem `NotEmpty` (`SorteioFilter.php:26-60`).
  - O limite de 100 caracteres do nome só existe no navegador (`Form/Sorteio.php:36`). Com `STRICT_TRANS_TABLES` (C12), 101 caracteres dão 500.
  - Um emoji dá 500 no MySQL (achado 7).
  - 31/02 vira 03/03 (correção 3), e uma data sem sentido dá 500.
  - O fim antes do início passa, e o início no passado também.
- **Conserto:** `RegraDoSorteio::conferir`, a mesma regra na API e na tela antiga (o molde do D257 e do D268). Fica para a F3.

**DS-5 — Erro em vez de recusa.**

- `finalizarInterno` sem `id` no POST faz `find(null)` (`SorteiosController.php:201`). O Doctrine lança uma exceção, e a resposta é 500.
- Texto no código da venda ou do sorteio deve dar 500 no PostgreSQL, como no D279. Não medi.
- **Conserto:** inteiros na fronteira, e recusa (422) com frase. Fica para a F2.

**DS-6 — Sortear sem trava.**

- As duas finalizações leem o estado e gravam sem trava (`SorteioService.php:24-38`, `:62-94`). Dois pedidos ao mesmo tempo podem marcar dois ganhadores.
- **Medido:** nenhum caso; cada sorteio feito tem 1 ganhador (C2).
- **Conserto:** travar a linha do sorteio (`PESSIMISTIC_WRITE`) e reler o estado depois da trava, como no D232 e no D228. Fica para a F2.

**DS-7 — A frase errada.**

- Quando não há cupom de venda ativa, o sortear diz "todas as vendas participantes estão canceladas" (`SorteiosController.php:211`). Diz isso mesmo quando não há venda nenhuma.
- Fica para a F2.

**DS-8 — O aplicativo mostra cupom de venda cancelada.**

- `selecionarCuponsPorSorteioCliente` não olha a situação da venda (`SorteioVendaRepository.php:91-101`).
- **Medido:** 2 clientes do #4 só têm cupom de venda cancelada, que não pode ganhar (C10).
- **Só registrar.** O conserto muda o contrato do aplicativo (achado 9) e exige fixture nova.

**DS-9 — O sorteio criado depois do início perde as vendas anteriores, sem aviso.**

- O cupom só nasce quando a venda é finalizada (`VendaPagamentoService.php:255`), e o formulário aceita início no passado (`SorteioFilter.php:38-48`).
- **Medido** (C9; é um teto, porque o programa considerado é o de hoje):
  - #1: 728 vendas de 01 a 04/10/2023, nenhuma com cupom;
  - #3: 115 vendas de 07/10/2024 feitas antes das 17:38, nenhuma com cupom.
- A regra espera a P5. O aviso na tela já entra na F1.

**DS-10 — O aviso ao ganhador não deixa rastro.**

- A rotina apaga cada aviso depois de enviá-lo (`CronJobService.php:1502-1503`).
- **Medido:** a fila só tem linhas de 16/08/2026 em diante, e nenhuma com o título "Sorteio VIP" (C8). Não há como saber se os dois ganhadores foram avisados.
- Espera a P4.

**DS-11 — O "Exportar" aparece a quem não pode usá-lo.**

- O botão aparece a quem só lê (`index.phtml:115-119`), mas a ação exige escrever (`SorteiosController.php:136`). Quem só lê recebe 404.
- Hoje ninguém tem só leitura (C6).
- Fica para a F2, junto com o DS-2.

**DS-12 — Código morto que daria erro fatal.**

- `SorteioService::update` usa uma classe `ClassMethods` do subnamespace `Hydrator` do próprio serviço (`SorteioService.php:96-106`). Essa classe não existe: o módulo não tem a pasta `Service/Hydrator`.
- Ninguém chama o método.
- Sai na F3, quando a alteração nascer com a regra.

**Os que já têm débito:**

- o link de editar morto é o D105 (`conferir-links-mortos.php:161`). Ele some quando a F3 der o editar, ou então o link sai;
- os nomes crus (`index.phtml:104`, `:108`; achado 8) entram no D203.

**Não há travessia de franquia a corrigir:** o sorteio é de rede por desenho (D144, seção "Não havia travessia de franquia").

### O que não muda

- as chaves de estado gravadas (`Arrays.php:359-377`);
- a forma das duas rotas do aplicativo e os quatro snapshots `tests/Contrato/snapshots/*sorteios*`;
- o sorteio continua sem reposição e com chance igual por venda (D95, `SorteioService.php:40-61`);
- o cupom marcado continua sendo o primeiro da venda (D72, `SorteioVendaRepository.php:27-36`).

## 3. As fatias, em ordem

**O ciclo de cada fatia** é o da casa:

- os casos que ficam vermelhos sem a linha nova;
- os mutantes, cada um com o caso que o mata;
- a caracterização nos dois bancos, pelo `rodar-caracterizacao-como-ci.sh`;
- o contrato do aplicativo, os ensaios HTTP, os portões e o front (`--maxWorkers=4`);
- **o navegador**, contra o banco de contrato, a 375 px e nos dois temas.

Os cenários são montados no `setUp` de cada teste, como em `BilheteDoSorteioTest.php:39-68`. Assim não se toca no `fixtures.sql`, que outro agente está alterando agora.

### F0 — a leitura no servidor, sem tela (não depende de decisão)

- **Arquivos:**
  - `RegraDoSorteio.php`: a `situacao` e o rótulo do aplicativo;
  - `SorteioRepository.php`: a lista com vendas ativas distintas e clientes, os números do sorteio e a tabela por unidade. As somas usam `CASE`, e não `SUM` de comparação, por causa do PostgreSQL;
  - `Api/SorteioJson.php` (novo);
  - `Controller/Adm/SorteiosController.php` (novo), com `index` e `ver`, mais a fábrica e as rotas em `module.config.php`;
  - `AplicativoController.php:211` e `V4/ClientesController.php:281`, que passam a ler o rótulo da regra (DS-1).
- **Testes:**
  - `RegraDoSorteioTest`, pura: os cinco selos, o último dia da janela e o sorteio vencido;
  - `SorteioApiTest`, em processo com o `ControllerEmProcesso`, sem sessão: quem lê, quem não lê, o contato escondido de quem só lê, e o GET que não muda o banco (retrato antes e depois);
  - `SorteiosNoAplicativoTest`: o `ativo` vencido sai "Aguardando Sorteio" nas duas rotas, sem gravar nada;
  - o caso da contagem, com cupom em dobro e cupom de venda cancelada.
- **Mutantes:**
  - o último dia da janela deixando de contar (`<=` trocado por `<`);
  - o `aguardandoSorteio` ignorado;
  - o rótulo do aplicativo voltando a ler só o estado gravado;
  - `COUNT(*)` no lugar das vendas distintas;
  - o `status = 1` retirado;
  - a guarda de leitura devolvendo `true`;
  - a funcionalidade apagada (pego pelo portão `conferir-acl-da-api.php`);
  - o contato aparecendo a quem só lê;
  - o `validarVencimento` chamado no GET novo.
- **Portões afetados:**
  - `conferir-acl-da-api`;
  - `conferir-cobertura-por-par`: os métodos novos do repositório pedem um teste que os chame;
  - `conferir-booleanos-em-consulta`;
  - `conferir-relogio-do-banco` e `conferir-relogio-nos-testes`: o "hoje" vem do PHP e, nos testes, do `agora()` da suíte;
  - `conferir-ordenacao-empates`, `conferir-injecao-dql`;
  - `conferir-inventario-atual`, por causa das ações novas.

  O ensaio de permissão (`ensaiar-permissao-adm-api.php`) ganha as duas rotas.
- **Tamanho:** ~500 linhas de servidor e ~450 de teste, com 12 a 15 casos.

### F1 — a tela, só lendo (não depende de decisão)

- **Arquivos:**
  - `frontend/src/api/sorteios.ts`;
  - `telas/Sorteios.tsx`, a tela pura: a lista, o sorteio aberto, os selos, os avisos e "como funciona";
  - `telas/SorteiosLigado.tsx`;
  - `Console.tsx`: `CAMINHO_DOS_SORTEIOS`, `PRIVILEGIO_DE_EDITAR_SORTEIOS`, o ramo novo antes da `TelaNaoMigrada`, e o `podeEditar` com o administrador e o `escreveSemSerAdministrador`, como nos convênios (`:870-877`);
  - `src/index.ts` e `.design-sync/previews/Sorteios.tsx`, a prévia, com números que batem entre si.
- **Testes:**
  - `SorteiosLigado.teste.tsx`: os selos, o aviso de criação tardia, o "sem descrição do prêmio", a página que encolhe, o foco pelo `useFocoDaLista` e o 403;
  - `SorteiosLigado.contrato.teste.tsx`: só vale a resposta do último pedido;
  - o caso do Console: a entrada abre a tela nova, e não a `TelaNaoMigrada`.
- **Mutantes:** o selo trocado; o aviso de criação tardia sumindo; o foco caindo no `body` durante a releitura (o D261); a página cravada em 20 em vez de vir do envelope.
- **No navegador:** o #4 com "falta sortear há 352 dias" e sem botão de sortear para quem só lê; o #1 com o aviso dos 3 dias.
- **Tamanho:** ~900 linhas de tela e ~700 de teste.

### F2 — sortear, informar, cancelar e a planilha

Construir não depende de decisão. A P1 decide o que fazer com o #4, a P4 decide o aviso e a P6 decide o formato da planilha.

- **Arquivos:**
  - `SorteioService.php`: `sortear` e `informarGanhador` sob trava, devolvendo o motivo da recusa; `cancelar` sob a mesma trava;
  - a API: `sorteio`, `ganhador`, `cancelamento` e `participantes`;
  - **a tela antiga, pela mesma regra:** `finalizarInterno` com id inteiro e recusa (DS-5), a frase certa (DS-7), o `Exportar` só para quem escreve e a planilha por venda ativa (DS-2, DS-11), e a coluna de vendas ativas;
  - a tela nova: os três botões, com a pergunta no próprio botão.
- **O aviso:** a F2 **não põe nada na fila** até a P4. Ela mostra o contato do ganhador a quem sorteou. O `notificarganhador` da tela antiga fica como está.
  - *Substituído pela resposta 217 (c) do dono (15/10/2026), "nada automático": o `notificarganhador` saiu das duas finalizações da tela antiga, e a mensagem dela mostra o contato. Ver a seção 6, a F2.*
- **Testes:**
  - `SorteioSobTravaTest`, no molde do `DevedorSobTravaTest.php`: outra conexão prende a linha e o sortear espera; o segundo sortear dá 409; o cancelar e o sortear disputam a mesma linha;
  - `SorteioApiTest`:
    - o `ativo` vencido pode ser sorteado, e o `recebendo` dá 409;
    - só venda ativa ganha;
    - venda de outro sorteio e texto dão 422;
    - cancelar com cupom de venda ativa dá 422;
  - `TelasDosSorteiosDoErpTest`: o pedido sem id é recusado; a planilha sai sem repetidos e sem cancelados;
  - os testes de hoje continuam: `BilheteDoSorteioTest`, `RegrasDoMarketingTest.php:55` e `EstadoPorPostNoMarketingTest.php:54-91`;
  - um ensaio HTTP novo, `ensaiar-sorteios-adm-api.php`, com recusas e controles. Ele restaura as fixtures antes e depois de cada caso, no molde dos cupons, e **não** sorteia nenhum dos quatro sorteios reais.
- **Mutantes:**
  - no servidor:
    - a trava retirada;
    - o estado lido antes da trava;
    - a venda cancelada entrando no sorteio;
    - o `recebendo` aceito;
    - o D94 desfeito, aceitando venda de outro sorteio;
    - `(int) '12abc'` aceito;
    - a `RegraDoSorteio` contornada;
    - a guarda do administrador retirada;
    - o pedido sem id voltando a dar 500;
  - no front: o duplo clique que confirma; o botão aparecendo a quem só lê; o 409 que não relê a lista.
- **Portões:**
  - `conferir-remocao-por-post`: o cancelar já é POST, então o limite continua em 13 ou 14;
  - `conferir-autorizacao-privilegiada`.
- **Tamanho:** ~450 linhas de servidor e ~650 de teste; na tela, ~500 linhas e ~500 de teste.

### F3 — cadastrar e alterar (espera P2, P3 e P5)

- **Arquivos:**
  - `RegraDoSorteio::conferir`:
    - nome de 1 a 100 caracteres, sem emoji, sem `<` ou `>` e sem quebra de linha (o molde do D257);
    - descrição com limite de tamanho;
    - datas que existem, conferidas com `checkdate`;
    - fim no mesmo dia do início ou depois;
    - início conforme a P5;
    - as chaves que a API não altera;
  - `criar` e `atualizar` na API;
  - **o `novoAction` antigo pela mesma regra, gravando só os campos escolhidos** (DS-4);
  - `SorteioService::update` substituído (DS-12);
  - o editar da tela antiga ligado à regra, ou o link retirado (D105);
  - o `salvarSorteios` que não repete cupom (DS-3);
  - `telas/formularioDoSorteio.ts`.
- **Testes:**
  - `RegraDoSorteioTest`: nome com 100 e com 101 caracteres, emoji, 31/02, fim antes do início e as chaves proibidas;
  - `SorteioApiTest`: o POST e o PUT, o `PUT {}` e as datas com cupom;
  - `TelasDosSorteiosDoErpTest`: um POST com `ganhador`, `data_sorteio` e `tipo_sorteio` não grava nenhum dos três;
  - `BilheteDoSorteioTest`: a venda finalizada duas vezes ganha um cupom só;
  - `formularioDoSorteio.teste.ts`.
- **Mutantes:** cada recusa da regra retirada, uma por vez; o `getPost()` cru de volta; o cupom repetido.
- **Tamanho:** ~350 linhas de servidor e ~500 de teste; o formulário, ~450 linhas e ~400 de teste.

**No total**, são ~5.500 linhas em ~30 arquivos, metade delas de teste. Para comparar: os convênios foram 43 arquivos e 7.452 linhas num commit só (`1b5b315`), feitos entre 08:45 e 14:53 de 28/09. Os sorteios têm menos campos, nenhuma unidade e 4 linhas reais. **A estimativa é de 4 a 6 horas de trabalho assistido para F0 a F2**, e mais 2 a 3 horas para a F3.

## 4. As perguntas ao dono

**A tela, no todo, não depende de decisão:** migrar está decidido (`roadmap.md:2117`). Dependem de decisão:

- o uso do sortear no #4 (P1);
- o cadastro (P2, P3 e P5);
- o aviso ao ganhador (P4), que **depende da decisão do WhatsApp**;
- a planilha (P6), que **depende da decisão da LGPD**.

**P1 — O que fazer com o "Dia das Crianças" de 2025 (#4), parado em "Aguardando Sorteio" desde 15/10/2025?**

- (A) **Sortear agora, pelo sistema,** entre 3.473 vendas ativas de 2.994 clientes (C2). Desses clientes, 2.993 continuam ativos e no programa (C10). O aplicativo passa a mostrar "Finalizado", e o ganhador vê a marca de ganhador.
- (B) **Se ele já foi sorteado fora do sistema:** informar a venda ganhadora pela "Plataforma de Terceiros", sem sortear de novo.
- (C) **Encerrar sem sorteio,** com uma situação nova que o aplicativo mostra aos 2.996 clientes. Esse texto novo chega ao aplicativo instalado, e daqui não consigo ver como ele trata um rótulo que não conhece.
- (D) **Cancelar:** o sorteio some do aplicativo dos 2.996 clientes. Hoje o cancelamento é recusado, porque há 3.475 cupons de venda ativa (D299). Seria preciso abrir uma exceção, só para o administrador.
- **Recomendo (B), se o sorteio aconteceu fora; senão, (A).** São as duas opções que cumprem o que o aplicativo mostra há um ano.
- Qualquer opção exige saber **qual era o prêmio**: a descrição do #4 está vazia (C1).

**P2 — Os sorteios voltam a ser criados? Há um de Dia das Crianças em 2026?**

- (A) **Só manutenção:** a tela nova lista, sorteia e cancela; o cadastro fica na tela antiga, como hoje.
- (B) **A tela completa,** com cadastrar e alterar (a F3).
- (C) **Um sorteio de 2026 agora,** criado pela tela antiga, que funciona; a F3 vem depois.
- **Medido:** os quatro sorteios reais foram em outubro, com início entre 01 e 07/10 e fim entre 12 e 15/10 (C1). Hoje é 02/10. **Um sorteio criado agora já perde as vendas de 01 e 02/10** (DS-9).
- **Recomendo (A), a não ser que haja sorteio em 2026.** Nesse caso, (C), com início no próprio dia da criação.
  - O cadastro novo depende da P3 e da P5.
  - Nenhum sorteio foi criado desde 30/09/2025.

**P3 — O sorteio continua sendo da rede inteira, ou passa a poder ser de uma unidade?**

- (A) **Da rede,** como hoje (D144). A tela passa a dizer "vale para todas as unidades".
- (B) **Da rede ou de uma unidade:** exige coluna nova, e a finalização da venda passa a dar cupom só no sorteio da própria unidade.
- (C) **De várias unidades,** como os convênios: exige tabela nova.
- **Medido** (C5):
  - o #3 tem no nome a unidade Santa Mônica, mas só 217 dos 1.386 cupons (15,7%) são dela;
  - ele recebeu cupons de 33 unidades, e o ganhador saiu de MG – Ubá – Centro;
  - o #1 e o #4 têm nomes de campanha da rede.
- **Recomendo (A).**
  - Os quatro sorteios foram em outubro, na semana do Dia das Crianças, e dois têm nome de campanha da rede.
  - (B) e (C) exigem esquema novo nos dois bancos e uma regra nova dentro da finalização da venda.
  - Se o #3 era só da Santa Mônica, ele já foi sorteado na rede (13/10/2024), e isso não se desfaz.

**P4 — Como o ganhador fica sabendo?**

- (A) **Como hoje:** WhatsApp da unidade da venda ganhadora, quando a unidade tem WhatsApp ligado (26 das 32 unidades ativas têm, C8). Senão, push do aplicativo, pelo OneSignal, que está fora da migração.
- (B) **Só WhatsApp.** Sem WhatsApp, a tela mostra o contato.
- (C) **Nada automático.** A tela mostra nome, telefone e e-mail do ganhador a quem sorteou, para o contato ser feito à mão. A mensagem de hoje já promete "entraremos em contato em breve" (`SorteiosController.php:233`).
- **Medido:** 2 ganhadores em 3 anos, e nenhum rastro de aviso (C8, DS-10).
- **Recomendo (C) até o WhatsApp ser decidido,** e (B) depois, se o WhatsApp ficar. Esta pergunta **depende da decisão do WhatsApp**.

**P5 — O sorteio pode começar antes do dia em que é cadastrado?**

- (A) **Não:** o início é hoje ou depois, como no cadastro de cupom (`f2-fatia-cupons.md:173`).
- (B) **Pode, e as vendas da janela já finalizadas ganham cupom no momento do cadastro.** Mas o programa de fidelidade da época não está gravado, então valeria o de hoje.
- (C) **Pode, com um aviso** de que as vendas anteriores ficam de fora.
- **Medido:** o #1 perdeu até 728 vendas e o #3 até 115 (C9).
- **Recomendo (A).** É a regra que não engana ninguém. A (B) daria cupom, pelo programa de hoje, a quem talvez não estivesse no programa na época.

**P6 — A planilha dos participantes e a "Plataforma de Terceiros" continuam?**

- (A) **Como hoje, corrigidas:** uma linha por venda ativa (3.473 no #4, em vez de 3.502), com nome, e-mail e telefone, só para quem escreve.
- (B) **Sem contato:** a venda, o dia, a unidade e o número do cliente. O contato aparece só para o ganhador, na tela.
- (C) **Sem a plataforma de fora:** só o sorteio interno.
- **Medido:** os dois sorteios feitos foram internos (C1).
- **Recomendo (A).** É a sua resposta sobre as exportações ("como hoje", `f3-relatorios.md`, seção 6, item 10; linha 770 no HEAD), e a LGPD é débito futuro. A (B) já fica pronta para a LGPD, sem custo a mais. Esta pergunta **depende da decisão da LGPD**.

### Decisões que tomo para seguir, e que continuam suas

1. **Só o administrador escreve,** como nos convênios. Ninguém perde acesso (C6). Para voltar atrás, basta tirar a guarda.
2. **A situação é calculada,** e o aplicativo lê o rótulo da mesma regra (DS-1). Para voltar atrás, é uma linha em cada rota.
3. **Cada venda ativa é um cupom,** e a contagem é de vendas distintas. É a regra do sorteio interno desde sempre.
4. **O contato do ganhador aparece só para quem escreve.**
5. **Na alteração (F3),** nome e descrição podem mudar até o sorteio ou o cancelamento; as datas, só enquanto não houver cupom.
6. **Sem ordenação por coluna,** e 20 por página.

## 5. O que pode ser feito já, sem esperar o dono

1. **A F0 e a F1 inteiras:** a leitura no servidor, o DS-1 e a tela só lendo, ligada no Console.
2. **A F2 construída:** o sortear, o informar e o cancelar sob trava, e os consertos da tela antiga (DS-2, DS-5, DS-6, DS-7 e DS-11). Com duas ressalvas:
   - **não sortear, não informar e não cancelar o #4,** que espera a P1;
   - **nenhum aviso automático,** que espera a P4.
   - *As duas ressalvas foram respondidas: pela 216 (a), o #4 é sorteado pelo sistema -- e quem sorteia é o dono, na produção, depois do corte (aqui, ninguém toca os quatro sorteios reais); pela 217 (c), nada é automático, e o aviso saiu das duas telas. Ver a seção 6, a F2.*
3. **Registrar os DS-1 a DS-12** no `debitos-tecnicos.md`, cada um com o número da vez, e o DS-8 com a nota sobre o contrato. *Ao versionar, em 03/10/2026: os rótulos que já têm número estão na tabela do `README.md` desta pasta e não se registram de novo; os outros ganham número no commit da fatia que os conserta.*
4. **Não fazer agora:**
   - a F3 (P2, P3 e P5);
   - o "encerrar sem sorteio", que só entra se a P1 pedir;
   - o índice único do DS-3, que fica para a migração final;
   - o conserto do DS-8, que muda o contrato.

**Uma questão com prazo, que só o dono decide:** se houver sorteio de Dia das Crianças em 2026, ele já devia ter começado (P2).

## 6. O que foi feito: a F0 e a F1 (21/10/2026, a fatia 24 da fila das telas que faltam, SORTEIOS-LEITURA), a F2 (a fatia 25) e a F3 (a fatia 26)

Pela resposta 36 (c) do dono, Sorteios entrou na vez dele na ordem das telas; pela 1 (c), o cadastro espera a
F3. A fatia 24 foi a leitura: a F0 e a F1, sem nenhuma escrita na tela nova. As escritas -- sortear, informar o ganhador,
cancelar e a planilha -- vieram na fatia 25 (a F2, no fim desta seção).

**O servidor (a F0).**

- `RegraDoSorteio` ganhou a `situacao(status, inicio, fim, hoje)` -- os cinco selos da seção 1, com os nomes
  `agendado`, `recebendo`, `faltaSortear`, `sorteado` e `cancelado`, e `null` para um estado que o sistema não
  conhece --, o `rotuloDoAplicativo` (o DS-1), o `diasDesdeOFim`, o `criadoDepoisDoInicio` e o `diasDeAtraso`
  (o DS-9: o instante da criação contra a meia-noite do início; o #3, criado às 17:38 do primeiro dia, é
  "depois"), e o `id` e a `pagina` das outras regras. Pura: o "hoje" é de quem chama.
- `SorteioRepository`: `listagemDaApi` (a situação pela própria `RegraDoSorteio`, em PHP, com o mesmo "hoje" --
  o banco filtra só o nome; sem filtro, sem os cancelados),
  `numerosDaListagem` (vendas ativas distintas e os clientes delas), `numerosDoSorteio` (cupons, vendas,
  clientes, unidades, e à parte o cupom em dobro e o de venda cancelada -- as três partes somam os cupons),
  `porUnidade` (a unidade do vendedor) e `ganhadores`. As somas usam `CASE`.
- `Api/SorteioJson.php` e `Controller/Adm/SorteiosController.php` (`index` e `ver`, a funcionalidade
  `sorteios` no construtor), a fábrica, as duas rotas (`adm-api-sorteios/listar` e `/um/ver`), a família na
  lista de rotas públicas da `/adm/api` e no portão das rotas sem login.
- **O contato do ganhador** (telefone e e-mail) só sai a quem tem `marketing/sorteios/escrever` e é
  administrador (a decisão 4); a quem só lê, `contato: null`. O ganhador em si -- a venda, o dia do cupom, a
  unidade, o nome do cliente, como e por quem -- sai a quem lê, como a lista antiga já mostrava.
- **As duas rotas do aplicativo** leem o rótulo da regra. **A lista antiga** continua trocando o estado,
  agora gravando a `data_alteracao`.
- Diferenças do que a seção 2 previa: a tabela por unidade traz vendas e clientes (sem os cupons), e o
  ganhador da lista traz a venda e a unidade (o nome fica no sorteio aberto).

**A tela (a F1).** `frontend/src/api/sorteios.ts`, `telas/situacaoDoSorteio.ts` (as frases, sem conta de data),
`telas/Sorteios.tsx` e `telas/SorteiosLigado.tsx`; o Console abre a tela nova em Marketing › Sorteios
(`CAMINHO_DOS_SORTEIOS`, `PRIVILEGIO_DE_EDITAR_SORTEIOS`, o `podeEditar` com o administrador e o
`escreveSemSerAdministrador`, como nos convênios), e os destinos do menu que caem na tela não migrada descem de
23 para 22. A prévia nova, `frontend/.design-sync/previews/Sorteios.tsx` (sete células).

- **A lista:** a busca por nome e por situação ("todos, menos os cancelados" por padrão), 20 por página pelo
  envelope; cada linha com o nome e a janela, o selo e a frase ("encerrado há 371 dias, falta sortear"), a
  participação ("3.473 vendas · 2.994 clientes") e o ganhador ("venda 1.234.567 · Brasil - MG - Ubá - Centro").
- **O sorteio aberto:** o aviso da criação tardia (o D371), o prêmio ("sem descrição do prêmio" quando vazio),
  a janela, a situação, a criação, "vale para a rede inteira"; os números e os que não contam; a tabela por
  unidade; o ganhador; e "como funciona".
- **Nenhuma escrita, e nenhum convite à lista antiga.** Ao administrador que escreve, a tela diz "Esta tela
  ainda só lê: sortear, informar o ganhador e cancelar chegam aqui na próxima etapa, e cadastrar depois dela.
  Na lista antiga, sortear e informar o ganhador ainda mandam um aviso automático a ele." -- sem link; a quem
  escreve sem ser administrador, "Só o administrador sorteia, informa o ganhador e cancela: o sorteio vale para
  a rede inteira."; a quem só lê, nada. A lista antiga continua no ar pelo endereço, como nas outras telas
  migradas (seção 1).
- **Voltar do detalhe não relê a lista**, a menos que o "atualizar" tenha trazido o sorteio diferente do que a
  linha mostra (sorteado ou cancelado na lista antiga).

**O que ficou de fora, e por quê:** o sortear, o informar, o cancelar e a planilha (a F2, a fatia 25 da fila);
o cadastrar e o alterar (a F3, pelas respostas 1 (c) e 36 (c)). Até a fatia 25, o sortear da tela antiga
continua exigindo `aguardandoSorteio`, e é o GET dela que o grava -- por isso o D334 fica PARCIAL. As P3 a P6
seguem o que o consolidado decidiu para seguir; aqui, a P3 aparece como "vale para a rede inteira".

**Provado:** a caracterização da fatia nos dois bancos, em série -- `RegraDoSorteioTest` (9),
`SorteiosPelaApiTest` (15) e `SorteiosNoAplicativoTest` (2), e com os vizinhos (os do marketing, do aplicativo
instalado, da V4 e da etiqueta), 265 em cada banco; o contrato do aplicativo, 74 de 74, com os snapshots
`*sorteios*` iguais; o ensaio de permissão da `/adm/api`, com as duas rotas novas em 403; o front inteiro, 2.543
casos (os da tela também com `AMPLIFICAR_EFEITOS_MS=50`), o `tsc`, o `oxlint` e o build; 44 mutantes, cada um
sozinho -- 25 do servidor, 1 de portão (a funcionalidade apagada, pego pelo `conferir-acl-da-api.php`) e 18 do
front --, todos vermelhos; e os 66 portões do `conferir-tudo.sh`, com o da cobertura por par rebaseado (345
pares, 128 sem menção). O ciclo inteiro, os ensaios e o navegador ficam com o orquestrador.

**A verificação da fatia (21/10/2026)** confirmou 13 achados, todos tratados:

- **A tela nova não manda mais o administrador à lista antiga.** A frase com o link convidava a cadastrar lá
  (o dono adiou o cadastro para a tela nova, a resposta 1 (c)) e a sortear lá, onde o `notificarganhador` põe um
  aviso automático na fila (a resposta 217 (c), "nada automático"), e prometia um "atualizar" que a lista não
  tem. A frase nova (`SO_LE_POR_ENQUANTO`) diz o que vem e o que a antiga ainda faz, sem link.
- **A situação num lugar só, de fato:** o filtro `?situacao=` e o vencimento da lista antiga passam pela
  `RegraDoSorteio::situacao`, em PHP, e não pela regra reescrita em DQL; o cenário ganhou o sorteio que começa
  hoje (a fronteira do primeiro dia, que dois mutantes atravessavam).
- **O relógio do caso:** a `/adm/api/sorteios` e as duas rotas do aplicativo leem o "hoje" de um relógio
  injetável (nulo é o do servidor; o molde do `UnidadesController`), e os casos o fixam no `agora()` do cenário
  -- o caso que atravessasse a meia-noite já não muda de dia no meio. Um caso novo, com o relógio de amanhã,
  prova que o relógio é o lido.
- **A funcionalidade do construtor:** o caso lê o módulo e a funcionalidade do controller montado pela fábrica,
  e não os escreve à mão; trocar `'sorteios'` por `'cupons'` no construtor fica vermelho.
- O "atualizar" do sorteio aberto não repete o 403 nem o 404 (fica desligado, e não sai da tela); os casos do
  vazio da lista e da busca, da tabela por unidade vazia, do "Atualizando" e do "Abrindo" na região viva, da
  página e da busca que falham, e do 403, do 404 e do 500 no detalhe; o nome e o `sorteadoEm` presos por valor; o
  `max(0, ...)` do `diasDesdeOFim` com caso; e a célula do Console na prévia, que abria os Sorteios (agora a
  tela nova, que sem servidor mostraria o erro de rede), passou a Notificações.

### A F2: sortear, informar o ganhador, cancelar e a planilha (21/10/2026, a fatia 25 da fila das telas que faltam, SORTEIOS-SORTEAR)

Pela resposta 216 (a) do dono, o #4 é **sorteado pelo sistema**, entre as vendas ativas, uma chance por venda --
e quem sorteia é o dono, na produção, depois do corte: aqui nenhum dos quatro sorteios reais foi sorteado,
informado nem cancelado (os casos e o ensaio usam sorteios próprios, desfeitos depois). Pela 217 (c), **nada é
automático**: o aviso ao ganhador saiu das duas telas, e a nova mostra o contato a quem sorteou. Pela P6 (a), a
planilha é a de hoje, corrigida.

**O servidor.**

- `SorteioService::sortear`, `::informarGanhador` e `::cancelar`, **sob trava**: a transação, a linha do sorteio
  com `PESSIMISTIC_WRITE` (o `refresh`, a primeira leitura) e o estado relido dela -- o molde do
  `AppBannerService::definirEstado` (o DS-6). Só o `faltaSortear` se finaliza, pela `RegraDoSorteio` com o "hoje"
  de quem chama: o `aguardandoSorteio` **e o `ativo` vencido** (o D334). O sorteio sai das **vendas ativas**, cada
  uma uma vez, pelo código (`SorteioRepository::vendasParaSortear`), sem reposição (o D95), com o primeiro cupom
  da venda (o D72); a venda cancelada entre a lista e a escolha sai da lista. A escolha é de um **sorteador
  injetável** (`random_int`; os casos passam um dublê que escolhe a posição). Os três devolvem o motivo da
  recusa, um código da regra.
- `RegraDoSorteio`: `motivoParaNaoFinalizar`, `motivoParaNaoCancelar`, `frase` e `ehDeEstado` (409 para
  `jaSorteado`, `cancelado`, `agendado`, `recebendo` e `situacaoDesconhecida`; 422 para `semCupom`,
  `soVendaCancelada`, `vendaSemCupom`, `vendaCancelada` e `temCupom`). A frase de "não há quem sortear" é a certa
  (o DS-7): sem cupom nenhum não é "todas canceladas".
- A API: `POST /adm/api/sorteios/:id/sorteio`, `/ganhador` (`{venda: inteiro}`; outra chave é 422 com o nome
  dela) e `/cancelamento`, e `GET /:id/participantes` (o XLSX; a regra 9 de `docs/convencao-api.md`). Todas pedem
  `marketing/sorteios/escrever` **e ser administrador** (a decisão 1), com 401, 403 e 404 de sempre; a recusa leva
  `detalhes.motivo` e a situação relida (o `erroValidacao` ganhou o `detalhes` opcional). O sucesso é o sorteio
  aberto, com o contato do ganhador. O autor gravado é **quem entrou** (`autorDaEscrita`, o D315).
- **A planilha:** uma linha por venda ativa (`SorteioRepository::participantes`, o DS-2), com o dia do primeiro
  cupom e o nome, o e-mail e o telefone do cliente, nas colunas de sempre; o texto do cliente vai como texto (o
  nome que começa com "=" não vira fórmula). `Xls\Sorteio::conteudo` dá os bytes às duas portas.
- **A tela antiga, pela mesma regra:** o `finalizarInterno` e o `finalizarTerceiros` passam pelo serviço -- o id e
  a venda pela `RegraDoSorteio::id` (o pedido sem `id` era o 500 do `find(null)`, o DS-5), o vencido também se
  sorteia, a frase certa --; o `notificarganhador` saiu (era o único chamador), e a mensagem de sucesso tem o nome
  escapado (o achado 8) e diz que o contato é à mão; o `cancelar` é o do serviço, sob a mesma trava; o
  "Exportar" só aparece a quem escreve (o DS-11), e a coluna "Vendas ativas" conta as vendas ativas distintas.

**A tela.** No sorteio aberto, ao administrador que escreve, o cartão **"O que fazer"** com só o botão que serve
(`oQueSePodeFazer`) e a frase de por que não: **sortear** e **cancelar** perguntam no próprio botão (entre quantas
vendas e clientes; que o cancelado some do aplicativo), o duplo clique e o Enter segurado não confirmam;
**informar o ganhador** abre o formulário do código da venda, conferido antes de ir e de novo no servidor, com a
recusa no campo; **baixar a planilha** pede o arquivo pela API (`baixar`, no `cliente.ts`) e o entrega ao
navegador. O sucesso mostra o sorteio que voltou, com o ganhador e o contato logo abaixo das ações, e o aviso "O
sistema não avisa o ganhador: o contato é feito à mão…" como descrição do título, que recebe o foco. O 409 relê o
sorteio, os botões esperam a releitura, e o voltar relê a lista. A frase da F1 sobre o aviso da lista antiga
saiu, e a do topo diz que cadastrar e alterar vêm depois. A prévia ganhou duas células (o #4 logo depois de
sorteado e o informar aberto).

**Diferenças do que a seção 2 previa:** a planilha é XLSX pela própria API, e não um formato à espera da P6 (a P6
foi respondida: "como hoje"); o 422 do cancelar e o "não há quem sortear" levam o motivo em `detalhes`; e a tela
antiga não ganhou a guarda do administrador (ela continua no privilégio, como sempre esteve -- a decisão 1 é da
tela nova).

**A verificação da F2 (21/10/2026)** confirmou 16 achados; todos tratados, menos um, que não procedia.

- **Dois escritores da linha do sorteio fora da trava**, e os dois passaram por ela: o GET da lista antiga, que passa o
  vencido a `aguardandoSorteio`, podia gravar por cima do sorteio recém-feito (o sorteado voltava a "falta sortear", com o
  ganhador) -- agora `SorteioService::passarAAguardando` relê a linha dentro da trava (o D559; por isso o D334 voltou a
  PARCIAL: o GET ainda grava); e a finalização da venda dava o cupom sem esperar o cancelar -- agora
  `SorteioService::darCupom`, com a trava de leitura (`FOR SHARE`), relida (o D560).
- **A tela antiga:** a mensagem do sortear e do informar mostra o telefone e o e-mail do ganhador, escapados; e o autor
  gravado por ela (quem entrou, e não a pessoa assumida) ganhou caso.
- **A tela nova:** informar o ganhador pergunta antes, no próprio botão de enviar (a venda que vai, que não se desfaz e o
  efeito para o cliente), e o duplo clique e o Enter segurado não confirmam; a pergunta do sortear diz o efeito no aplicativo
  ("Finalizado", a marca do ganhador); o 409 do informar fecha o formulário e diz a recusa na região viva do sorteio; a
  recusa do informar que não é do campo fica numa região de alerta; durante o download da planilha, o formulário do informar
  não envia nem fecha; o 422 que prova que os números mudaram (`temCupom`, `soVendaCancelada`, `semCupom`) relê o sorteio,
  como o 409; o código da venda com zeros à esquerda vale como no servidor. O Enter segurado no sortear e no cancelar ganhou
  caso.
- **Não procedia:** a planilha da API pedir ser administrador além de escrever. A tabela da seção 2 já dizia "escrever +
  administrador" para `/participantes`, e a decisão 1 vale para a tela nova; a planilha da tela antiga segue só no privilégio,
  como o resto dela (a diferença está dita acima). Hoje ninguém tem o privilégio sem ser administrador (C6).

**O que ficou de fora:** o cadastrar e o alterar (a F3, a fatia 26 -- feita, abaixo); o DS-8 (o aplicativo mostra cupom de venda
cancelada, que muda o contrato) e o índice único do DS-3; a P4 além da 217 (c) -- se o WhatsApp voltar, o aviso
volta por decisão nova. O sortear do #4 é do dono, na produção.

### A F3: cadastrar e alterar, com a regra do cupom (21/10/2026, a fatia 26 da fila das telas que faltam, SORTEIOS-CADASTRO)

Pelas respostas 1 (c) e 36 (c), o cadastro sai pela tela nova; pela 120 ("crie-as para que exista um destino"), o editar da
tela antiga passa a existir, pela mesma regra (o D105). E **o dono pediu, nesta fatia, o cupom por valor e por serviço**, com
duas escolhas: **"o cadastro escolhe a regra"** e **"trocar a regra do banco"** (o D561). Uma primeira versão da fatia, parada
no meio por essa troca de pedido, tinha criado a chave (sorteio, venda) nas cópias; ela deu lugar à (sorteio, venda, numero).

**A regra do cupom.** Cada sorteio escolhe, no cadastro, como a venda finalizada ganha cupom:

| regra | quantos cupons | exemplo |
|---|---|---|
| `venda` (a de sempre, o padrão) | 1 | toda venda, 1 |
| `valor` | um a cada R$ X do valor da venda, pelos centavos | a R$ 50,00: R$ 120,00 dá 2; R$ 49,99, nenhum |
| `servico` | a soma das quantidades dos itens de serviço | 2 cortes e 1 barba, 3; os produtos não contam |

- **O valor da venda é a soma dos itens dela, cada um ao centavo, depois dos descontos** (`SorteioService::valorDosItens`), e
  por quê: é o `vendas.valor_total` sem o que o "Quitar" somou a ele. O total é incremental sobre os itens
  (`RegraDosDebitosNaVenda::excedente`) -- é o que o caixa soma no dia (`VendaRepository::totalVendaPorAnoMesDiaUnidade`) e o
  consumo do cliente (`::selecionarConsumoPorCliente`) --, mas o "Quitar" põe nele o que o cliente devia, e com o total a
  dívida dava cupom DUAS VEZES: na venda fiada, ao ser finalizada, e na que a quitou (a de R$ 20,00 que quitou R$ 100,00
  ganhava 2 cupons a cada R$ 50,00), às vezes por um consumo de fora da janela (o D564, achado na verificação; a primeira
  versão usava o total). Sem o "Quitar", a soma é o próprio total. O `valor_recebido` não serve: ele leva o troco (o dinheiro
  que voltou ao cliente) e fica abaixo do total no fiado, que o cliente deve e paga depois. A sessão de pacote já pago entra
  com 0 -- o item dela vale 0 na venda, e o pacote contou na venda em que foi comprado. **A forma de pagamento não entra**:
  a venda paga em permuta ou com o crédito do programa de fidelidade conta o valor dos itens inteiro -- a pontuação
  (`VendaPagamentoService::salvarPontuacao`) desconta as duas e soma só os serviços e os produtos de barbearia; qual das
  duas o sorteio segue é do dono, e foi ao lote.
- **O serviço é o item do tipo `ser`, pela quantidade inteira**, e também a sessão de pacote já pago (é um serviço lançado na
  venda). O pacote comprado (`pac`) e os produtos não contam. **O Lava VIP (`lavavip`) também não, por enquanto**: o catálogo
  de Serviços do ERP o lista junto dos serviços (`ProdutoJson::CATALOGOS`), e contar a lavagem do carro é do dono -- foi ao
  lote (7 lavagens na cópia). O item tirado da venda é apagado da tabela: não há item cancelado. Na cópia, nenhum dos
  3.001.613 itens de serviço tem quantidade quebrada, e 7.651 são de pacote.
- **A regra e o valor não mudam depois do primeiro cupom**, como as datas (a decisão 5, estendida).
- **Quatro escolhas que o dono não fez** foram ao lote, e seguem até a resposta: a venda abaixo de R$ X não ganha cupom (a
  leitura literal de "a cada R$ X"); a sessão de pacote conta (o que não deixa ninguém sem o cupom do corte que fez); a
  permuta e o crédito do programa contam no valor (a soma dos itens, como está); e o Lava VIP não conta como serviço (o que
  não inventa cupom).

**O banco.** `sorteios.regra_cupom` e `sorteios.valor_cupom` (`decimal(10,2)`), e `sorteios_vendas.numero` -- o número do
cupom dentro da venda, 1, 2, 3..., o 1 em todo cupom de antes. Só acrescentam: `scripts/migracao-sorteios-regra-do-cupom.sql`,
no laço do passo 0 do `procedimento-de-corte.md` e no do ensaio geral, nas duas cópias locais em 21/10/2026 (a resposta 83
(a)). **Ficam fora do mapeamento** (o D473, `ColunaNoBanco`): sem elas -- a base do teste do BVIP --, todo sorteio é da regra de
sempre, todo cupom é o 1, e a API recusa a outra regra no campo. **A chave única é (sorteio, venda, numero)**: o repetido é o
mesmo número duas vezes; os 4 pares de hoje saem no passo 3 (`migracao-sorteios-vendas-unicos.sql`, as respostas 124 (a) e
218 (a)), ensaiados em `ensaiar-sql-do-passo-3.php`; nas cópias locais, já.

**O servidor.**

- `RegraDoSorteio::conferir` -- a mesma regra na API e nos dois formulários antigos (o DS-4, agora o D562): o nome pela regra
  do texto (de 1 a 100, sem emoji, sem `<` nem `>`, numa linha), a descrição de até 1.000, as datas que existem (`checkdate`:
  o 31/02 não vira 03/03), o fim no dia do início ou depois e de hoje em diante, a regra numa lista fechada, o valor só na
  regra por valor (de 1.00 a 99999.99, decimal com ponto), e as chaves que a API não escreve com o nome delas. O início
  antes de hoje continua aceito (a P5 foi ao lote; ver abaixo), com o aviso.
- `POST /adm/api/sorteios` (201, o sorteio aberto e o `Location`) e `PUT /adm/api/sorteios/:id` (só o que mudou,
  `soOQueMudou`; `PUT {}` é 400; nada mudou, 200 sem gravar; o `alteradoEm` diferente é 409 `mudou`; o sorteado e o
  cancelado, 409; as datas, a regra e o valor de quem já tem cupom, 422 `datasComCupom` e `regraComCupom` nos campos), só
  a quem escreve E é administrador (a decisão 1). `SorteioService::criar` (numa transação, a regra junto) e `::alterar` (sob
  a trava do sorteio, os cupons contados depois dela; o `update` morto saiu, o DS-12, agora o D563).
- **De ponta a ponta:** `SorteioService::darCupom` dá os cupons que a regra manda, numerados, só os que faltam (a venda
  finalizada duas vezes não ganha o dobro), sob a trava de leitura (o D560); o sorteio interno sorteia entre os CUPONS de
  vendas ativas (`cuponsParaSortear`) e marca o cupom sorteado (`selecionarCupom`); os números ganham os `cuponsQueContam`
  (os que contam + o em dobro + o de venda cancelada = os cupons), a lista e a tabela por unidade os `cupons`; a planilha, a
  coluna "Cupons"; o aplicativo, uma linha por cupom (o código da venda repetido por cupom: a soma é a dos cupons). Na regra
  de sempre, tudo como antes: o contrato 74 de 74, e os retratos do aplicativo iguais.
- **A tela antiga:** o `novoAction` grava só os campos escolhidos, pela regra de sempre, e diz que as outras regras se
  escolhem na tela nova; o `editarAction` existe (o D105), altera pela mesma regra e mostra a regra do cupom, sem a mudar.
  **Os dois pedem escrever E ser administrador**, como a API (a decisão 1 e a nota da tarefa: "o novoAction e o editar antigos
  pela mesma regra"; a verificação): o editar não existia, e nascia como uma porta de escrita nova, só no privilégio, para o
  nome e as datas de qualquer sorteio da rede. Quem escreve sem ser administrador volta à lista com a frase, e a lista não
  lhe mostra o "Novo" nem o editar. O sortear, o informar, o cancelar e a planilha da tela antiga seguem no privilégio, como
  a F2 registrou. **O editar confere o que leu** (o `alterado_em` oculto, o D565): o formulário aberto antes de outra tela
  gravar desfazia calado o que ela gravou; agora é recusado com a frase do `mudou`, e reabre com o sorteio de agora. **A
  lista diz a regra de cada sorteio** sob o nome ("1 cupom a cada R$ 25,00 do valor da venda"), e o editar (pela data de
  início) leva a todo sorteio aberto, com cupom ou sem -- até aqui só o que se cancelava o tinha, e o destino da 120 quase
  não se alcançava.

**A tela.** O "cadastrar sorteio" na lista e o "alterar" no sorteio aberto (até o sorteio ou o cancelamento), a quem escreve e
é administrador; o formulário (`formularioDoSorteio.ts`, o espelho da regra) com o nome, o prêmio, a janela com o aviso do
início (o D371) e "Como a venda ganha cupom" em rádios, com o valor -- que aceita a vírgula de quem digita e vai com o ponto
-- e a prévia ("Vai ficar assim: 1 cupom a cada R$ 50,00 do valor da venda."). Na alteração de quem tem cupom, a janela e a
regra aparecem como texto, com o porquê (as frases do servidor, uma para as datas e outra para a regra, presas pelo
`formularioDoSorteio.casos.json`, que a `RegraDoSorteioTest` e o teste do front leem). O duplo clique não cadastra duas vezes,
e o envio no ar trava os campos (o molde dos banners). O 409 fecha o formulário, relê o sorteio e diz a recusa -- e o que
mudou, campo a campo, e que o digitado não foi gravado (fechar é o molde das unidades: reenviar o digitado gravaria por
cima o que a outra aba gravou); o 422 de quem acabou de ganhar cupom relê o sorteio, o formulário passa a mostrar a regra
como texto, e o foco, que estava no rádio que saiu, vai ao texto. No
sorteio aberto, a regra no cartão "O sorteio", os "cupons que contam" e a coluna deles (na regra que não é a de sempre),
"como funciona" com a regra -- a primeira frase também: "toda venda finalizada ganha cupom" só na de sempre -- e a pergunta
do sortear entre os cupons; a nota da planilha diz a coluna "Cupons". A frase "cadastrar e alterar vêm depois" saiu. A
prévia ganhou quatro células (o cadastro, um sorteio por valor, a alteração de quem tem cupom e a de quem ainda não tem,
com o campo do valor); o #4 recém-sorteado não mostra mais o "alterar".

**A verificação da F3 (21/10/2026)** confirmou 19 achados, todos tratados: o débito quitado que dava cupom duas vezes (o
D564) e o editar antigo sem o `alteradoEm` (o D565), consertados; a permuta e o crédito do programa no valor, e o Lava VIP no
serviço, que eram escolhas do dono feitas sem pergunta, foram ao lote; a guarda do administrador no novo e no editar
antigos; a regra na lista antiga; a coluna "Cupons" provada no XLSX baixado (e sem o padrão `?? 1`); o caminho do banco sem
as colunas, com casos (`CuponsPelaRegraDoSorteioTest::testSemAsColunasTudoEUmCupomPorVenda` e
`SorteiosCadastroPelaApiTest::testSemAsColunasSoARegraDeSempreSeCadastra`, fingindo a ausência pela `ColunaNoBanco`, o molde
de `CupomSemAsColunasTest`); e, na tela, o foco do 422, os campos travados no envio, o "como funciona" sem contradição, o
exemplo do valor formatado de R$ 40.000,00 em diante, a nota da planilha, as frases presas ao servidor e o 409 que diz o que
mudou.

**O que ficou de fora, e por quê:** a P5 (o início antes do cadastro) ficou como hoje, "pode, com o aviso", e foi ao lote --
a ordem da fatia; atenção: a tabela "o que ficou para depois" do `telas-consolidado.md` anotava "não: o início é hoje ou
depois", e a pergunta diz as duas coisas. A P3 ficou da rede, sem pergunta. A chave na produção espera o passo 3; as colunas,
o passo 0. O DS-8 (o aplicativo conta o cupom de venda cancelada) segue registrado, sem conserto: muda o contrato.

## Apêndice — as consultas

Todas são SELECT agregado no `franquiabv_dev`, por `docker exec franquiabv-mysql-1`, em 02/10/2026 às 04:03 pelo relógio do banco. Nenhum dado pessoal foi impresso; nomes de unidade não são dado pessoal.

- **C1 — a tabela `sorteios`.** Estado, janela, criação, data do sorteio, tipo, `data_alteracao IS NULL`, `ganhador IS NOT NULL` e o tamanho do nome e da descrição.
  - #1: criado em 04/10/2023 às 23:52; janela de 01 a 15/10; sorteado em 19/10 (interno).
  - #2: cancelado.
  - #3: criado em 07/10/2024 às 17:38; janela de 07 a 12/10; sorteado em 13/10 (interno).
  - #4: criado em 30/09/2025; janela de 01 a 15/10/2025; estado `aguardandoSorteio`; descrição com 0 caracteres; `data_alteracao` vazia.
- **C2 — números por sorteio** (`sorteios_vendas` com `vendas` e `usuarios`):

  | sorteio | cupons | vendas | vendas ativas | cupons de venda ativa | cupons de venda não ativa | clientes | clientes com venda ativa | unidades | ganhadores |
  |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
  | #1 | 2.326 | 2.325 | 2.320 | 2.321 | 5 | 2.064 | 2.064 | 34 | 1 |
  | #3 | 1.386 | 1.385 | 1.376 | 1.377 | 9 | 1.326 | 1.326 | 33 | 1 |
  | #4 | 3.502 | 3.500 | 3.473 | 3.475 | 27 | 2.996 | 2.994 | 30 | 0 |

  A unidade é a do vendedor, como em `Venda::getUnidade`. As fixtures têm 1 cupom cada, de venda cancelada.
- **C3 — totais sem fixtures:** 7.214 cupons, sendo 7.173 de venda ativa e 41 de venda cancelada, de 5.002 clientes.
- **C4 — cupons em dobro** (`GROUP BY sorteio, venda HAVING COUNT(*) > 1`): 4 pares, 1 no #1, 1 no #3 e 2 no #4. O `TIMESTAMPDIFF` entre os dois cupons de cada par é de 635, 1.419, 582 e 5 s.
- **C5 — unidades.** No #3: Santa Mônica com 217 cupons, Ponte Nova – Guarapiranga com 112 e Jurerê com 99. Unidade da venda ganhadora: #1, SC – Florianópolis – Estreito; #3, MG – Ubá – Centro.
- **C6 — privilégios.** Funcionalidade 52 ("Marketing sorteios"): 0 linhas em `privilegios` e 0 em `privilegios_usuarios`.
- **C7 — administradores.** Usuários ativos de grupo administrador, sem fixture: 10, de 3 unidades. Quem sorteou o #1 e o #3 era administrador.
- **C8 — avisos e WhatsApp.**
  - `push_app`: 5.141 linhas, de 16/08/2026 a 01/10/2026 (853 do aplicativo e 4.288 de WhatsApp), nenhuma com o título "Sorteio VIP".
  - `unidades`: 26 das 32 ativas têm WhatsApp ligado.
- **C9 — vendas sem cupom por criação tardia.** Vendas ativas de clientes que hoje estão no programa, por dia, com `LEFT JOIN` no cupom do sorteio:
  - #1, de 01 a 04/10/2023: 10, 200, 301 e 217 vendas, nenhuma com cupom; em 05/10, 343 vendas, 257 com cupom;
  - #3, em 07/10/2024: 115 vendas antes das 17:38:03, nenhuma com cupom; 75 depois, 58 com cupom;
  - #4, em 01/10/2025: 270 vendas, 249 com cupom.
- **C10 — clientes do #4.**
  - Com todos os cupons de venda cancelada: 2.
  - Estado e programa dos 2.996: 2.993 ativos e no programa, 3 inativos.
  - Vendas ativas por cliente: 1,16 em média, no máximo 12; 3 clientes com 5 ou mais.
- **C11 — programa de fidelidade** (`clientes.programa_fidelidade`): 25.453 de 242.586, sem fixture.
- **C12 — modo do MySQL** (`@@GLOBAL.sql_mode`): `STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION`.
