Skip to main content

Checkout Shield

Checkout Shield (plg_j2commerce_app_checkoutshield) is a non-core J2Commerce app plugin that runs a layered, score-based risk pipeline against the checkout funnel — honeypots, timing traps, velocity limits, a card-testing signature detector, optional IP/email reputation, native-captcha challenges, and an escalating ban ladder — with its own Analytics-menu dashboard and a companion task plugin (plg_task_j2commerce_checkoutshield, group task) for scheduled maintenance.

This page documents the plugin's extensibility surface: the three events other extensions can subscribe to in order to inject content into the Checkout Shield dashboard, the database schema, the full configuration parameter reference, and the task-plugin routines. It does not re-document the internal detection pipeline (signals, velocity, response ladder) — that is internal to the plugin and not a public API.

Identity​

Element (app plugin)app_checkoutshield
Element (task plugin)j2commerce_checkoutshield
Namespace (app plugin)J2Commerce\Plugin\J2Commerce\AppCheckoutshield
Namespace (task plugin)J2Commerce\Plugin\Task\J2commerceCheckoutshield
Path (app plugin)plugins/j2commerce/app_checkoutshield/
Path (task plugin)plugins/task/j2commerce_checkoutshield/
Dependencycom_j2commerce (installer script checks for it and refuses to install without it)

The two plugins are independent Joomla extensions that cooperate at runtime: the task plugin reads the app plugin's own params (PluginHelper::getPlugin('j2commerce', 'app_checkoutshield')) and constructs the app plugin's PSR-4 service classes directly (ListSyncService, Notifier, CardTestingService, GeoService, StateStore) rather than reaching into the running plugin instance. Neither extension owns any core file.

Architecture​

Enforcement is gated by the mode param (monitor default, enforce). In monitor mode nothing is ever denied — every request is logged with the action it would have taken. The single gating decision (allowlist -> active ban -> lockdown guest block -> velocity cooldown -> block band -> challenge band) is shared between onAfterRoute (the earliest possible gate, and the only veto path for event-less tasks such as checkout.confirm and the AJAX lookups) and the onJ2CommerceCheckoutValidate* listeners (which own scoring for tasks that do have a validate event). Every enforcement path is wrapped in try/catch and fails open — a DB, DNS, or HTTP error is logged and the checkout is allowed to continue.

Dashboard Extensibility Events​

The Checkout Shield dashboard (view=appplugin&plugin=app_checkoutshield&pluginview=dashboard) dispatches three events that let other plugins inject their own quick-link tiles, widget cards, and KPI tiles — without any core-file coupling. This mirrors the app_marketplace filter-style event pattern (EventHelper::getPayoutDashboardQuickLinks()). No other reference dashboard in the codebase (app_avalaratax included) exposes injection events like this — it is unique to Checkout Shield.

All three are dispatched from CheckoutShieldDashboardHelper (src/Helper/CheckoutShieldDashboardHelper.php), a static, dependency-free helper class — not an MVC model — that never makes a network call. Verified dispatch code:

// File: plugins/j2commerce/app_checkoutshield/src/Helper/CheckoutShieldDashboardHelper.php

public static function dashboardQuickLinks(): array
{
$links = [];

try {
$event = J2CommerceHelper::plugin()->event('CheckoutShieldDashboardQuickLinks', ['links' => &$links]);
$links = (array) $event->getArgument('links', $links);
} catch (\Throwable $e) {
Log::add('dashboardQuickLinks dispatch failed: ' . $e->getMessage(), Log::WARNING, self::LOG_CATEGORY);
}

return $links;
}

J2CommerceHelper::plugin()->event() prepends onJ2Commerce to the string you pass it — calling ->event('CheckoutShieldDashboardQuickLinks', ...) dispatches onJ2CommerceCheckoutShieldDashboardQuickLinks, which is the event name your listener subscribes to. All three dispatch calls are wrapped in try/catch: a broken or missing listener logs a WARNING under the plg_j2commerce_app_checkoutshield log category and the dashboard simply renders without that extension's content — a third-party plugin can never break the dashboard page.

Timing

All three events fire once, from AppCheckoutshield::renderDashboard(), on a full page load of pluginview=dashboard. They are not re-dispatched by the dashboard.data AJAX handler that powers the KPI-band/chart date-filter refresh (ajaxDashboardData()) — third-party quick links, widgets, and KPI tiles only update on page reload, not on every date-range change.

