Skip to content

Offers / Special Offer Pages

Flow ID: CF-21 | Module(s): offers, Cms domain | Complexity: Medium

Business Overview

The offers system provides a marketing-focused CMS module for publishing promotional offers organized by categories. Each offer has date-based visibility, multi-language content, optional coupon codes, and call-to-action URLs. A separate discount page aggregates products with high discount percentages from the product catalog.

What customers experience:

  • Browse categorized special offers on the storefront offers page
  • Filter offers by category via slug-based navigation
  • Only see offers whose start date is in the past (active offers)
  • View discount product pages aggregating products with discounts at or above a threshold (default 50%)
  • Each offer may display a coupon code and link to a destination URL

What admins manage:

  • Create/edit/delete offers with multi-language title, content, coupon, URL, and URL title
  • Organize offers into categories with images and slugs
  • Control offer ordering via manual sort position
  • Set start and optional end dates for time-gated visibility
  • Admin menu item is currently hidden (commented out) pending a product decision

API Reference

REST Endpoints

Offer

MethodPathAuthDescription
GET/rest/cms/offerGuestList offers (filterable, sortable, paginated)
GET/rest/cms/offer/{id}GuestGet single offer by ID
GET/rest/cms/offer/itemGuestGet single offer by filters
POST/rest/cms/offerBackend (Admin, Marketing)Create new offer
POST/rest/cms/offer/{id}Backend (Admin, Marketing)Update existing offer
DELETE/rest/cms/offer/{id}Backend (Admin, Marketing)Delete offer

Filters: id (exact), offerCategoryId (exact), title.{locale} (partial) Sorts: id, offerCategoryId, dateStart, dateEnd, ord, title.{locale}Relations: translations (one-to-many), category (belongs-to)

Offer Category

MethodPathAuthDescription
GET/rest/cms/offer-categoryGuestList offer categories
GET/rest/cms/offer-category/{id}GuestGet single category by ID
GET/rest/cms/offer-category/itemGuestGet single category by filters
POST/rest/cms/offer-categoryBackend (Admin, Marketing)Create category (multipart -- images)
POST/rest/cms/offer-category/{id}Backend (Admin, Marketing)Update category (multipart -- images)
DELETE/rest/cms/offer-category/{id}Backend (Admin, Marketing)Delete category

Filters: id (exact), title.{locale} (partial), slug.{locale} (exact) Sorts: id, title.{locale}, slug.{locale}Relations: translations (one-to-many) Upload fields: image, image_2 stored in files/offer_categories/

Legacy Storefront URLs

URLMethodDescription
/ειδικεσ-προσφορεσindex()Greek offers listing (all categories)
/ειδικεσ-προσφορεσ/{category-slug}index($slug)Offers filtered by category slug
/ειδικεσ-προσφορεσ/{category-slug}/{offset}index($slug, $offset)Paginated category view
/discountproductsWithDiscountGreaterThan()Products with 50%+ discount
/discount/{percent}productsWithDiscountGreaterThan($pct)Products with custom discount threshold
/{lang}/discountSameLanguage-prefixed discount page

Legacy Admin URLs

URLMethodDescription
/offers/offers_adminindex()List all offers
/offers/offers_admin/addadd()Create offer form
/offers/offers_admin/edit/{id}edit($id)Edit offer form
/offers/offers_admin/delete/{id}delete($id)Delete offer
/offers/offer_categories_adminindex()List categories
/offers/offer_categories_admin/addadd()Create category form
/offers/offer_categories_admin/edit/{id}edit($id)Edit category form
/offers/offer_categories_admin/delete/{id}delete($id)Delete category

Code Flow

Storefront Offers Listing

File: ecommercen/offers/controllers/Adv_offers.php -> index()

  1. Category resolution: If a category slug is provided (not the default "all" slug), look up the category via getCategoryMuiGeneric() and add offer_category_id to the filter
  2. Count: countRecords() for pagination total
  3. Sub-content: indexExtras() loads optional header text from subcontent system (slug: offers-header-text) with PSCache
  4. Categories sidebar: getCategoriesMui() loads all categories for the current language
  5. Fetch offers: getRecords() joins offers with offers_mui filtered by current language and date_start <= NOW(), ordered by ord ASC, paginated (20/page)
  6. Render: Template layouts/offers/offers (storefront view placeholder for theme customization)

Discount Products Page

File: ecommercen/offers/controllers/Adv_offers.php -> productsWithDiscountGreaterThan()

  1. Cached query: product_model->getProductsWithDiscountGreaterThan($greaterThan, $limit) via PSCache
  2. Discount calculation: If ENABLE_SPECIAL_DISCOUNTS registry key is set, uses CASE expression considering special_from/special_to date range and special_discount_percent; otherwise falls back to discount_persent
  3. Stock ordering: If LOW_STOCK_END_OF_LIST is enabled, low-stock items are pushed to the end
  4. Product parsing: defaultProductParseWithProductCodes() enriches product data
  5. Render: Template layouts/offers/discounts with product listing grid

