# Fase 2 — a primeira fatia: o PDV

Começada em 03/09/2026, por decisão de quem decide. A fatia estava proposta
desde 02/09 e não tinha sido escrita em lugar nenhum — existia só na conversa.

`docs/convencao-api.md` diz que **o momento de revisar as regras 1 a 7 é a
primeira fatia vertical**, porque construir um módulo inteiro mostra em uma
semana o que discussão nenhuma mostraria. Este documento é o que a fatia mostrou.

---

## Por que o PDV, e não outro módulo

Porque é onde o dinheiro entra, e porque é o fluxo mais exercitado do sistema:
~800 clientes por dia, 32 unidades. Se a convenção não servir aqui, não serve.

E porque o obstáculo é visível: as ações do PDV **já devolvem JSON** — o
inventário as classifica como `NORMALIZAR`, "ajuste de forma, não criação" — mas
o que elas devolvem inclui `tabela` e `tabela_pagamentos`, que são **trechos de
HTML montados no servidor**. Uma tela nova não tem o que fazer com isso.

**A regra de negócio não é o problema.** Ela já está nos serviços
(`VendaService`, `VendaProdutoService`, `VendaPagamentoService`), e a fatia não a
move. O que muda é quem traduz HTTP e o que sai no corpo.

---

## O fluxo, e as nove ações

"Comanda aberta até venda finalizada":

| # | Ação de hoje | Recurso novo |
|---|---|---|
| 1 | `Vendas::comandaAbrir` | `POST /adm/api/comandas` |
| 2 | `VendasProdutos::adicionar` | `POST /adm/api/comandas/:id/itens` |
| 3 | `VendasProdutos::remover` | `DELETE /adm/api/comandas/:id/itens/:item` |
| 4 | `VendasPagamentos::adicionar` | `POST /adm/api/comandas/:id/pagamentos` |
| 5 | `VendasPagamentos::remover` | `DELETE /adm/api/comandas/:id/pagamentos/:pagamento` |
| 6 | `VendasPagamentos::finalizar` | `POST /adm/api/comandas/:id/finalizacao` |
| 7 | `Vendas::cancelar` | `POST /adm/api/vendas/:id/cancelamento` |
| 8 | `VendasPagamentos::pagarDebitos` | `POST /adm/api/clientes/:id/debitos/quitacao` |
| 9 | `VendasPagamentos::autorizarVendaFiado` | `POST /adm/api/comandas/:id/autorizacao-fiado` |

**Nada disso substitui as ações atuais.** `/mod-vendas/...` continua servindo a
tela de hoje. Trocar os dois ao mesmo tempo seria migrar tela e API na mesma
jogada — o acoplamento que a fatia vertical existe para evitar.

---

## O que está pronto

| | |
|---|---|
| `GET /adm/api/comandas` | as comandas abertas da unidade, paginadas |
| `GET /adm/api/comandas/:id` | uma comanda, com itens e pagamentos |
| `POST /adm/api/comandas` | abre — ou reaproveita — a comanda de um cliente |
| `POST /adm/api/comandas/:id/itens` | acrescenta produto ou serviço |
| `DELETE /adm/api/comandas/:id/itens/:item` | remove o item, e devolve a comanda |
| `PUT /adm/api/comandas/:id/itens/:item` | ajusta a linha — quantidade e desconto, pela conta do lançamento (B9, 23/09/2026) |
| `POST /adm/api/comandas/:id/itens/:item/cupom` | a linha passa a levar o cupom do cliente -- a da agenda, pelo preço ou só com o convênio; com mais de uma unidade, a do cupom fica com uma, e as outras vão a uma linha à parte (16/10/2026, as respostas 155 (b) e 154 (c) do dono, o D276) |
| `POST /adm/api/comandas/:id/pagamentos` | lança um pagamento |
| `DELETE /adm/api/comandas/:id/pagamentos/:pagamento` | estorna o lançamento |
| `POST /adm/api/comandas/:id/autorizacao-fiado` | o gerente autoriza — **e fica gravado** |
| `POST /adm/api/comandas/:id/debitos` | traz os débitos do cliente para a comanda -- desde 06/10/2026, só o que ainda falta somar: o pedido repetido responde 200 sem somar de novo (o D380); desde 12/10/2026, 409 `debitos_em_outra_venda` quando outra venda aberta do cliente na unidade já os tem somados (a resposta 79) |
| `POST /adm/api/comandas/:id/finalizacao` | fecha a comanda como venda -- desde 12/10/2026, com os débitos somados em parte e sem a bandeira, pede no corpo `debitosEmParteVistos`, o somado que a tela mostrou, e sem ele responde 409 `debitos_em_parte` (a resposta 78) |
| `POST /adm/api/vendas/:id/cancelamento` | cancela, com senha de gerente |

