Skip to content

Gerenciamento de Filiais (Branch → PDV)

Visão Geral

A hierarquia enterprise distingue entre unidade física (Branch/Filial) e terminal de transação (PDV). Uma filial pode operar 10+ PDVs simultâneos. A relação é:

TENANT (1)
  └── BRANCH (N)          — unidade física / filial
        ├── CNPJ, endereço, cerca GPS, taxas IBS/CBS
        ├── managerId → USER (gerente da filial)
        └── PDV (N)        — terminal de caixa
              ├── branch → BRANCH  (FK)
              └── SHIFT (N)  — sessão de caixa
                    ├── branch → BRANCH  (denormalizado — agregação rápida)
                    └── ORDER (N)
                          └── shift → SHIFT  (FK sparse)

Shift.branch é denormalizado intencionalmente: o daily-report faz $match { branch: branchId } direto na coleção Shift — O(shifts) sem $lookup nos PDVs.


Schema: Branch

backend/src/branches/schemas/branch.schema.ts

CampoTipoDescrição
tenantObjectIdTenant pai
namestringNome da filial (max 120)
slugstringIdentificador único por tenant (lowercase, [a-z0-9-])
activebooleanSe a filial está operacional
isHQbooleanSe é a sede principal
cnpjstring?CNPJ desta unidade
razaoSocialstring?Razão social
inscricaoEstadualstring?IE
inscricaoMunicipalstring?IM
regimeTributario'1'|'2'|'3'Simples/Presumido/Real
focusNfeTokenstring?Token Focus NFe desta filial
ibsCbsEnabledbooleanHabilita cálculo IBS/CBS (Reforma Tributária)
ibsRatenumber?Alíquota IBS (0–100)
cbsRatenumber?Alíquota CBS (0–100)
ibgeMunicipioCodestring?Código IBGE para apuração regional
addressstring?Logradouro
addressNumberstring?Número
districtstring?Bairro
citystring?Cidade
statestring?UF (2 chars)
cepstring?CEP
latitudenumber?GPS latitude
longitudenumber?GPS longitude
gpsRadiusMetersnumberRaio de validação (default 200m)
openingHours[{dayOfWeek, opens, closes}]Horários por dia da semana
logostring?URL da logo
primaryColorstring?Cor primária
managerIdObjectId?Gerente responsável → USER
evolutionInstancestring?Instância WhatsApp (Evolution API)

Índices:

  • { slug: 1, tenant: 1 } — unique
  • { tenant: 1, active: 1 }

Schema: PDV (atualizado)

backend/src/pdvs/schemas/pdv.schema.ts

Campo adicionado nesta feature:

CampoTipoDescrição
branchObjectId?Branch pai — FK para a filial que contém este terminal

Schema: Shift (atualizado)

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

Campos adicionados nesta feature:

CampoTipoDescrição
branchObjectId?Branch (denormalizado de pdv.branch no momento da abertura)
cashDifferencenumber?declaredCash - (openingFloat + cashRevenue) — negativo = sobra
declaredCashnumber?Dinheiro contado pelo operador no fechamento
reconciledByObjectId?Gerente que reconciliou
reconciledAtDate?Timestamp da reconciliação
reconciliationNotesstring?Observações do gerente
clockInLatitudenumber?GPS do operador na abertura
clockInLongitudenumber?GPS do operador na abertura

Status do turno: open → closed → reconciled


RBAC

Permissões Branch

backend/src/common/enums/permissions.enum.ts

PermissãoDescrição
branch.view.reportsVer relatórios da filial
branch.manage.pdvsGerenciar terminais da filial
branch.manage.shiftsAbrir/fechar/reconciliar turnos
branch.manage.configEditar configurações da filial
branch.*Wildcard — todas as permissões da filial

Role: branch_manager

Definido em backend/src/roles/roles.service.ts (SYSTEM_ROLES).

EscopoAcesso
JWT payloadassignedBranchIds: [id1, id2] (embutido no sign time)
BranchScopeGuardResolve branch IDs → PDV IDs via Redis (branch:pdvs:{branchId}, TTL 300s)
AcessoSomente filiais/PDVs atribuídos; bypass de revogação por PDV individual

Guard chain

JwtAuthGuard → TenantGuard → BranchScopeGuard → RolesGuard

BranchScopeGuard (backend/src/common/guards/branch-scope.guard.ts):

  1. Extrai assignedBranchIds do JWT
  2. Para cada branchId, consulta Redis branch:pdvs:{branchId} ou faz query no MongoDB
  3. Popula request.assignedBranchIds para uso nos controllers
  4. Admins/superadmins bypass automaticamente

API Routes

Todas as rotas abaixo são prefixadas com /t/:slug/admin/branches.

Filiais

MétodoRotaAcessoDescrição
GET/AdminListar filiais
POST/AdminCriar filial
GET/compareAdminComparativo multi-filial
GET/:idAdminDetalhe da filial
PUT/:idAdminAtualizar filial
DELETE/:idAdminRemover filial
GET/:id/statsAdminKPIs da filial