Admin Offer CRUD

File: ecommercen/offers/controllers/Adv_offers_admin.php

  1. Authorization: Requires AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, or AUTH_ROLE_MARKETING
  2. List (index): Shows paginated offers (20/page) sorted by ord ASC, with inline order update form
  3. Create (add): Validates title, content, URL (required per language), category (required), date_start (required), date_end (optional). Calls afterAdd($id) hook
  4. Update (edit): Same validation; calls afterEdit($id) hook
  5. Delete (delete): Calls afterDelete($id) hook

Admin Category CRUD

File: ecommercen/offers/controllers/Adv_offer_categories_admin.php

  1. Authorization: Same roles as offers
  2. Create/Update: Category title per language (required), up to 2 image uploads via advuploader to files/offer_categories/. Slug auto-generated from title via createSlug()
  3. Delete: Blocked if category has linked offers (count check with error message showing linked offer count)
  4. Reorder: AJAX updateorder() for drag-and-drop category ordering

Domain Layer

Offer (Cms Context)

ComponentPath
Entitysrc/Domains/Cms/Offer/Repository/Entity.php
MuiEntitysrc/Domains/Cms/Offer/Repository/MuiEntity.php
Repositorysrc/Domains/Cms/Offer/Repository/Repository.php
MuiRepositorysrc/Domains/Cms/Offer/Repository/MuiRepository.php
RepositoryConfiguratorsrc/Domains/Cms/Offer/Repository/RepositoryConfigurator.php
Service (read)src/Domains/Cms/Offer/Service.php
WriteServicesrc/Domains/Cms/Offer/WriteService.php
WriteDatasrc/Domains/Cms/Offer/WriteData.php
MuiWriteDatasrc/Domains/Cms/Offer/MuiWriteData.php
Validatorsrc/Domains/Cms/Offer/Validator.php
ListRequestsrc/Domains/Cms/Offer/ListRequest.php
WriteRepositorysrc/Domains/Cms/Offer/Repository/WriteRepository.php
MuiWriteRepositorysrc/Domains/Cms/Offer/Repository/MuiWriteRepository.php

OfferCategory (Cms Context)

ComponentPath
Entitysrc/Domains/Cms/OfferCategory/Repository/Entity.php
MuiEntitysrc/Domains/Cms/OfferCategory/Repository/MuiEntity.php
Repositorysrc/Domains/Cms/OfferCategory/Repository/Repository.php
MuiRepositorysrc/Domains/Cms/OfferCategory/Repository/MuiRepository.php
RepositoryConfiguratorsrc/Domains/Cms/OfferCategory/Repository/RepositoryConfigurator.php
Service (read)src/Domains/Cms/OfferCategory/Service.php
WriteServicesrc/Domains/Cms/OfferCategory/WriteService.php
WriteDatasrc/Domains/Cms/OfferCategory/WriteData.php
MuiWriteDatasrc/Domains/Cms/OfferCategory/MuiWriteData.php
Validatorsrc/Domains/Cms/OfferCategory/Validator.php
ListRequestsrc/Domains/Cms/OfferCategory/ListRequest.php

REST Controllers & Resources

ComponentPath
Offer Controllersrc/Rest/Cms/Controllers/Offer.php
OfferCategory Controllersrc/Rest/Cms/Controllers/OfferCategory.php
Offer Resourcesrc/Rest/Cms/Resources/Offer/Resource.php
Offer MuiResourcesrc/Rest/Cms/Resources/Offer/MuiResource.php
Offer Collectionsrc/Rest/Cms/Resources/Offer/Collection.php
OfferCategory Resourcesrc/Rest/Cms/Resources/OfferCategory/Resource.php
OfferCategory MuiResourcesrc/Rest/Cms/Resources/OfferCategory/MuiResource.php
OfferCategory Collectionsrc/Rest/Cms/Resources/OfferCategory/Collection.php

Architecture

Key Components

ComponentPathDescription
Front controllerecommercen/offers/controllers/Adv_offers.phpStorefront listing + discount page
Admin offers controllerecommercen/offers/controllers/Adv_offers_admin.phpOffer CRUD
Admin categories controllerecommercen/offers/controllers/Adv_offer_categories_admin.phpCategory CRUD
Legacy modelecommercen/offers/models/Adv_offers_model.phpBoth offer and category DB operations
App-level overridesapplication/modules/offers/controllers/Offers.phpEmpty extension point
App-level modelapplication/modules/offers/models/Offers_model.phpEmpty extension point
Offers view templateapplication/views/main/layouts/offers/offers.phpStorefront offers layout (empty -- theme-driven)
Discounts view templateapplication/views/main/layouts/offers/discounts.phpDiscount products grid layout
Admin viewsapplication/views/admin/offers/List, create, update forms
Admin category viewsapplication/views/admin/offers/categories/Category list, create, update forms
Domain DI configsrc/Domains/Cms/container.phpOffer + OfferCategory registrations
REST DI configsrc/Rest/Cms/container.phpController wiring
REST routesapplication/config/rest_routes.phpREST endpoint routing
Storefront routesapplication/config/routes.phpLegacy URL routing
Auth policiesapplication/config/rest_policies.phpREST RBAC configuration
Template configapplication/config/mainTemplate.jsonoffers and discounts layout mapping

