Appearance
Coupon Management (Admin)
Flow ID: AD-08 Module(s): coupons Complexity: Medium Last Updated: 2026-04-04
Business Context
Coupon management follows a two-tier model: coupon templates (master records defining discount rules and targeting) and coupon codes (individual redeemable strings generated from a template). Admins create templates with percentage or fixed discounts, configure restriction rules (vendors, categories, tags, products, date ranges, cart thresholds), then batch-generate unique codes for distribution. Each code tracks its own sent/used lifecycle independently.
Two coupon types exist via CouponType enum: Default (0) for standard discount coupons and GiftCard (1) for gift card coupons. The admin list page filters by couponType = 0 (Default).
Architecture
ecommercen/coupons/
controllers/
Adv_coupons.php Admin CRUD (478 lines)
api/AdvApiPreviewCoupon.php Frontend coupon preview endpoint
models/
Adv_coupons_model.php Data access + validation + calculation (~1200 lines)
libraries/
CouponType.php Enum: Default (0), GiftCard (1)
CouponChecksConfig.php Toggle flags for validation checks
src/Domains/Promotion/
Coupon/ Modern domain layer (entity, repo, write service)
CouponCode/ Individual codes domain
CouponProduct/ Product restrictions domain
CouponRule/ Rules domain
CouponVendor/ Vendor restrictions domain
src/Rest/Promotion/Controllers/
Coupon.php Full CRUD REST (GET/POST/DELETE)
CouponCode.php Full CRUD REST
CouponProduct.php Full CRUD REST
CouponRule.php Full CRUD REST
CouponVendor.php Full CRUD RESTAccess Control
Restricted to roles: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_MARKETING. Checked in controller constructor via allowRole().
Inline coupon code text editing (editCouponText) has additional restrictions:
- Only
AUTH_ROLE_ADVISABLEandAUTH_ROLE_ADMINcan see the editable input - Only
AUTH_ROLE_ADVISABLEcan edit codes that have already been marked as sent
Data Model
| Table | Purpose | Key Columns |
|---|---|---|
coupon | Master template | id, audience_id, name (unique), display_name, is_default, discount_percent, discount_price, max_count_usage, coupon_type |
coupons | Individual codes | id, coupon_id (FK to coupon), coupon (unique, case-sensitive utf8_bin), is_sent, is_used (counter), old_total, old_total_vat |
coupon_rules | Date/price/logic rules | coupon_id, vendors_rule_type, categories_rule_type, tags_rule_type, products_rule_type, date_start, date_end, total_cart_from/to, product_price_from/to, product_count_min |
coupon_vendors | Vendor restrictions | coupon_id, vendor_id |
coupon_categories | Category restrictions | coupon_id, category_id |
coupon_product_tags | Tag restrictions | coupon_id, tag_id |
coupon_products | Product restrictions | coupon_id, product_id |
Order integration: shop_order.coupon_id references coupons.id (individual code), shop_order.coupon_value stores the discount amount applied.
Admin Controller Methods
| Method | Route | Description |
|---|---|---|
index() | coupons | List all Default-type coupons with code/vendor/product counts and date range |
add() | coupons/add | Create template with master data + rules + vendor/category/tag restrictions |
edit($id) | coupons/edit/{id} | Update template; diff-based update for vendors/categories/tags (add new, remove old) |
delete($id) | coupons/delete/{id} | Delete template + all related codes, products, vendors (cascading) |
showCoupons($id) | coupons/showCoupons/{id} | List all generated codes for a template with sent/used status |
showCouponProducts($id) | coupons/showCouponProducts/{id} | List products assigned to coupon with images |
generateCoupons($id) | coupons/generateCoupons/{id} | Batch generate N random unique codes |
editCouponText($id) | coupons/editCouponText/{id} | AJAX inline edit of code string (uniqueness-validated) |
markCouponSent($couponId, $couponsId) | coupons/markCouponSent/{couponId}/{couponsId} | Mark single code as distributed |
markCouponUnsent($couponId, $couponsId) | coupons/markCouponUnsent/{couponId}/{couponsId} | Unmark single code |
couponsDelete($couponId, $couponsId) | coupons/couponsDelete/{couponId}/{couponsId} | Delete single code |
setDefault($id) | coupons/setDefault/{id} | Set coupon as system default (clears previous) |
unsetDefault($id) | coupons/unsetDefault/{id} | Remove default status |
batch_action() | coupons/batch_action | Bulk sent/unsent toggle for selected codes |
removeProductFromCoupon($couponId, $productId) | coupons/removeProductFromCoupon/{couponId}/{productId} | Remove single product restriction |
batchRemoveProductsFromCoupon($couponId) | coupons/batchRemoveProductsFromCoupon/{couponId} | Bulk remove product restrictions |
exportAllCoupons($id) | coupons/exportAllCoupons/{id} | CSV export (ID, code, sent, used) with BOM header |
Template Create/Edit Form
Two form sections:
Master Data:
name(internal identifier, unique)display_name(shown to customer at checkout, max 30 chars)audience(dropdown, only visible ifSMART_RECOMMENDATIONS.IS_ENABLED_CUSTOMER_TAGenabled)max_count_usage(default 1; how many times each code can be redeemed)discount_percent(percentage discount)discount_price(fixed amount discount)is_default(checkbox; only one coupon system-wide can be default)
Rules:
date_start/date_end(datetime range for validity)total_cart_from/total_cart_to(cart subtotal range)product_price_from/product_price_to(per-product price range filter)product_count_min(minimum qualifying product quantity)categories_rule_type+ categories multiselect (OR=0 / AND=1)tags_rule_type+ tags multiselect (OR=0 / AND=1)vendors_rule_type+ vendors multiselect (OR=0 / AND=1)products_rule_type(OR=0 / AND=1); products assigned via batch action from product list
Business Rules
| Rule | Description |
|---|---|
| Discount exclusivity | Percentage OR fixed discount, not both (percentage takes priority in calculation) |
| Default coupon singleton | setDefault() clears is_default on ALL coupons first, then sets the target |
| Code generation | Uses random_string() in a retry loop until DB insert succeeds (unique constraint) |
| Code deletion guard | Only unsent codes can be deleted from the UI (!$row->is_sent check in view) |
| Code editing guard | Only Advisable role can edit sent codes; Admin can edit unsent codes only |
| Usage counter | is_used is an increment counter (is_used+1), not a boolean; compared against max_count_usage |
| Cascade delete | Template deletion removes all codes, product assignments, and vendor assignments |
| Product assignment | Products added via batch action in product admin (update_batch_product_coupon), not coupon form |
| Diff-based relation update | Edit computes add/remove sets for vendors, categories, and tags rather than delete-all-reinsert |
| Audience gating | Feature-flagged by SMART_RECOMMENDATIONS.IS_ENABLED_CUSTOMER_TAG registry value |
| Category inheritance | Validation expands selected categories to include all children recursively |
Coupon Code Lifecycle
[Generated] --markSent--> [Sent] --customer redeems--> [Used (is_used incremented)]
| |
markUnsent <-- markUnused (on order cancel)- Generated: Code exists with
is_sent=0,is_used=0 - Sent/Distributed: Admin marks as sent (
is_sent=1); only sent codes are valid for redemption - Used: Counter incremented on order completion; compared to
max_count_usage - Rolled back:
markCouponUnused()decrements counter on order cancellation
Validation Chain
The isValidCoupon() method on the model runs sequential checks (configurable via CouponCheckConfig):
- Code exists in
couponstable - Code is marked as sent (
is_sent = 1) - Usage count below max (
is_used < max_count_usage) - Date range valid (if
validDatescheck enabled) - Audience membership (if
validAudiencecheck enabled) - Cart total within range (
total_cart_from/total_cart_to) - Vendor restrictions with AND/OR logic + price range + product count minimum
- Category restrictions (expanded to include children) with AND/OR logic
- Tag restrictions with AND/OR logic
- Product restrictions with AND/OR logic
Discount Calculation
Percentage discount (calculateCartDiscountPercent()): hierarchical cascade where first match wins:
- Vendor-restricted items in cart
- Product-restricted items in cart
- Category-restricted items in cart
- Tag-restricted items in cart
- All cart items (no restrictions matched)
Per-item formula: round(final_price * quantity * discount_percent / 100, 2)
Each level respects product_price_from / product_price_to filters.
Fixed discount (calculateCartDiscountPrice()): simple subtraction cart_total_vat -= discount_price with no targeting.
REST API (Modern Layer)
Full CRUD via src/Rest/Promotion/Controllers/:
| Endpoint | Methods | Key Features |
|---|---|---|
/rest/promotion/coupon | GET, POST, DELETE | Filters: id, audienceId, isDefault, couponType, name, displayName. Relations: audience, codes, products, vendors, rules |
/rest/promotion/coupon-code | GET, POST, DELETE | Filters: id, couponId, code (partial), isSent, isUsed. Relations: coupon |
/rest/promotion/coupon-product | GET, POST, DELETE | Product restrictions |
/rest/promotion/coupon-rule | GET, POST, DELETE | Rule data |
/rest/promotion/coupon-vendor | GET, POST, DELETE | Vendor restrictions |
Domain entities in src/Domains/Promotion/Coupon/ with WriteService + Validator for CQRS write operations. The RepositoryConfigurator defines relations: audience (belongs_to), codes/products/vendors/rules (one_to_many).
Coupon Preview API
AdvApiPreviewCoupon (Front_c, not admin-only) at route POST /order/preview_coupon:
- Accepts JSON with coupon code, cart data, VAT address info
- Validates coupon via
isValidCoupon()with fullCouponCheckConfig - Returns calculated discount amount as JSON
- Used by checkout UI to show discount before order submission
CSV Export Format
Semicolon-delimited with BOM header for Excel compatibility:
ID;COUPON;SENT;USED;
1;ABC123XYZ;Yes;No;Filename pattern: coupons_YYYYMMDDHHmmss.csv
Extension Points
All admin controller mutations have empty after*() hooks for client repo overrides:
afterAdd($id),afterEdit($id),afterDelete($id)afterMarkCouponSent($couponsId),afterMarkCouponUnsent($couponsId)afterCouponsDelete($couponsId),afterGenerateCoupons($couponId)afterRemoveProductFromCoupon($couponId, $productId)
Client repos can extend Adv_coupons in application/controllers/Coupons.php and override these hooks.
Key File Paths
| File | Purpose |
|---|---|
ecommercen/coupons/controllers/Adv_coupons.php | Admin controller |
ecommercen/coupons/models/Adv_coupons_model.php | Model with validation and calculation |
ecommercen/coupons/libraries/CouponType.php | Enum (Default=0, GiftCard=1) |
ecommercen/coupons/CouponChecksConfig.php | Configurable validation toggle flags |
ecommercen/coupons/controllers/api/AdvApiPreviewCoupon.php | Checkout coupon preview API |
ecommercen/helpers/coupons_helper.php | couponRuleTypes() helper (OR/AND labels) |
ecommercen/traits/EntityCustomerAudienceCheck.php | Audience membership validation trait |
application/views/admin/coupons/ | 6 view templates (list, create, update, show_coupons, generate_coupons, coupon_products) |
src/Domains/Promotion/Coupon/ | Modern domain entity, repository, write service |
src/Rest/Promotion/Controllers/Coupon.php | REST API controller with OpenAPI annotations |
Related Flows
- CF-13 Coupons -- full validation and calculation logic from customer perspective
- AD-22 Audience & Campaigns -- audience targeting that gates coupon visibility
- AD-02 Product Management -- batch product-to-coupon assignment
- AD-23 Gift Cards -- gift card orders generate single-use coupons with
coupon_type = GiftCard - CF-06 Order Preview -- coupon applied during checkout
- CF-08 Payment -- coupon usage rolled back on payment failure