Skip to content

Insight Sections

Hyvä POS is in closed beta

Hyvä POS is currently in a closed beta (pilot phase) with a small group of merchants. It is not yet generally available: the App Store release follows the pilot, and features and configuration may still change - possibly in backwards-incompatible ways - before the general release. Want to take part? Sign up at hyva.io/pos.

The Customer Insights sheet has an extension half: below the built-in statistics, the register renders HTML sections your module supplies. Loyalty balances, ERP flags, B2B credit terms, appointment history - if you can render it about a customer, the cashier can see it. Sections ship with a Magento deploy; no app release is involved.

This page walks through hyva-themes/magento2-hyva-pos-insights-poc, a reference module with two complete sections built on Adobe Commerce data: a reward-points panel (headline balance, progress bar, a pure-CSS mutation chart, tinted delta chips) and a store-credit ledger (added-vs-used flow bar, running balances). Copy its shape and replace the queries with your own data source.

The Interface

Hyva\Pos\Api\InsightSectionInterface
interface InsightSectionInterface
{
    /** Card title shown above the fragment (already translated). */
    public function getTitle(): string;

    /** Ascending sort position among sections. */
    public function getPosition(): int;

    /**
     * Render the fragment for this customer and range, or null to omit
     * the section for this customer entirely.
     *
     * @param string $range Applied range: '1y', '2y' or 'lifetime'
     */
    public function getHtml(int $customerId, string $range): ?string;
}

Returning null (or an empty string) omits the card for that customer - a loyalty section on a customer with no loyalty account simply isn't there. The reference sections also return null when their Adobe Commerce tables don't exist, so the same module installs cleanly on Open Source.

Registration

One di.xml entry on the section pool - the same pool idiom as customer validators and the credit providers:

<type name="Hyva\Pos\Model\Customer\InsightSectionPool">
    <arguments>
        <argument name="sections" xsi:type="array">
            <item name="acme_loyalty" xsi:type="object">Acme\PosLoyalty\Model\Section\LoyaltySection</item>
        </argument>
    </arguments>
</type>

The item key (acme_loyalty) becomes the wire-visible section id. Sections render in getPosition() order, below the built-in statistics.

Positioning

The Recent Items list is itself a positioned block: it sits at the store's Recent Items Position (customer_insights/items_position, default 100). Sections with a lower position render above the list, equal or higher render below it - so a loyalty balance the cashier should quote goes at 10, a bulky ERP history at 110. If your section conflicts with the list's slot, either side can move: change your getPosition(), or the merchant (or your module's config.xml default) moves the list.

Two sections with the same position fall back to module load order - not something to rely on. Use distinct values; gaps of ten leave room to slot between.

The Fragment Contract

The register renders your HTML in a script-disabled, sandboxed web view sized to its content. The rules:

  • No JavaScript. It is disabled outright; a <script> tag is dead weight.
  • Any CSS. The full WebKit CSS engine is available - inline styles, a <style> block with classes and @keyframes, media queries, grid. Images load from anywhere reachable (the sheet only exists online, so there is no offline case to design for); data: URIs or the store's own host are still the reliable choice. Each section renders in its own isolated web view, so your selectors can't leak into other sections or the app.
  • No navigation. Links, meta refresh, form posts and iframes are denied by the renderer - a section card can never turn into a browser. Render information, not destinations.
  • Escape everything. Use Magento\Framework\Escaper::escapeHtml() on every dynamic value. The fragment renders on the register.
  • Respect the range. getHtml() receives the sheet's active range; reuse Hyva\Pos\Model\Customer\InsightsRange::cutoff() so your section and the built-in statistics agree on the window.

Matching the App's Design

The register wraps your fragment in a shell that exposes its design system as CSS variables, resolved for the active light or dark scheme, with a body font that follows the user's accessibility settings:

Variable Use for
--pos-text-primary Body text (the default)
--pos-text-secondary Labels, dates, captions
--pos-surface Inner card backgrounds
--pos-accent The store's accent color
--pos-positive Gains: points earned, credit added
--pos-negative Losses: points spent, credit used
--pos-border Hairlines and dividers

color-mix() on these variables produces tints that stay correct in both schemes - the reference sections use it for their delta chips and chart tracks:

background: color-mix(in srgb, var(--pos-positive) 12%, transparent);

Pure CSS goes a long way inside the sandbox: the reference module builds a bar chart from flex columns with data-driven heights, a progress bar from two nested rounded divs, and pill badges from border-radius: 999px. If you write no colors at all, you inherit --pos-text-primary and adapt to dark mode for free.

Performance and Failure

Every registered section runs on every insights request for that customer and range, so keep queries indexed and capped - both reference sections read at most five rows through an indexed customer join. A section that throws is skipped server-side and cannot break the sheet, but it disappears silently, so keep your own error visibility in mind.

The Endpoint

Sections travel in the insights payload:

GET /rest/V1/pos/customer-insights/:customerId?range=1y
{
  "stats": { "...": "built-in statistics" },
  "items": [ { "...": "recent order lines" } ],
  "sections": [
    { "section_id": "acme_loyalty", "title": "Loyalty", "html": "<div>…</div>", "position": 10 }
  ],
  "range": "1y"
}

The whole feature is gated by Stores → Configuration → Hyvä POS → Advanced → Customer Insights; when it's off, the endpoint returns an empty payload and the register hides the tile.