Appearance
Audience & Campaign Management
Flow ID: AD-22 Module(s): audience, campaigns, plus Complexity: Medium Last Updated: 2026-04-08
Business Overview
Audiences are criteria-based customer segments used for personalized experiences. Customers are auto-assigned to audiences via background jobs based on 8 tag groups (vendor, category, registration, referral, turnover, avg cart, country, county). Audiences gate access to coupons, slides (campaigns), and gift rules. The system uses AND/OR/ALL criteria logic for flexible segment definition.
Tag generation runs daily via AdvAddTagsToCustomers, analyzing order history, customer profiles, and geographic data. Audience membership is then computed by AdvAddCustomersToSpecificAudience which compiles SQL from criteria and bulk-inserts matching customer IDs. The entire pipeline is feature-gated by SMART_RECOMMENDATIONS.IS_ENABLED_CUSTOMER_TAG.
Both a legacy admin CRUD interface and a modern REST API with full CRUD operate on the audience data model.
API Reference
REST Endpoints (Modern Layer)
Audience Endpoints:
| Method | Path | Controller | Description |
|---|---|---|---|
| GET | /rest/plus/audience | Advisable\Rest\Plus\Controllers\Audience::index | List all audiences (paginated, filterable) |
| GET | /rest/plus/audience/item | Audience::item | Field metadata |
| GET | /rest/plus/audience/{id} | Audience::show | Single audience with details |
| POST | /rest/plus/audience | Audience::store | Create audience |
| POST | /rest/plus/audience/{id} | Audience::update | Update audience |
| DELETE | /rest/plus/audience/{id} | Audience::destroy | Delete audience |
Audience Criteria Endpoints:
| Method | Path | Controller | Description |
|---|---|---|---|
| GET | /rest/plus/audience-criteria | Advisable\Rest\Plus\Controllers\AudienceCriteria::index | List all criteria (paginated, filterable) |
| GET | /rest/plus/audience-criteria/item | AudienceCriteria::item | Field metadata |
| GET | /rest/plus/audience-criteria/{id} | AudienceCriteria::show | Single criterion |
| POST | /rest/plus/audience-criteria | AudienceCriteria::store | Create criterion |
| POST | /rest/plus/audience-criteria/{id} | AudienceCriteria::update | Update criterion |
| DELETE | /rest/plus/audience-criteria/{id} | AudienceCriteria::destroy | Delete criterion |
All endpoints also support language-prefixed routes: /{lang}/rest/plus/audience/....
Total: 12 REST endpoints (6 per entity x 2 entities), each duplicated with language prefix = 24 route entries.
Legacy Admin Routes
| Route | Controller | Description | Required Roles |
|---|---|---|---|
audience_admin | Adv_audience_admin::index | Audience list with member counts and campaign counts | ADVISABLE, ADMIN, MARKETING |
audience_admin/add | Adv_audience_admin::add | Create audience with criteria | ADVISABLE, ADMIN, MARKETING |
audience_admin/edit/{id} | Adv_audience_admin::edit | Edit audience criteria | ADVISABLE, ADMIN, MARKETING |
audience_admin/delete/{id} | Adv_audience_admin::delete | Delete audience + criteria + memberships | ADVISABLE, ADMIN, MARKETING |
audience_admin/getCustomersAudience/{id} | Adv_audience_admin::getCustomersAudience | CSV export of audience members | ADVISABLE, ADMIN, MARKETING |
Background Jobs
| Job Class | CLI Command | Description |
|---|---|---|
AdvAddTagsToCustomers | php cli.php job/AddTagsToCustomers | Generate customer tags from order data (runs daily) |
AdvAddCustomersToAudience | php cli.php job/AddCustomersToAudience | Enqueue per-audience membership rebuild jobs for all audiences |
AdvAddCustomersToSpecificAudience | php cli.php job/AddCustomersToSpecificAudience/{audienceId} | Rebuild membership for a specific audience |
Code Flow
Tag Generation Pipeline
AdvAddTagsToCustomers::executeCommand([])
|-- $fromDate = yesterday (incremental, last 24h)
|-- customer_tag_model->saveCustomersTags($fromDate)
|-- saveCategoryTags($fromDate)
| |-- INSERT IGNORE INTO shop_customer_tag(customer_id, tag_id, group_id, custom_value)
| |-- SELECT order.customer_id, product_category.category_id, GROUP_CATEGORY, NULL
| |-- FROM shop_order_basket
| |-- JOIN shop_order -> JOIN shop_customer (is_guest=0, is_sanitized=0)
| |-- JOIN product_codes -> JOIN shop_product_category_lp
| \-- WHERE gift_id IS NULL [exclude gift items]
|
|-- saveVendorTags($fromDate)
| |-- Same pattern: tag_id = shop_product.vendor_id
| \-- GROUP_VENDOR=1
|
|-- saveRegisteredTags($fromDate)
| |-- INSERT IGNORE: tag_id = IF(is_guest=0, 1, 0)
| |-- GROUP_REGISTERED=3
| \-- WHERE is_guest=0, is_sanitized=0
|
|-- saveLastReferralTags($fromDate)
| |-- Complex self-join: find last order per customer
| |-- tag_id = IFNULL(skroutz_referer, 0)
| |-- GROUP_LAST_REFERRAL=4
| \-- Delete-then-insert (last referral changes per customer)
|
|-- saveCountryTags($fromDate)
| |-- REPLACE INTO: tag_id=0, custom_value=pricing_country
| |-- GROUP_COUNTRY=7
| \-- Validated against country table
|
|-- saveCountyTags($fromDate)
| |-- REPLACE INTO: tag_id=0, custom_value=pricing_county
| |-- GROUP_COUNTY=8
| \-- Validated against county table
|
|-- saveTurnoverTags() [always full recalc, no fromDate]
| |-- REPLACE INTO: tag_id=0, custom_value=SUM(total_vat)
| |-- GROUP_TURNOVER=5
| \-- Only orders with status IN ('SENT', 'PAID_SENT', 'INVOICED')
|
\-- saveAvgCartTags() [always full recalc, no fromDate]
|-- REPLACE INTO: tag_id=0, custom_value=AVG(total_vat)
|-- GROUP_AVG_CART=6
\-- Only orders with status IN ('SENT', 'PAID_SENT', 'INVOICED')Audience Membership Rebuild
AdvAddCustomersToAudience::executeCommand([])
|-- audience_model->getRecords() [all audiences]
|-- For each audience:
| |-- Schedule AddCustomersToSpecificAudience job
| |-- Stagger: +5 minutes per audience
| \-- Queue: 'personalization', grace_time: 60s, retries: 1
AdvAddCustomersToSpecificAudience::executeCommand(['audience' => $id])
|-- audience_model->saveCustomersToAudience($audienceId)
|-- getCustomersOfAudienceCompile($audienceId)
| |-- Load audience criteria
| |-- For criteria_type AND (2):
| | |-- Build SQL with INNER JOINs for each criterion
| | \-- All criteria must match
| |-- For criteria_type OR (1):
| | |-- Build SQL with single JOIN + multiple WHERE conditions
| | \-- Any criterion may match
| |-- For criteria_type ALL (null):
| | \-- SELECT all non-guest, non-sanitized customers
| \-- Return compiled SQL
|-- DELETE FROM shop_customer_audience WHERE audience_id = $id
\-- INSERT INTO shop_customer_audience (audience_id, customer_id)
SELECT $id, tt.id FROM ({compiled_sql}) AS ttAdmin Audience CRUD
Adv_audience_admin::add()
|-- Validate form (name, tagType, tags[group_id] arrays)
|-- audience_model->addRecord($audience, $criteria)
| |-- Insert into audience table (name, user_id, criteria_type, creation_datetime)
| |-- Build criteria batch from tags:
| | |-- Groups with custom values (AVG.CART, COUNTRY, COUNTY):
| | | \-- Store tag_id + custom_value per criterion
| | |-- Groups with tag IDs (VENDOR, CATEGORY, etc.):
| | | \-- Store tag_id per criterion
| | \-- INSERT batch into audience_criteria
| \-- Return audienceId
|-- afterAdd($audienceId)
| \-- Enqueue AddCustomersToSpecificAudience job (immediate, queue=personalization)
Adv_audience_admin::edit($id)
|-- audience_model->updateRecord($id, $audience, $criteria)
| |-- UPDATE audience table
| |-- DELETE all existing criteria for this audience
| \-- INSERT new criteria batch
|-- afterEdit($id)
| \-- Enqueue AddCustomersToSpecificAudience job (immediate rebuild)Frontend Audience Check (Every Storefront Page)
Adv_front_controller::__construct()
|-- setCustomerAudience()
| |-- Guard: customer_logged_in AND SMART_RECOMMENDATIONS.IS_ENABLED_CUSTOMER_TAG
| |-- audience_model->getCustomerAudience($customer_id)
| | \-- SELECT audience_id FROM shop_customer_audience WHERE customer_id = $id
| \-- CartResource::getInstance()->setCustomerAudience($audienceIds)
|
[Later, during coupon/slide rendering:]
|-- EntityCustomerAudienceCheck::audiencesCheck($customerAudiences, $entityAudienceId)
| |-- null audience_id -> true (no restriction)
| |-- criteria_type ALL -> true
| |-- No criteria -> true
| |-- No customer audiences -> false
| \-- in_array($entityAudienceId, $customerAudiences)Domain Layer
Modern Domain (src/Domains/Plus/)
Audience Entity:
| File | Description |
|---|---|
Audience/Repository/Entity.php | Properties: id, name, criteria_type, creation_datetime, update_datetime, user_id |
Audience/Repository/Repository.php | BaseRepository for audience table |
Audience/Repository/RepositoryConfigurator.php | Table name and relationships |
Audience/Repository/WriteRepository.php | Insert/update operations |
Audience/Service.php | Read: match (paginated), get (single) |
Audience/WriteService.php | Write: store, update, destroy |
Audience/WriteData.php | Value object for audience fields |
Audience/Validator.php | Validates required fields |
Audience/ListRequest.php | Filter/sort/pagination params |
AudienceCriteria Entity:
| File | Description |
|---|---|
AudienceCriteria/Repository/Entity.php | Properties: id, audience_id, tag_id, group_id, custom_value |
AudienceCriteria/Repository/Repository.php | BaseRepository for audience_criteria table |
AudienceCriteria/Repository/RepositoryConfigurator.php | Table name and relationships |
AudienceCriteria/Repository/WriteRepository.php | Insert/update operations |
AudienceCriteria/Service.php | Read operations |
AudienceCriteria/WriteService.php | Write operations |
AudienceCriteria/WriteData.php | Value object for criteria fields |
AudienceCriteria/Validator.php | Validates required fields |
AudienceCriteria/ListRequest.php | Filter/sort/pagination params |
DI Container: src/Domains/Plus/container.php
Legacy Models (ecommercen/audience/)
| File | Description |
|---|---|
models/AdvCustomerTagModel.php | Tag generation model (~500 lines). saveCustomersTags() runs all 8 tag generators. Each uses raw SQL with INSERT IGNORE or REPLACE INTO for idempotent bulk operations. |
models/AdvAudienceModel.php | Extends AdvCustomerTagModel. Audience CRUD + audience compilation SQL. saveCustomersToAudience() compiles criteria into SQL and bulk-inserts matching customers. |
Legacy Controllers
| File | Description |
|---|---|
audience/controllers/Adv_audience_admin.php | Audience CRUD (290 lines). Uses AdminCustomersTagsFilters trait for filter dropdowns. Auto-triggers job on add/edit. CSV export of audience members. |
Integration Traits and Models
| File | Description |
|---|---|
ecommercen/traits/EntityCustomerAudienceCheck.php | audiencesCheck() method used by coupons, slides, and cart resource to validate customer audience membership |
ecommercen/coupons/models/Adv_coupons_model.php | Uses EntityCustomerAudienceCheck for audience-restricted coupons |
ecommercen/sliders/models/Adv_sliders_model.php | Uses audience check for slide filtering |
ecommercen/libraries/AdvCartResource.php | Stores customer audiences for use during cart rendering |
ecommercen/eshop/models/Adv_customer_campaign_model.php | Tracks customer campaign participation |
Architecture
8 Customer Tag Groups
| Group ID | Constant Key | Tag Source | tag_id Meaning | custom_value |
|---|---|---|---|---|
| 1 | VENDOR | Products purchased | shop_vendor.id | NULL |
| 2 | CATEGORY | Products purchased | shop_product_category.id | NULL |
| 3 | REGISTERED | Account status | 1 (registered) | NULL |
| 4 | LAST.REFERRAL | Last order referral | Referral channel ID (0=none) | NULL |
| 5 | TURNOVER | Lifetime spend | 0 (single entry) | SUM(total_vat) |
| 6 | AVG.CART | Average order value | 0 (single entry) | AVG(total_vat) |
| 7 | COUNTRY | Billing country | 0 (single entry) | Country alpha-2 code |
| 8 | COUNTY | Billing county | 0 (single entry) | County alpha code |
Tag groups are defined as constants in application/config/constants.php:
php
define('CUSTOMER_TAGS_GROUPS', [
'VENDOR' => 1, 'CATEGORY' => 2, 'REGISTERED' => 3, 'LAST.REFERRAL' => 4,
'TURNOVER' => 5, 'AVG.CART' => 6, 'COUNTRY' => 7, 'COUNTY' => 8
]);Criteria Types
| Constant | Value | SQL Strategy | Description |
|---|---|---|---|
ALL | null | Select all non-guest customers | All customers match |
OR | 1 | Single JOIN + multiple WHERE conditions | Customer matches ANY criterion |
AND | 2 | INNER JOIN per criterion | Customer matches ALL criteria |
php
define('CRITERIA_TYPES', ['ALL' => null, 'OR' => 1, 'AND' => 2]);Tag Generation Strategy
| Groups 1-4 | Groups 5-8 |
|---|---|
Incremental: INSERT IGNORE with fromDate filter | Full recalculation: REPLACE INTO (no date filter) |
| Order-history based | Aggregate-based (SUM/AVG) |
| Grow over time (new tags added) | Overwrite with latest values |
Uses fromDate = yesterday for daily runs | Always recalculates for all customers |
Data Flow Diagram
[Order History] [Customer Profiles]
| |
v v
AdvAddTagsToCustomers (daily job)
|
v
[shop_customer_tag] (tag_id + group_id per customer)
|
v
AdvAddCustomersToAudience (enqueue per-audience jobs)
|
v
AdvAddCustomersToSpecificAudience (per audience)
|-- Load audience_criteria for this audience
|-- Compile SQL based on criteria_type (AND/OR/ALL)
|-- DELETE existing memberships
\-- INSERT matching customer IDs
|
v
[shop_customer_audience] (audience_id + customer_id)
|
+---> Coupon validation (EntityCustomerAudienceCheck)
+---> Slide filtering (audience_id check)
\---> Cart resource (customer audience list)Data Model
audience Table
| Column | Type | Description |
|---|---|---|
id | INT(11) UNSIGNED PK AI | Audience ID |
name | VARCHAR(255) | Audience name |
criteria_type | TINYINT(4) NULL | null=ALL, 1=OR, 2=AND |
creation_datetime | DATETIME | Created timestamp |
update_datetime | DATETIME NULL | Last updated timestamp |
user_id | INT(11) NULL | Admin user who created/edited |
Index: user_id.
audience_criteria Table
| Column | Type | Description |
|---|---|---|
id | INT(11) UNSIGNED PK AI | Criterion ID |
audience_id | INT(11) UNSIGNED | FK to audience.id |
tag_id | INT(11) | Tag identifier (vendor ID, category ID, 0 for value-based, etc.) |
group_id | INT(11) | Tag group (1-8) |
custom_value | VARCHAR(50) NULL | For value-based groups (AVG.CART ranges, country codes) |
Unique key: (audience_id, tag_id, group_id, custom_value).
shop_customer_audience Table
| Column | Type | Description |
|---|---|---|
audience_id | INT(11) UNSIGNED | FK to audience.id |
customer_id | INT(11) | FK to shop_customer.id |
Primary key: (audience_id, customer_id). Indexes: audience_id, customer_id.
shop_customer_tag Table
| Column | Type | Description |
|---|---|---|
id | INT(11) PK AI | Tag entry ID |
customer_id | INT(11) | FK to shop_customer.id |
tag_id | INT(11) | Identifier within group (vendor ID, category ID, 0, etc.) |
group_id | INT(11) | Tag group (1-8) |
custom_value | VARCHAR(50) NULL | Value for value-based groups |
Unique key: (customer_id, tag_id, group_id). Index: (group_id, custom_value). Engine: InnoDB (converted from MyISAM in migration 20260408100114).
shop_customer_campaign Table
| Column | Type | Description |
|---|---|---|
id | INT(11) PK AI | Campaign entry ID |
customer_id | INT(11) | FK to shop_customer.id |
campaign_type | INT(11) | Campaign type identifier |
entry_datetime | DATETIME | When customer entered campaign |
Indexes: entry_datetime, customer_id, campaign_type, (customer_id, campaign_type), (customer_id, campaign_type, entry_datetime).
Configuration
Feature Gate
The entire audience/tag system is gated by:
php
$this->registry->value('SMART_RECOMMENDATIONS', 'IS_ENABLED_CUSTOMER_TAG')When disabled, the audience admin returns 404 and the frontend skips audience loading.
Job Scheduling
| Job | Schedule | Queue | Grace Time | Retries |
|---|---|---|---|---|
AddTagsToCustomers | Daily (cron) | default | N/A | N/A |
AddCustomersToAudience | Daily (after tags) | personalization | 60s per audience | 1 |
AddCustomersToSpecificAudience | On-demand (after audience edit) | personalization | 60s | 1 |
Jobs are staggered by +5 minutes per audience when the bulk rebuild runs to prevent database overload.
Related Config
| Config | Location | Description |
|---|---|---|
CUSTOMER_TAGS_GROUPS | application/config/constants.php | Group ID mapping |
CRITERIA_TYPES | application/config/constants.php | Criteria type constants |
referrerChannels | application/config/referrer_channels.php | Referral channel definitions for LAST.REFERRAL tags |
Client Extension Points
Adv_audience_adminhas emptyafterAdd($id)andafterDelete($id)hooks (both trigger job enqueue by default, butafterDeleteis a no-op hook for client extension).- Custom tag groups: The system is limited to 8 built-in groups. Client repos can add custom SQL tag generators by extending
AdvCustomerTagModelinapplication/models/. - Custom criteria logic: The
getCustomersOfAudienceCompile()method can be overridden in client repos for custom audience compilation strategies. - REST endpoints: Can be overridden via DI alias in
custom/Domains/Plus/container.php.
Business Rules
- Gift items excluded:
gift_id IS NULLfilter ensures gift line items do not contribute to vendor/category tags. - Only real customers: Tags only generated for non-guest (
is_guest = 0) and non-sanitized (is_sanitized = 0) customers. - Order status filter: Turnover and avg cart tags only count orders with status IN (
SENT,PAID_SENT,INVOICED). Canceled and returned orders are excluded. - Delete-then-insert: Audience membership rebuild uses DELETE + INSERT pattern (not upsert) to handle removed customers.
- Incremental vs full: Category, vendor, registered, referral, country, county tags are incremental (daily delta). Turnover and avg cart are always fully recalculated.
- Last referral override: The LAST.REFERRAL tag group uses delete-then-insert per customer because only the most recent order's referral matters.
- InnoDB for tags:
shop_customer_tagwas converted to InnoDB (migration20260408100114_convert_myisam_tables_to_innodb.php).INSERT IGNOREcontinues to work identically under InnoDB; row-level locking replaces the former table-level lock. - Job staggering: Bulk audience rebuild staggers jobs by 5 minutes per audience to prevent DB contention.
- CSV export: The
getCustomersAudience()endpoint exports audience members as CSV (ID, email, phone, mobile, turnover value) with BOM bytes for Excel compatibility. - Audience deletion: Deletes from all three tables (audience, audience_criteria, shop_customer_audience). No cascade constraints -- handled in application code.
Related Flows
- CF-13 Coupons -- audience-restricted coupons via
coupon.audience_idandEntityCustomerAudienceCheck - AD-08 Coupon Management -- coupon audience targeting via
coupon.audience_id - AD-09 Gift Rules -- gift rules do not use audience targeting directly but share the
EntityCustomerAudienceChecktrait - AD-21 Slider Management -- audience-targeted slides via
slide.audience_id - AD-04 Customer Management -- customer tags display in admin
- AD-13 Settings & Configuration --
SMART_RECOMMENDATIONS.IS_ENABLED_CUSTOMER_TAGfeature flag - AD-46 Audience REST -- modern REST API for audience management