Appearance
Audience Reporting Dashboard
Flow ID: AD-59 | Module(s):
ecommercen/audience/| Complexity: Low Last Updated: 2026-06-28
Business Context
A single-page, read-only admin analytics view that aggregates non-canceled order revenue grouped by audience (customer segment) over a selectable date range. It answers "which customer segments drive revenue/VAT". It is the analytics readout of the segmentation investment documented in AD-22 Audience & Campaign Management — AD-22 builds the audience definitions and the shop_customer_audience membership pivot; AD-59 reads them.
The flow is one legacy controller (index()), one model method (getOrdersByAudience()), one view, and one Chart.js asset. There is no modern src/ or REST equivalent of the reporting aggregation (the modern src/Domains/Plus/Audience/ layer is CRUD-only — AD-22 territory). It is feature-gated behind SMART_RECOMMENDATIONS.IS_ENABLED_CUSTOMER_TAG.
API Reference
REST Endpoints (Modern Layer)
None. There is no REST surface for the reporting dashboard. The only audience REST endpoints are the AD-22 CRUD ones (rest/plus/audience*, application/config/rest_routes.php:1122-1132) — none target the aggregation.
Legacy Admin Routes
| Route | Maps to | file:line |
|---|---|---|
audience/reporting | audience/AudienceReporting/index | application/config/routes.php:434 |
(\w{2})/audience/reporting | audience/AudienceReporting/index | routes.php:435 (locale twin) |
POST to the same route (submit named rearange) triggers the date-range override branch.
Code Flow
GET/POST audience/reporting
└─ AudienceReporting::index() (application/modules/audience/controllers/AudienceReporting.php -> AdvAudienceReporting.php:29-61)
├─ RBAC: allowRole([ADVISABLE, ADMIN, MARKETING]) else error_401 :10-19
├─ feature gate: registry SMART_RECOMMENDATIONS/IS_ENABLED_CUSTOMER_TAG else error_404 :31-34
├─ default range: dateEnd=today, dateStart=today-P1M :36-39
├─ if POST rearange: read date_start/date_end :41-46
├─ where = [entry_datetime >= "$start 00:00:00", entry_datetime <= "$end 23:59:59"] :48-51
└─ data = audience_model->getOrdersByAudience(where) :55
└─ AdvAudienceModel.php:284-323
├─ getTotalVatPerPeriod(where) :264-282 (percentage denominator: SUM(total_vat) over all non-canceled orders)
└─ SELECT audience.name, COUNT(shop_order.id), SUM(total_vat), AVG(total_vat), SUM(total)
FROM audience
LEFT JOIN shop_customer_audience ON ...audience_id
LEFT JOIN shop_order ON ...customer_id
WHERE shop_order.status NOT IN ('CANCELED','RETURN')
GROUP BY audience.id ORDER BY total_vat DESC
└─ render admin/audience/report (3 Chart.js canvases + results table)Data Model
The query touches three tables; none have declared FK constraints. The audience and shop_customer_audience tables are the canonical property of AD-22 — see there for full schema. This flow only reads:
| Table | Columns read | Citation |
|---|---|---|
audience | id, name (select + group_by axis) | database/initial/initial.sql:75-84 |
shop_customer_audience | audience_id, customer_id (membership pivot; composite PK) | initial.sql:1174-1180 |
shop_order | id (count), customer_id (join), total (sum), total_vat (sum+avg), status (filter), entry_datetime (date filter) | initial.sql:1304-1423 (cols :1305,1344,1348,1349,1352,1362; indexes status:1398, customer_id:1400, entry_datetime:1405, status_entry_datetime:1406) |
No Phinx migration is specific to this flow.
Domain Layer
Modern Domain / REST (src/)
No reporting equivalent. A modern Audience CRUD domain exists (src/Domains/Plus/Audience/, src/Rest/Plus/Controllers/Audience.php) but Service.php exposes only all/item/get (:61,70,76) — no aggregation. That CRUD layer is documented in AD-22. AD-59 is legacy-only.
Legacy Layer (ecommercen/)
| File | Responsibility |
|---|---|
ecommercen/audience/controllers/AdvAudienceReporting.php:1-62 | Controller — RBAC :10-19, feature gate :31-34, date defaults :36-39, POST branch :41-46, render :53-60. |
application/modules/audience/controllers/AudienceReporting.php:1-6 | Routable empty override. |
ecommercen/audience/models/AdvAudienceModel.php:284-323 | getOrdersByAudience() (the aggregation). |
ecommercen/audience/models/AdvAudienceModel.php:264-282 | getTotalVatPerPeriod() (percentage denominator). |
application/views/admin/audience/report.php:1-103 | Date-range form + 3 chart canvases + results table. |
public/ui/admin/js/reporting/audience.js:1-85 | Chart.js wiring (count / total_vat / avg_cart charts). |
Helpers: formatNumber() (ecommercen/helpers/shopmodule_helper.php:216-219), calcPercentage() (:223-230, div-by-zero safe). Date filtering uses inherited fixWhereCondition() (ecommercen/core/models/Adv_base_model.php:57-78).
Configuration
| Key | Where | Effect |
|---|---|---|
SMART_RECOMMENDATIONS / IS_ENABLED_CUSTOMER_TAG | read at AdvAudienceReporting.php:31; toggled via ecommercen/plus/controllers/AdvGeneralSettings.php:62,72-73 | Master feature gate; off → index() returns error_404(). Same flag gates the whole AD-22 pipeline. |
No dedicated config file or env var; the date range is request-driven (default last 1 month). Page title key eshop.admin.menu.reporting.referrers; menu label admin.menu.ecommercenPlus.marketing.reporting.label.
Client Extension Points
application/modules/audience/controllers/AudienceReporting.php and application/modules/audience/models/Audience_model.php are the empty override seams (both extends Adv*).
Business Rules
- Audience = customer segment — the report reads the materialized
shop_customer_audiencepivot built by the AD-22 pipeline; it does not recompute membership. - Order attribution by current membership — an order counts toward an audience if its
customer_idis in that audience at query time (no historical snapshot) (AdvAudienceModel.php:300). - Non-canceled only —
status NOT IN ('CANCELED','RETURN')(:301). - Per-row metrics grouped by
audience.id—count= COUNT(orders),total_vat= SUM,avg_cart= AVG(total_vat),total= SUM(total),percentage= this audience's VAT as a share ofgetTotalVatPerPeriod()(:288-313). - Ordering —
ORDER BY total_vat DESC(highest-revenue audience first) (:303). - Date range — filters
shop_order.entry_datetimebetween$start 00:00:00and$end 23:59:59; default last 1 month (AdvAudienceReporting.php:36-51). - Feature-gated —
error_404()whenIS_ENABLED_CUSTOMER_TAGis off (:31-34).
Known Issues & Security Gaps
- Double-counting across overlapping audiences —
shop_customer_audiencePK is(audience_id, customer_id)(initial.sql:1177), so a customer can belong to multiple audiences; the join (AdvAudienceModel.php:300) sums each order's VAT/total into every matching audience. The per-audiencetotal_vatcan sum to more thangetTotalVatPerPeriod(), and thepercentagecolumn can exceed 100%. Misleading, not a crash. avg_cartaverages VAT, not cart total (mislabeled) —select_avg('shop_order.total_vat','avg_cart')(:296) averagestotal_vat, but the view labels it "Avg Cart" (report.php:56,71) and renders it as a money value (report.php:92). Average order value should derive fromtotal. Likely a copy-paste error.- Percentage denominator is store-wide, not audience-scoped —
getTotalVatPerPeriod()(:264-282) sums VAT over all non-canceled orders, including customers in no audience. So percentages measure "audience VAT vs total store VAT", and combined with #1 the column's meaning is ambiguous (no documented intended semantics — commit body empty). - Unbounded query, no pagination, no cache — the aggregation joins the large
shop_ordertable with no LIMIT and no caching (live on every load). Indexes help but the all-orders denominator query runs unbounded. - Menu link not feature-gated while the controller is — the
audience/reportingmenu entry (admin_menu.php:69-75) is not removed byAdminMenu::parseCDXP(application/libraries/AdminMenu.php:141-153) whenIS_ENABLED_CUSTOMER_TAGis off, so the link appears but clicking yieldserror_404()— a dead-end for disabled installs. - Fragile date defaulting —
index():36-39derivesdateStartby mutating the sameDateTimeobject used fordateEndviasub('P1M'); correct only becausedateEndis captured first.$endDate->format('Y-m-d')at:37is a discarded no-op. - No POST date validation —
index():42,44feeds POSTdate_start/date_endstraight intonew DateTime(...)with no try/catch (malformed input throws) and no range-width or start≤end check (compounds #4). - Scribe How tour is generic + missing for FR/DE — the embedded tour is "Audience Manager" (AD-22 CRUD), reused on the reporting page; FR/DE values are empty strings (
ecommercen/language/english/adv_scribe_lang.php:96,197and the FR/DE equivalents). - No FK constraints — orphan pivot rows (deleted customer/audience) silently skew aggregates (platform-wide pattern). No SQL-injection risk: filter values bind via the CI QueryBuilder.
Tests
None for this flow. getOrdersByAudience()/getTotalVatPerPeriod() (the aggregation SQL, percentage math, double-counting, avg-cart-uses-VAT quirk) and the controller index() are entirely untested. Existing audience tests cover only the modern CRUD domain (AD-22 territory): tests/Integration/Domains/Plus/Audience/{RepositoryTest,ServiceTest}.php, tests/Unit/Domains/Plus/Audience/ServiceTest.php.
Related Flows
- AD-22 Audience & Campaign Management — parent flow and canonical home for audience definitions, criteria, the membership pipeline jobs, the
SMART_RECOMMENDATIONS.IS_ENABLED_CUSTOMER_TAGgate, the modern CRUD/REST layer, and theaudience/shop_customer_audienceschemas. AD-59 is its analytics readout. - AD-13 Settings — where
SMART_RECOMMENDATIONSregistry settings are configured.