Adds tiles to an extra "Extension Links" quick-icon row (rendered only when non-empty, via the core dashboard.quickicon layout — the same layout the shield's own Attempts/Blocked IPs/Allowlist/Settings row uses).

Argument['links' => &$links]
Item shape['link', 'image', 'name', 'id'?]
// File: plugins/j2commerce/app_example/src/Extension/AppExample.php

declare(strict_types=1);

namespace J2Commerce\Plugin\J2Commerce\AppExample\Extension;

use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;

final class AppExample extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return ['onJ2CommerceCheckoutShieldDashboardQuickLinks' => 'onShieldQuickLinks'];
}

public function onShieldQuickLinks(Event $event): void
{
$links = (array) $event->getArgument('links', []);
$links[] = [
'link' => 'index.php?option=com_j2commerce&view=appplugin&plugin=app_example&pluginview=report',
'image' => 'fa-solid fa-chart-column',
'name' => Text::_('PLG_J2COMMERCE_APP_EXAMPLE_SHIELD_LINK'),
'id' => 'example-shield-link',
];
$event->setArgument('links', $links);
}
}

onJ2CommerceCheckoutShieldDashboardWidgets​

Adds a card to the dashboard's data-card grid, after the shield's own "Recent attempts / Top offenders / Top domains / List sync status" cards.

Arguments['widgets' => &$widgets, 'fromDate' => $fromDate, 'toDate' => $toDate]
Item shape['title', 'html', 'col'?] — col defaults to col-md-6
ContractThe provider MUST pre-escape html. The dashboard template echoes it raw: <?php echo (string) ($widget['html'] ?? ''); ?> — this is the one documented spot in Checkout Shield where trusted-provider raw HTML is intentional, on the same trust model as core pluginWidgets.
// File: plugins/j2commerce/app_example/src/Extension/AppExample.php

public static function getSubscribedEvents(): array
{
return [
'onJ2CommerceCheckoutShieldDashboardQuickLinks' => 'onShieldQuickLinks',
'onJ2CommerceCheckoutShieldDashboardWidgets' => 'onShieldWidgets',
];
}

public function onShieldWidgets(Event $event): void
{
$widgets = (array) $event->getArgument('widgets', []);
$fromDate = (string) $event->getArgument('fromDate', '');
$toDate = (string) $event->getArgument('toDate', '');

$flagged = $this->countExampleFlagsInRange($fromDate, $toDate);

$widgets[] = [
'title' => Text::_('PLG_J2COMMERCE_APP_EXAMPLE_SHIELD_WIDGET_TITLE'),
// Contract: pre-escape everything yourself — the shield echoes 'html' RAW.
'html' => '<p>' . htmlspecialchars(
Text::sprintf('PLG_J2COMMERCE_APP_EXAMPLE_SHIELD_WIDGET_BODY', $flagged),
ENT_QUOTES,
'UTF-8'
) . '</p>',
'col' => 'col-md-6',
];
$event->setArgument('widgets', $widgets);
}

onJ2CommerceCheckoutShieldDashboardKpis​

Adds a tile to a second KPI band, rendered below the shield's own six-tile KPI band (same quickicon/alert alert-{accent} markup), only when at least one listener contributes a tile.

Arguments['kpis' => &$kpis, 'fromDate' => $fromDate, 'toDate' => $toDate]
Item shape['id', 'label', 'value', 'accent', 'change'?]
accent valuessuccess | info | warning | danger | purple (an invalid value silently falls back to info in the template)
public static function getSubscribedEvents(): array
{
return ['onJ2CommerceCheckoutShieldDashboardKpis' => 'onShieldKpis'];
}

public function onShieldKpis(Event $event): void
{
$kpis = (array) $event->getArgument('kpis', []);
$fromDate = (string) $event->getArgument('fromDate', '');
$toDate = (string) $event->getArgument('toDate', '');

$kpis[] = [
'id' => 'example-flags',
'label' => Text::_('PLG_J2COMMERCE_APP_EXAMPLE_SHIELD_KPI_LABEL'),
'value' => (string) $this->countExampleFlagsInRange($fromDate, $toDate),
'accent' => 'info',
'change' => null,
];
$event->setArgument('kpis', $kpis);
}

