# Fase 2 — a terceira fatia: o caixa

Escolhida em 06/09/2026, logo depois da revisão das sete regras da API. O
roadmap diz que escolher a fatia é o único passo que não depende nem de decisão
de quem manda nem de janela de produção; este documento é a escolha e o que a
sustenta.

---

## Por que o caixa

**Porque a fatia do PDV termina no meio de uma frase.** Ela vai da comanda
aberta até a venda finalizada — e uma venda só pode ser finalizada com um caixa
aberto. Os dezenove endpoints de hoje assumem que o caixa existe; nenhum sabe
abri-lo, fechá-lo ou tirar dinheiro dele.

E porque é pequeno o suficiente para terminar bem. O módulo inteiro são **três
ações**:

| ação | o que devolve hoje |
|---|---|
| `CaixasController::gerenciar` | tela — **e duas telas diferentes**, conforme o estado |
| `CaixasController::retirada` | JSON `{"sucesso": bool, "mensagem": "<br/> - ..."}` |
| `CaixasController::retiradaCancelar` | redireciona |

A do meio devolve mensagem com **HTML dentro** (`<br/> - Caixa fechado`), que é
a mesma forma que abriu as duas fatias anteriores: texto de tela viajando como
se fosse dado.

E a primeira é o caso mais claro do sistema inteiro de **uma ação que é duas**:

```php
public function gerenciarAction() {
    $caixa = ...->selecionarPorUsuario($this->usuarioId, true);

    if (empty($caixa)) {
        return $this->abrir();
    } else {
        return $this->fechar($caixa);
    }
}
```

Um endereço, dois recursos, e o cliente não tem como saber qual vai receber sem
pedir e olhar. Separar isso é o trabalho.

---

## O mapa, com o verbo tirado do que o serviço FAZ

A emenda à regra 1 nasceu na fatia da agenda: o verbo vem do que o serviço faz
com a linha, não do substantivo da tela. Nesta fatia ela foi aplicada **antes**
de escrever o mapa, e mudou duas escolhas — nos dois casos para o lado oposto do
que a agenda tinha ensinado.

| recurso | verbo | por que este verbo |
|---|---|---|
| `/adm/api/caixas/atual` | `GET` | o caixa aberto de quem está logado, ou 404 |
| `/adm/api/caixas/:id` | `GET` | o caixa com os totais do dia |
| `/adm/api/caixas` | `POST` | `abrir()` faz `new Caixa()` e `persist` — cria mesmo |
| `/adm/api/caixas/:id/fechamento` | `POST` | `fechar()` NÃO apaga nada: faz `setStatus(false)` na mesma linha |
| `/adm/api/caixas/:id/retiradas` | `POST` | `retirada()` grava uma linha nova |
| `/adm/api/caixas/:id/retiradas/:retirada/cancelamento` | `POST` | **não é `DELETE`** — ver abaixo |

### Por que cancelar uma retirada não é `DELETE`

Na agenda, `desmarcar()` chamava `remove()` e a linha sumia: `DELETE` era o
verbo honesto. Aqui é o contrário, e o serviço diz isso em quatro linhas:

```php
$retirada->setStatus(false);
$retirada->setDataAlteracao();
$retirada->setSaida(null);
```

A retirada **fica**, marcada como cancelada, com a hora da alteração. O valor
volta para o caixa, e a *saída* ligada a ela — uma conta a pagar — essa sim é
removida. `DELETE /retiradas/:id` prometeria que o recurso daquele endereço
deixa de existir, e ele continua lá, pedível e legível.

> **13/10/2026 (o D375, a resposta 141 do dono):** a conta a pagar também fica. O cancelamento estorna o
> pagamento dela, com o motivo "Retirada do caixa cancelada" e quem cancelou, e a exclui sem apagar; a retirada
> continua ligada a ela (o `setSaida(null)` acima saiu). E o estorno dela em Contas a Pagar cancela a retirada --
> com o caixa já fechado, é recusado. Com mais de um pagamento ativo na conta a pagar (uma parcela lançada à mão nela,
> e paga), as duas portas recusam, com 409 na API: não se sabe qual é o dinheiro da gaveta (a verificação do D375).
>
> **14/10/2026 (as respostas 171 a 175 do dono):** o caixa fechado continua recusando o estorno; o estorno que cancelou
> a retirada exclui a conta a pagar junto, como o cancelamento daqui; a conta a pagar da sangria não recebe parcela nova
> enquanto a retirada está ativa; e só quem poderia cancelar a retirada aqui -- o privilégio do caixa e o caixa ser seu --
> a cancela pelo estorno em Contas a Pagar. E a retirada de R$ 0 é recusada (422 com o campo `valor`, com ou sem
> `financeiro`; o D480).