**A fatia está completa: as nove ações do fluxo têm recurso.**

*Uma delas mudou de endereço no caminho.* Eu havia proposto
`POST /adm/api/clientes/:id/debitos/quitacao`, antes de ler a ação com atenção.
Está errado: quem muda de estado é a **comanda** — o total dela cresce —, e o
débito do cliente só é baixado depois, ao finalizar. O recurso é
`POST /adm/api/comandas/:id/debitos`.

*E ela deixou de aceitar o valor de fora.* A ação antiga
(`VendasPagamentos::pagarDebitos`) lê `valor` do pedido: **quem chama decide
quanto de débito somar**. O servidor já sabe a resposta —
`selecionarValorDebitosCliente($cliente, $unidade)` —, e um número que o próprio
sistema calcula não pode ser contestado pelo cliente da API.

Peças: `ISBase\Controller\ApiAdmController` (a base que faz da convenção o
caminho mais curto), `ISVenda\Api\ComandaJson` (a forma do recurso, num lugar
só), `ISVenda\Controller\Adm\ComandasController`, e
`VendaRepository::selecionarComandasAbertas`.

Provado por `curl` contra a aplicação de pé, com sessão real, e por
`tests/Caracterizacao/ComandaApiTest.php` nos dois bancos.

---

## O que a fatia revelou — que é para isto que ela serve

### 1. A aplicação imprimia avisos ANTES da resposta, e isso anula o código HTTP

O primeiro endpoint que define status de resposta expôs o que nenhum outro
poderia: `SessaoAcl::getUnidade()` fazia `return $acl['unidade']` direto, e sem
sessão `$acl` é nulo. O PHP emite *"Trying to access array offset on value of
type null"* — e o aviso **vai para a saída**.

Saída antes da resposta significa cabeçalho já enviado. O resultado medido:

```
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8

<b>Warning</b>: ... SessaoAcl.php on line <b>173</b>
{"erro":{"codigo":"sem_permissao", ...}}
```

Corpo dizendo "sem permissão", resposta dizendo **200**, tipo dizendo **HTML**.
É exatamente a mentira que a Fase 2 existe para acabar.

Enquanto tudo respondia 200 com HTML, o aviso passava por sujeira de log — e
`CrudController::__construct` o disparava na construção de **qualquer**
controller sem sessão. Corrigido com `?? null` nos dois acessores.

### 2. `JsonModel` não serve para erro 4xx

Com `JsonModel`, o Laminas ainda passa pela camada de visão — e ali a estratégia
de "página não encontrada" vê o status 404, monta o próprio modelo e **funde**
com o nosso:

```json
{"erro":{"codigo":"nao_encontrado","mensagem":"..."},"message":"Page not found."}
```

Um corpo com duas formas ao mesmo tempo. A base passou a devolver um `Response`
pronto, o que encerra o despacho: o corpo é exatamente o escrito, e o
`Content-Type` também.

*Isto é emenda à regra 3 da convenção:* a forma fixa do erro só se sustenta se a
resposta não passar pela renderização.

### 3. Criar nem sempre cria — e 201 mentiria

`comandaAbrir` reaproveita: se o cliente já tem comanda aberta na unidade, ela
volta com os itens de antes dentro. Foi o **D52** que descobriu o custo de não
dizer isso ao operador — ele via "comanda aberta com sucesso" e não entendia os
itens que já estavam lá.

Então `POST /adm/api/comandas` devolve **201 quando cria** e **200 quando
reaproveita**, com `reaproveitada` no corpo. O código diz o que aconteceu; o
campo dá à tela a frase que ela precisa mostrar.

*Desde 06/10/2026 há uma terceira resposta (o D384):* **409** quando o cliente não tem comanda aberta e a
venda em andamento do operador, no PDV antigo, tem pagamento lançado para outro cliente -- abrir a
viraria a comanda deste com o dinheiro do outro dentro. O corpo traz a frase e
`detalhes = {motivo: "venda_em_andamento_com_pagamento", venda: <id>}`, para a tela não comparar texto.