Other Events Checkout Shield Subscribes To​

Not extensibility hooks — listed for context on how the plugin integrates with J2Commerce core. All handlers are registered in AppCheckoutshield::getSubscribedEvents().

EventPriorityPurpose
onAfterRoute (core Joomla)defaultEarliest enforcement gate for option=com_j2commerce; the only veto path for event-less tasks.
onJ2CommerceBeforeCheckoutdefaultCheckout-only asset hook (WebAssetManager unlocked here) — registers shield-checkout.js/css, issues signed tokens via script options, sets the session-continuity marker.
onAfterRender (core Joomla)defaultSplices the off-screen honeypot input into the checkout page HTML buffer.
onAjaxApp_checkoutshield (com_ajax, site)defaultServes challenge state + captcha widget markup to the checkout JS. Guest-accessible by design, CSRF-checked, no personal data, self velocity-limited.
onJ2CommercePostPaymentPriority::LOWCaptures the payment-attempt outcome for the card-testing detector, after payment plugins have populated the event result.
onJ2CommerceAfterPaymentdefaultSuccess-confirmation fallback (rarely fires — onPostPayment normally already logs).
onJ2CommerceCheckoutValidateGuest / ValidateBilling / BeforeCheckoutValidateShipping / BeforeCheckoutValidateGuestShippingPriority::HIGHScoring + veto for the checkout steps that have a validate event.
onJ2CommerceRegisterEmailTypes / GetEmailTemplateCards / GetEmailTemplates / ProcessEmailTagsdefaultRegisters the "Checkout Shield Alert" and "Checkout Shield Digest" email types under Design → Email Templates (J2Commerce has no Joomla MailTemplate — it rolls its own DB-backed template system).
onJ2CommerceAddDashboardMenuInJ2CommercedefaultAppends the "Checkout Shield" child under the Analytics menu group — only when the current user passes both j2commerce.viewreports and j2commerce.viewsetup.
onJ2CommerceAppPluginViewdefaultThe plugin-owned admin container: routes pluginview (dashboard/attempts/blocklist/allowlist) to a render method.
onJ2CommerceAppPluginAjaxdefaultAll admin AJAX actions (see below) — three-gated: CSRF token, authenticated, core.admin on com_j2commerce.
onJ2CommerceAfterAdminOrderDetailsdefaultInjects the read-only per-order risk card into the admin order view via the addResult pattern.
onJ2CommerceGetAnalyticsWidgetsdefaultOptional widget on the core Statistics Dashboard (attempts/blocked in range + health badge).
onPrivacyExportRequest / onPrivacyRemoveData (core Joomla)defaultGDPR export/anonymize of the subject's own attempt rows.

Admin AJAX actions​

onAppPluginAjax routes on the action GET/POST param (task=appPlugin.ajax&plugin=app_checkoutshield&action=...). Every action passes through the same three-gate check before the match: CSRF token (Session::checkToken('request')), authenticated user ((int) $user->id !== 0), and $user->authorise('core.admin', 'com_j2commerce').

action valuePurpose
toggleBlockPer-row Attempts Log / Blocked IPs jgrid-style block/unblock toggle.
blockBulk-block the stored IPs of selected Attempts Log rows (duration from a toolbar dropdown).
blockManualBlocklist "Add Block…" form — blocks a single admin-typed IP.
unblockBulk-unblock selected Blocked IPs rows.
makePermanentPromote selected temporary bans to permanent.
allowlistIpAllowlist the stored IPs of selected Attempts Log rows.
allowlistAddAllowlist "Add Entry…" form — IP/CIDR/Email/Email domain.
allowlistDeleteDelete selected Allowlist rows.
lockdownToggle Lockdown Mode (state=on|off).
clearCountersTruncates the velocity counters table.
purgePurges the entire attempts log.
sendTestAlertSends a test alert through the configured notification channels.
exportCsvStreams a formula-injection-guarded CSV export for attempts, blocklist, or allowlist.
dashboard.dataRefresh payload for the dashboard date-filter (KPIs + charts only — does not re-dispatch the three F.1a extensibility events).

Database Schema​

