Pular para o conteúdo

Fluxo de pedido

Uma máquina de estados explícita, que recusa a transição impossível e guarda o histórico.

Um pedido tem estados, e os bugs caros moram nas transições: o pedido cancelado que é despachado, o pago que volta a aguardar pagamento. Com o estado num texto livre, nada impede isso. Com uma tabela de transições, a transição impossível é recusada — e o histórico diz quem mudou o quê.

PeçaO que ela exercita
enumos estados possíveis — e só eles
tabela de transiçõesde onde se pode ir para onde
históricocada mudança com quem e quando
match exaustivoo check avisa do estado esquecido

Estrutura#

text
pedidos/
  src/
    estados.df     o enum e a tabela
    pedido.df      transicionar, historico
  tests/
    fluxo_test.df   um trial por transicao PROIBIDA
forge.toml
[project]
name = "pedidos"
version = "0.1.0"
description = "Fluxo de pedidos"
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/pedido.df
enum Estado:
    Criado
    Pago
    Separado
    Enviado
    Entregue
    Cancelado

TRANSICOES := {
    "Criado": ["Pago", "Cancelado"],
    "Pago": ["Separado", "Cancelado"],
    "Separado": ["Enviado"],
    "Enviado": ["Entregue"],
    "Entregue": [],
    "Cancelado": []
}

blueprint Pedido(id):
    estado := Estado.Criado
    historico: Cluster := []

    action mover(para, quem):
        permitidos := TRANSICOES[self.estado.name]
        given para.name not in permitidos:
            trigger $"pedido {self.id}: '{self.estado.name}' nao vai para '{para.name}'. Pode ir para: {permitidos}"
        self.historico.append({"de": self.estado.name, "para": para.name, "quem": quem})
        self.estado := para

    action terminal():
        yield len(TRANSICOES[self.estado.name]) is 0

p := spawn Pedido(42)
p.mover(Estado.Pago, "gateway")
p.mover(Estado.Separado, "ana")
p.mover(Estado.Enviado, "transportadora")

monitor:
    p.mover(Estado.Cancelado, "cliente")
    assert no
handle Error as e:
    out e.message

p.mover(Estado.Entregue, "transportadora")
assert p.terminal()
assert len(p.historico) is 4
assert p.historico[0]["quem"] is "gateway"

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/pedido as P

crucible "transicoes proibidas":
    trial "cancelado nao e enviado":
        p := spawn P.Pedido(1)
        p.mover(P.Estado.Cancelado, "x")
        expect(lambda => p.mover(P.Estado.Enviado, "x")).to_raise()

As decisões#

DecisãoSem ela
o estado é um enum"Pagu" é aceito e o pedido some de todo relatório
a tabela fica num lugar sóa regra “cancelado não volta” está escrita em cinco rotas, e uma esquece
a mensagem lista para onde pode irquem integra descobre as transições por tentativa e erro
o histórico nasce com a transiçãoo cliente pergunta quem cancelou, e ninguém sabe

Para ir além#

Volte para todos os tipos de projeto.