Data Model

offers

ColumnTypeDescription
idint (PK)Auto-increment
offer_category_idint (FK)Reference to offer_categories.id
date_startdatetimeOffer activation date (required)
date_enddatetime (nullable)Offer expiration date (optional)
ordintSort position for manual ordering

offers_mui

ColumnTypeDescription
idint (PK)Auto-increment
offer_idint (FK)Reference to offers.id
titlevarcharOffer title (required per language)
contenttextOffer body content (required per language)
couponvarchar (nullable)Optional coupon code
urlvarcharCall-to-action URL (required per language in legacy admin)
url_titlevarchar (nullable)Display text for the URL
langvarcharLanguage code (e.g., el, en)

offer_categories

ColumnTypeDescription
idint (PK)Auto-increment
imagevarchar (nullable)Primary category image filename
image_2varchar (nullable)Secondary/label category image

offer_categories_mui

ColumnTypeDescription
idint (PK)Auto-increment
offer_category_idint (FK)Reference to offer_categories.id
titlevarcharCategory title (required per language)
slugvarcharURL-friendly slug (auto-generated from title)
langvarcharLanguage code

Configuration

Registry Keys (Discount Page)

GroupKeyDescription
OTHERENABLE_SPECIAL_DISCOUNTSEnables time-bounded special discount prices (special_from/special_to date range) in discount percentage calculation
OTHERLOW_STOCK_END_OF_LISTPushes low-stock products to the end of discount product listings

Template Configuration

In application/config/mainTemplate.json:

  • offers layout: layouts/offers/offers -- storefront offers listing page
  • discounts layout: layouts/offers/discounts -- discount products aggregation page

Subcontent Integration

The offers listing page loads optional header content from the subcontent system using the slug offers-header-text. This allows admins to configure introductory text above the offers grid via the subcontent admin.

Admin Menu

The offers admin menu item is currently commented out in application/config/admin_menu.php pending a product decision. The route offers/offers_admin with icon mdi-tag-plus and label "Offers" is gated to AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, and AUTH_ROLE_MARKETING roles.


Client Extension Points

  • Controller hooks: afterAdd($id), afterEdit($id), afterDelete($id) are empty protected methods in both Adv_offers_admin and Adv_offer_categories_admin -- client repos override these for custom post-save logic
  • indexExtras() method: Protected method in Adv_offers for adding custom data to the offers listing context (e.g., subcontent, banners)
  • Application-level overrides: application/modules/offers/controllers/Offers.php and Offers_admin.php extend the upstream classes and are empty -- clients can add custom behavior here
  • Model override: application/modules/offers/models/Offers_model.php extends Adv_offers_model for custom queries
  • View templates: application/views/main/layouts/offers/offers.php is empty (theme-driven) -- client themes provide the actual markup; discounts.php renders a product grid
  • Category image storage: Upload path files/offer_categories/ can be customized in client repos
  • REST layer: Client repos can create Custom\Domains\Cms\Offer\ classes and register DI aliases to override services, validators, or repository configurators

Business Rules

  1. Active offer visibility: Storefront only shows offers where date_start <= NOW(). The date_end is stored but not enforced in the storefront query (offers remain visible after expiry)
  2. Category deletion guard: Cannot delete a category that has linked offers. The model counts offers in the category and returns an error message with the count
  3. Offer ordering: Offers are sorted by ord ASC on both storefront and admin. Admin allows inline order updates via form submission
  4. Category slug generation: Slugs are auto-generated from the category title using createSlug() on create and update
  5. Pagination: Storefront shows 20 offers per page; admin shows 20 per page
  6. Discount page defaults: Without parameters, shows products with 50%+ discount, limited to 100 products
  7. Special discount dates: When ENABLE_SPECIAL_DISCOUNTS is enabled, the discount page considers time-bounded special prices (special_from/special_to with special_discount_percent) over the regular discount_persent
  8. REST ord field visibility: The ord sort position is only returned in API responses when the request context is backend (admin panel)
  9. MUI translation replacement: On REST update, translations are fully replaced (delete all + re-insert), not merged
  10. REST auth: Read endpoints (index, show, item) are guest-accessible; write endpoints (store, update, destroy) require backend auth with Admin or Marketing role