Pular para o conteúdo

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çaO que ela exercita
blueprint com invarianteo saldo inconsistente é recusado na saída de cada método
movimento como registroo saldo é a soma dos movimentos, e pode ser reconstruído
reservaa diferença entre físico e disponível
alerta de mínimoo 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.type

O 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.fisico

As decisões#

DecisãoSem ela
invariant no blueprinto método que esqueceu uma conferência deixa o saldo negativo
físico e reservado separadosdois pedidos vendem a mesma última unidade
os movimentos são recordsa auditoria mostra um saldo que ninguém sabe de onde veio
reconstruir a partir dos movimentoso saldo corrompido não tem como ser conferido

Para ir além#

Volte para todos os tipos de projeto.