Shipmind Labs

If your API can list a cancellation option that the same API will then refuse to perform, you do not have a UI bug. You have two different answers to the same question.

In order flows this is the most common shape we see. One endpoint builds the list of buttons. Another endpoint performs the move. They were written weeks apart, by different hands, and each one decides on its own who is allowed to cancel and under what conditions. The list is generous because a list feels harmless. The move is strict because the move touches money. So the customer taps a button that was never real.

In our open-source project order-lifecycle we made the offer and the move read the same predicate.

The listing call takes a state plus role and context as keywords, and returns every cancellation path whose source equals that state and whose transition answers the same allows check the move will later run. The acceptance side walks paths in declaration order, skips a foreign source, skips a named trigger that does not match, and returns the first path that permits the caller, and if none does, it refuses outright. The chosen path then validates the reason, and the move itself replays the role guard, the conditions and the hooks on the way through. An empty listing is therefore a promise: cancel will refuse.

The default took us the longest to get right.

Role and context both default to None, and None means absent, not unfiltered. So a path that names roles narrower than anyone is withheld while role is unknown. A path carrying real conditions is withheld while context is unknown. We deliberately do not call a condition predicate against an order we were not given, because guessing there is how you end up offering a refund path to a caller whose identity you never established. Unknown resolves to fewer options, not more.

The reasons list follows the same rule. It forwards role and context to the same listing call and collects the reasons of the surviving paths, in first-seen order. So the dropdown of cancellation reasons shrinks along with the paths. You cannot pick a reason that belongs to a path you are not allowed to walk.

Outside order flows the general principle holds: any "what can you do here" endpoint should be a filter over the same rules the "do it" endpoint enforces, not a second opinion about them. Permissions duplicated in two places are permissions that will probably disagree.

Where you keep that rule is the part still open for us: one predicate shared by both paths, or a policy service that both of them call.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com