Promo validation returning false is a design smell. Expired, usage-limit-hit, not-yours — those are different answers, and every caller ends up reverse-engineering them from a message string.
We made the rejection reasons a typed part of the contract (open-sourced it at github.com/shipmindlabs/promocodes). Feels obvious after payments work, where "declined" without a code is useless.
Where do you draw the line on exposing reasons to the client?