# TasquinhaPOS — Testing Checklist (Hardware + Real)

> Checklist para validação **com hardware real** antes de operação ao vivo.
> Última atualização: Fase 4 polish.
>
> **PINs:** admin `1111` · manager `2222` · cashier `3333` · waiter `4444` · kitchen `5555`

---

## A. Pré-flight (uma vez)

- [ ] `git pull && composer install && npm install`
- [ ] `cp .env.example .env` e configurar:
  - [ ] `APP_URL=http://<IP-da-maquina>:8000`
  - [ ] `DB_*` apontar para MySQL (Docker ou local)
  - [ ] `BROADCAST_CONNECTION=reverb`
  - [ ] `REVERB_APP_KEY`, `REVERB_APP_SECRET`, `REVERB_APP_ID` (gerar)
  - [ ] `VITE_REVERB_APP_KEY=${REVERB_APP_KEY}` `VITE_REVERB_HOST="<IP-da-maquina>"` `VITE_REVERB_PORT=8080` `VITE_REVERB_SCHEME=http`
  - [ ] `QUEUE_CONNECTION=database` (ou redis)
- [ ] `php artisan key:generate`
- [ ] `docker compose up -d` (MySQL + Redis)
- [ ] `php artisan migrate:fresh --seed --force`
- [ ] `npm run build`
- [ ] Confirmar acessível na LAN: `curl http://<IP>:8000/up`

## B. Processos a manter vivos

Em janelas separadas (ou supervisor/systemd):

```bash
php artisan serve --host=0.0.0.0 --port=8000
php artisan reverb:start --host=0.0.0.0 --port=8080
php artisan queue:work --tries=8 --backoff=30,60,120,300,600,1800,3600,7200
```

- [ ] Os 3 processos arrancam sem erros
- [ ] `storage/logs/laravel.log` não tem ERROR após arranque

## C. Tablets / dispositivos POS

- [ ] Cada tablet liga ao mesmo Wi-Fi da máquina servidor
- [ ] Browser abre `http://<IP>:8000/pos` → redireciona a `/pin-login`
- [ ] Adicionar à home screen (PWA-like) em iPad/Android
- [ ] Touch targets clicáveis sem zoom acidental
- [ ] Rotação landscape funciona

## D. Login PIN

- [ ] PIN errado → erro vermelho, tablet não bloqueia
- [ ] PIN `4444` (waiter) → entra `/pos`
- [ ] PIN `4444` em `/caixa` → **403** (role guard cashier|manager|admin)
- [ ] PIN `3333` (cashier) → entra `/caixa`
- [ ] Auto-submit ao 4º dígito
- [ ] Botão **Sair** no badge → POST `/pin-logout` → volta a `/pin-login`
- [ ] Sessão persiste após refresh

## E. POS — Pedido

- [ ] FloorPlan mostra mesas posicionadas (x/y/shape)
- [ ] Cores: livre = zinc · ocupada = âmbar · com pedido = verde
- [ ] Clicar mesa abre `OrderScreen`
- [ ] Categorias filtram produtos
- [ ] Adicionar produto com modificadores → preço atualiza com deltas
- [ ] Quantidade ajustável no carrinho
- [ ] Nota por item / por order
- [ ] **Enviar para cozinha** cria Order(sent) + tickets por route distinta
- [ ] FloorPlan reflete estado em tempo real (broadcast `account.updated`)

## F. KDS — Cozinha realtime

- [ ] `/kds/cozinha` num ecrã grande
- [ ] Enviar pedido em `/pos` → ticket aparece no KDS em **<2s** (sem refresh)
- [ ] Som "ding" toca em novo ticket (verificar volume; primeiro ding pode exigir interação do utilizador para autorizar áudio)
- [ ] Card mostra: mesa, hora, items+modificadores, notas
- [ ] **Iniciar** → amarelo (`preparing`)
- [ ] **Pronto** → verde (`ready`); marca `kds_ready_at`
- [ ] **Entregue** → cartão sai do board
- [ ] Cards >5min ficam âmbar; >10min ficam rosa (urgência)
- [ ] Múltiplos KDS (`/kds/cozinha`, `/kds/bar`) recebem só os seus tickets

## G. Impressora HPRT TP80K — Setup

> Modelo: **HPRT TP80K** (térmica 80mm, 200mm/s, ESC/POS).

- [ ] Ligar impressora à corrente; rolo de papel térmico inserido
- [ ] Ligar cabo Ethernet ao mesmo switch/router da máquina servidor
- [ ] Test print da impressora (botão FEED + Power) imprime auto-teste com IP atual
- [ ] Anotar IP (ex.: `192.168.1.50`)
- [ ] Se DHCP variar: configurar IP estático no router (DHCP reservation) **OU** via utilitário Windows da HPRT
- [ ] `ping <IP-impressora>` responde
- [ ] `nc -zv <IP-impressora> 9100` → "succeeded"