*Desde 08/10/2026 (as respostas 40 e 42 do dono):* o mesmo 409 quando a venda em andamento tem os débitos
de outro cliente somados pelo "Quitar", sem pagamento -- `detalhes.motivo = "venda_em_andamento_com_debitos"`,
com a frase de finalizar a venda ou recarregar o PDV (o D433); e a venda do "Sem Cadastro" (o cliente 2)
com pagamento deixou de ser recusada: vira a comanda do cliente pedido, com o pagamento dentro (o D434).

*Isto é emenda à regra 2:* "201 cria" não basta como regra quando a operação é
idempotente por natureza.

### 4. O decimal do protocolo multiplicava o dinheiro por cem

O achado mais caro da fatia, e ele só aparece quando a API encontra um serviço.

A regra 7 manda dinheiro viajar como texto decimal com ponto — `"89.90"`. Os
serviços da casa chamam `Monetario::removeMask()`, que **apaga o ponto**, por ser
separador de **milhar** no formato brasileiro:

```
removeMask("89.90")   ->  "8990"
removeMask("89,90")   ->  "89.90"
```

Repassar o decimal do protocolo direto a um serviço transformaria oitenta e nove
reais em oito mil novecentos e noventa — **em silêncio, num ponto de venda**. Não
há erro nem exceção: há um número cem vezes maior gravado.

A tradução acontece na **fronteira**, uma vez, em `Monetario::deDecimal()`. E o
formato brasileiro chegando de fora é **recusado com 422**, não adivinhado:
aceitar e converter esconderia o engano de quem escreve a tela.

Três testes seguram isso, e um deles guarda o perigo **à vista** — afirma que
`removeMask("89.90")` dá `"8990"`, para que ninguém "simplifique" tirando a
tradução. Outro passa pelo serviço de verdade e confere que o total fecha em
89,90: sem ele, um teste de *forma* continuaria verde com o valor errado.

### 5. A API nova pode exigir o que a tela velha não pode

O **D86** deixou pendente o segundo tempo: o servidor recusar fiado sem
autorização registrada. A trava nunca foi de rigor — era o **cache**. O navegador
do operador pode estar com o script anterior, e exigir de repente pararia o PDV
das 32 unidades.

**A API nova não tem consumidor**, então `finalizacao` recusa desde o primeiro
dia (409, medido), e `autorizacao-fiado` **grava** o autorizador em vez de
responder um sim que o navegador aplica.

*É um argumento geral, e não um detalhe deste débito:* uma superfície sem parque
instalado pode nascer com as regras que a superfície velha só consegue adotar
depois de uma janela de implantação. Vale a pena procurar outros casos assim
quando as próximas fatias forem escolhidas.

### 6. A paginação da casa não é `setFirstResult`

A convenção já havia corrigido isto uma vez (29/08). Ao escrever a consulta nova
eu introduzi o primeiro `setFirstResult` do projeto — e desfiz: a paginação passa
por `ISBase\Paginator\AdapterQuery`. Dois jeitos de paginar obrigariam quem lê a
saber os dois.

### 7. O inventário rotulava 42 ações como "rota declarada" sem que fossem

Declarar a primeira rota nova fez o portão do inventário ficar vermelho, e o
diff apontava para o lugar errado: `VendasPagamentosController::finalizar`, que
eu não tinha tocado, passou de `curinga` para `rota declarada`.

**A causa.** O gerador indexava as rotas declaradas por `{módulo}:{ação}` — sem o
controller. Bastou existir `'action' => 'finalizar'` em qualquer lugar da
configuração do `ISVenda` para que **toda** ação chamada `finalizar` daquele
módulo fosse marcada como tendo rota própria.

O defeito era anterior à fatia, e maior do que o caso que o revelou: qualquer
`'action' => 'index'` num módulo rotulava **todos** os `index` dele.

| | antes | depois |
|---|---:|---:|
| "rota declarada" | 127 | **85** |
| curinga | 233 | **275** |

**42 ações estavam rotuladas errado.** A chave passou a incluir o controller, que
é o último declarado antes da ação no arquivo — é assim que as rotas do Laminas
se aninham. Conferido nos dois sentidos: as quatro ações de IA do
`ClientesController`, que **são** declaradas, mantiveram o rótulo; o `index` do
mesmo controller, que não é, virou curinga.

*É a família do D70:* rótulo que afirma coisa falsa num documento que dimensiona
a fase. Dizer que uma ação já tem rota própria muda o tamanho do trabalho.