Duas fatias seguidas, a mesma pergunta, respostas opostas. É o argumento de que
a emenda precisava: nenhum dos dois verbos se deduz do nome da ação.

---

## A pergunta que esta fatia tem de responder: o `GET` que escreve

`gerenciarAction`, no ramo de fechar, chama isto **a cada vez que a tela abre**:

```php
$service->atualizar($caixa, $pagamentos, $retiradas);
```

E `atualizar()` recalcula `valor_final` = inicial + pagamentos − retiradas e
**grava**. Ou seja: a tela de fechamento é uma leitura que escreve, e o número
guardado na coluna é um cache de algo derivável.

A regra 1 existe justamente por isso — `GET` não muta. Então o recurso vai
**calcular e devolver, sem persistir**. Consequência escrita aqui para não ser
esquecida: a coluna `caixas.valor_final` continua sendo o que a tela velha
gravou por último, e a API pode responder um número mais novo que o da coluna.
Não é divergência: é a diferença entre o valor calculado agora e o carimbo da
última vez que alguém abriu a tela.

Se isso incomodar, o conserto não é a API gravar — é a coluna deixar de existir.
Essa decisão não é desta fatia.

---

## O que esta fatia não vai resolver

- **O D28 continua como está.** `VendaPagamentoService::finalizar()` recusa a
  venda quando o caixa é de data anterior; a fatia não muda a regra, só a
  expõe com um código HTTP em vez de uma mensagem.
- **As saídas** (contas a pagar) são outro módulo. Aqui elas aparecem só onde o
  cancelamento de retirada as remove.
- **O relatório de caixa** (`rel-caixas`) fica onde está: é relatório, e a
  Fase 2 não converte relatório.

---

## O diário da construção

### 06/09/2026 — os seis recursos de pé

Construídos no mesmo dia da escolha. **Vinte e cinco endpoints** em `/adm/api`,
somando as três fatias; 286 testes de caracterização nos dois bancos.

| recurso | verbo | resposta na medição |
|---|---|---|
| `/adm/api/caixas/atual` | `GET` | 200 |
| `/adm/api/caixas/:id` | `GET` | 200 |
| `/adm/api/caixas` | `POST` | 422 sem corpo |
| `/adm/api/caixas/:id/fechamento` | `POST` | 200 |
| `/adm/api/caixas/:id/retiradas` | `POST` | 409 (o fechamento roda antes, na ordem alfabética) |
| `/adm/api/caixas/:id/retiradas/:retirada/cancelamento` | `POST` | 404 |

O mapa não mudou durante a construção — é a primeira fatia em que isso acontece,
e a razão é direta: o verbo foi tirado do serviço **antes** de o mapa ser
escrito. Nas duas anteriores ele foi tirado do nome da ação, e as duas
precisaram de correção.

#### Uma decisão que a construção acrescentou

O plano dizia que o `GET` calcula sem persistir. O que ele não previa é que **a
escrita precisa persistir antes de delegar**:

```php
if ($caixa->getValorFinal(false) < $retirada->getValor(false)) {
    return false;
}
```

`CaixaService::retirada()` compara a sangria com a **coluna**, não com um total
recalculado. Se o endpoint não chamar `atualizar()` antes, a trava olha um
número velho: recusa sangria legítima depois de uma venda, ou deixa passar o que
não cabe. A tela velha acerta por acidente — o operador sempre passa pela tela de
fechamento, que atualiza, antes de chegar ao botão.

Então a regra da fatia ficou com duas metades: **leitura calcula e não grava;
escrita grava antes de delegar.** O mesmo vale para o fechamento, que só vira o
status — sem `atualizar()` antes, o caixa fecharia carimbando um total anterior
às vendas do dia.

