A state machine for a workflow
An order moves from draft to submitted to paid to shipped, and can be cancelled until it is paid. Status checks scattered through the code drift apart. Writing the allowed moves down in one table makes the workflow explicit, easy to test and easy to draw.
MOVES = {
("draft", "submit"): "submitted",
("submitted", "pay"): "paid",
("paid", "ship"): "shipped",
}
def step(state, action):
if (state, action) not in MOVES:
msg = f"no {action} from {state}"
raise ValueError(msg)
return MOVES[(state, action)]
state = step("draft", "submit")
print(state, step(state, "pay"))The order keeps its current state, and every change goes through one method that consults the table. A list of past states gives you an audit trail for free.
States as an Enum
Make the states an Enum. A typo such as Status.PAYED then fails at once, where a misspelt string would quietly create a new state.
Your task
Write an Enum Status with DRAFT, SUBMITTED, PAID, SHIPPED and CANCELLED, an exception InvalidTransition, and Order with status starting at DRAFT, history listing every status it has had, and apply(action) for "submit", "pay", "ship" and "cancel" (allowed from draft or submitted). Add allowed_actions() returning the sorted actions possible now.
The explanation opens here once every check passes.