PDVs da Filial

MétodoRotaAcessoDescrição
GET/:id/pdvsAdmin/ManagerPDVs desta filial

Turnos via Filial

MétodoRotaAcessoDescrição
GET/:id/shifts/activeAdmin/ManagerTurnos abertos em todos os PDVs da filial
POST/:id/shifts/openAdmin/ManagerAbrir turno em um PDV desta filial
GET/:id/daily-reportAdmin/ManagerRelatório diário com breakdown por PDV

Daily Report

Request:

GET /t/:slug/admin/branches/:id/daily-report?date=2026-04-25

Response:

json
{
  "totals": {
    "revenue": 3420.50,
    "cash": 980.00,
    "card": 1840.50,
    "pix": 600.00,
    "orders": 47,
    "shifts": 3
  },
  "byPdv": [
    {
      "pdvId": "...",
      "pdvName": "Caixa 1",
      "revenue": 1240.00,
      "cash": 320.00,
      "card": 720.00,
      "pix": 200.00,
      "orders": 18,
      "shifts": 1
    }
  ]
}

Abrir Turno via Filial

Request:

POST /t/:slug/admin/branches/:id/shifts/open
Body: { "pdvId": "...", "openingFloat": 100.00, "userId": "..." }

Validação server-side: pdv.branch === branchId — rejeita se o PDV não pertencer a esta filial.


Lógica de Cash Sessions (Rollup)

Abertura

  1. Operador abre turno no PDV (POST /shifts ou via POST /branches/:id/shifts/open)
  2. ShiftsService.openShift(): fecha qualquer turno aberto anterior do mesmo user+pdv
  3. Busca pdv.branch → denormaliza branch no Shift criado
  4. Se a filial tem GPS configurado e o device enviou coordenadas → valida haversineMetres() ≤ gpsRadiusMeters

Fechamento

ShiftsService.closeShift() usa union query migration-safe:

typescript
orders WHERE shift = shiftId          // pedidos com FK novo
OR (pdv = pdvId AND shift IS NULL     // pedidos legados sem FK
    AND createdAt IN [start, end])

Agrega: totalRevenue, cashRevenue, cardRevenue, pixRevenue, totalOrders, voidedOrders

Reconciliação: cashDifference = declaredCash - (openingFloat + cashRevenue)

Rollup para Relatório da Filial

ShiftsService.getBranchDailyReport() — pipeline MongoDB:

$match  { branch: branchId, startedAt: [dayStart, dayEnd] }
$group  by pdv → { revenue, cash, card, pix, orders, shifts: count }
$lookup pdvs → pdvName
$project pdvId, pdvName, revenue, cash, card, pix, orders, shifts

Complexidade: O(shifts) — scan único na coleção Shifts sem joins em Orders.


Frontend

Views

ViewArquivoDescrição
BranchManagementViewviews/admin/BranchManagementView.tsxLista de filiais + criar
BranchDetailViewviews/admin/BranchDetailView.tsxDetalhe com tabs
BranchPdvManagementViewviews/admin/BranchPdvManagementView.tsxGrid Quiet Luxury de terminais
BranchCashSessionsViewviews/admin/BranchCashSessionsView.tsxSessões de caixa da filial
BranchOverviewViewviews/admin/branch/BranchOverviewView.tsxOverview na BranchLayout
BranchSettingsViewviews/admin/branch/BranchSettingsView.tsxConfigurações da filial

Rotas React

/t/:slug/admin/branches              → BranchManagementView
/t/:slug/admin/branches/:id          → BranchDetailView
/t/:slug/admin/branches/:id/cash-sessions → BranchCashSessionsView
/t/:slug/admin/terminals             → BranchPdvManagementView
/t/:slug/admin/b/:branchSlug/overview   → BranchOverviewView
/t/:slug/admin/b/:branchSlug/settings   → BranchSettingsView
/t/:slug/admin/b/:branchSlug/stock      → BranchStockView
/t/:slug/admin/b/:branchSlug/inventory  → BranchInventoryView
/t/:slug/admin/b/:branchSlug/orders     → BranchOrdersView
/t/:slug/admin/b/:branchSlug/payments   → BranchPaymentsView
/t/:slug/admin/b/:branchSlug/tables     → BranchTablesView
/t/:slug/admin/b/:branchSlug/balcao     → BranchBalcaoView

BranchPdvManagementView — Design Quiet Luxury

Layout two-column:

  • Esquerda: stats da filial (receita total, turnos ativos, operadores)
  • Direita: grid de PdvCard com StatusDot (green=ativo, gray=inativo), MonoNumber para receita, botão "Abrir Caixa" → FormModal

Modal "Abrir Caixa" envia POST /branches/:id/shifts/open { pdvId, openingFloat }.


Relacionados

Lançado sob a licença MIT.