Advanced voucher restriction rules for OXID eShop 7 with payment, delivery, time window, first-order and combination rules.
Live Demo: Try all OXID eShop and Shopware plugins by Markus Michalski live β no installation, no risk. demo.markus-michalski.net
OXID eShop ships with basic voucher support β percentage discounts, fixed amounts, and minimum order values. What it does not provide is any way to restrict vouchers to specific payment methods, shipping methods, or time slots, or to control which vouchers may be combined.
The Extended Voucher Rules module (mmd/oxid7-voucherrules) fills exactly this gap. It hooks directly into the existing voucher chain without replacing core tables, adds seven additional restriction rule types per voucher series, and introduces a "pending voucher" mechanism that keeps the customer journey smooth even when required context (payment, delivery) has not yet been selected.
Problem without the module: A customer enters a voucher that is tied to a specific payment method, but OXID accepts it immediately regardless of the selected payment method. The discount is applied even when the wrong payment method is used at checkout, creating revenue leakage and complex manual correction workflows.
Solution with the module: Voucher validation is deferred intelligently. If the required payment method has not been selected yet, the voucher is stored as "pending" with a clear hint to the customer. The moment the correct payment method is chosen, the voucher activates automatically β no re-entry required.
| Feature | Description |
|---|---|
| Payment Restrictions | Whitelist of payment methods that allow a voucher |
| Delivery Restrictions | Whitelist of shipping methods that allow a voucher |
| First Order Only | Voucher valid only for customers with no previous orders |
| Time Windows (Happy Hour) | Weekday bitmask + start/end time per time window, multiple windows per voucher |
| Combination Blacklist | Explicit pairs of vouchers that cannot be used together |
| Max Combined Count | Maximum number of vouchers combinable in a single order |
| Payment-specific Minimum Order Values | Different MOV thresholds per payment method |
| Delivery-specific Minimum Order Values | Different MOV thresholds per shipping method |
| Pending Voucher Pattern | Graceful deferral instead of hard rejection for context-dependent rules |
| 4 Admin Tabs | Clean admin UI integrated into the existing voucher series detail view |
| DBAL-based Repositories | No Doctrine ORM overhead, fast raw DBAL queries |
| Full Translation Support | German and English out of the box |
| Shop Isolation | All tables carry OXSHOPID, multi-shop safe |
| Component | Requirement |
|---|---|
| OXID eShop | 7.x (CE, PE, EE) |
| PHP | >= 8.2 |
| Composer | >= 2.0 |
| Database | MySQL / MariaDB (standard OXID requirement) |
# 1. Add the package
composer require mmd/oxid7-voucherrules
# 2. Run OXID module activation
vendor/bin/oe-console oe:module:activate mmd_voucherrules
# 3. Apply database migrations
vendor/bin/oe-console oe:migrations:apply-module --module-id=mmd_voucherrules
After activation, clear the shop cache:
vendor/bin/oe-console oe:cache:clear
Manual installation bypasses Composer autoloading. This method is not recommended for production environments and will complicate future updates.
source/modules/mmd/voucherrules/.composer.json:{
"autoload": {
"psr-4": {
"Mmd\\VoucherRules\\": "source/modules/mmd/voucherrules/src/"
}
}
}
composer dump-autoload.vendor/bin/oe-console oe:module:activate mmd_voucherrules
vendor/bin/oe-console oe:migrations:apply-module --module-id=mmd_voucherrules
# Pull the new version
composer update mmd/oxid7-voucherrules
# Apply any new migrations
vendor/bin/oe-console oe:migrations:apply-module --module-id=mmd_voucherrules
# Clear cache
vendor/bin/oe-console oe:cache:clear
Always back up your database before running migrations on a production system.
After installation, every voucher series in the OXID Admin gains four additional detail tabs. Navigate to Admin > Shop Settings > Vouchers, open or create a voucher series, and you will find the following tabs alongside the standard OXID tabs.
This is the main configuration tab (VoucherRulesMain). It covers:
Payment Restrictions
A multi-select list of all active payment methods in the shop. Selecting one or more methods restricts the voucher to those methods only. Leaving the list empty means all payment methods are permitted (whitelist semantics: empty = no restriction).
Delivery Restrictions
Identical semantics to payment restrictions, but applied to shipping methods. Selecting specific methods means the voucher is only valid when one of those shipping methods is active in the basket.
First Order Only
A single checkbox. When enabled, the voucher is only accepted if the currently logged-in customer has placed zero previous orders. Guest checkouts count as a first order.
Max Combined Vouchers
An integer field (0β99). When set to a value greater than zero, it caps the total number of vouchers that can be active in a single basket at the same time. A value of 0 disables this restriction.
The VoucherRulesTimeWindow tab allows you to define one or more time windows during which the voucher is valid.
Each time window entry consists of:
HH:MM formatHH:MM formatMultiple time windows per voucher series are supported. The voucher is valid if the current server time falls within any active time window. You can add, edit, and delete individual windows from this tab.
Time validation uses the server's local time. Ensure your server timezone matches the timezone your marketing team works with.
The VoucherRulesCombination tab manages the explicit combination blacklist.
Each blacklist entry is a pair of voucher series IDs that may not be used together in the same order. Pairs are stored canonically (A < B) to prevent duplicate entries.
Use cases:
The tab shows a table of all current blacklist pairs with a delete button per row, and an "Add pair" form at the bottom.
The VoucherRulesPaymentMinVal tab provides payment-specific and delivery-specific minimum order values β separate from the standard OXID minimum order value on the voucher series.
Payment-specific MOV
A table with one row per payment method. Enter a minimum basket value (net or gross, matching your shop's pricing mode) that must be reached for the voucher to be valid when that payment method is selected.
Delivery-specific MOV
Same structure, but keyed by shipping method instead.
These thresholds are checked after the customer selects their payment/delivery method. If the basket value is below the threshold, the voucher is rejected with a clear error message.
This is the architectural centerpiece of the module. Understanding it is essential to predict how the module behaves in edge cases.
Several rule types (payment restrictions, delivery restrictions, payment MOV, delivery MOV) can only be evaluated once the customer has selected a payment or delivery method. This selection typically happens at the end of the checkout β well after the customer has already entered their voucher code on the basket page.
A naive implementation would reject the voucher immediately with an error like "This voucher requires PayPal". The customer then has to re-enter the voucher after switching payment methods, which degrades UX and increases abandonment.
The module distinguishes between two fundamentally different failure modes:
| Exception Type | Meaning | UI Treatment |
|---|---|---|
VoucherRuleViolationException |
Rule permanently violated, voucher invalid | Red error box, voucher removed |
VoucherRulePendingException |
Context not yet available, decision deferred | Yellow/blue hint box, voucher stored in session |
When a VoucherRulePendingException is thrown during validation, the voucher is not removed from the basket. Instead, its ID is stored in the session under a "pending vouchers" key, and a hint message is shown to the customer explaining what condition needs to be met.
The Basket extension hooks into basket recalculation. Each time the basket is recalculated (e.g. after the customer changes their payment method), it attempts to re-add all pending vouchers. If the condition is now satisfied, the voucher moves from "pending" to "active" automatically.
The central VoucherValidationService runs seven checks in sequence. Each check either passes silently, throws a VoucherRulePendingException (deferred), or throws a VoucherRuleViolationException (rejected).
Note that
validateFirstOrder(),validateTimeWindow(), andvalidateCombinations()never produce a pending state β their outcome is always immediately determinable and either passes or permanently rejects.
The module uses OXID's standard chain extension mechanism. It extends six core classes without replacing them, ensuring compatibility with other modules that extend the same classes.
| Core Class | Extension Class | Purpose |
|---|---|---|
OxidEsales\...\Voucher |
Extension\Model\Voucher |
Central validation entry point, pending logic |
OxidEsales\...\VoucherSerie |
Extension\Model\VoucherSerie |
Config accessor for module rule data |
OxidEsales\...\Basket |
Extension\Model\Basket |
Auto-add pending vouchers from session, generate hint messages |
OxidEsales\...\BasketController |
Extension\Controller\BasketController |
Frontend error/hint rendering, removePendingVoucher action |
OxidEsales\...\ViewConfig |
Extension\Core\ViewConfig |
Exposes hint data and pending voucher list to templates |
OxidEsales\...\Order |
Extension\Model\Order |
Session cleanup after order completion |
The
Orderextension ensures that any remaining pending vouchers in the session are cleared after a successful order, preventing stale session state from affecting subsequent visits.
Scenario: Marketing wants to issue a 15% voucher (PAYPAL15) that is only valid when the customer checks out via PayPal.
Configuration:
PAYPAL15 in Admin.Customer journey:
PAYPAL15.Scenario: Flash sale β 20% off on Monday mornings between 08:00 and 10:00.
Configuration:
08:00, End Time: 10:00.Behavior:
Scenario: You have two voucher series β "WELCOME20" (20% for new customers) and "SUMMER30" (30% summer sale). You want to prevent customers from stacking both.
Configuration:
Behavior:
SUMMER30 after already having WELCOME20 active.SUMMER30 with an error message.WELCOME20 first would allow SUMMER30 to be applied.Symptoms: Customer enters a voucher code that requires PayPal. Instead of the yellow "pending" hint, they immediately see a red error and the voucher is not stored.
Check:
VoucherRulePendingException is actually being thrown by the payment validation. Check your shop's exception log.VoucherRuleViolationException during the same validation pass, the pending logic is bypassed.Solution: Fix the additional violation first (e.g. the time window is currently inactive, or the customer has previous orders). The pending mechanism only applies when the only failing rules are context-dependent ones (payment, delivery, MOVs).
Symptoms: Customer selects the correct payment method but the pending voucher is not activated. The hint message remains.
Check:
Basket::reAddPendingVouchers() is being called. Add a temporary log line if needed.Solution: The most common cause is a custom checkout module that overrides Basket::calculateBasket() without calling the parent. Ensure that the module call chain includes the Basket extension from mmd_voucherrules. Check the module chain order in Admin > Extensions > Modules.
Symptoms: A time window voucher shows "not valid during current time" even though the current time appears to be inside the configured window.
Check:
date on the command line or <?php echo date_default_timezone_get(); ?>.Solution: Align the server timezone with your shop's operational timezone, or adjust the time window boundaries to compensate for the offset. OXID does not automatically apply timezone conversion β times are stored and compared in server local time.
Symptoms: The four extended rule tabs are missing from the voucher series detail view.
Check:
vendor/bin/oe-console oe:module:list | grep voucherrulesoevomigrations log table for the module's migration entries.Solution: Re-run activation and migration:
vendor/bin/oe-console oe:module:deactivate mmd_voucherrules
vendor/bin/oe-console oe:module:activate mmd_voucherrules
vendor/bin/oe-console oe:migrations:apply-module --module-id=mmd_voucherrules
vendor/bin/oe-console oe:cache:clear
oxid7-voucherrules/
βββ src/
β βββ Controller/
β β βββ Admin/
β β βββ VoucherRulesMain.php
β β βββ VoucherRulesTimeWindow.php
β β βββ VoucherRulesCombination.php
β β βββ VoucherRulesPaymentMinVal.php
β βββ Extension/
β β βββ Model/
β β β βββ Voucher.php
β β β βββ VoucherSerie.php
β β β βββ Basket.php
β β β βββ Order.php
β β βββ Controller/
β β β βββ BasketController.php
β β βββ Core/
β β βββ ViewConfig.php
β βββ Service/
β β βββ VoucherValidationService.php
β β βββ FirstOrderCheckService.php
β β βββ CombinationCheckService.php
β β βββ TimeWindowCheckService.php
β β βββ VoucherRestrictionHintService.php
β βββ Repository/
β β βββ VoucherRulesConfigRepository.php
β β βββ PaymentRestrictionRepository.php
β β βββ DeliveryRestrictionRepository.php
β β βββ TimeWindowRepository.php
β β βββ CombinationRepository.php
β β βββ PaymentMinValueRepository.php
β β βββ DeliveryMinValueRepository.php
β βββ Exception/
β β βββ VoucherRuleViolationException.php
β β βββ VoucherRulePendingException.php
β βββ Core/
β βββ ModuleEvents.php
βββ views/
β βββ twig/
β βββ admin/
β β βββ voucherrulesMain.html.twig
β β βββ voucherrulesTimewindow.html.twig
β β βββ voucherrulesCombination.html.twig
β β βββ voucherrulesPaymentminval.html.twig
β βββ extensions/
β βββ themes/
β βββ apex/
β βββ (storefront overrides)
βββ translations/
β βββ de/
β βββ en/
βββ metadata.php
βββ composer.json
βββ services.yaml
All tables use the prefix mmd_voucherrules_. All tables carry OXSHOPID for multi-shop isolation.
| Table | Relationship | Key Columns | Description |
|---|---|---|---|
mmd_voucherrules_config |
1:1 per voucher series | OXVOUCHERSERIEID, FIRSTORDERONLY (bool), MAXCOMBINED (int) |
Global flags for a voucher series |
mmd_voucherrules_payment |
n:m | OXVOUCHERSERIEID, OXPAYMENTID |
Payment method whitelist |
mmd_voucherrules_delivery |
n:m | OXVOUCHERSERIEID, OXDELIVERYID |
Shipping method whitelist |
mmd_voucherrules_timewindow |
1:n | OXVOUCHERSERIEID, WEEKDAYS (bitmask), STARTTIME, ENDTIME, ACTIVE |
Time window entries |
mmd_voucherrules_combination |
n:m | VOUCHERSERIEID_A, VOUCHERSERIEID_B |
Combination blacklist pairs |
mmd_voucherrules_payment_minval |
1:n | OXVOUCHERSERIEID, OXPAYMENTID, MINVALUE |
Payment-specific MOV |
mmd_voucherrules_delivery_minval |
1:n | OXVOUCHERSERIEID, OXDELIVERYID, MINVALUE |
Delivery-specific MOV |
Whitelist semantics: An empty _payment or _delivery table for a given voucher series means no restriction β all methods are permitted. Only when at least one row exists does the whitelist actively filter.
Combination blacklist canonicalization: Pairs are always stored with the lexicographically smaller ID in column A (VOUCHERSERIEID_A < VOUCHERSERIEID_B). This prevents duplicate entries like (A, B) and (B, A) representing the same restriction.
OXSHOPID) and the correct voucher series ID before executing the query.WHERE OXSHOPID = ? clause. A rule configured in Shop A cannot affect voucher validation in Shop B.MaxCombined is validated as an integer in range 0β99. Time fields are validated against the HH:MM format before being stored. All IDs are type-cast before being used in queries.executeQuery() / executeStatement() API. No string interpolation of user input into SQL.Q: Does the module work with multi-shop (EE) setups?
Yes. All module tables carry an OXSHOPID column. Rules are always scoped to the shop they were configured in. A voucher series shared across shops will have its rules evaluated independently per shop.
Q: What happens to pending vouchers if the customer logs out or their session expires?
Pending vouchers are stored in the PHP session. If the session expires or the customer logs out, the pending voucher list is lost. The customer will need to re-enter the voucher code. This is intentional β retaining voucher codes across sessions would create security concerns.
Q: Can I configure multiple time windows for the same voucher?
Yes. The Time Windows tab supports an unlimited number of time window entries per voucher series. The voucher is valid if the current time falls within any active window (OR semantics). This allows you to configure, for example, a lunch promotion (12:00β14:00) and an evening promotion (19:00β21:00) on the same voucher.
Q: The combination blacklist only supports pairs. What if I want to block a voucher from combining with any other voucher?
Use the Max Combined Vouchers field on the Extended Rules tab instead. Setting it to 1 means only a single voucher can be active in the basket at any time, regardless of which specific vouchers they are.
Q: Does the First Order Only check work for guest checkouts?
A guest customer by definition has no registered purchase history. The FirstOrderCheckService checks the order history for the current customer account. Since a guest has no account, the check passes (the guest is treated as a first-time customer). If you want to prevent abuse by guests who have previously ordered, this cannot be enforced at the voucher level without a custom account-linking strategy.
Q: Are the admin tabs visible in all OXID editions (CE/PE/EE)?
Yes. The module targets OXID 7.x across all three editions. The admin controller integration uses standard OXID metadata tab extension, which is available in CE, PE, and EE.
Q: Can I combine payment restrictions with time window restrictions on the same voucher?
Yes. All rule types are independent and additive. A voucher can simultaneously require PayPal, be valid only on Fridays between 17:00 and 20:00, and require a minimum order value of β¬50 when using DHL Express. All configured rules must pass for the voucher to be accepted.
This module is proprietary software. All rights reserved.
Contact the author for licensing inquiries.
For bug reports and feature requests, please use the issue tracker associated with the repository.
For direct support inquiries: markus-michalski.net