### Configurar no backoffice

- [ ] `/admin/printers` → **Nova impressora**
  - Nome: `Caixa principal`
  - Kind: `receipt`
  - Connection: `ethernet`
  - Host: `192.168.1.50` (o IP anotado)
  - Port: `9100`
  - Width chars: `48` (default 80mm)
  - Cut paper: ✓
  - Open drawer: ✓ (se gaveta ligada à RJ11 da impressora)
  - Active: ✓
- [ ] Action **Test print** → imprime talão "TASQUINHA POS · talão de teste"
- [ ] Repetir para cozinha:
  - Kind: `kds`, Route: `cozinha`
- [ ] (Opcional) Bar:
  - Kind: `kds`, Route: `bar`

### Cenários impressora

- [ ] Cabo desligado → enviar pedido NÃO rebenta o request; toast informativo
- [ ] `storage/logs/laravel.log` regista warning "Falha ao ligar a Caixa principal"
- [ ] Religar cabo → próximo pedido imprime ok
- [ ] Sem papel → impressora pisca LED; servidor envia ok mas nada sai → repor papel e re-imprimir do `/admin/orders` (TODO Fase 5)
- [ ] Caracteres acentuados (à á ç ã) saem corretos no talão (CP858 default da TP80K)
- [ ] Largura 48 chars: nenhuma linha trunca

## H. Caixa — Sessão

- [ ] `/caixa` carrega lista de contas em aberto à esquerda
- [ ] Sem sessão: badge **"Caixa fechada"** vermelho
- [ ] **Abrir Caixa** com fundo `50,00` → cria CashSession, badge fica verde com hora
- [ ] Tentar pagar com caixa fechada → erro "Sessão de caixa não está aberta."
- [ ] **Fechar Caixa (Z)**:
  - mostra Esperado = fundo + dinheiro recebido
  - introduzir contado real → grava `diff_cents`
  - imprime Z-Report na impressora `kind=receipt`
  - Z-Report inclui: cabeçalho, sessão #, abertura/fecho, fundo, totais por método (cash/card/mbway/transfer/other) com contagem, esperado, contado, **DIFERENÇA**

## I. Caixa — Pagamento Total

- [ ] Selecionar conta → painel direito mostra Total / Pago / **Falta receber** (caixa âmbar destacada, fonte 5xl)
- [ ] Default toggle "🧾 Emitir Fatura no Moloni" está **ON**
- [ ] Atalho **EXATO** preenche montante = balance
- [ ] Método **Dinheiro** + Entregue > Montante → **TROCO** ao vivo a verde
- [ ] **COBRAR** → abre **modal de confirmação** com:
  - Montante grande
  - Método
  - Mesa, Conta #
  - Linha "Será emitida Fatura Simplificada no Moloni" (se toggle ON)
- [ ] Cancelar fecha modal sem nada cobrado
- [ ] **CONFIRMAR** → cria Payment, imprime talão, toast `"Pagamento registado: X,XX € · Fatura Moloni em emissão…"`
- [ ] Após queue processar, próximo refresh mostra nº fatura na lista de pagamentos
- [ ] Cobrar valor parcial < balance → **NÃO** mostra confirm modal (pagamento direto)

## J. Caixa — Split por Pessoas

- [ ] **👥 Dividir pessoas** abre modal aba "Por Pessoas"
- [ ] Botões 2/3/4/5/6/8/10 atualizam share `intdiv(balance, n)`; resto vai para a última
- [ ] Cada linha: dropdown método independente
- [ ] **Cobrar todos** → cria N payments com `meta.split=people, index=i, of=n`
- [ ] Apenas a última chamada dispara EmitMoloniInvoice (1 fatura agregada)
- [ ] Talão imprime para cada pagamento

## K. Caixa — Split por Itens

- [ ] **🧾 Dividir itens** abre modal aba "Por Itens"
- [ ] Lista todos OrderItem com checkbox; **Total selecionado** atualiza ao vivo
- [ ] Escolher método e **Cobrar selecionados** → 1 Payment com `meta.split=items, lines=[{order_item_id, qty}]`
- [ ] Pagar resto depois fecha conta normalmente

## L. Moloni — Sandbox

### Setup

- [ ] Conta de testes em https://moloni.pt → criar empresa **sandbox**
- [ ] App registada em https://www.moloni.pt/dev/ → obter `client_id` + `client_secret`
- [ ] User com permissões API (username + password)
- [ ] No `/admin/settings` (Moloni tab):
  - [ ] Enabled: ✓
  - [ ] Sandbox: ✓
  - [ ] client_id, client_secret, username, password
  - [ ] company_id (do dashboard Moloni)
  - [ ] document_set_id (Documentos → Séries)
  - [ ] options.final_consumer_id (cliente "Consumidor final" criado em Moloni)
  - [ ] options.payment_method_map (mapear cash→1, card→2, mbway→3, transfer→4, other→5 — IDs reais do tenant Moloni)

