Search by

magenxcommerce / module-price-history-graph-ql

magenxcommerce

Tracks a rolling daily snapshot of each product's final price and exposes the lowest price over a configurable look-back window (default 30 days) over GraphQL as `ProductInterface.price_history`, plus a `price_history_enabled` store-config flag. This is the EU Omnibus Directive (Art. 6a of Directive

Package info

github.com/magenxcommerce/module-price-history-graph-ql

Type:magento2-module

pkg:composer/magenxcommerce/module-price-history-graph-ql

Statistics

Installs: 0

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

v1.0.2 2026-10-08 08:19 UTC

This package is auto-updated.

Last update: 2026-10-24 19:35:48 UTC


README

Built with AI assistance. This module is built with significant use of Claude Code, under active human review and guidance. It is open source and in active development. AI assistance does not lower the engineering bar: every change must meet the same correctness, security, documentation and maintainability requirements as any other contribution.

No warranty. We are not responsible for any incompatibility. By installing this software, you accept responsibility for its security, upgrades, patches and monitoring.

Tracks a rolling price history per product and exposes the lowest price over a configurable look-back window (default 30 days) over GraphQL. This is the EU Omnibus Directive (Art. 6a of Directive 98/6/EC) prior price: the lowest price applied during at least the 30 days before a price reduction, which a trader must display whenever a reduction is announced.

Stock Magento only exposes point-in-time prices, so nothing records what a product cost in the past. This module adds that record and surfaces it, without touching any core module (the same companion-module split as DealGraphQl / BestSellerGraphQl).

How it works

  • Daily snapshot cron magenx_price_history_snapshot (Cron/SnapshotPrices.php) copies each product's current price from the native price index catalog_product_index_price into magenx_price_history, one row per (product, website, customer group, day). The copy is walked in entity_id batches rather than one statement, so a large catalog doesn't turn the nightly run into a single long transaction.
    • It captures IF(final_price > 0, final_price, min_price):
      • Simple products → final_price (post special-price + catalog-rule), the authoritative single-unit selling price — not min_price (which would fold in tier / quantity-break prices).
      • Composite parents (configurable / bundle / grouped) have final_price = 0 in the index — the parent carries no own price and the storefront shows min_price (the cheapest child's "from" price). Capturing final_price there stored €0.00, so the Omnibus disclosure rendered "Lowest price in the last 30 days: €0.00". These now capture min_price, matching the displayed figure. A selected variant still gets its own precise history (children are simple products with their own index rows).
      • Genuinely free products (both 0) are skipped rather than storing a 0.
    • All (website, customer_group) rows are captured, because groups can be shown different prices and the prior price must match what that customer saw.
    • Re-runs on the same day keep the lowest value seen (ON DUPLICATE KEY UPDATE price = LEAST(...)), so an intraday markdown isn't lost.
    • The snapshot is skipped entirely while the feature is disabled in every scope, so a default install never accumulates rows nothing reads. Pruning still runs, so an existing table drains after the feature is switched off.
    • Rows older than window_days + 10 are pruned (batched), where window_days is the longest window configured in any scope — a store on a 90-day window keeps 100 days of history even if the default scope is still 30.
  • ProductInterface.price_history is a batch resolver — a whole grid's products are resolved with one MIN(price) ... GROUP BY product_id query, so the field is safe on listing grids (category / search / carousels), not just the PDP. It returns { lowest_price, currency, days, since } in the store's display currency, or null when disabled or no history exists. The MIN ignores non-positive rows (price > 0), so a legacy €0.00 snapshot (a configurable parent captured before the min_price fix) can't drag the result to €0.00 — such a product resolves to null until real prices accumulate. The field is also selected on each configurable variant product, so choosing a swatch on the PDP updates the prior-price disclosure to that specific child's history.
  • StoreConfig.price_history_enabled mirrors the admin toggle.
{
  products(filter: { sku: { eq: "24-MB01" } }) {
    items {
      price_history { lowest_price currency days since }
    }
  }
}

Configuration

Stores → Configuration → Magenx → Price History (EU Omnibus) (magenx_price_history/...):

  • Enable Prior-Price Tracking (general/enabled, default off).
  • Look-back Window (days) (general/window_days, default 30 — the EU minimum).
  • Snapshot Schedule (cron/schedule, default 0 3 * * *).

Operational notes

  • Warm-up. The window fills in over the first N days the cron runs — a fresh install has no history, and the lowest price is only the minimum of what has been captured so far. Enable the module and let the cron run.
  • Run after the price reindex. The snapshot is only as fresh as the last product-price reindex. If the price indexer runs on schedule, keep the snapshot cron (default 03:00) after the indexer's window.
  • Indexer Dimensions mode. When the product-price indexer is dimension-based, Magento refreshes the base catalog_product_index_price only on full reindex; the cron reads that base table intentionally, so ensure it is populated.
  • Table growth. ≈ products × websites × customer_groups × (window_days + 10) rows; the prune keeps it flat. Size the DB accordingly.
  • Timezone. The snapshot day and the resolver's look-back cutoff are both anchored to the default scope's timezone (scopeDate(0)), so the 30-day window is exact at day boundaries. Anchoring matters because cron runs in the admin scope while the resolver runs in a store scope, and store views may each carry a different general/locale/timezone.

Verification

php -l Cron/SnapshotPrices.php   # (and the other Model/*.php)
bin/magento setup:upgrade        # creates magenx_price_history + unique key + indexes
bin/magento cron:run --group=default
# then query price_history over GraphQL and confirm a 24-item grid issues a
# single MIN(...) GROUP BY product_id query (no per-card fan-out).