Controle de estoque
Entrada, saída, reserva e o estoque mínimo — com a invariante que nunca deixa o saldo negativo.
Estoque parece soma e subtração até a primeira venda de um item que não existe. A distinção que resolve é entre disponível e físico: o que está reservado para um pedido ainda está na prateleira, mas não pode ser vendido de novo. E a invariante “disponível nunca é negativo” é cobrada em toda operação, não em algumas.
| Peça | O que ela exercita |
|---|---|
| blueprint com invariante | o saldo inconsistente é recusado na saída de cada método |
| movimento como registro | o saldo é a soma dos movimentos, e pode ser reconstruído |
| reserva | a diferença entre físico e disponível |
| alerta de mínimo | o ponto de pedido |
Estrutura#
text
estoque/
src/
produto.df saldo fisico, reservado, minimo
movimento.df entrada, saida, ajuste — imutaveis
relatorio.df o que repor
tests/forge.toml
[project]
name = "estoque"
version = "0.1.0"
description = "Controle de estoque"
entry = "src/main.df"
dataforge = ">=1.1"
[dependencies]
[scripts]
start = "run src/main.df"
test = "test tests/"
O núcleo#
Este bloco roda sozinho — copie para um arquivo e rode dataforge run. Ele termina com assert, e é assim que esta página é conferida a cada build.
src/produto.df
record Movimento:
tipo: String
qtd: Integer
motivo: String
blueprint Item(sku, minimo):
fisico := 0
reservado := 0
movimentos: Cluster := []
invariant self.fisico bigger_eq 0
invariant self.reservado bigger_eq 0 and self.reservado smaller_eq self.fisico
action disponivel():
yield self.fisico - self.reservado
action entrar(qtd, motivo):
given qtd smaller_eq 0:
trigger "entrada precisa ser positiva"
self.fisico += qtd
self.movimentos.append(Movimento("entrada", qtd, motivo))
action reservar(qtd):
given qtd bigger self.disponivel():
trigger $"{self.sku}: pedidos {qtd}, disponiveis {self.disponivel()}"
self.reservado += qtd
action baixar(qtd, motivo):
self.reservado -= qtd
self.fisico -= qtd
self.movimentos.append(Movimento("saida", qtd, motivo))
action repor():
yield self.disponivel() smaller_eq self.minimo
action reconstruir():
yield sum(self.movimentos >> morph m: (m.qtd given m.tipo is "entrada" otherwise -m.qtd))
cafe := spawn Item("CAFE-500", 5)
cafe.entrar(12, "nota 1234")
cafe.reservar(8)
assert cafe.disponivel() is 4
assert cafe.repor()
monitor:
cafe.reservar(5)
assert no
handle Error as e:
out e.message
cafe.baixar(8, "pedido 77")
assert cafe.fisico is 4
assert cafe.reconstruir() is cafe.fisico
// A invariante pega o que a regra esqueceu: baixar sem ter reservado.
monitor:
cafe.baixar(3, "erro de operacao")
assert no
handle Error as e:
out "recusado pela invariante: " + e.typeO teste#
No projeto, a regra mora em src/ e o teste a importa pelo caminho relativo — dataforge test tests/ descobre o arquivo sozinho.
tests/nucleo_test.df
adopt ../src/produto as P
crucible "estoque":
trial "o saldo reconstruido bate":
i := spawn P.Item("X", 1)
i.entrar(10, "a")
i.reservar(4)
i.baixar(4, "b")
expect i.reconstruir() is i.fisicoAs decisões#
| Decisão | Sem ela |
|---|---|
invariant no blueprint | o método que esqueceu uma conferência deixa o saldo negativo |
| físico e reservado separados | dois pedidos vendem a mesma última unidade |
| os movimentos são records | a auditoria mostra um saldo que ninguém sabe de onde veio |
reconstruir a partir dos movimentos | o saldo corrompido não tem como ser conferido |
Para ir além#
- As peças de DDD que cobram as distinções: Arcane.Dominio.
- Dois operadores ao mesmo tempo: Concorrência.
- Receita completa: Inventário.
Volte para todos os tipos de projeto.