Appearance
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
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/cms/offer | Guest | List offers (filterable, sortable, paginated) |
| GET | /rest/cms/offer/{id} | Guest | Get single offer by ID |
| GET | /rest/cms/offer/item | Guest | Get single offer by filters |
| POST | /rest/cms/offer | Backend (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
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/cms/offer-category | Guest | List offer categories |
| GET | /rest/cms/offer-category/{id} | Guest | Get single category by ID |
| GET | /rest/cms/offer-category/item | Guest | Get single category by filters |
| POST | /rest/cms/offer-category | Backend (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
| URL | Method | Description |
|---|---|---|
/ειδικεσ-προσφορεσ | index() | Greek offers listing (all categories) |
/ειδικεσ-προσφορεσ/{category-slug} | index($slug) | Offers filtered by category slug |
/ειδικεσ-προσφορεσ/{category-slug}/{offset} | index($slug, $offset) | Paginated category view |
/discount | productsWithDiscountGreaterThan() | Products with 50%+ discount |
/discount/{percent} | productsWithDiscountGreaterThan($pct) | Products with custom discount threshold |
/{lang}/discount | Same | Language-prefixed discount page |
Legacy Admin URLs
| URL | Method | Description |
|---|---|---|
/offers/offers_admin | index() | List all offers |
/offers/offers_admin/add | add() | 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_admin | index() | List categories |
/offers/offer_categories_admin/add | add() | 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()
- Category resolution: If a category slug is provided (not the default "all" slug), look up the category via
getCategoryMuiGeneric()and addoffer_category_idto the filter - Count:
countRecords()for pagination total - Sub-content:
indexExtras()loads optional header text from subcontent system (slug:offers-header-text) with PSCache - Categories sidebar:
getCategoriesMui()loads all categories for the current language - Fetch offers:
getRecords()joinsofferswithoffers_muifiltered by current language anddate_start <= NOW(), ordered byord ASC, paginated (20/page) - Render: Template
layouts/offers/offers(storefront view placeholder for theme customization)
Discount Products Page
File: ecommercen/offers/controllers/Adv_offers.php -> productsWithDiscountGreaterThan()
- Cached query:
product_model->getProductsWithDiscountGreaterThan($greaterThan, $limit)via PSCache - Discount calculation: If
ENABLE_SPECIAL_DISCOUNTSregistry key is set, uses CASE expression consideringspecial_from/special_todate range andspecial_discount_percent; otherwise falls back todiscount_persent - Stock ordering: If
LOW_STOCK_END_OF_LISTis enabled, low-stock items are pushed to the end - Product parsing:
defaultProductParseWithProductCodes()enriches product data - Render: Template
layouts/offers/discountswith product listing grid
Admin Offer CRUD
File: ecommercen/offers/controllers/Adv_offers_admin.php
- Authorization: Requires
AUTH_ROLE_ADVISABLE,AUTH_ROLE_ADMIN, orAUTH_ROLE_MARKETING - List (
index): Shows paginated offers (20/page) sorted byord ASC, with inline order update form - Create (
add): Validates title, content, URL (required per language), category (required), date_start (required), date_end (optional). CallsafterAdd($id)hook - Update (
edit): Same validation; callsafterEdit($id)hook - Delete (
delete): CallsafterDelete($id)hook
Admin Category CRUD
File: ecommercen/offers/controllers/Adv_offer_categories_admin.php
- Authorization: Same roles as offers
- Create/Update: Category title per language (required), up to 2 image uploads via
advuploadertofiles/offer_categories/. Slug auto-generated from title viacreateSlug() - Delete: Blocked if category has linked offers (count check with error message showing linked offer count)
- Reorder: AJAX
updateorder()for drag-and-drop category ordering
Domain Layer
Offer (Cms Context)
| Component | Path |
|---|---|
| Entity | src/Domains/Cms/Offer/Repository/Entity.php |
| MuiEntity | src/Domains/Cms/Offer/Repository/MuiEntity.php |
| Repository | src/Domains/Cms/Offer/Repository/Repository.php |
| MuiRepository | src/Domains/Cms/Offer/Repository/MuiRepository.php |
| RepositoryConfigurator | src/Domains/Cms/Offer/Repository/RepositoryConfigurator.php |
| Service (read) | src/Domains/Cms/Offer/Service.php |
| WriteService | src/Domains/Cms/Offer/WriteService.php |
| WriteData | src/Domains/Cms/Offer/WriteData.php |
| MuiWriteData | src/Domains/Cms/Offer/MuiWriteData.php |
| Validator | src/Domains/Cms/Offer/Validator.php |
| ListRequest | src/Domains/Cms/Offer/ListRequest.php |
| WriteRepository | src/Domains/Cms/Offer/Repository/WriteRepository.php |
| MuiWriteRepository | src/Domains/Cms/Offer/Repository/MuiWriteRepository.php |
OfferCategory (Cms Context)
| Component | Path |
|---|---|
| Entity | src/Domains/Cms/OfferCategory/Repository/Entity.php |
| MuiEntity | src/Domains/Cms/OfferCategory/Repository/MuiEntity.php |
| Repository | src/Domains/Cms/OfferCategory/Repository/Repository.php |
| MuiRepository | src/Domains/Cms/OfferCategory/Repository/MuiRepository.php |
| RepositoryConfigurator | src/Domains/Cms/OfferCategory/Repository/RepositoryConfigurator.php |
| Service (read) | src/Domains/Cms/OfferCategory/Service.php |
| WriteService | src/Domains/Cms/OfferCategory/WriteService.php |
| WriteData | src/Domains/Cms/OfferCategory/WriteData.php |
| MuiWriteData | src/Domains/Cms/OfferCategory/MuiWriteData.php |
| Validator | src/Domains/Cms/OfferCategory/Validator.php |
| ListRequest | src/Domains/Cms/OfferCategory/ListRequest.php |
REST Controllers & Resources
| Component | Path |
|---|---|
| Offer Controller | src/Rest/Cms/Controllers/Offer.php |
| OfferCategory Controller | src/Rest/Cms/Controllers/OfferCategory.php |
| Offer Resource | src/Rest/Cms/Resources/Offer/Resource.php |
| Offer MuiResource | src/Rest/Cms/Resources/Offer/MuiResource.php |
| Offer Collection | src/Rest/Cms/Resources/Offer/Collection.php |
| OfferCategory Resource | src/Rest/Cms/Resources/OfferCategory/Resource.php |
| OfferCategory MuiResource | src/Rest/Cms/Resources/OfferCategory/MuiResource.php |
| OfferCategory Collection | src/Rest/Cms/Resources/OfferCategory/Collection.php |
Architecture
Key Components
| Component | Path | Description |
|---|---|---|
| Front controller | ecommercen/offers/controllers/Adv_offers.php | Storefront listing + discount page |
| Admin offers controller | ecommercen/offers/controllers/Adv_offers_admin.php | Offer CRUD |
| Admin categories controller | ecommercen/offers/controllers/Adv_offer_categories_admin.php | Category CRUD |
| Legacy model | ecommercen/offers/models/Adv_offers_model.php | Both offer and category DB operations |
| App-level overrides | application/modules/offers/controllers/Offers.php | Empty extension point |
| App-level model | application/modules/offers/models/Offers_model.php | Empty extension point |
| Offers view template | application/views/main/layouts/offers/offers.php | Storefront offers layout (empty -- theme-driven) |
| Discounts view template | application/views/main/layouts/offers/discounts.php | Discount products grid layout |
| Admin views | application/views/admin/offers/ | List, create, update forms |
| Admin category views | application/views/admin/offers/categories/ | Category list, create, update forms |
| Domain DI config | src/Domains/Cms/container.php | Offer + OfferCategory registrations |
| REST DI config | src/Rest/Cms/container.php | Controller wiring |
| REST routes | application/config/rest_routes.php | REST endpoint routing |
| Storefront routes | application/config/routes.php | Legacy URL routing |
| Auth policies | application/config/rest_policies.php | REST RBAC configuration |
| Template config | application/config/mainTemplate.json | offers and discounts layout mapping |
Data Model
offers
| Column | Type | Description |
|---|---|---|
| id | int (PK) | Auto-increment |
| offer_category_id | int (FK) | Reference to offer_categories.id |
| date_start | datetime | Offer activation date (required) |
| date_end | datetime (nullable) | Offer expiration date (optional) |
| ord | int | Sort position for manual ordering |
offers_mui
| Column | Type | Description |
|---|---|---|
| id | int (PK) | Auto-increment |
| offer_id | int (FK) | Reference to offers.id |
| title | varchar | Offer title (required per language) |
| content | text | Offer body content (required per language) |
| coupon | varchar (nullable) | Optional coupon code |
| url | varchar | Call-to-action URL (required per language in legacy admin) |
| url_title | varchar (nullable) | Display text for the URL |
| lang | varchar | Language code (e.g., el, en) |
offer_categories
| Column | Type | Description |
|---|---|---|
| id | int (PK) | Auto-increment |
| image | varchar (nullable) | Primary category image filename |
| image_2 | varchar (nullable) | Secondary/label category image |
offer_categories_mui
| Column | Type | Description |
|---|---|---|
| id | int (PK) | Auto-increment |
| offer_category_id | int (FK) | Reference to offer_categories.id |
| title | varchar | Category title (required per language) |
| slug | varchar | URL-friendly slug (auto-generated from title) |
| lang | varchar | Language code |
Configuration
Registry Keys (Discount Page)
| Group | Key | Description |
|---|---|---|
| OTHER | ENABLE_SPECIAL_DISCOUNTS | Enables time-bounded special discount prices (special_from/special_to date range) in discount percentage calculation |
| OTHER | LOW_STOCK_END_OF_LIST | Pushes low-stock products to the end of discount product listings |
Template Configuration
In application/config/mainTemplate.json:
offerslayout:layouts/offers/offers-- storefront offers listing pagediscountslayout: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 bothAdv_offers_adminandAdv_offer_categories_admin-- client repos override these for custom post-save logic indexExtras()method: Protected method inAdv_offersfor adding custom data to the offers listing context (e.g., subcontent, banners)- Application-level overrides:
application/modules/offers/controllers/Offers.phpandOffers_admin.phpextend the upstream classes and are empty -- clients can add custom behavior here - Model override:
application/modules/offers/models/Offers_model.phpextendsAdv_offers_modelfor custom queries - View templates:
application/views/main/layouts/offers/offers.phpis empty (theme-driven) -- client themes provide the actual markup;discounts.phprenders 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
- Active offer visibility: Storefront only shows offers where
date_start <= NOW(). Thedate_endis stored but not enforced in the storefront query (offers remain visible after expiry) - 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
- Offer ordering: Offers are sorted by
ord ASCon both storefront and admin. Admin allows inline order updates via form submission - Category slug generation: Slugs are auto-generated from the category title using
createSlug()on create and update - Pagination: Storefront shows 20 offers per page; admin shows 20 per page
- Discount page defaults: Without parameters, shows products with 50%+ discount, limited to 100 products
- Special discount dates: When
ENABLE_SPECIAL_DISCOUNTSis enabled, the discount page considers time-bounded special prices (special_from/special_towithspecial_discount_percent) over the regulardiscount_persent - REST
ordfield visibility: Theordsort position is only returned in API responses when the request context is backend (admin panel) - MUI translation replacement: On REST update, translations are fully replaced (delete all + re-insert), not merged
- REST auth: Read endpoints (index, show, item) are guest-accessible; write endpoints (store, update, destroy) require backend auth with Admin or Marketing role
Related Flows
- CF-12 Promotions -- product-level discount pricing and promo rules
- CF-13 Coupons -- coupon codes that can be linked from offer content
- CF-01 Product Browsing -- product listing used by the discount page
- CF-26 Home Page -- may feature offers via subcontent blocks
- CF-22 Events -- time-bounded content, sibling CMS module
- CF-35 Page Builder --
promoPagebuilder block position used by promo/landing pages