I would abstract the intent so that it doesn't really matter how it's implemented, or how the source code is organised. At a basic level what you have is a collection of checkers generating a collection of failure explanations.
The psuedo-code would basically be:
let reasons be a new, empty collection of failure reasons
let checkers be the list of relevant checkers
for each checker in checkers
if checker passes, continue
if checker fails, add explanation to reasons
if number of reasons is zero,
voucher is valid, success
if number of reasons > zero,
the voucher is invalid, format each element in reasons for display to the user
With this logic in place, it doesn't matter how the checkers are organised, as long as they can be obtained by this part of the code in a list. You could have a single method with many checks, possibly adding many reasons. You could have many classes, with one instance of each in the list of checkers. Crucially, the actual checkers are decoupled from this logic, and different checkers can be used at any time (think of business rules changing, or different rules in different regions).
Depending on your language, this would, at a minimum, involve abstracting the type used for checkers.
Start with that. If you find identical database queries, start to consider a cache for the run of the collection of checkers.
As far as how the checkers are organised on a source level... it doesn't matter. Only the abstraction does. The details are fairly flexible once you provide a level of abstraction you can hide behind.
Pros:
- The bit that performs the checking doesn't care about the actual, concrete checks (which will probably change based on business rules).
- The checkers can be organised however you would like: defined together in one source file, or spread out based on some other scheme. It also allows the freedom to separate out a single checker for testing.
- The collection, and the pretty formatting, only needs to be implemented once, regardless of the type or number of checks.
Cons:
- It takes time upfront to do a decent job of designing the abstractions. This will probably pay off later.
- There's a layer of indirection which may make it more difficult to understand, and to track down the actual checks which are being made. Flexibility is to the detriment of understandability. These are things you will have to consider for your scenario. In my opinion, the rules for vouchers seems to be something which is likely to change over time, and the abstractions here are not difficult to understand, so I'd say it's worth it.
I spent time writing an example in Java, but it really didn't add much. If you try and think in terms of the abstraction, the actual mechanism is less important.