Isso apareceu como teste vermelho antes de aparecer como raciocínio: a fixture
não grava `valor_final`, a coluna vem nula, e a primeira versão do teste de
retirada não conseguiu criar retirada nenhuma.

#### E uma diferença deliberada em relação à tela velha

`selecionarRetiradasPorCaixa()` filtra `status = true`. Quer dizer que a tela de
fechamento **não mostra sangria cancelada** — ela some da lista como se nunca
tivesse existido. Para conferir um caixa isso é ruim: sangria cancelada é um fato
do dia, com hora.

O recurso lista todas, com o estado de cada uma, e **soma só as ativas** —
cancelada devolveu o dinheiro, e contá-la subtrairia duas vezes. É a primeira vez
que uma fatia da Fase 2 decide mostrar mais do que a tela atual mostra, e por
isso está escrito aqui em vez de ficar implícito no código.

### 06/09/2026 — o ensaio de fluxo, que só existiu porque há três fatias

O medidor de `/adm/api` diz, no próprio cabeçalho, o que não faz:

> Ele NÃO prova que o fluxo funciona: para isso é preciso encadear as chamadas
> com os ids que cada uma devolve, e isso é teste, não medição.

Com o caixa de pé, encadear passou a ser possível — o caixa que a terceira fatia
abre é o mesmo em que a primeira derruba o pagamento. `scripts/ensaiar-fluxo-adm-api.php`
faz **um dia da barbearia** em dez passos e confere os números:

```
  1. o caixa aberto do operador                        200
     saldo de partida: 269.80
  2. abrir comanda para o cliente                      201
  3. lancar o servico na comanda                       201
  4. receber o pagamento em dinheiro                   201
  5. finalizar a venda                                 200
  6. o pagamento apareceu no saldo do caixa            200
     saldo depois da venda: 359.70
  7. tirar dinheiro do caixa                           200
  8. fechar o caixa                                    200
  9. nao ha mais caixa atual                           404
 10. abrir o caixa do dia seguinte                     201
```

**269,80 + 89,90 = 359,70.** É a conta que nenhum endpoint sozinho prova: o
pagamento entra pela fatia do PDV e aparece no saldo que a fatia do caixa
calcula.

#### O ensaio corrigiu quem o escreveu, duas vezes

Ele é teste, e não medição: cada passo declara o código que espera, e qualquer
outro reprova. As duas primeiras execuções falharam **por causa do ensaio**, não
do sistema:

| o que eu escrevi | o que a API respondeu |
|---|---|
| item sem `valorUnitario` | 422 com `campos: {valorUnitario: "decimal com ponto"}` |
| esperava 200 ao lançar item | **201** — lançar item cria um recurso |

A primeira é a regra 3 da convenção fazendo exatamente o trabalho para o qual
foi escrita: o corpo do erro disse **qual campo** faltava, e não só que algo
faltava.

#### O passo 10 existe por causa de interferência

Fechar o caixa da fixture deixaria o cenário diferente de como as fixtures o
entregam, e o medidor, que roda logo depois, encontraria `/caixas/atual` em 404 —
a cobertura dele cairia de 8 para 6 sem que nada estivesse pior.

Abrir um caixa novo no fim devolve o cenário ao lugar **e** prova o único
endpoint que nenhuma outra medição alcança: com um caixa já aberto,
`POST /adm/api/caixas` sempre responderia 409. A interferência virou cobertura.

### 06/09/2026 — a agenda entrou no fluxo, e agora são as três fatias

O ensaio terminava dizendo que não provava nada sobre a agenda. Provava pouco
porque começava no caixa; a barbearia não começa aí. Com três passos na frente,
ele passou a fazer o dia inteiro, **na ordem em que acontece**:

```
  1. os horarios livres do colaborador                 200
     dia 2026-10-06, primeiro horario livre: 08:00
  2. marcar o horario para o cliente                   201
  3. o cliente chegou (check-in)                       200
  4. o caixa aberto do operador                        200
     ... (o resto do dia)
 13. abrir o caixa do dia seguinte                     201
```