Eight new tables, all CREATE TABLE IF NOT EXISTS, InnoDB, utf8mb4_unicode_ci, prefixed #__j2commerce_appcheckoutshield_*. Shipped via sql/install.mysql.utf8.sql / sql/uninstall.mysql.utf8.sql / sql/updates/mysql/.

#__j2commerce_appcheckoutshield_attempts​

The core log — one row per evaluated checkout request. Primary key id (BIGINT UNSIGNED AUTO_INCREMENT).

ColumnTypeNotes
created_onDATETIMEUTC.
ipVARCHAR(64)Raw, hashed, or truncated per the ip_storage privacy setting.
countryCHAR(2)Set only when Geolocation is enabled.
session_hashVARCHAR(64)Keyed hash of the checkout session.
fingerprintVARCHAR(64)Lightweight, no-canvas composite device hash.
user_idINT UNSIGNED0 for guest.
email_hashVARCHAR(64)Keyed hash — never a raw email.
email_domainVARCHAR(190)Bare domain only.
taskVARCHAR(64)e.g. checkout.confirmPayment.
signalsTEXTJSON: triggered signals + values.
scoreSMALLINT UNSIGNED0-100 composite.
bandTINYINT UNSIGNED0 monitor, 1 challenge, 2 block.
actionTINYINT UNSIGNED0 logged, 1 would-block, 2 challenged, 3 throttled, 4 blocked, 5 banned.
outcomeTINYINTPayment rows only: NULL n/a, 1 success, 0 declined.
gateway_codeVARCHAR(32)Best-effort.
amountDECIMAL(15,4)
card_hashVARCHAR(64)Best-effort BIN+last4 hash when a gateway exposes a short masked fragment — never a full PAN.
user_agentVARCHAR(255)

Indexed on (ip, created_on), (session_hash, created_on), (fingerprint, created_on), (created_on), (action, created_on). Retention: retention_days (default 180, purged daily by the task plugin).

#__j2commerce_appcheckoutshield_blocklist​

id INT UNSIGNED AUTO_INCREMENT. ip VARCHAR(64) (raw or SHA-256 hex in hashed-IP mode), cidr TINYINT UNSIGNED (prefix length, NULL = single IP), type TINYINT UNSIGNED (0 temp / 1 permanent), reason VARCHAR(255), source TINYINT UNSIGNED (0 auto / 1 manual / 2 reputation), hits INT UNSIGNED, ban_count TINYINT UNSIGNED (escalation level), created_on DATETIME, expires_on DATETIME (NULL = permanent), created_by INT UNSIGNED. UNIQUE (ip, cidr).

#__j2commerce_appcheckoutshield_allowlist​

id INT UNSIGNED AUTO_INCREMENT. type TINYINT UNSIGNED (0 ip / 1 cidr / 2 email / 3 email_domain), value VARCHAR(190), note VARCHAR(255), created_on DATETIME, created_by INT UNSIGNED. UNIQUE (type, value).

#__j2commerce_appcheckoutshield_counters​

The velocity engine's sliding-window store. id BIGINT UNSIGNED AUTO_INCREMENT. counter_key VARCHAR(120) (e.g. ip:1.2.3.4:attempts), bucket INT UNSIGNED (unix minute — 0 is a reserved namespace holding a cooldown-expiry unix timestamp in value, not a count), value INT UNSIGNED. UNIQUE (counter_key, bucket).

warning

The task plugin's purgeCounters routine runs two separate deletes: bucket > 0 AND bucket < minBucket ages out real sliding-window buckets, and bucket = 0 AND value < now ages out expired cooldowns only. An age-based purge that ignored the bucket = 0 reservation would silently un-throttle active offenders.

#__j2commerce_appcheckoutshield_reputation​

The per-IP live-lookup cache — strictly separate from _iplists below. id INT UNSIGNED AUTO_INCREMENT. ip VARCHAR(45), provider VARCHAR(32), score SMALLINT, flags VARCHAR(190) (csv: tor,vpn,proxy,datacenter,abuse), checked_on DATETIME, expires_on DATETIME. UNIQUE (ip). Stores raw IPs only — hashed-IP privacy mode disables L3 reputation lookups entirely because this table has nowhere to put a hash.

#__j2commerce_appcheckoutshield_iplists​