### Testes

- [ ] Liquidar conta no `/caixa` → toast "Fatura Moloni em emissão…"
- [ ] `php artisan queue:work` mostra job processado
- [ ] `payments.moloni_document_id`, `moloni_document_number`, `moloni_pdf_url` preenchidos
- [ ] `accounts.status = closed`, `closed_at` preenchido
- [ ] PDF abre via `moloni_pdf_url`
- [ ] Document aparece no dashboard sandbox
- [ ] **Falha de credenciais**: meter password errada → job retry com backoff `[30s,1m,2m,5m,10m,30m,1h,2h]`; ao fim de 8 tentativas log warning, payment fica sem doc
- [ ] Botão manual **Emitir Fatura Moloni** (em conta liquidada sem doc) → tenta agora; toast com `Doc #XXXX`
- [ ] `MoloniSetting.enabled=false` → liquidar conta NÃO chama API

## M. Offline / Falhas simuladas

> Nota: backend exige rede para SQL/broadcast. Esta secção testa **resiliência parcial**.

- [ ] **Reverb desligado** (`pkill -f reverb:start`):
  - [ ] POS continua a operar; KDS deixa de receber realtime mas refresh manual mostra
  - [ ] Sem crashes; reconexão automática quando Reverb volta
- [ ] **Queue worker desligado**:
  - [ ] Pagamentos continuam a registar-se
  - [ ] Faturas Moloni acumulam em `jobs` table
  - [ ] Reiniciar `queue:work` → faturas emitidas na ordem
- [ ] **Moloni indisponível** (firewall block `api.moloni.es`):
  - [ ] Job tenta, falha, reagenda — todas as 8 tentativas
  - [ ] Log "Moloni emit failed" com erro
  - [ ] Conta fica `locked` (paga) mas sem `moloni_document_id`
  - [ ] Reabrir firewall → próximo retry sucede
- [ ] **Impressora offline mid-shift**:
  - [ ] Pagamento regista-se; sem talão; warning no log
  - [ ] Botão "Reimprimir" (TODO Fase 5) — por agora reimprime via re-emitir manualmente
- [ ] **Wi-Fi tablet cai**:
  - [ ] Livewire mostra "Connecting…"; retoma quando volta
  - [ ] Pedido em curso não duplicado (DB::transaction garante)
- [ ] **MySQL cai**:
  - [ ] Tudo congela; nada é gravado (correto — não há fila local)
  - [ ] Recuperar Docker MySQL → operação retoma sem corrupção

## N. Multi-tenant + auditoria

- [ ] Trocar tenant em `/admin` → recursos filtrados por TenantScope
- [ ] Pagamento regista `received_by_user_id` (operador)
- [ ] Talão imprime nome operador
- [ ] `audit_logs` regista CRUD admin

## O. Smoke automatizado (CI / pré-deploy)

```bash
php artisan migrate:fresh --seed --force
npm run build
php artisan serve --port=8765 &
sleep 2
curl -s -o /dev/null -w '/pin-login → %{http_code}\n' http://127.0.0.1:8765/pin-login   # 200
curl -s -o /dev/null -w '/pos       → %{http_code}\n' http://127.0.0.1:8765/pos        # 302
curl -s -o /dev/null -w '/caixa     → %{http_code}\n' http://127.0.0.1:8765/caixa      # 302
curl -s -o /dev/null -w '/kds       → %{http_code}\n' http://127.0.0.1:8765/kds        # 302
curl -s -o /dev/null -w '/admin     → %{http_code}\n' http://127.0.0.1:8765/admin      # 302 (login)
pkill -f 'artisan serve'
```

Esperado: `200,302,302,302,302`.

## P. Go / No-Go produção

- [ ] Todos os blocos D, E, F, G, H, I marcados ✓
- [ ] Pelo menos 1 ciclo completo: PIN → pedido → KDS → entregue → caixa → split → fatura Moloni → talão final
- [ ] Z-Report do dia confere com soma manual
- [ ] Backup MySQL configurado (cron `mysqldump`)
- [ ] Logs com rotação (`storage/logs/laravel.log`)
- [ ] Wi-Fi de backup (router secundário) testado
- [ ] UPS na máquina servidor + impressora caixa
- [ ] Operadores treinados (mínimo 1 cashier + 2 waiters + 1 kitchen)
- [ ] Plano B em papel se sistema cair (talões manuais + folha de caixa)
