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
| Campo | Tipo | Descrição |
|---|---|---|
tenant | ObjectId | Tenant |
pdv | ObjectId | Terminal onde o turno ocorre |
branch | ObjectId? | Filial (denormalizado de pdv.branch na abertura) |
user | ObjectId | Operador que abriu o turno |
startedAt | Date | Início do turno |
endedAt | Date? | Fim do turno |
status | open|closed|reconciled | Estado atual |
openingFloat | number | Fundo de caixa (default 0) |
declaredCash | number? | Dinheiro contado pelo operador no fechamento |
totalOrders | number | Pedidos entregues no período |
voidedOrders | number | Pedidos cancelados no período |
totalRevenue | number | Receita total |
cashRevenue | number | Receita em dinheiro |
cardRevenue | number | Receita em cartão (crédito + débito) |
pixRevenue | number | Receita em PIX |
cashDifference | number? | declaredCash − (openingFloat + cashRevenue) — negativo=sobra, positivo=falta |
notes | string? | Observações do operador no fechamento |
reconciledBy | ObjectId? | Gerente que reconciliou |
reconciledAt | Date? | Timestamp da reconciliação |
reconciliationNotes | string? | Observações da reconciliação |
clockInLatitude | number? | GPS latitude do operador na abertura |
clockInLongitude | number? | GPS longitude do operador na abertura |
Índices:
{ tenant, pdv, status }— consultas de turno ativo{ branch, tenant }— rollup por filial
Fluxo do Turno
Abertura
- Operador faz login no PDV
- Sistema verifica turno aberto: se já houver, apresenta-o; senão, exibe modal
- Operador informa fundo de caixa (dinheiro na gaveta para troco)
ShiftsService.openShift():- Fecha automaticamente qualquer turno
openanterior do mesmo user+pdv - Busca
pdv.branche denormaliza obranchno registro do turno - Se filial tem GPS e device enviou coordenadas → valida distância com
haversineMetres()
- Fecha automaticamente qualquer turno
- 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
- Operador clica "Fechar Turno" no PDV ou gerente fecha pelo painel admin
- Operador informa o dinheiro contado na gaveta (
declaredCash) 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)
- Union query migration-safe (para compatibilidade com pedidos sem FK de turno):
- Turno atualizado para
status: closed
Reconciliação
Após conferência física, o gerente de filial reconcilia:
- Gerente acessa Turnos → filtra por
status: closed - Revisa
cashDifference— negativo=sobra (caixa tem mais dinheiro que esperado), positivo=falta - Adiciona
reconciliationNotese confirma ShiftsService.reconcileShift(): turno atualizado parastatus: 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=20Turno ativo de um terminal
GET /t/:slug/shifts/active?pdvId=:idRetorna 200 com o turno ou 404 se não houver turno aberto.
Turnos ativos de uma filial
GET /t/:slug/admin/branches/:branchId/shifts/activeLista todos os turnos open nos PDVs desta filial.
Rollup para Relatório da Filial
ShiftsService.getBranchDailyReport(tenantId, branchId, date):
// Pipeline MongoDB — complexidade O(shifts)
$match { branch: branchId, startedAt: [dayStart, dayEnd] }
$group by pdv → soma revenue, cash, card, pix, orders, shifts
$lookup pdvs → pdvNameO 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():
await this.shiftModel.updateMany(
{ tenant, user, pdv, status: 'open' },
{ status: 'closed', endedAt: new Date() }
);