Skip to content

Turnos (Sessões de Caixa)

Visão Geral

Um turno rastreia o período de trabalho de um operador em um PDV específico. Ao abrir, o sistema começa a contabilizar vendas; ao fechar, gera resumo financeiro. O gerente de filial pode reconciliar o turno após conferência manual do caixa.

Hierarquia: Branch → PDV → Shift → Order

Localização

backend/src/shifts/shifts.service.tsfrontend-react/src/views/admin/ShiftsView.tsx

Rota

/t/:slug/admin/shifts


Schema

backend/src/shifts/schemas/shift.schema.ts

CampoTipoDescrição
tenantObjectIdTenant
pdvObjectIdTerminal onde o turno ocorre
branchObjectId?Filial (denormalizado de pdv.branch na abertura)
userObjectIdOperador que abriu o turno
startedAtDateInício do turno
endedAtDate?Fim do turno
statusopen|closed|reconciledEstado atual
openingFloatnumberFundo de caixa (default 0)
declaredCashnumber?Dinheiro contado pelo operador no fechamento
totalOrdersnumberPedidos entregues no período
voidedOrdersnumberPedidos cancelados no período
totalRevenuenumberReceita total
cashRevenuenumberReceita em dinheiro
cardRevenuenumberReceita em cartão (crédito + débito)
pixRevenuenumberReceita em PIX
cashDifferencenumber?declaredCash − (openingFloat + cashRevenue) — negativo=sobra, positivo=falta
notesstring?Observações do operador no fechamento
reconciledByObjectId?Gerente que reconciliou
reconciledAtDate?Timestamp da reconciliação
reconciliationNotesstring?Observações da reconciliação
clockInLatitudenumber?GPS latitude do operador na abertura
clockInLongitudenumber?GPS longitude do operador na abertura

Índices:

  • { tenant, pdv, status } — consultas de turno ativo
  • { branch, tenant } — rollup por filial

Fluxo do Turno

Abertura

  1. Operador faz login no PDV
  2. Sistema verifica turno aberto: se já houver, apresenta-o; senão, exibe modal
  3. Operador informa fundo de caixa (dinheiro na gaveta para troco)
  4. ShiftsService.openShift():
    • Fecha automaticamente qualquer turno open anterior do mesmo user+pdv
    • Busca pdv.branch e denormaliza o branch no registro do turno
    • Se filial tem GPS e device enviou coordenadas → valida distância com haversineMetres()
  5. Turno criado com status: open

GPS fence:

  • Só valida quando ambos os lados têm coordenadas (device + filial)
  • Raio padrão: 200m (configurável por filial em gpsRadiusMeters)
  • Erro: 400 Localização fora do raio permitido (Xm — máximo Ym)

Fechamento

  1. Operador clica "Fechar Turno" no PDV ou gerente fecha pelo painel admin
  2. Operador informa o dinheiro contado na gaveta (declaredCash)
  3. ShiftsService.closeShift():
    • Union query migration-safe (para compatibilidade com pedidos sem FK de turno):
      orders WHERE shift = shiftId
      OR (pdv = pdvId AND shift IS NULL AND createdAt IN [startedAt, now])
    • Agrega totalRevenue, cashRevenue, cardRevenue, pixRevenue
    • Calcula: cashDifference = declaredCash - (openingFloat + cashRevenue)
  4. Turno atualizado para status: closed

Reconciliação

Após conferência física, o gerente de filial reconcilia:

  1. Gerente acessa Turnos → filtra por status: closed
  2. Revisa cashDifference — negativo=sobra (caixa tem mais dinheiro que esperado), positivo=falta
  3. Adiciona reconciliationNotes e confirma
  4. ShiftsService.reconcileShift(): turno atualizado para status: reconciled

API

Abrir turno (via PDV)

POST /t/:slug/shifts
Body: { pdvId, openingFloat?, clockInLatitude?, clockInLongitude? }

Abrir turno (via Filial)

POST /t/:slug/admin/branches/:branchId/shifts/open
Body: { pdvId, openingFloat?, userId? }

Valida que pdv.branch === branchId.

Fechar turno

POST /t/:slug/shifts/:id/close
Body: { declaredCash?, notes? }

Reconciliar turno

POST /t/:slug/shifts/:id/reconcile
Body: { reconciliationNotes? }

Listar turnos

GET /t/:slug/shifts?pdvId=&branchId=&status=&from=&to=&page=1&limit=20

Turno ativo de um terminal

GET /t/:slug/shifts/active?pdvId=:id

Retorna 200 com o turno ou 404 se não houver turno aberto.

Turnos ativos de uma filial

GET /t/:slug/admin/branches/:branchId/shifts/active

Lista todos os turnos open nos PDVs desta filial.


Rollup para Relatório da Filial

ShiftsService.getBranchDailyReport(tenantId, branchId, date):

javascript
// Pipeline MongoDB — complexidade O(shifts)
$match  { branch: branchId, startedAt: [dayStart, dayEnd] }
$group  by pdv → soma revenue, cash, card, pix, orders, shifts
$lookup pdvs → pdvName

O campo branch denormalizado no Shift é o que permite esta consulta sem $lookup em Orders — crítico para performance com 10+ PDVs simultâneos.


Fechamento Automático

Se o operador sair sem fechar o turno, o sistema fecha automaticamente quando o mesmo user+pdv abre um novo turno. O fechamento automático ocorre em openShift():

typescript
await this.shiftModel.updateMany(
  { tenant, user, pdv, status: 'open' },
  { status: 'closed', endedAt: new Date() }
);

Relacionados

Lançado sob a licença MIT.