Skip to main content

Platform Overview

Quantaprice is a pricing engine. Its job is to answer one question — "what is the right price for this SKU, for this customer, right now?" — correctly, consistently, and in under 10 milliseconds at any query volume. This page explains how the system is structured to do that.


Core Entities

Articles (SKUs)

An Article is the catalog record for a product — its SKU code, display name, and a free-form metadata map you can use to attach any attributes relevant to your business (brand, category, product line, and so on). Article metadata is queryable, so you can use it to target pricing rules at product segments without modifying any pricelist.

An Article can also be a bundle — a composite SKU made up of component SKUs and quantities. Bundles are priced by a rule defined per pricelist, not hard-coded on the article itself.

Pricelists

A Pricelist defines a pricing context: which currency prices are stored in, how VAT is handled (NET or GROSS), and an optional parent pricelist. Pricelists form a tree — each node has at most one parent, and price resolution walks up the chain until a value is found.

Child pricelists inherit everything from their parent by default. Prices, VAT mode, rounding rules, and bundle rules can all be overridden at any level. Overrides are always explicit; nothing is merged implicitly.

Pricelists also carry a price_rules array — a list of JEXL expressions that are evaluated during price calculation to transform the base price. Rules are the mechanism for modelling discounts, markups, floors, and conditional adjustments across a hierarchy.

Prices

A Price is the core data record: it binds a SKU to a Pricelist at a specific quantity break and effective date. Each Price record carries several price fields (catalog, cost, floor, recommended, fixed) giving the rules engine enough signal to compute meaningful transformations. The engine derives the final sales_price from these fields by running the full calculation pipeline.

A single SKU/pricelist combination can have multiple price records, one per quantity break. When a query specifies a quantity, the engine selects the highest break that does not exceed the requested quantity.

Every Price record is effective-dated. Writing a new price never deletes the previous one — it creates a new version with a later effective_from. Queries use an as_of parameter (defaulting to now) to select the version that was active at a given moment. The full version history is always retained.

Price Rules

Price Rules are JEXL expressions attached to a pricelist. At query time, the engine evaluates each rule in sequence against the resolved base price and the article's attributes. Rules can read from the price fields, article metadata, quantity, and the results of prior rules. Common patterns include percentage markups, absolute discounts, floor enforcement, and conditional logic based on product attributes.

Rules are defined on the pricelist, not on individual SKUs, which means a single rule covers every SKU on that pricelist. To target a rule at a subset of SKUs, add a condition using article metadata.

Rules on a child pricelist replace — they do not stack on top of — the parent's rules. If you need a child to apply a parent's rule plus its own, define both explicitly.

Bundles

A bundle article's price is determined by its pricing rule, which is defined per pricelist and follows normal inheritance. There are two methods:

  • sum_of_parts: The bundle price is the sum of each component's quantity multiplied by that component's effective price on the querying pricelist. An optional percentage or absolute discount can be applied to the total. Because component prices are resolved at the customer's own pricelist level, a customer with negotiated component prices automatically sees a bundle price derived from those negotiated rates — without any special configuration.
  • fixed: The bundle has an explicit fixed price regardless of component prices. VAT is still computed per component to handle bundles with mixed VAT classes correctly.

Bundle prices are materialised at write time. When a component price changes or a rule is updated, the bundle price is recomputed and stored immediately. Reads are always a single keyed lookup — O(1).

Markets and Currencies

Each pricelist is denominated in a single currency. If a query requests a different output currency, the engine applies FX conversion using its internal exchange rates. Rates are sourced daily from the European Central Bank's published XML feed. EUR is the pivot currency: all rates are stored as EUR→X, and conversions between any two non-EUR currencies are computed as cross-rates through EUR automatically.

Because rates are fetched daily and stored with their publication date, the system handles weekends and public holidays gracefully — it finds the most recent available rate on or before the requested date. All historical rates are retained permanently, so any historical price query can be reproduced with the correct rate for that date.

VAT

VAT is determined by three cooperating entities:

  • Tax Areas represent geographic or regulatory jurisdictions (for example, SE, DE, US-CA).
  • VAT Classes represent product tax categories (for example, STANDARD, REDUCED, EXEMPT). Each Article is assigned a VAT class per Tax Area — the same product can be STANDARD-rated in one jurisdiction and REDUCED-rated in another.
  • VAT Rates record the actual percentage for a (Tax Area, VAT Class) pair, with effective dating so rate changes can be scheduled in advance.

A pricelist's vat_mode determines the direction of VAT handling. In NET mode, stored prices exclude VAT and the engine adds it at query time. In GROSS mode, stored prices include VAT and the engine extracts it when a net figure is needed. VAT is always the last substantive transformation before rounding, applied after rules and FX conversion are complete.

Rounding

Rounding profiles define tier-based rounding behaviour: different rounding precision and method apply depending on where a price falls within a range. A profile can also apply a charm pricing offset (for example, adjusting a rounded price to end in .99 or .95). Profiles can be set as the default for a currency or globally, and can be overridden per pricelist.