Bulk lists synced wholesale by the task plugin's syncLists routine — never merged into _reputation. id INT UNSIGNED AUTO_INCREMENT. list ENUM('tor','sfs'), ip VARCHAR(64), cidr VARCHAR(64), synced_on DATETIME.

#__j2commerce_appcheckoutshield_maildomains​

Disposable-email domain list. id INT UNSIGNED AUTO_INCREMENT. domain VARCHAR(190) (UNIQUE), source TINYINT UNSIGNED (0 bundled seed / 1 synced / 2 manual), created_on DATETIME.

#__j2commerce_appcheckoutshield_state​

Generic key-value store for Lockdown state, spike-detection baselines, sync stamps, and alert dedup counters. id INT UNSIGNED AUTO_INCREMENT. keyname VARCHAR(64) (UNIQUE), value TEXT (JSON), modified_on DATETIME. Read via StateStore (src/Service/StateStore.php) — the app plugin and the task plugin both construct their own StateStore instance against the same table, so state set by one is visible to the other.

Configuration Parameter Reference​

All fields live on the app_checkoutshield plugin's own params (administrator/components/com_j2commerce/... never touched — this is entirely the plugin's own manifest XML). Yes/no fields use type="radio" layout="joomla.form.field.radio.switcher"; multi-selects use type="list" multiple="true" layout="joomla.form.field.list-fancy-select".

General​

ParamTypeDefaultNotes
modelistmonitormonitor | enforce. Master switch.
protect_tasksmulti-selectall 7 stepsguest,billing,shipping,payment,confirm,register,ajax_lookups

Signals​

ParamTypeDefault
honeypot_enable / honeypot_weightradio / number1 / 60
jstoken_enable / jstoken_weightradio / number1 / 35
timing_enable / timing_min_seconds / timing_weight / timing_max_minutes / timing_stale_weightradio / numbers1 / 2 / 20 / 30 / 15
session_enable / session_weightradio / number1 / 25
header_enable / header_weightradio / number1 / 10
behavior_enable / behavior_weightradio / number1 / 15
fingerprint_enableradio1

Velocity​

ParamTypeDefault
velocity_enableradio1
window_minutesnumber10
guest_ip_limit / auth_user_limitnumber30 / 60
session_limit / email_limit / fingerprint_limitnumber15 / 10 / 15
confirm_ip_limitnumber10
cooldown_multipliernumber3
velocity_weight_ip / _session / _email / _fingerprintnumber30 / 35 / 30 / 35

Card Testing​

ParamTypeDefault
ct_enableradio1
ct_window_minutes / ct_max_attemptsnumber5 / 5
ct_decline_ratio / ct_min_attemptsnumber0.5 / 5
ct_distinct_cardsnumber3
ct_small_amount / ct_small_amount_weightnumber5.00 / 10
ct_weightnumber40

IP & Email Reputation​

ParamTypeDefaultNotes
rep_enableradio0Gates AbuseIPDB/httpBL/IPQS only; requires ip_storage=full.
rep_cache_hoursnumber24
abuseipdb_key / abuseipdb_min_confidence / abuseipdb_weighttext / number— / 75 / 30
httpbl_key / httpbl_weighttext / number— / 25Project Honey Pot.
ipqs_key / ipqs_threshold / ipqs_weighttext / number— / 85 / 30
stopforumspam_enable / _weightradio / number1 / 25Local match against _iplists.
tor_mode / tor_weightlist / numberscore / 35off | score | block.
disposable_enable / _weightradio / number1 / 25

Geolocation​

ParamTypeDefault
geo_enableradio0
maxmind_license_keytext—
geo_modelistblocklisted (blocklisted | allowlisted)
geo_countriesmulti-select—
geo_actionlistscore (score | challenge | block)
geo_weightnumber25
asn_enable / asn_weightradio / number0 / 15

Challenge​

ParamTypeDefault
challenge_captchatype="plugins" folder="captcha"site default
challenge_min_scorenumber40
challenge_always_stepsmulti-selectnone
challenge_pass_ttlnumber (minutes)30

Response & Banning​

ParamTypeDefault
block_min_scorenumber70
ban_enableradio1
ban_after_blocks / ban_window_minutesnumber3 / 60
ban_laddertext (csv minutes)60,360,1440,10080
perm_after_bansnumber4

Notifications​