**E o inventário não enxerga `Controller/Adm/`** — as ações novas não entram nos
360. Isso está certo (aquele diretório é o *destino* da fase, não a superfície a
migrar; contá-lo faria o número *a fazer* crescer a cada fatia entregue), mas
estava certo **por acidente**, pela profundidade do `glob`. Agora está escrito no
cabeçalho do gerador, para ser decisão.

**O mesmo rótulo falso voltou horas depois, por outra porta.** Ao criar
`Adm\VendasController::cancelar`, o `VendasController::cancelar` **antigo** foi
marcado como "rota declarada" — de novo.

A causa é o achatamento do espaço de nomes: a chave usa o *basename*, e
`Adm\VendasController` e `VendasController` viram a mesma coisa. Meu primeiro
conserto trocou o módulo pelo controller e resolveu o caso que eu tinha na mão;
não notei que dois controllers de namespaces diferentes colidem na mesma chave.

O conserto que ficou: rotas apontando para `Adm\` **não rotulam ação nenhuma** —
o que é a mesma decisão de escopo, aplicada do outro lado. Vale como aviso: uma
correção validada contra o caso que a motivou pode continuar errada para o caso
seguinte, e aqui os dois estavam a horas de distância.

### 8. 403, e não 401

Quem chega sem sessão não está "não autenticado" no sentido do protocolo: não há
desafio a devolver, nem `WWW-Authenticate` que faça sentido enquanto o mecanismo
de token não existir. Quando existir, isto vira 401 com o desafio certo.

---

## O que a TELA revelou — 09/09/2026

As três seções abaixo não vieram de ler o código: vieram de tentar usá-lo. A
Fase 3 foi começada de propósito antes de a Fase 2 acabar, para que a tela fosse
o instrumento de medida — e no primeiro dia de PDV em React ela achou um defeito
que fabricava dinheiro, um endpoint que não existia, e um portão meio cego.

### 9. O troco sobrevivia ao pagamento que o gerou — e virava dinheiro do nada

`VendaProdutoService` apaga **todos** os pagamentos da venda e zera o valor
recebido quando um item entra ou sai. É o desenho do PDV: monta-se o carrinho,
depois se recebe. Mas ele não mexia em `vendas.valor_troco`:

```
comanda de 89,90; o cliente paga 100,00  ->  troco 10,10
lança-se mais um item                    ->  pagamentos apagados,
                                             recebido 0,
                                             troco CONTINUA 10,10