Tax Areas

Tax Areas are the geographic anchor for VAT resolution. They carry no rate information directly — they are simply codes that act as the first dimension of the VAT Rate lookup. A query must name a Tax Area to receive VAT-inclusive prices.


Entity Relationships

┌────────────────────────────┐
│ Pricelist │
│ code, currency, vat_mode │
│ parent_code (→ Pricelist) │
│ price_rules (JEXL[]) │
└──────────┬─────────┬────────┘
│ │
has prices ────┘ └──── bundle rules

┌────▼─────┐
│ Price │
│ sku ────┼──► Article (SKU)
│ qty_brk │ │
│ eff_from│ │ vat class per tax area
└──────────┘ ▼
┌──────────────┐
│ Tax Area │
│ VAT Class │◄── VAT Rate (rate, eff_from)
└──────────────┘

Currency ◄── FX Rate (EUR pivot, ECB daily, eff_from)

Rounding Profile (tier ranges, charm offset) ── applied per currency or pricelist

The Price is the central join. Pricelists provide the pricing context. Articles contribute the product identity and VAT classification. Currencies and FX Rates bridge pricelists to query output currencies. Tax Areas and VAT Classes resolve the applicable VAT Rate. Rounding Profiles apply the final numeric adjustment.


The Price Calculation Pipeline

Every price request runs through the same fixed pipeline, in this order:

1. Rules

JEXL price rules on the matched pricelist are evaluated against the resolved base price and article attributes. The output is the rule-adjusted price, still in the pricelist's native currency.

2. FX Conversion

If the query requests a currency different from the pricelist's currency, the engine converts using the ECB-sourced rate effective at the query's as_of date. Cross-rates are computed automatically through EUR.

3. VAT

Using the query's Tax Area, the engine looks up the article's VAT class for that area, then finds the VAT rate effective at the query date. In NET mode, VAT is added; in GROSS mode, VAT is extracted. VAT amounts are rounded to the currency's configured VAT precision.

4. Rounding

The final numeric amount is passed through the applicable rounding profile. Tier ranges determine precision; charm offsets are applied if configured.

Why the order is fixed

Rules must run before FX conversion because they are authored in the pricelist's native currency — a "10% markup" rule is currency-agnostic, but a floor rule is not. FX must run before VAT so that tax is computed on the correct amount in the correct currency. Rounding is last so it is applied to the final presented figure, not an intermediate that will be further transformed.


Change Propagation

Quantaprice uses a push model for propagating changes through the pricelist hierarchy.

When a base price is written to a pricelist, the engine immediately re-evaluates price rules for every child pricelist in the hierarchy that inherits from it. This propagates downward — a change at the root of a deep hierarchy flows through every descendant automatically. The sales_price stored at each derived pricelist is updated before the write call returns.

Manual overrides are always preserved. A price explicitly written on a child pricelist is never overwritten by propagation from the parent. Only derived prices — those that have no explicit record at that level and rely on inheritance — are recomputed.

Because every Price record is effective-dated, propagation creates new versions at descendant levels rather than mutating existing records. The full history of every derived price is therefore retained and queryable.

Every change — to any entity — is written to an append-only changelog recording what changed, when, who made the change, and a content hash for deduplication. The changelog is accessible by polling and by webhook. Nothing in Quantaprice is hard-deleted; every entity uses soft-delete with a deleted_at timestamp.


Read / Write Separation

Quantaprice is designed around a sharp boundary between ingestion and query.

Ingestion (write path) is a bulk, push-model operation. You write prices, pricelists, articles, and rules via the REST API. The engine does the work at write time: propagating changes down the hierarchy, materialising bundle prices, computing derived prices. Write operations can be batched.

Query (read path) is served by POST /price/query (and its single-SKU GET variants). The endpoint is designed for O(1) reads at p99 under 10 milliseconds, regardless of the complexity of the pricelist hierarchy, bundle composition, or number of active pricelists. This latency is achievable because all derivation and materialisation has already happened at write time — a read is a keyed lookup, not a computation. Bundle prices, in particular, are never computed at read time.

The POST /price/query endpoint accepts multiple SKUs in a single request, a pricelist filter that can select by code, metadata, or currency, a quantity for break-tier selection, an as_of timestamp for historical queries, a target currency for FX conversion, and a tax area for VAT resolution. When the filter matches multiple pricelists for the same SKU, the engine normalises all candidates to NET in the target currency and returns the lowest (best-price selection).

This separation means the query path carries no write-side complexity. Storefronts and commerce systems query against a stable, precomputed surface that is updated asynchronously as the pricing team changes rates and rules.


Learn more

  • Bundles — full documentation for bundle pricing rules and the component breakdown.
  • VAT — tax areas, VAT classes, and the article-taxclass mapping.
  • Rounding — rounding profile configuration.
  • Currencies & FX — currency management and ECB rate integration.
  • Querying Prices — how to call the pricing API.
  • OpenAPI Reference — complete machine-readable API specification.