**Treze passos, e o horário sai da resposta anterior** — o ensaio pergunta quais
estão livres e marca no primeiro que a API oferecer. É o encadeamento de
verdade: se a grade e a marcação discordarem sobre o que é um horário livre, o
passo 2 quebra.

**A data é relativa**, `+30 dias`, e não a data fixa que os testes usam. O
motivo que estava escrito aqui era falso — dizia que `RegraDeAgendamento` recusa
agendamento no passado, e ela não olha o dia (corrigido em 23/09/2026, junto com
o mesmo comentário no ensaio). A data relativa mantém o ensaio fora do dia em que
as fixtures e o ensaio de recusas escrevem e apagam.

E ele corrigiu quem o escreveu pela terceira vez: eu li `estado` na raiz da
resposta do check-in, e ele vem dentro de `agendamentos[0]` — o check-in
responde a **família**, porque serviço de dois períodos ocupa dois horários
seguidos e a chegada vale para os dois.

### 26/09/2026 — a tela nova pediu três chaves, um motivo e uma trava

A tela do caixa da Fase 3 foi a primeira consumidora desta fatia, e -- como a
roadmap prometia da tela que serve de instrumento de medida da API -- achou o que
faltava antes de achar defeito de tela.

**O que o recurso ganhou**, sem rota nova:

| chave | por quê |
|---|---|
| `totais.pagamentosPorForma` | `selecionarPagamentosPorCaixa` já devolvia a soma agrupada por forma, e o controller só somava e jogava o agrupamento fora. É o que o operador confere na gaveta. Sai das MESMAS linhas da soma, para as duas baterem. |
| `deOutroDia` | a regra do D28 foi para `ISVenda\Regra\RegraDoCaixaDoDia`, sem relógio dentro; a venda e o recurso a chamam. O navegador não compara data: o relógio do balcão pode estar errado. |
| `retiradas[].contaAPagar` | a sangria com `financeiro` cria uma conta a pagar quitada, e o cancelamento a estorna e a exclui, sem apagar desde 13/10/2026 (D199, D375). A tela precisa saber que ela existe antes de oferecer cancelar; na retirada cancelada, continua verdadeiro. |
| `retiradas[].devolucaoDeCredito` | (16/10/2026, o D500) a retirada que veio de uma devolução de crédito (a resposta 89) traz `{cliente: {id, nome}, desfeitaEm}`; a sangria, `null`. Cancelar essa retirada desfaz a devolução -- o crédito volta ao cliente (a resposta 231 (a)) --, e a resposta do cancelamento diz isso em `desfeitaEm`. A tela precisa saber antes de oferecer o gesto; separar a devolução da sangria é por aqui, e nunca pelo motivo. Vem de `RegistroDaDevolucao::doCaixa`, e é `null` em todas sem a tabela no banco. |

E o 409 da finalização da comanda ganhou `detalhes.motivo` -- `sem_caixa` ou
`caixa_de_outro_dia` --, para a comanda oferecer "ir ao caixa" sem ler a frase.

**A guarda mudou** de `vendas/caixa` para `vendas/nova`, o bit do caixa do ERP
e do menu (D227).

**A retirada e o fechamento passaram a gravar sob a trava da linha do caixa**
(D228). A escrita já recalculava o saldo antes de delegar -- a decisão escrita
no cabeçalho do controller --, mas fora da trava; agora o recálculo mora no
serviço, depois dela. A leitura continua sem escrever nada.

**Depois da revisão adversarial da mesma tarde** (22 achados, os 22
confirmados), a trava chegou às outras escritas do caixa: o recálculo e o
fechamento da tela antiga, o cancelamento de retirada -- que também passou a
recusar caixa fechado --, e a abertura, que trava a linha do operador para não
abrir dois caixas. A comparação do saldo passou a ser em centavos, e o valor
ganhou teto nas duas pontas. Ver o D228.

**O que a tela não oferece:** cancelar retirada. O serviço agora o faz sob a
trava, mas o cancelamento ainda apaga a conta a pagar quitada que a sangria
criou (D199, D133), e isso pede decisão antes de virar botão. *13/10/2026: decidido (a
resposta 141, o D375) -- o cancelamento estorna e exclui a conta a pagar, sem apagar.*
