Skip to content

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

RouteMaps tofile:line
audience/reportingaudience/AudienceReporting/indexapplication/config/routes.php:434
(\w{2})/audience/reportingaudience/AudienceReporting/indexroutes.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:

TableColumns readCitation
audienceid, name (select + group_by axis)database/initial/initial.sql:75-84
shop_customer_audienceaudience_id, customer_id (membership pivot; composite PK)initial.sql:1174-1180
shop_orderid (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/)

FileResponsibility
ecommercen/audience/controllers/AdvAudienceReporting.php:1-62Controller — 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-6Routable empty override.
ecommercen/audience/models/AdvAudienceModel.php:284-323getOrdersByAudience() (the aggregation).
ecommercen/audience/models/AdvAudienceModel.php:264-282getTotalVatPerPeriod() (percentage denominator).
application/views/admin/audience/report.php:1-103Date-range form + 3 chart canvases + results table.
public/ui/admin/js/reporting/audience.js:1-85Chart.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

KeyWhereEffect
SMART_RECOMMENDATIONS / IS_ENABLED_CUSTOMER_TAGread at AdvAudienceReporting.php:31; toggled via ecommercen/plus/controllers/AdvGeneralSettings.php:62,72-73Master 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

  1. Audience = customer segment — the report reads the materialized shop_customer_audience pivot built by the AD-22 pipeline; it does not recompute membership.
  2. Order attribution by current membership — an order counts toward an audience if its customer_id is in that audience at query time (no historical snapshot) (AdvAudienceModel.php:300).
  3. Non-canceled onlystatus NOT IN ('CANCELED','RETURN') (:301).
  4. Per-row metrics grouped by audience.idcount = COUNT(orders), total_vat = SUM, avg_cart = AVG(total_vat), total = SUM(total), percentage = this audience's VAT as a share of getTotalVatPerPeriod() (:288-313).
  5. OrderingORDER BY total_vat DESC (highest-revenue audience first) (:303).
  6. Date range — filters shop_order.entry_datetime between $start 00:00:00 and $end 23:59:59; default last 1 month (AdvAudienceReporting.php:36-51).
  7. Feature-gatederror_404() when IS_ENABLED_CUSTOMER_TAG is off (:31-34).

Known Issues & Security Gaps

  1. Double-counting across overlapping audiencesshop_customer_audience PK 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-audience total_vat can sum to more than getTotalVatPerPeriod(), and the percentage column can exceed 100%. Misleading, not a crash.
  2. avg_cart averages VAT, not cart total (mislabeled)select_avg('shop_order.total_vat','avg_cart') (:296) averages total_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 from total. Likely a copy-paste error.
  3. Percentage denominator is store-wide, not audience-scopedgetTotalVatPerPeriod() (: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).
  4. Unbounded query, no pagination, no cache — the aggregation joins the large shop_order table with no LIMIT and no caching (live on every load). Indexes help but the all-orders denominator query runs unbounded.
  5. Menu link not feature-gated while the controller is — the audience/reporting menu entry (admin_menu.php:69-75) is not removed by AdminMenu::parseCDXP (application/libraries/AdminMenu.php:141-153) when IS_ENABLED_CUSTOMER_TAG is off, so the link appears but clicking yields error_404() — a dead-end for disabled installs.
  6. Fragile date defaultingindex():36-39 derives dateStart by mutating the same DateTime object used for dateEnd via sub('P1M'); correct only because dateEnd is captured first. $endDate->format('Y-m-d') at :37 is a discarded no-op.
  7. No POST date validationindex():42,44 feeds POST date_start/date_end straight into new DateTime(...) with no try/catch (malformed input throws) and no range-width or start≤end check (compounds #4).
  8. 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,197 and the FR/DE equivalents).
  9. 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.

  • AD-22 Audience & Campaign Managementparent flow and canonical home for audience definitions, criteria, the membership pipeline jobs, the SMART_RECOMMENDATIONS.IS_ENABLED_CUSTOMER_TAG gate, the modern CRUD/REST layer, and the audience/shop_customer_audience schemas. AD-59 is its analytics readout.
  • AD-13 Settings — where SMART_RECOMMENDATIONS registry settings are configured.