API - Estoque
Endpoints
GET /t/:slug/inventory
Lista todos os itens do estoque.
Response:
[
{
"_id": "...",
"name": "Filé Mignon",
"sku": "CAR-001",
"currentStock": 15,
"minStock": 5,
"unit": "kg",
"cost": 4500,
"status": "active"
}
]POST /t/:slug/inventory
Cria um novo item.
Body:
{
"name": "Picanha",
"sku": "CAR-002",
"currentStock": 10,
"minStock": 3,
"unit": "kg",
"cost": 5500
}PUT /t/:slug/inventory/:id
Atualiza um item.
DELETE /t/:slug/inventory/:id
Remove um item.
Movimentações
POST /t/:slug/inventory/:id/movements
Registra uma movimentação.
Body:
{
"type": "entry",
"quantity": 5,
"reason": "Compra fornecedor"
}Tipos de Movimentação
| Tipo | Descrição |
|---|---|
entry | Entrada (compra, importação NFS) |
exit | Saída (venda, perda) |
adjustment | Ajuste de inventário |
transfer | Transferência entre filiais |
reservation | Reserva interna |
Motivos de Movimentação
| Motivo | Descrição |
|---|---|
purchase | Compra de fornecedor |
sale | Venda (dedução automática) |
transfer | Transferência |
adjustment | Ajuste manual |
production | Produção interna |
loss | Perda |
theft | Furto |
expiration | Vencimento |
return | Devolução |
Ficha Técnica (Ingredientes por Prato)
Gerencia o vínculo bill-of-materials entre itens de cardápio e itens de estoque. Requer role admin.
GET /t/:slug/inventory/ingredients
Lista todos os ingredientes de um item de cardápio.
Query params:
| Parâmetro | Obrigatório | Descrição |
|---|---|---|
menuItemId | sim | ID do item de cardápio |
Response 200:
[
{
"_id": "66a1b2c3d4e5f6a7b8c9d0e1",
"menuItem": "66a1b2c3d4e5f6a7b8c9d0e2",
"inventoryItem": {
"_id": "66a1b2c3d4e5f6a7b8c9d0e3",
"name": "Carne Bovina",
"sku": "CAR-001",
"unit": "kg"
},
"quantityPerUnit": 0.2,
"unit": "kg",
"tenant": "66a1b2c3d4e5f6a7b8c9d0e4"
}
]POST /t/:slug/inventory/ingredients
Cria um novo vínculo ingrediente ↔ prato.
Body:
{
"menuItemId": "66a1b2c3d4e5f6a7b8c9d0e2",
"inventoryItemId": "66a1b2c3d4e5f6a7b8c9d0e3",
"quantityPerUnit": 0.2,
"unit": "kg"
}| Campo | Tipo | Regras |
|---|---|---|
menuItemId | MongoId | obrigatório |
inventoryItemId | MongoId | obrigatório |
quantityPerUnit | number | obrigatório, mín. 0.001 |
unit | string | opcional — apenas informativo |
Response 201: documento ProductIngredient criado.
Response 409: par (menuItem, inventoryItem) já existe para este tenant.
DELETE /t/:slug/inventory/ingredients/:id
Remove um vínculo de ingrediente.
Response 200:
{ "deleted": true }Response 404: ingrediente não encontrado.
POST /t/:slug/inventory/import-nfs
Importa uma NF-e XML de fornecedor e cria/atualiza itens de estoque.
Content-Type: multipart/form-data
| Campo | Tipo | Descrição |
|---|---|---|
file | File | Arquivo XML da NF-e (.xml) |
Response 201:
{
"success": true,
"nfeNumber": "42",
"nfeKey": "35260312345678000195550010000000421234567890",
"emitter": "FORNECEDOR LTDA",
"totalItems": 12,
"created": 3,
"updated": 9,
"movements": 12,
"skipped": 0,
"errors": []
}Requer: role admin ou superadmin
GET /t/:slug/inventory/expiration-alerts
Retorna itens com validade vencida ou próxima.
Response 200:
{
"expiredItems": [
{
"_id": "...",
"name": "Leite Integral",
"sku": "LAT-001",
"expirationDate": "2026-02-28T00:00:00.000Z",
"availableQuantity": 5,
"unit": "l"
}
],
"expiringIn7": [...],
"expiringIn30": [...]
}Requer: role admin, staff ou superadmin
GET /t/:slug/inventory/stats
Retorna estatísticas agregadas do estoque do tenant.
Permissão: ManageInventory
Response:
{
"totalItems": 142,
"totalValue": 18450.75,
"lowStock": 8,
"expired": 2,
"blocked": 1
}| Campo | Descrição |
|---|---|
totalItems | Total de itens ativos |
totalValue | Soma de physicalQuantity × costPrice |
lowStock | Itens com availableQuantity ≤ minQuantity |
expired | Itens com status: expired |
blocked | Itens com status: blocked |
Alertas de Estoque
Base: /t/:slug/inventory/alerts
Permissão: ManageInventory
GET /t/:slug/inventory/alerts
Retorna alertas de estoque baixo não lidos pelo usuário atual (não presente em readBy[]). Máximo 50 alertas, ordenados por createdAt desc.
Response:
[
{
"_id": "64a1b2c3d4e5f6aaa",
"item": "64a1b2c3d4e5f6789",
"itemName": "Pão de hambúrguer",
"availableQuantity": 8,
"minQuantity": 10,
"pdv": null,
"pdvName": null,
"expiresAt": "2026-04-17T14:30:00.000Z",
"createdAt": "2026-04-10T14:30:00.000Z"
}
]GET /t/:slug/inventory/alerts/count
Retorna contagem de alertas não lidos. Usado para o badge no header.
Response:
{ "count": 3 }PUT /t/:slug/inventory/alerts/:id/read
Marca o alerta como lido pelo usuário atual (adiciona userId ao array readBy[]).
Response: 200 OK com o documento StockAlert atualizado.
Label Tokens (QR Code)
Sistema de rastreamento de perdas, furtos e vencimentos via QR code físico.
POST /t/:slug/inventory/labels
Gera um token QR para registro de baixa física.
Permissão: ManageSettings
Body:
{
"itemId": "64a1b2c3d4e5f6789",
"quantity": 5,
"reason": "loss",
"notes": "Embalagem danificada"
}| Campo | Tipo | Valores aceitos |
|---|---|---|
itemId | string | MongoId do InventoryItem |
quantity | number (min 0.001) | Quantidade a baixar |
reason | string | loss | theft | expiration | adjustment |
notes | string? | Observação opcional |
Response: Documento LabelToken com o campo token (UUID).
{
"_id": "64a1b2c3d4e5f6bbb",
"token": "550e8400-e29b-41d4-a716-446655440000",
"item": "64a1b2c3d4e5f6789",
"quantity": 5,
"reason": "loss",
"notes": "Embalagem danificada",
"status": "pending",
"createdAt": "2026-04-10T10:00:00.000Z"
}POST /t/:slug/inventory/scan/:token (público)
Consome o token QR e registra a baixa de estoque. Endpoint público — sem autenticação.
Parâmetro: :token — UUID do LabelToken
Processo:
findOneAndUpdate({ token, status: 'pending' })— garante uso único (race-condition safe)- Cria
InventoryMovement(type:exit, reason: conforme oreasondo token) - Decrementa
physicalQuantityeavailableQuantity - Marca
LabelToken.status = 'used'
Response (sucesso): Documento InventoryMovement criado.
Errors:
404— token não encontrado ou já utilizado
Fornecedores
Base: /t/:slug/inventory/suppliers
Guards: JwtAuthGuard, TenantGuard, RolesGuard
Roles: Admin, Superadmin
Plan feature: supplierManagement
GET /t/:slug/inventory/suppliers
Lista fornecedores do tenant.
Query: ?active=true (opcional) — filtra por status ativo/inativo.
Response: Supplier[]
[
{
"_id": "64a1b2c3d4e5f6ccc",
"name": "Distribuidora ABC",
"cnpj": "12345678000195",
"email": "contato@abc.com",
"phone": "11999999999",
"contactName": "João Silva",
"categories": ["bebidas", "laticínios"],
"active": true
}
]GET /t/:slug/inventory/suppliers/:id
Retorna um fornecedor pelo ID.
POST /t/:slug/inventory/suppliers
Cria um fornecedor.
Body:
{
"name": "Distribuidora ABC",
"cnpj": "12345678000195",
"email": "contato@abc.com",
"phone": "11999999999",
"contactName": "João Silva",
"address": "Rua das Flores, 123, São Paulo-SP",
"categories": ["bebidas"],
"notes": "Entrega às terças e quintas"
}Único obrigatório: name. Todos os demais campos são opcionais.
PUT /t/:slug/inventory/suppliers/:id
Atualiza campos do fornecedor (todos opcionais).
DELETE /t/:slug/inventory/suppliers/:id
Remove o fornecedor.
Pedidos de Compra
Base: /t/:slug/inventory/purchase-orders
Guards: JwtAuthGuard, TenantGuard, RolesGuard
Roles: Admin, Superadmin
Plan feature: supplierManagement
GET /t/:slug/inventory/purchase-orders
Lista pedidos de compra do tenant.
Query: ?status=draft (opcional) — filtra por status (draft|sent|partial|received|cancelled).
Response: PurchaseOrder[] com supplier populado (name, cnpj).
GET /t/:slug/inventory/purchase-orders/:id
Retorna PO com supplier populado.
POST /t/:slug/inventory/purchase-orders
Cria um Pedido de Compra.
Body:
{
"supplierId": "64a1b2c3d4e5f6ccc",
"items": [
{
"inventoryItemId": "64a1b2c3d4e5f6789",
"itemName": "Pão de hambúrguer",
"quantity": 100,
"unitCost": 2.00
}
],
"expectedDelivery": "2026-04-15",
"notes": "Urgente"
}Campos calculados pelo servidor: orderNumber (auto PO-YYYYMMDD-NNNN), totalCost, subtotal por item. Status inicial: draft.
POST /t/:slug/inventory/purchase-orders/generate-from-low-stock
Gera automaticamente um draft PO com base nos itens abaixo do estoque mínimo.
Body (opcional):
{ "supplierId": "64a1b2c3d4e5f6ccc" }Lógica: Para cada item com availableQuantity ≤ minQuantity > 0:
- Quantidade sugerida =
max(1, minQuantity × 2 − availableQuantity) - Custo estimado =
quantidade × costPrice
Response: Documento PurchaseOrder criado com status: draft e nota "Gerado automaticamente — N itens com estoque baixo".
PUT /t/:slug/inventory/purchase-orders/:id
Atualiza itens, status, supplier, expectedDelivery, notes. Recalcula totalCost se items forem alterados.
DELETE /t/:slug/inventory/purchase-orders/:id
Remove o PO.
POST /t/:slug/inventory/purchase-orders/:id/receive
Registra o recebimento físico dos itens e atualiza o estoque.
Body:
{
"items": [
{ "inventoryItemId": "64a1b2c3d4e5f6789", "receivedQuantity": 95 }
]
}Side effects por item:
- Cria
InventoryMovement(type:entry, reason:purchase, unitCost do PO) - Incrementa
physicalQuantityeavailableQuantitydoInventoryItem
Status resultante:
- Todos os itens recebidos na quantidade total →
received - Quantidade parcial em algum item →
partial
Response: Documento PurchaseOrder atualizado.
Previsões de Demanda
Base: /t/:slug/inventory/predictions
Guards: JwtAuthGuard, TenantGuard, RolesGuard
Roles: Admin, Superadmin
Plan feature: inventoryPrediction
GET /t/:slug/inventory/predictions/demand
Retorna previsão de demanda por item do cardápio usando Weighted Moving Average.
Query: ?horizon=7 (padrão: 7) — horizonte em dias.
Response: DemandForecast[]
[
{
"menuItemId": "64a1b2c3d4e5f6001",
"menuItemName": "X-Burguer",
"avgDaily": 12.5,
"predicted": 87.5,
"confidence": 0.95,
"trend": "up"
}
]| Campo | Descrição |
|---|---|
avgDaily | Média diária ponderada calculada |
predicted | avgDaily × horizonDays |
confidence | min(1.0, dataPoints / 21) — 0 a 1 |
trend | up / down / stable — comparação últimos 7d vs 7d anteriores |
GET /t/:slug/inventory/predictions/suggestions
Retorna demanda de ingredientes com daysUntilStockout. Ordenado crescente por daysUntilStockout (itens mais críticos primeiro).
Query: ?horizon=7
Response: IngredientDemand[]
[
{
"inventoryItemId": "64a1b2c3d4e5f6002",
"itemName": "Pão de hambúrguer",
"unit": "un",
"currentStock": 50,
"predictedDemand": 96.25,
"daysUntilStockout": 3.6,
"suggestedOrder": 46.25,
"estimatedCost": 92.50
}
]GET /t/:slug/inventory/predictions/suggested-orders
Lista SuggestedPurchaseOrder do tenant. Máximo 20, ordenado por createdAt desc.
Response: SuggestedPurchaseOrder[]
POST /t/:slug/inventory/predictions/generate
Executa generateSuggestions() e persiste novo SuggestedPurchaseOrder. Retorna null se nenhum ingrediente precisar de reposição.
Query: ?horizon=7
Response: Documento SuggestedPurchaseOrder com status: pending.
PUT /t/:slug/inventory/predictions/suggested-orders/:id/:action
Atualiza o status do SuggestedPurchaseOrder.
Path param :action: approve ou dismiss
Response: Documento atualizado com novo status.
POST /t/:slug/inventory/predictions/snapshots/build
Trigger manual do cron de snapshots diários. Processa a data de ontem.
Normalmente executado pelo cron às 02:00. Use este endpoint para seed inicial ou reconstrução.
Response:
{ "message": "Snapshots built for tenant <id>" }Estoque por Filial (BranchStock)
Base: /t/:slug/branch-stock
Guards: JwtAuthGuard, TenantGuard, PermissionsGuard, BranchScopeGuard
BranchScopeGuard: Usuários
staffvisualizam apenas os PDVs aos quais estão atribuídos.adminesuperadminveem todos.
GET /t/:slug/branch-stock
Lista estoque por filial.
Query params:
?pdv=<id>— estoque de um PDV específico?item=<id>— um item em todos os PDVs- Sem params — todo o estoque de todos os PDVs (BranchScopeGuard filtra por acesso)
Permissão: ManageInventory
Response: BranchStock[] com item e pdv opcionalmente populados.
GET /t/:slug/branch-stock/alerts
Retorna todos os BranchStock onde availableQuantity ≤ minQuantity e minQuantity está definido.
Permissão: ManageSettings
PUT /t/:slug/branch-stock/:pdvId
Upsert — define quantidade absoluta para um item em um PDV. Cria o row se não existir.
Permissão: ManageInventory + BranchScope
Body:
{
"itemId": "64a1b2c3d4e5f6789",
"physicalQuantity": 50,
"minQuantity": 10
}Response: Documento BranchStock criado ou atualizado.
POST /t/:slug/branch-stock/:pdvId/adjust
Ajuste por delta (positivo ou negativo). O row deve existir previamente.
Permissão: ManageInventory + BranchScope
Body:
{
"itemId": "64a1b2c3d4e5f6789",
"delta": -5
}Errors:
404— row não encontrado para este item/PDV400—physicalQuantity + deltaseria negativo
POST /t/:slug/branch-stock/transfer
Transferência atômica entre dois PDVs.
Permissão: ManageSettings
Body:
{
"fromPdvId": "64a1b2c3d4e5f6001",
"toPdvId": "64a1b2c3d4e5f6002",
"itemId": "64a1b2c3d4e5f6789",
"quantity": 10
}Response:
{
"from": { "_id": "...", "pdv": "64a1b2c3d4e5f6001", "physicalQuantity": 40, "availableQuantity": 40 },
"to": { "_id": "...", "pdv": "64a1b2c3d4e5f6002", "physicalQuantity": 10, "availableQuantity": 10 }
}Errors:
404— item não vinculado ao PDV de origem400— quantidade insuficiente na origem
DELETE /t/:slug/branch-stock/:pdvId/items/:itemId
Desvincula o item do PDV (remove o row BranchStock).
Permissão: ManageInventory + BranchScope
WebSocket — namespace /inventory
Gateway: backend/src/inventory/inventory.gateway.ts
Conexão: io(VITE_API_URL, { auth: { token: jwt } }) — namespace /inventory
Auth: JWT obrigatório. Roles aceitas: staff, admin, superadmin.
Eventos
| Direção | Evento | Payload |
|---|---|---|
| Cliente → Servidor | joinInventory | tenantId: string |
| Servidor → Cliente | stockUpdate | Ver abaixo |
| Servidor → Cliente | lowStockAlert | Ver abaixo |
joinInventory: Entra na sala inventory-<tenantId>. Superadmin pode passar qualquer tenantId. Usuários regulares devem passar seu próprio tenantId.
stockUpdate — catálogo master:
{ "itemId": "...", "itemName": "...", "physicalQuantity": 45, "availableQuantity": 40 }stockUpdate — estoque de filial:
{ "type": "branchStock", "pdvId": "...", "itemId": "...", "physicalQuantity": 20, "availableQuantity": 18, "minQuantity": 10 }lowStockAlert — catálogo master:
{ "_id": "...", "itemId": "...", "itemName": "...", "availableQuantity": 8, "minQuantity": 10, "createdAt": "..." }lowStockAlert — estoque de filial: adiciona type: "branchStock", pdvId, pdvName.