Skip to content

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 REST

Access 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_ADVISABLE and AUTH_ROLE_ADMIN can see the editable input
  • Only AUTH_ROLE_ADVISABLE can edit codes that have already been marked as sent

Data Model

TablePurposeKey Columns
couponMaster templateid, audience_id, name (unique), display_name, is_default, discount_percent, discount_price, max_count_usage, coupon_type
couponsIndividual codesid, coupon_id (FK to coupon), coupon (unique, case-sensitive utf8_bin), is_sent, is_used (counter), old_total, old_total_vat
coupon_rulesDate/price/logic rulescoupon_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_vendorsVendor restrictionscoupon_id, vendor_id
coupon_categoriesCategory restrictionscoupon_id, category_id
coupon_product_tagsTag restrictionscoupon_id, tag_id
coupon_productsProduct restrictionscoupon_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

MethodRouteDescription
index()couponsList all Default-type coupons with code/vendor/product counts and date range
add()coupons/addCreate 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_actionBulk 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 if SMART_RECOMMENDATIONS.IS_ENABLED_CUSTOMER_TAG enabled)
  • 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

RuleDescription
Discount exclusivityPercentage OR fixed discount, not both (percentage takes priority in calculation)
Default coupon singletonsetDefault() clears is_default on ALL coupons first, then sets the target
Code generationUses random_string() in a retry loop until DB insert succeeds (unique constraint)
Code deletion guardOnly unsent codes can be deleted from the UI (!$row->is_sent check in view)
Code editing guardOnly Advisable role can edit sent codes; Admin can edit unsent codes only
Usage counteris_used is an increment counter (is_used+1), not a boolean; compared against max_count_usage
Cascade deleteTemplate deletion removes all codes, product assignments, and vendor assignments
Product assignmentProducts added via batch action in product admin (update_batch_product_coupon), not coupon form
Diff-based relation updateEdit computes add/remove sets for vendors, categories, and tags rather than delete-all-reinsert
Audience gatingFeature-flagged by SMART_RECOMMENDATIONS.IS_ENABLED_CUSTOMER_TAG registry value
Category inheritanceValidation 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)
  1. Generated: Code exists with is_sent=0, is_used=0
  2. Sent/Distributed: Admin marks as sent (is_sent=1); only sent codes are valid for redemption
  3. Used: Counter incremented on order completion; compared to max_count_usage
  4. Rolled back: markCouponUnused() decrements counter on order cancellation

Validation Chain

The isValidCoupon() method on the model runs sequential checks (configurable via CouponCheckConfig):

  1. Code exists in coupons table
  2. Code is marked as sent (is_sent = 1)
  3. Usage count below max (is_used < max_count_usage)
  4. Date range valid (if validDates check enabled)
  5. Audience membership (if validAudience check enabled)
  6. Cart total within range (total_cart_from / total_cart_to)
  7. Vendor restrictions with AND/OR logic + price range + product count minimum
  8. Category restrictions (expanded to include children) with AND/OR logic
  9. Tag restrictions with AND/OR logic
  10. Product restrictions with AND/OR logic

Discount Calculation

Percentage discount (calculateCartDiscountPercent()): hierarchical cascade where first match wins:

  1. Vendor-restricted items in cart
  2. Product-restricted items in cart
  3. Category-restricted items in cart
  4. Tag-restricted items in cart
  5. 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/:

EndpointMethodsKey Features
/rest/promotion/couponGET, POST, DELETEFilters: id, audienceId, isDefault, couponType, name, displayName. Relations: audience, codes, products, vendors, rules
/rest/promotion/coupon-codeGET, POST, DELETEFilters: id, couponId, code (partial), isSent, isUsed. Relations: coupon
/rest/promotion/coupon-productGET, POST, DELETEProduct restrictions
/rest/promotion/coupon-ruleGET, POST, DELETERule data
/rest/promotion/coupon-vendorGET, POST, DELETEVendor 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 full CouponCheckConfig
  • 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

FilePurpose
ecommercen/coupons/controllers/Adv_coupons.phpAdmin controller
ecommercen/coupons/models/Adv_coupons_model.phpModel with validation and calculation
ecommercen/coupons/libraries/CouponType.phpEnum (Default=0, GiftCard=1)
ecommercen/coupons/CouponChecksConfig.phpConfigurable validation toggle flags
ecommercen/coupons/controllers/api/AdvApiPreviewCoupon.phpCheckout coupon preview API
ecommercen/helpers/coupons_helper.phpcouponRuleTypes() helper (OR/AND labels)
ecommercen/traits/EntityCustomerAudienceCheck.phpAudience 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.phpREST API controller with OpenAPI annotations