ParamTypeDefault
notify_email_enableradio1
notify_recipientstext(site mailfrom)
notify_min_severitylisthigh
notify_min_scorenumber40
notify_cooldown_hoursnumber6
spike_multiplier / spike_min_attempts / decline_spike_rationumber5 / 10 / 0.5
digest_modelistdaily (off | hourly | daily)
webhook_url / webhook_min_severityurl / list— / critical

Privacy​

ParamTypeDefault
retention_daysnumber180 (range 30-365)
ip_storagelistfull (full | hashed | truncated)

Lockdown​

ParamTypeDefault
lockdown_force_challengeradio1
lockdown_block_guestradio1
lockdown_threshold_multipliernumber0.25
lockdown_auto_expire_hoursnumber24 (0 = manual only)

Advanced​

ParamTypeDefaultNotes
min_log_scorenumber0Payment-outcome rows are always logged regardless of this setting — they feed the card-testing detector.

Task Plugin Routines​

plugins/task/j2commerce_checkoutshield/src/Extension/Checkoutshield.php implements TaskPluginTrait with a 7-entry TASKS_MAP. Every routine returns Status::OK on success or Status::KNOCKOUT on a caught \Throwable — never throws, so a bad run never blocks the next scheduler tick or the checkout path. Routines that need the companion app plugin's params call PluginHelper::getPlugin('j2commerce', 'app_checkoutshield') and return Status::NO_RUN with a warning log if it isn't installed/enabled.

Task nameMethodSuggested cadenceBehavior
checkoutshield.purgeBanspurgeBans()HourlyDeletes _blocklist rows where type=0 (temp) and expires_on is more than 30 days in the past.
checkoutshield.purgeAttemptspurgeAttempts()DailyDeletes _attempts rows older than retention_days (clamped 30-365), or a per-task override_retention_days form field when set.
checkoutshield.purgeCounterspurgeCounters()HourlyTwo-phase delete: stale bucket > 0 velocity buckets, and expired bucket = 0 cooldowns (see the counters-table warning above).
checkoutshield.syncListssyncLists()DailyCalls ListSyncService::syncDisposableDomains() / syncStopForumSpam() / syncTor(), constructed with the bundled seed file at data/disposable-domains-seed.txt.
checkoutshield.sendDigestsendDigest()HourlyCalls Notifier::sendDigest(), which self-gates on digest_mode — safe to schedule hourly regardless of whether the merchant chose hourly or daily.
checkoutshield.recomputeBaselinerecomputeBaseline()HourlyWrites trailing 7-day same-hour attempt-volume and decline-rate baselines into StateStore::KEY_ATTEMPTS_BASELINE / KEY_DECLINE_BASELINE, read by the spike detectors and CheckoutShieldDashboardHelper::declineRatio().
checkoutshield.refreshGeoDbrefreshGeoDb()WeeklyNo-ops (fail-open, Status::OK) if maxmind_license_key is empty. On download/verify failure, fires a medium-severity geo_db_refresh_failed Notifier alert and returns Status::KNOCKOUT; the existing database (if any) stays in use.

The task plugin's geoDataDir() helper mirrors AppCheckoutshield::geoDataDir() exactly ({tmp_path}/plg_j2commerce_app_checkoutshield/geoip) — both plugins must resolve to the identical server-fixed directory, never a request-derived path.

Best Practices for Extension Authors​

  • Always guard getArgument() returns with a cast ((array), (string)) — the shield's own dispatch code does this defensively and your listener should too.
  • Escape everything you put in a widgets item's html key. The contract is explicit: the shield template echoes it raw (echo (string) ($widget['html'] ?? '')). This is the one place in the whole extension where unescaped output is intentional and documented.
  • Don't assume your listener runs on every dashboard interaction. The three events fire once per full page load of pluginview=dashboard; they are not re-dispatched by the AJAX date-filter refresh.
  • Fail open in your own listener. Wrap your handler body in try/catch and never let an exception propagate — Checkout Shield's own dispatch already tolerates a broken listener, but a well-behaved third party shouldn't rely on that safety net alone.
  • Never write to the shield's own tables directly. There is no public data-access API for _attempts/_blocklist/_allowlist beyond the dashboard events — reach for onJ2CommerceAfterAdminOrderDetails (per-order data) or the dashboard events (aggregate data) instead.