```

A partir daí a comanda só sai fiada, e `finalizarAction` recusa com **422**
pedindo o destino de um troco que não existe mais. Obedecendo a recusa —
`destinoTroco: "cliente"` —, `VendaPagamentoService::finalizar` transforma os
10,10 em crédito do cliente. Com um id de colaborador no lugar, vira **gorjeta**,
que sai do caixa em dinheiro e não tem estorno.

**Reproduzido pela API contra `franquiabv_contrato`:** valor recebido `0`,
crédito de `10,10` e débito de `99,90` no mesmo cliente, na mesma venda. A loja
não recebeu nada e ficou devendo dez reais e dez centavos.

**Na cópia de produção o rastro são cinco vendas e R$ 117,10**, entre 2022 e
2023 — quatro concluídas (R$ 108,00) e uma cancelada. A consulta
`valor_troco > 0 AND valor_recebido < valor_total` devolve sete, mas duas são de
2018 e carregam troco de `2e-16`: ruído de ponto flutuante, e não dinheiro.

É pouco porque o defeito **se cura sozinho** quando entra outro pagamento —
`VendaPagamentoService` recalcula o troco a cada um. Só sobrevive quando a
comanda vai embora fiada. Raro não é inofensivo: é difícil de ver.

Corrigido nos dois métodos do serviço, com teste de regressão nos dois bancos
(`ComandaApiTest::testAcrescentarItemNaoDeixaTrocoParaTras` e o irmão de
remover). Os testes foram falsificados: arrancando as duas linhas do conserto,
os dois ficam vermelhos.

**E o campo passou a sair na resposta.** `valorTroco` morava só em `VendaJson`,
entre as coisas "que interessam depois de fechada". A premissa estava errada: é
a coluna de troco que faz a finalização exigir um destino, e a tela precisa
vê-la **enquanto a comanda ainda está aberta** — senão só descobre a exigência
levando o 422.

### 10. Não havia como descobrir uma forma de pagamento

`POST /adm/api/comandas/:id/pagamentos` exige `formaPagamentoId` e responde 422
quando o id não existe. E **nenhuma rota de `/adm/api` listava formas de
pagamento** — havia comandas, vendas, caixas, clientes, agendas, produtos,
usuários, unidades, convênios, pacotes, cupons, saídas, funcionalidades e
privilégios, e nenhum catálogo de formas.

A tela teria de cravar os ids. E cravar não funcionaria: produção usa 1 a 9, o
cenário de fixtures usa 90001000 e 90001001 — o mesmo código estaria certo num
banco e errado no outro.

Daí `GET /adm/api/formas-pagamento`, com a mesma consulta da tela velha
(`popularCombobox()`, ativas, ordenadas). O recurso devolve **três campos de
seis colunas**:

- `tipo` e `abreviacao` ficaram de fora porque não significam nada — **nenhuma
  linha do sistema lê essas colunas**, e quem as interpretasse erraria: em
  produção `Crédito Cliente` e `PicPay` são os dois `car`;
- `ordenacao` fica de fora porque a coleção já sai ordenada e arranjo JSON
  preserva ordem;
- `status` fica de fora porque a coleção só traz as ativas.

No lugar delas entra um campo derivado, `saldo` (`null`, `"cliente"` ou
`"programaFidelidade"`), que responde a única pergunta que a tela precisa fazer
e que hoje só se responde comparando id: **qual saldo esta forma gasta.** Quem
compara é o servidor, que já tem as constantes do **D18**. A tela lê uma
palavra — tirar comparação de id cru do navegador é a lição do **D124**.

### 11. O portão de ACL não conhecia a terceira forma de guarda

`conferir-acl-da-api.php` reconhecia `getAcesso(` e `getAcesso2(`. O catálogo de
formas mora em `ISConfiguracao` e quem precisa dele é o PDV, então ele se guarda
com `getAcessoDe('vendas', 'comandasNova', 'ler')` — a guarda do **D138**, para
quando a ação mora num módulo e o privilégio é de outro.

O portão acusou a ação de **não ter guarda nenhuma**, e a guarda estava escrita
na linha que ele leu. O ERP usa `getAcessoDe` em 24 lugares desde antes do
portão existir, mas nenhum deles era controller de `/adm/api` — e como o roteiro
só lê `Controller/Adm/`, ele nunca precisou conhecê-la.

Ensinado, e falsificado depois: com a guarda arrancada, ele volta a acusar.

**E o D146 funcionou.** A rota nova nasceu fora de `rotas-publicas.php`, e o
portão de login a interceptou — sem sessão ela respondia **200 em `text/html`**,
a tela de login disfarçada de sucesso, enquanto as outras famílias de `/adm/api`
respondiam 403 em JSON. Falhou fechada, que é como uma guarda deve falhar.

### 12. Provar o endpoint por `curl` não é provar a tela

Os três defeitos desta seção são **meus**, e nenhum apareceu em teste, em portão
ou em revisão de código. Todos apareceram no segundo em que a tela foi aberta num
navegador — coisa que eu não tinha feito.

**O primeiro deixava a lista de produtos vazia em toda unidade, sempre.**
`catalogo.ts` declarava `catalogo?: string`, opcional, e a tela chamava
`listarProdutos()` sem argumento. `ProdutosController::indexAction` lê
`fromQuery('catalogo', '')` e recusa com **422** o que não está em
`ProdutoJson::CATALOGOS` — e `''` nunca esteve. O TypeScript não reclamou porque
o tipo dizia que era opcional; o docblock logo acima afirmava ter conferido o
controller antes de escrever cada parâmetro. Tinha conferido os nomes, não a
obrigatoriedade.

**O segundo cortava as listas no vigésimo item, em silêncio.**
`ProdutosController` e `UsuariosController` terminam em
`colecao($dados, $total, $pagina, 20)` e **nenhum dos dois lê `porPagina`** — o
20 está cravado nos repositórios. A tela lia `dados` e jogava `total` fora. Numa
barbearia com mais de vinte serviços, o vigésimo primeiro simplesmente não
existia para o operador, e ele não tinha o que reportar. Ironia registrada: a
listagem de comandas, na mesma tela, escreve *"1 de 11 nesta unidade"* justamente
para não mentir sobre o recorte.

**O terceiro fazia todo defeito parecer o mesmo.** As três cargas de catálogo
faziam `.catch(() => setProdutos([]))`. Um 403 de permissão, um 422 de
parâmetro, a sessão caída e o cabo desligado ficavam todos com a mesma aparência
na tela: *a barbearia não tem produtos*. Foi este que escondeu os outros dois.

Os três estão corrigidos: `catalogo` virou obrigatório no tipo, as coleções são
percorridas até o fim, e a falha aparece escrita ao lado do campo. E o terceiro
conserto revelou de imediato o **D168** — as duas listas seguem vazias para quem
opera o caixa, mas agora a tela diz por quê.

*A lição não é sobre React.* É que "provado por `curl` com sessão real" prova o
endpoint, e o consumidor é uma segunda coisa a provar.

---

## As regras, depois do primeiro exercício

| Regra | Situação |
|---|---|
| 1 — verbo declarado na rota | **serve.** Primeira rota do sistema a declarar; `DELETE` e `PUT` não declarados dão 404, medido |
| 2 — código HTTP diz o que aconteceu | **serve, com emenda:** 200 quando a criação reaproveita |
| 3 — corpo de erro com forma fixa | **serve, com pré-requisito:** só se a resposta não passar pela camada de visão |
| 4 — sucesso devolve o recurso | **serve** |
| 5 — recurso no plural, em português | **serve** |
| 6 — ISO 8601 com fuso | **serve** |
| 7 — dinheiro em decimal, como texto | **serve** |

Nenhuma regra caiu. Duas ganharam ressalva que só apareceria construindo.

---

## O que falta — e o que não falta

**As nove ações do fluxo têm recurso.** A finalização, que carrega a maior parte
da regra — caixa do dia (**D28**), débito do cliente, pontuação, cupons, comissão
de indicação —, **não foi reescrita**: o endpoint chama
`VendaPagamentoService::finalizar()`, e as recusas que ele acrescenta são as
mesmas do controller antigo, traduzidas para 422 e 409.

**~~Falta a tela.~~ A tela existe desde 09/09/2026** — `frontend/src/telas/`
`Comandas.tsx`, em React, usando a sessão do ERP pelo proxy de mesma origem.
Ela lista comandas abertas, abre uma, lança e tira item, recebe e estorna
pagamento, autoriza fiado e finaliza: o ciclo inteiro de um atendimento.

O parágrafo original dizia que chamar a fatia de vertical seria dizer mais do
que se mediu. Estava certo — e o que a medição de verdade custou está nas
seções **9**, **10** e **11**: um defeito que fabricava dinheiro, um endpoint
que faltava, e um portão que não enxergava uma das três guardas da casa.
Nenhum dos três apareceria lendo o servidor de novo.

**O que a tela ainda NÃO oferece, de propósito:** `pagarDebitos` na finalização.
A bandeira quita todos os débitos abertos do cliente na unidade sem conferir se
a comanda os cobre — sem a etapa de lançar os débitos antes
(`POST /adm/api/comandas/:id/debitos`), mandá-la **perdoa a dívida sem
cobrá-la**. Um botão assim não entra numa tela antes da etapa que o justifica.
*(Ressalva de 08/10/2026: o botão continua fora, mas a tela passou a mandar a
bandeira sozinha quando a comanda diz que os débitos já foram somados -- ver as
notas abaixo.)*

*Notas de 06/10/2026.* O servidor confere desde 09/09/2026: a bandeira sem a soma recebe 422 (o D159).
E fica um caminho aberto por esta omissão: a comanda em que o "Quitar" do ERP já somou os débitos,
fechada por esta tela, cobra os débitos no total e não os baixa -- a tela não manda a bandeira, e o
recurso não diz que a comanda os tem. É o D380, parcial, com a pergunta ao dono.

*08/10/2026, a resposta 41 do dono (o D380 ainda parcial).* O recurso da comanda ganhou `debitosSomados`
(decimal em texto, ou nulo) em toda resposta de `/adm/api/comandas` -- o devido do cliente na unidade da
sessão, quando o total já o cobre (`RegraDosDebitosNaVenda::jaSomados`, a conta da comanda reaberta do
ERP), e só na comanda aberta. A tela mostra "Débitos de R$ X já somados; serão quitados ao finalizar." e
manda `pagarDebitos: true` na finalização; sem o campo, não manda. O botão de somar os débitos pela tela
nova continua fora: a bandeira só vai quando o servidor diz que a soma já foi feita (pelo "Quitar" do ERP,
ou por `POST .../debitos`). Na verificação do mesmo dia, o finalizar SEM a bandeira, com os débitos já
somados, passou a ser recusado com 409 `debitos_somados` (a tela que abriu a comanda antes do "Quitar" do
ERP mandava o corpo sem ela, e o cliente pagava os débitos com eles abertos); a tela relê a comanda nessa
recusa, o aviso aparece, e o operador finaliza de novo com a bandeira. Nada se baixa sem a bandeira.

**O que ficava (até 10/10/2026):** a comanda com um débito que nasceu depois do "Quitar" do ERP,
fechada por esta tela. O total não cobre o devido, o campo vem nulo, a tela não avisa nem manda a bandeira,
e o cliente paga a parte já somada com todos os débitos abertos. Se o débito nasce com a comanda já aberta
aqui e o aviso já na tela, a bandeira vai e o servidor a recusa com 422 a cada clique, com a frase de somar
os débitos -- que esta tela não oferece; a saída hoje passa pelo ERP, cujo "Quitar" soma o que falta. Os
dois casos foram ao dono (o lote de 08/10/2026).

*10/10/2026, a resposta 54 do dono (o item PDV-TERCEIRA-RODADA).* Os dois casos acima, pela opção (a): o recurso
ganhou `debitosSomadosEmParte` (`{somado, devido}` em decimal de texto, ou nulo) -- o que o total já tem e o que o
cliente deve, quando a soma não cobre o devido --, e a tela mostra "Débitos de R$ 50,00 somados, mas o cliente deve
R$ 60,00: some o que falta pelo ERP antes de finalizar, ou os débitos ficam abertos." e finaliza sem a bandeira. Na
recusa da bandeira (o 422 no campo `pagarDebitos`) a tela relê a comanda, pelo campo e não pela frase: o aviso troca,
e a recusa não se repete. O botão de somar os débitos continua fora (a opção (b), que o dono não escolheu): quitá-los
pede o "Quitar" do ERP.

**O que fica nesta tela (a verificação do mesmo item, 10/10/2026; o D380 segue PARCIAL):** o aviso aparece para quem
leu a comanda **depois** do "Quitar" do ERP. A tela que a abriu antes e não a releu -- ela só relê nas duas recusas, o
409 `debitos_somados` e o 422 da bandeira -- finaliza com o corpo `{}`; se nasceu um débito no meio, o total cobre só
parte do devido, nenhuma das duas recusas sai, e a comanda fecha com os débitos abertos, sem o aviso. Precisa do fiado
já autorizado (o "Quitar" do ERP não revoga a autorização, o resto do D165) ou de um pagamento que já cobria o total
novo. Foi ao lote, com a recomendação de um 409 `debitos_em_parte` até a tela mostrar o aviso; o D380 também segue
parcial por um caso do ERP (ver o registro).

*12/10/2026, as respostas 77 a 79 do dono (o item PDV-QUITAR-E-PACOTE; o D380 corrigido).* O caso de cima, pela 78 (a
opção (a)): o finalizar sem a bandeira, com os débitos somados em parte, responde **409 `debitos_em_parte`** (com
`detalhes.somado` e `detalhes.devido`) quando o corpo não traz em `debitosEmParteVistos` o somado de agora, em decimal
de texto. Nada é gravado. A tela manda o campo sempre que mostra o aviso da 54, com o somado do aviso; nessa recusa ela
relê a comanda (o terceiro caminho que relê, pelo `motivo`), diz que a comanda mudou em outra tela, o aviso aparece, e o
clique seguinte manda o somado relido e fecha como a 54 decidiu -- sem quitar, os débitos abertos. Se o somado mudar no
meio (outro "Quitar"), a recusa volta. O ERP não muda: na 77 o dono escolheu "Como está". E o `POST .../debitos`, pela 79
(a opção (a)), responde **409 `debitos_em_outra_venda`** (com `detalhes.venda`) quando outra venda aberta do cliente na
unidade já tem os débitos somados -- "Os débitos deste cliente já estão somados na comanda 123. Finalize-os lá." --, a
mesma recusa do "Quitar" do ERP. A tela nova não tem o botão de somar, e não muda por isso.

*12/10/2026, a verificação do mesmo item (o D380 de novo parcial).* A dívida recebida pela tela de Devedores -- a do ERP
ou a da API, `POST /adm/api/devedores/:id/quitacao`, o mesmo serviço -- depois do "Quitar" ainda é cobrada de novo no total da
comanda: com o devido em zero, `debitosSomados` e `debitosSomadosEmParte` saem nulos, a tela não avisa, e a recusa da 78
não dispara. Fixado em teste e levado ao lote. Na venda em andamento, a frase da 79 passou a dizer quem a solta ("quem
abriu essa venda a finaliza ou recarrega o PDV"), porque ela só sai do PDV de quem a abriu.

*10/10/2026, as respostas 60 e 61 do dono (o item PACOTE-TERCEIRA-RODADA; o D452 e o D453 corrigidos).* A
finalização recusa com **409 `sessoes_do_pacote`** a comanda cujas sessões passam do que o pacote ainda tem -- a segunda
de duas vendas abertas do cliente com a mesma última sessão, ou o item de 2 sessões no pacote que vence antes sem saldo
para as duas. A frase diz qual pacote e quantas sessões faltam, e `detalhes.pacotes` traz, por pacote, `pacote`, `nome`,
`servico` e `faltam`. Nada é gravado. A tela mostra a frase como mostra todo 409 que não é `debitos_somados`; nenhum
`.tsx` mudou. E o `DELETE .../itens/:item` do item que gastou a sessão devolve a comanda com o item mais antigo do
mesmo serviço que estava pelo preço já como sessão (`sessaoDePacote: true`, `valorTotal: "0.00"`), quando o pacote o
cobre; o total cai junto, e os pagamentos saem, como em toda remoção.

*11/10/2026, a verificação do item (o D464 corrigido).* O 409 `sessoes_do_pacote` vem **antes** do 409 do fiado sem
autorização: a saída dele é tirar o item da sessão, e tirar item revoga a autorização do fiado -- na ordem de antes, o
gerente autorizava duas vezes. E ele vale também na **corrida**: dois caixas fechando no mesmo instante vendas com a
mesma última sessão. A recusa é a própria baixa da sessão, condicional e por conta no banco, a primeira escrita do
finalizar; quando só ela recusa (a conferência do controller passou antes de o outro caixa fechar), o controller
pergunta de novo, e o 409 é o mesmo, com a frase e os pacotes -- e não o "Não foi possível finalizar a venda.".

*03/11/2026, a resposta 286 (a) do dono (a fatia 74g, RESPOSTAS-286-E-287; o D687 corrigido).* A finalização recusa com
**409 `pacote_encerrado`** a comanda com uma sessão lançada num pacote que foi encerrado depois do lançamento -- a venda que
deu o pacote cancelada (desde a 74f o cancelamento o encerra) ou o pacote removido da ficha do cliente; o vencido não é
encerrado, e não muda. A frase diz qual pacote e o que fazer ("O pacote "<nome>" (<serviço>) foi encerrado depois que a sessão
dele foi lançada nesta venda (...). Tire da venda o item da sessão e lance-o de novo: a sessão sai de outro pacote ativo do
cliente, ou pelo preço."), e `detalhes.pacotes` traz, por pacote encerrado, `pacote`, `nome` e `servico` (sem `faltam`). Só
conta o item que gasta sessão (quantidade maior que zero). Nada é gravado. **A ordem:** ele vem **antes** do
`sessoes_do_pacote` -- que passou a ler só o pacote ativo, e por isso não diz o encerrado -- e, como ele, antes do 409 do fiado
sem autorização e das recusas do caixa (a saída é a mesma: tirar o item, que revoga a autorização do fiado). **Na corrida** (o
cancelamento entre a conferência do controller e a baixa), a própria baixa condicional recusa -- ela só baixa o pacote ativo --,
o controller pergunta de novo, e o 409 é o mesmo, também antes do `sessoes_do_pacote`. A tela mostra a frase como mostra todo
409 de `motivo` que ela não conhece; nenhum `.tsx` mudou (caso novo no `ComandasLigada.teste.tsx`). No ERP, a mesma frase no
aviso, com os nomes escapados (o `toastr` desenha HTML). Provas no `PacoteEncerradoNoCancelamentoTest`, nos dois bancos.

**Faltam duas coisas que a convenção adiou de propósito**, e nenhuma é desta
fatia: a autenticação por token (decisão de 29/08 — nenhuma tela React existe, e
a troca é de uma camada só) e a paginação por cursor, que ninguém pediu.

**E fica um débito pequeno, com prazo:** os endpoints exigem sessão do ERP e
respondem **403** sem ela. Quando o token existir, isso vira **401** com o
desafio certo — está escrito no código, no lugar onde a troca acontece.
