
EU Omnibus Pricing: A 30-Day-Low Data Model
A practical data model for auditable price history, stable campaign references and consistent storefront output.
In Germany, §11(1) of the Price Indication Ordinance requires a trader who announces a price reduction for goods to consumers to state the lowest total price applied during the 30 days before that reduction. The legal trigger matters: it is the announcement of a reduction, not merely a database row where today's price happens to be lower than another value.
That detail changes the software design. The storefront should not infer a legal claim from currentPrice < referencePrice, and a rolling minimum should not keep moving after a campaign starts. The system needs an explicit reduction event, an auditable price history and a prior price anchored to the start of that event.
This article is an engineering guide, not legal advice. Have the responsible legal reviewer confirm scope, wording and exceptions for every market in which the shop operates.
What the 30-day prior price must do
Article 6a of the EU Price Indication Directive defines the prior price as the lowest price applied by the trader during at least the 30 days before the reduction. The European Commission's interpretation guidance explains the rule and its scope.
The Court of Justice made the calculation point explicit in its 2024 Aldi Süd judgment. If an advertisement states a percentage reduction or uses a promotional claim that stresses the price advantage, that claim must be based on the 30-day low. Merely printing the low somewhere beside a percentage calculated from a higher price is not enough. The Court's case C-330/23 press release gives a concise example.
For the application, this produces three separate values:
- the current selling price;
- the prior price for the announced reduction, calculated before the reduction starts;
- the promotional statement derived from those two values.
Do not collapse them into one rolling field.
Store every price that consumers actually received
The durable record is a history of effective consumer prices. A useful logical key includes the trader, product variant, market, sales channel and currency. If taxes or customer groups change the total price a consumer sees, the model needs enough context to reproduce that price too.
1price_history
2 trader_id
3 variant_id
4 market_id
5 sales_channel_id
6 currency
7 gross_price_minor
8 valid_from
9 valid_toEach price change closes the previous validity interval and opens the next one in the same transaction. Store integer minor units rather than floating-point amounts. Preserve the event even when there is no active promotion. That history is the evidence; a search index or cached product document is only a projection of it.
This is also why a single product_id is usually too coarse. Different variants can have different prices, and a six-market shop can show different gross prices for the same variant at the same moment. A marketplace may also have more than one trader, which changes whose price history is legally relevant.
Anchor the window before the campaign starts
At campaign activation, query the prices that were effective during the preceding 30-day window. An interval-based query can look like this:
1SELECT MIN(gross_price_minor)
2FROM price_history
3WHERE trader_id = :trader_id
4 AND variant_id = :variant_id
5 AND market_id = :market_id
6 AND sales_channel_id = :sales_channel_id
7 AND currency = :currency
8 AND valid_from < :promotion_starts_at
9 AND COALESCE(valid_to, 'infinity')
10 > :promotion_starts_at - INTERVAL '30 days';The strict valid_from < :promotion_starts_at boundary excludes the new promotional price. The overlap condition still includes a normal price that began before the window and remained effective inside it.
Save the result on the active reduction record:
1price_reduction
2 id
3 variant_id
4 market_id
5 starts_at
6 ends_at
7 sale_price_minor
8 prior_price_minor
9 policy_versionThat snapshot stays stable for the campaign. A daily rolling minimum that includes the active sale price will eventually lower its own reference and can make the display wrong. Recalculate only when the legal event or its governing policy requires a new reference.
In Germany, §11(2) PAngV contains a specific rule for an uninterrupted, progressively increasing reduction: the shop may continue to use the lowest total price applied before the progressive reduction began. Other national implementations and exceptions need their own reviewed policy. Keep that policy version beside the snapshot so an audit can reconstruct which rule produced the value.
Publish a projection, not a second source of truth
Once the prior price is fixed, copy the display-ready values into the read model the storefront already uses. In a search document that might be:
1display_price
2 current
3 prior
4 reduction_announced
5 reduction_percentage
6 currency
7 valid_untilProduct pages, category grids, search results and feeds should consume the same projection. The frontend formats labels and currency but does not choose the reference window or calculate a different saving. If the promotion changes, update the durable record first and then rebuild the projection.
This makes the fast path simple without pretending the search index is the legal record. If the index is lost, the database can reproduce it. If the business asks why a particular prior price appeared, the price intervals, campaign snapshot and policy version can answer.
Treat the exceptions as explicit policy
The EU rule permits Member States to define exceptions or shorter periods in certain cases. German §11 PAngV, for example, excludes qualifying individual reductions and certain reductions for perishable goods when the conditions in subsection 4 are met. Progressive reductions have the separate rule described above.
Do not encode those outcomes as guesses in a component. Give each market an approved policy with an owner and effective date. For a new product with less than 30 days of history, preserve the full history that does exist and let that reviewed policy determine the announcement and label. The software's job is to apply the decision consistently, not invent it.
Before release, verify at least these paths:
- a regular reduction after several price changes;
- a second reduction that starts while a previous low still falls inside the 30-day window;
- an uninterrupted progressive reduction;
- a scheduled campaign at a daylight-saving or market-time boundary;
- the same variant in different markets, channels or currencies;
- cancellation, expiry and reindexing of a campaign.
In normal operation, a merchant schedules a reduction, the backend snapshots the prior price, every surface receives the same values and the history remains available for review. No page performs legal arithmetic while it renders.
If the platform choice comes before the implementation, my Medusa vs Shopify comparison explains when owning this kind of workflow is worth the additional operating responsibility.
Sources
Sources checked on 1 August 2026.
Working through a difficult platform decision?
Tell me what the system needs to do. I can help turn the requirements into a clear architecture and a buildable next step.
Discuss your project