Extended Voucher Rules: Technical Details
Technical details of Extended Voucher Rules: module ID, validation order, reservation mechanism, extended shop classes, services, template extension and database tables.
Extended Voucher Rules: Technical Details
Live Demo: Try this and other plugins live — no installation, no risk: demo.markus-michalski.net
Module at a glance
| Property | Value |
|---|---|
| Composer package | mmd/oxid7-voucherrules |
| Module ID | mmd_voucherrules |
| Namespace | Mmd\VoucherRules (PSR-4, directory src/) |
| Target directory | mmd/voucherrules |
| Metadata version | 2.1 |
| Events | onActivate creates the tables, onDeactivate is intentionally empty |
Validation order
The central check lives in VoucherValidationService::validate(). It is called from the extended Voucher class after OXID's standard check has passed. The rules run in this order, and the first exception thrown ends the check:
- Payment restriction
- Delivery restriction
- First order
- Time window
- Combinations (maximum, then block list)
- Minimum order value per payment method
- Minimum order value per delivery method
Consequence of the order: as long as a payment or delivery rule is still pending, the rules after it (for example the time window) are not evaluated yet.
graph TD
A[VoucherValidationService.validate] --> B[Payment restriction]
B --> C[Delivery restriction]
C --> D[First order]
D --> E[Time window]
E --> F[Combinations]
F --> G[Min order value payment]
G --> H[Min order value delivery]
H --> I[All rules passed]
B -->|payment missing or not matching| P[VoucherRulePendingException]
C -->|delivery missing or not matching| P
G -->|payment missing or value not reached| P
H -->|delivery missing or value not reached| P
D -->|not a first order| V[VoucherRuleViolationException]
E -->|outside the window| V
F -->|blocked or maximum| V
Behaviour per rule
| Rule | Condition | Result |
|---|---|---|
| Payment | Restriction present, no payment method in the session | reserved (hint "payment pending") |
| Payment | Restriction present, payment method not allowed | reserved (hint "payment not matching") |
| Delivery | analogous to payment | reserved |
| First order | Option set and the user has non-cancelled orders (OXSTORNO = 0, same shop); without a user ID nothing is checked |
rejected |
| Time window | Active windows exist, none matches weekday and time of day | rejected |
| Combination | Maximum > 0 and number of distinct series of the other applied vouchers (excluding the voucher itself, reserved ones not counted) >= maximum, or a blocked pair with an applied voucher | rejected |
| Min order value | Entries exist, no payment/delivery method selected | reserved |
| Min order value | Entry for the selected payment/delivery method, basket value lower | reserved (hint with amount) |
The weekday is determined with date('N'), the time of day with the database's CURTIME(). The window check is CURTIME() BETWEEN STARTTIME AND ENDTIME, so a window entered as HH:MM ends at second 00.
Reservation mechanism
A voucher with an open condition raises a VoucherRulePendingException. The class Extension\Model\Voucher catches it, remembers the voucher number and stores a hint. The exception is re-thrown so that OXID applies no discount. A VoucherRuleViolationException removes the reservation and rejects the voucher. Both exceptions extend OXID's VoucherException.
Data is stored in the session under two keys: mmd_voucherrules_pending (list of voucher numbers) and mmd_voucherrules_hints (hints with key and minimum value). The session values paymentid and sShipSet are read.
graph TD
A[Customer selects payment or delivery method] --> B[Basket.calculateBasket]
B --> C{Reservations in the session?}
C -->|No| D[Normal calculation]
C -->|Yes| E[addVoucher per reserved number]
E --> F{Result}
F -->|passed| G[Voucher active, reservation removed]
F -->|still open| H[stays reserved]
F -->|permanently invalid| I[Reservation removed]
Auto-activation only runs if a payment or delivery method is set in the session. After Order::finalizeOrder(), reservations, hints as well as paymentid and sShipSet are deleted. With an empty basket (Basket::calculateBasket()), only reservations and hints are deleted.
Extended shop classes
| OXID class | Module class | Purpose |
|---|---|---|
Voucher |
Extension\Model\Voucher |
Extended check in checkVoucherAvailability() and checkBasketVoucherAvailability(), reservation |
VoucherSerie |
Extension\Model\VoucherSerie |
Access to the first-order flag and the maximum |
Basket |
Extension\Model\Basket |
Auto-activation and hints in calculateBasket(), hides red error banners for reservations |
BasketController |
Extension\Controller\BasketController |
addVoucher() without error banner for a reservation, action removePendingVoucher |
ViewConfig |
Extension\Core\ViewConfig |
getVoucherRulesHints(), getPendingVouchers(), hasPendingVouchers() for templates |
Order |
Extension\Model\Order |
Cleanup in finalizeOrder() |
Admin controllers
| Controller ID | Class | Tab ID in menu.xml |
|---|---|---|
mmd_voucherrules_main |
VoucherRulesMain |
tbclvoucherserie_rules_main |
mmd_voucherrules_timewindow |
VoucherRulesTimeWindow |
tbclvoucherserie_rules_timewindow |
mmd_voucherrules_combination |
VoucherRulesCombination |
tbclvoucherserie_rules_combination |
mmd_voucherrules_payment_minval |
VoucherRulesPaymentMinVal |
tbclvoucherserie_rules_payment_minval |
The tabs are attached to the menu item mxvouchers (main menu mxshopsett). All writing actions check the session challenge; deleting and changing additionally verify the series and shop of the entry.
Services and extensibility
The services and repositories are marked public in the Symfony container (services.yaml) and can be fetched through the ContainerFactory: VoucherValidationService, FirstOrderCheckService, TimeWindowCheckService, CombinationCheckService, VoucherRestrictionHintService as well as the repositories VoucherRulesConfigRepository, PaymentRestrictionRepository, DeliveryRestrictionRepository, TimeWindowRepository, CombinationRepository, PaymentMinValueRepository and DeliveryMinValueRepository. There is no documented extension API and no events.
Template extension
For the Apex theme the module ships two template extensions:
layout/base.html.twigincludes the stylesheetcss/voucherrules.cssin the blockbase_style.page/checkout/inc/summary_sidebar.html.twigadds the hint box to the blockcheckout_basketcontents_grandtotal. It usesoViewConf.getPendingVouchers()andoViewConf.getVoucherRulesHints(); the remove link callsindex.php?cl=basket&fnc=removePendingVoucherwithvoucherNrand the session token.
For other themes the same pattern can be implemented in the theme's checkout summary.
Database tables
Seven tables with the prefix mmd_voucherrules_, all with OXSHOPID; the voucher series is stored in OXVOUCHERSERIEID:
| Table | Purpose | Key columns |
|---|---|---|
mmd_voucherrules_config |
Settings per series (unique per series and shop) | FIRSTORDERONLY, MAXCOMBINED |
mmd_voucherrules_payment |
Allowed payment methods | OXPAYMENTID |
mmd_voucherrules_delivery |
Allowed delivery methods | OXDELIVERYSETID |
mmd_voucherrules_timewindow |
Time windows | WEEKDAYS (bitmask bit 0 = Monday to bit 6 = Sunday, 127 = all), STARTTIME, ENDTIME, ACTIVE |
mmd_voucherrules_combination |
Blocked pairs, series IDs stored in sorted order | OXVOUCHERSERIEID_A, OXVOUCHERSERIEID_B |
mmd_voucherrules_payment_minval |
Minimum order value per payment method | OXPAYMENTID, MINORDERVALUE |
mmd_voucherrules_delivery_minval |
Minimum order value per delivery method | OXDELIVERYSETID, MINORDERVALUE |
The tables are created on activation with
CREATE TABLE IF NOT EXISTS. Data is kept on deactivation.