[{"data":1,"prerenderedAt":844},["ShallowReactive",2],{"blog-b2b-pricing-engine-en":3},{"slug":4,"published":5,"minutes":6,"category":7,"tags":8,"keywords":14,"about":24,"sources":33,"cover":70,"og":71,"expertise":72,"locales":73,"lang":74,"title":77,"description":78,"coverAlt":79,"metaTitle":80,"takeaways":81,"faq":87,"toc":103,"blocks":134,"others":571},"b2b-pricing-engine","2026-10-07",12,"web",[9,10,11,12,13],"Pricing","Spryker","SAP","Caching","VAT",[15,16,17,18,19,20,21,22,23],"b2b pricing engine","customer specific pricing b2b","spryker price dimensions customer specific","sap condition technique pricing","commercetools price selection","price cache invalidation customer group","b2b volume pricing tiers","price audit trail explainability","vat rounding invoice line items",[25,27,30],{"name":9,"url":26},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FPricing",{"name":28,"url":29},"Value-added tax","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FValue-added_tax",{"name":31,"url":32},"Cache invalidation","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCache_invalidation",[34,37,40,43,46,49,52,55,58,61,64,67],{"title":35,"url":36},"Spryker docs: prices feature overview (Sep 2026)","https:\u002F\u002Fraw.githubusercontent.com\u002Fspryker\u002Fspryker-docs\u002Fmaster\u002Fdocs\u002Fpbc\u002Fall\u002Fprice-management\u002Flatest\u002Fbase-shop\u002Fprices-feature-overview\u002Fprices-feature-overview.md",{"title":38,"url":39},"Spryker docs: price explosion and ERP-owned pricing in B2B (Sep 2026)","https:\u002F\u002Fraw.githubusercontent.com\u002Fspryker\u002Fspryker-docs\u002Fmaster\u002Fdocs\u002Fpbc\u002Fall\u002Fprice-management\u002Flatest\u002Fbase-shop\u002Ftutorials-and-howtos\u002Fhandle-price-explosion-and-erp-owned-pricing-in-b2b.md",{"title":41,"url":42},"Spryker docs: volume prices overview","https:\u002F\u002Fraw.githubusercontent.com\u002Fspryker\u002Fspryker-docs\u002Fmaster\u002Fdocs\u002Fpbc\u002Fall\u002Fprice-management\u002Flatest\u002Fbase-shop\u002Fprices-feature-overview\u002Fvolume-prices-overview.md",{"title":44,"url":45},"commercetools docs: pricing and discounts overview","https:\u002F\u002Fdocs.commercetools.com\u002Fapi\u002Fpricing-and-discounts-overview",{"title":47,"url":48},"SAP Learning: introducing the condition technique","https:\u002F\u002Flearning.sap.com\u002Fcourses\u002Fconfiguring-pricing-in-sap-s-4hana-sales\u002Fintroducing-the-condition-technique_dce0f313-dee6-470e-8851-3f1773cb5d45",{"title":50,"url":51},"Pimcore docs: price systems and getting prices (2026.3)","https:\u002F\u002Fdocs.pimcore.com\u002Fplatform\u002FEcommerce_Framework\u002FWorking_with_Prices\u002FPrice_Systems_and_getting_Prices\u002F",{"title":53,"url":54},"Adobe Experience League: shared catalog pricing structure (Oct 2026)","https:\u002F\u002Fexperienceleague.adobe.com\u002Fen\u002Fdocs\u002Fcommerce-admin\u002Fb2b\u002Fshared-catalogs\u002Fdefine\u002Fcatalog-shared-pricing-structure",{"title":56,"url":57},"Shopify Help Center: B2B quantity rules and volume pricing","https:\u002F\u002Fhelp.shopify.com\u002Fen\u002Fmanual\u002Fb2b\u002Fcatalogs\u002Fquantity-pricing",{"title":59,"url":60},"Varnish libvmod-xkey README","https:\u002F\u002Fraw.githubusercontent.com\u002Fvarnish\u002Flibvmod-xkey\u002Fmaster\u002FREADME.rst",{"title":62,"url":63},"Opinion of the Advocate General in Case C-484\u002F06 (Koninklijke Ahold)","https:\u002F\u002Feur-lex.europa.eu\u002Flegal-content\u002FEN\u002FTXT\u002FHTML\u002F?uri=CELEX:62006CC0484",{"title":65,"url":66},"HMRC internal manual VATREC12030: rounding of VAT","https:\u002F\u002Fwww.gov.uk\u002Fhmrc-internal-manuals\u002Fvat-trader-records\u002Fvatrec12030",{"title":68,"url":69},"Playwright docs: snapshot testing","https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Ftest-snapshots","\u002Fimages\u002Fblog\u002Fb2b-pricing-engine\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fb2b-pricing-engine\u002Fog.jpg","b2b-ecommerce-developer",[74,75,76],"en","de","hu","B2B pricing engines: customer-specific prices, caching and audits","How B2B platforms resolve customer-specific prices, how to cache them without leaks, explain them in a dispute, test them and round VAT.","Four price tags in a descending staircase, the first one highlighted.","B2B pricing engines: caching, audits · Balázs Csorba",[82,83,84,85,86],"A B2B price depends on up to seven dimensions: customer, customer group or company, contract, region, quantity tier, currency and validity date; the engine picks the most specific matching rule and falls back to a base price.","Platforms document their resolution order: SAP access sequences search condition tables from specific to general, commercetools uses an eight-step fallback where the cheapest price wins inside a step, and Spryker falls back from concrete to abstract products.","Cache personalized prices per price list or customer group, purge by tag when a contract changes, revalidate at checkout, and never let the cache be the only copy of a price.","Store an order-time price snapshot with the winning rule, its version, the tax rate and the rounding mode, so a dispute months later is a lookup instead of a reconstruction.","Test prices with a golden matrix and property-based invariants after every import, and write down one rounding rule that the shop, the ERP and the invoice share.",[88,91,94,97,100],{"q":89,"a":90},"What is a B2B pricing engine?","The component that resolves the net price a specific business customer pays for a SKU, considering customer, group, contract, region, quantity, currency and date. It finds the most specific valid rule and falls back to general prices. In many setups the ERP holds the rules and the shop keeps a fast read copy.",{"q":92,"a":93},"How do you cache customer-specific prices safely?","Put every price-changing dimension, usually the price list or customer group, into the cache key, never the URL alone and never the user id. Tag responses by SKU and price list, purge by tag when a contract changes, revalidate the price at checkout, and keep the authoritative price outside the cache.",{"q":95,"a":96},"How does SAP determine a price?","Through the condition technique. A pricing procedure is chosen from the sales area, the sales document type and the sold-to party. Each condition type has an access sequence that searches condition tables from the most specific to the most general key until it finds a valid record, and quantity scales choose the scale level.",{"q":98,"a":99},"What should an order store to explain its price later?","A snapshot per order line: the resolved unit price and currency, the winning rule with its id and version, the candidates it beat, the quantity tier, the tax rate, the rounding mode, and where the price came from and how old it was at that moment.",{"q":101,"a":102},"Should VAT be rounded per line or per invoice?","The EU VAT directives do not regulate rounding explicitly, so national rules apply; HMRC, for example, rounds below 0.5p down and 0.5p or more up. Pick a documented rule your tax adviser accepts and apply it identically in the shop, the ERP and the invoice. With net B2B invoicing the distortions are much smaller than with gross retail prices.",[104,107,110,113,116,119,122,125,128,131],{"id":105,"title":106},"how-b2b-prices-get-decided","How do B2B prices get decided?",{"id":108,"title":109},"where-prices-live-and-the-resolution-order","Where prices live and the resolution order",{"id":111,"title":112},"platform-support-compared","Platform support compared",{"id":114,"title":115},"caching-personalized-prices","Caching personalized prices without leaking them",{"id":117,"title":118},"auditing-and-explainability","Auditing and explaining a price",{"id":120,"title":121},"testing-price-logic-with-golden-snapshots","Testing price logic with golden snapshots",{"id":123,"title":124},"vat-rounding-and-price-mode-pitfalls","VAT, rounding and price-mode pitfalls",{"id":126,"title":127},"when-not-to-build-a-pricing-engine","When not to build your own pricing engine",{"id":129,"title":130},"checklist","A checklist before you touch prices",{"id":132,"title":133},"sources","Sources",[135,144,158,161,164,203,210,211,218,224,231,237,248,255,256,259,336,346,347,350,381,384,388,389,392,395,398,406,413,414,428,430,438,441,442,447,459,470,482,483,486,489,496,497,500,520,532,533],{"type":136,"content":137},"paragraph",[138,139,143],"A ",{"tag":140,"children":141},"strong",[142],"B2B pricing engine"," decides which net price a logged-in buyer sees for a SKU, given who they are, which contract their company has, where they buy, how many units they order, in which currency and on which date. The computation is the easy half. The hard half is operational: caching prices that differ per customer without showing them to the wrong customer, explaining a price three months later in a dispute, and rounding VAT the same way the invoice will.",{"type":136,"content":145},[146,147,152,153,157],"This article walks through how the major platforms resolve customer-specific prices (Spryker, Pimcore, the SAP condition technique, commercetools, Adobe Commerce and Shopify B2B), then covers caching, auditing, testing and VAT. How a configurator prices options is covered in ",{"tag":148,"to":149,"children":150},"link","\u002Fblog\u002Fheadless-product-configurator-b2b",[151],"headless product configurators for B2B","; how prices are exposed as structured data behind a login is covered in ",{"tag":148,"to":154,"children":155},"\u002Fblog\u002Fschema-org-structured-data-b2b-shop",[156],"schema.org structured data for B2B shops",".",{"type":159,"level":160,"id":105,"text":106},"heading",2,{"type":136,"content":162},[163],"Seven dimensions usually decide a B2B price: the customer, the customer group or company, the contract, the region or sales channel, the quantity tier, the currency and the validity date. A pricing engine looks for the most specific rule that matches all of them and falls back step by step to a general base price when nothing more specific exists.",{"type":165,"ordered":166,"items":167},"list",false,[168,173,178,183,188,193,198],[169,172],{"tag":140,"children":170},[171],"Customer",": an individual price agreed for one buyer account, often for a handful of core SKUs.",[174,177],{"tag":140,"children":175},[176],"Customer group or company",": a price list shared by a segment, such as distributors, installers or all subsidiaries of one company.",[179,182],{"tag":140,"children":180},[181],"Contract",": a negotiated agreement with a validity period, sometimes with its own volume commitments.",[184,187],{"tag":140,"children":185},[186],"Region or channel",": country, store or sales channel, which also decides the tax treatment.",[189,192],{"tag":140,"children":190},[191],"Quantity tier",": the scale level reached by the ordered quantity.",[194,197],{"tag":140,"children":195},[196],"Currency",": prices are maintained per currency, not converted on the fly.",[199,202],{"tag":140,"children":200},[201],"Validity date",": start and end dates, so next quarter’s prices can be loaded before they apply.",{"type":136,"content":204},[205,206,209],"The dimensions multiply. Spryker’s guide to ERP-owned pricing gives an example: 1,000 customers times 25,000 products, counted for abstract and concrete products, is 50 million theoretical price records. At 20 percent contract coverage that is still about 20 million, the point where, according to the guide, the standard sync pipeline “begins to degrade seriously” (",{"tag":207,"href":39,"children":208},"a",[10],"). That number is why the questions of where prices live and how they are cached matter more than the formula.",{"type":159,"level":160,"id":108,"text":109},{"type":136,"content":212},[213,214,157],"Prices usually have three possible homes: the ERP, which holds negotiated conditions and is usually the system of record; the PIM, which may hold list prices as product attributes; and the commerce platform, which holds price lists or price dimensions for fast reads. The decision that matters is which one is authoritative and how stale a copy in another system may be. Product data sync has the same question, see ",{"tag":148,"to":215,"children":216},"\u002Fblog\u002Fpimcore-erp-delta-sync",[217],"the Pimcore ERP delta sync",{"type":219,"src":220,"alt":221,"wide":222,"caption":223},"image","\u002Fimages\u002Fblog\u002Fb2b-pricing-engine\u002Fdiagram-8.webp","B2B price resolution chain Five steps from left to right: the request context with customer, company, region, quantity, currency and date; the contract price as the most specific rule; the customer group price as the next fallback; the base price as the last resort; and a price snapshot persisted on the order line.",true,"Resolution walks from the most specific rule to the most general one, and the winning price is frozen on the order line.",{"type":136,"content":225},[226,227,230],"In SAP S\u002F4HANA the condition technique decides. The system picks a pricing procedure from the sales area, the sales document type and the sold-to party. Each condition type in the procedure has an access sequence that searches condition tables in order, from the most specific key to the most general, until it finds a valid condition record. Quantity scales then pick the scale level, and header conditions are distributed across items by net value unless configured otherwise (",{"tag":207,"href":48,"children":228},[229],"SAP Learning",").",{"type":136,"content":232},[233,234,230],"commercetools documents its price selection as an eight-step fallback, from customer group plus channel plus country down to the base price. Inside each step, prices with a validity period win over prices without one, and if several prices still match, the cheapest one wins. Tiered prices are ignored when a product discount already applies (",{"tag":207,"href":45,"children":235},[236],"commercetools",{"type":136,"content":238},[239,240,243,244,247],"Spryker stores prices as integers in the smallest currency unit, each with a price type and a gross and a net value per price mode. Retrieval checks the concrete product first and falls back to the abstract product. Volume prices follow the same inheritance: a concrete product’s own volume prices override the abstract ones, and they support only the default price type (",{"tag":207,"href":36,"children":241},[242],"Spryker prices",", ",{"tag":207,"href":42,"children":245},[246],"Spryker volume prices","). Customer-specific prices are built with price dimensions: the documented route extends the customer and price tables with a group id and exports per-group prices to the key-value store.",{"type":136,"content":249},[250,251,254],"Pimcore’s E-Commerce Framework gives each product a price system that returns PriceInfo objects. The built-in AttributePriceSystem reads the price from a product attribute, and anything ERP-driven becomes a custom price system registered in the configuration (",{"tag":207,"href":51,"children":252},[253],"Pimcore","). Pricing rules then apply discounts as condition and action pairs on top of that price.",{"type":159,"level":160,"id":111,"text":112},{"type":136,"content":257},[258],"The platforms agree on the idea that the most specific rule wins and differ in where the tie-breaks live and what you get for audits. The table shows what each documents as of October 2026.",{"type":260,"head":261,"rows":272},"table",[262,264,266,268,270],[263],"Platform",[265],"Price dimensions",[267],"Volume tiers",[269],"Precedence",[271],"Audit or explain hook",[273,283,294,305,315,326],[274,275,277,279,281],[10],[276],"store, currency, price mode; customer groups via price dimensions",[278],"JSON per store and currency, default price type only",[280],"concrete over abstract product",[282],"hybrid pattern with checkout revalidation",[284,286,288,290,292],[285],"Pimcore E-Commerce Framework",[287],"whatever the price system implements",[289],"inside a custom price system",[291],"price system per product, then pricing rules",[293],"applied rules readable per PriceInfo",[295,297,299,301,303],[296],"SAP S\u002F4HANA SD",[298],"condition tables keyed on customer, material, sales area and more",[300],"quantity scales on condition types",[302],"access sequence, most specific table first",[304],"condition records found per document item",[306,307,309,311,313],[236],[308],"customer group, channel, country, validity",[310],"tiers on embedded and standalone prices",[312],"eight-step fallback, cheapest wins in a step",[314],"Pricing and Discount Messages, opt-in",[316,318,320,322,324],[317],"Adobe Commerce B2B",[319],"shared catalog per company, website scope",[321],"tier prices per shared catalog",[323],"fixed custom price: minimum of base and fixed value",[325],"none named in the cited docs",[327,329,331,333,335],[328],"Shopify B2B",[330],"catalogs with quantity rules per variant",[332],"up to 10 price breaks per product",[334],"volume price fixes the price; catalog adjustment no longer applies",[325],{"type":136,"content":337},[338,339,342,343,230],"Two documented rules surprise people. In Adobe Commerce, a fixed custom price in a shared catalog never raises the price: the minimum of the base price and the fixed value is used (",{"tag":207,"href":54,"children":340},[341],"Adobe","). In Shopify B2B, once volume pricing applies the price becomes fixed, so a catalog-wide percentage adjustment no longer reduces it, and quantity rules apply per variant, so a buyer cannot combine variants to reach a minimum (",{"tag":207,"href":57,"children":344},[345],"Shopify",{"type":159,"level":160,"id":114,"text":115},{"type":136,"content":348},[349],"A personalized price can be cached as long as every dimension that changes the price is part of the cache key. If the key is the URL alone, the first buyer to load a product page decides the price that every later buyer sees. That is both a wrong price and a data leak: one customer’s contract price is shown to another customer.",{"type":165,"ordered":166,"items":351},[352,361,369,374],[353,356,357,230],{"tag":140,"children":354},[355],"Key on the pricing dimensions, not the user."," Cache per price list or customer group, not per user id, otherwise the hit rate collapses. Cloudflare allows headers, cookies and user features in custom cache keys, but those controls are Enterprise-only, and the docs warn that custom keys may shard the cache (",{"tag":207,"href":358,"children":359},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcache\u002Fhow-to\u002Fcache-keys\u002F",[360],"Cloudflare",[362,365,366,230],{"tag":140,"children":363},[364],"Tag responses for invalidation."," Varnish’s xkey module lets the backend tag each response with surrogate keys such as the SKU and the price list. One purge call clears every object that carries a key, softpurge keeps them for grace mode, and the README warns to protect the purge endpoint with an ACL (",{"tag":207,"href":60,"children":367},[368],"xkey",[370,373],{"tag":140,"children":371},[372],"Purge on the business event."," A contract change, a new price import or an ERP delta emits the affected SKU and price-list tags. A TTL alone leaves the old contract price live until it expires.",[375,378,379,230],{"tag":140,"children":376},[377],"Never make the cache the only store."," Spryker’s guide warns that writing prices only to a cache creates a data-loss risk if the cache is lost (",{"tag":207,"href":39,"children":380},[10],{"type":136,"content":382},[383],"For ERP-owned prices, the default the same guide recommends is a hybrid: a local read model serves browsing and the cart, and checkout revalidates against the ERP so the final price is confirmed at order placement. When cached ERP prices age, the page shows a staleness indicator such as “Price as of 14:23” instead of pretending the number is live. The guide’s target is price retrieval under 100 ms per storefront request.",{"type":219,"src":385,"alt":386,"wide":222,"caption":387},"\u002Fimages\u002Fblog\u002Fb2b-pricing-engine\u002Fdiagram-21.webp","Caching topology for personalized prices Top row from left to right: the browser of a logged-in buyer, an edge cache keyed on URL plus customer group, the shop API that resolves the price, and a local read model that shows the price age. Below the read model sits the ERP as the source of truth, which feeds the read model by delta sync. The shop API revalidates the price against the ERP at checkout. When a contract changes, the ERP emits an event to a contract-change box, which purges the edge cache by SKU and price-list tags.","Personalized prices are cached per customer group, purged by tag on contract change, and confirmed against the ERP at checkout; the read model shows how old a price is.",{"type":159,"level":160,"id":117,"text":118},{"type":136,"content":390},[391],"Sooner or later someone asks why customer X saw price Y on a given day. If the answer requires replaying today’s rules against today’s data, you cannot give it, because the contract, the tier table and the base price have all changed since. The answer has to be stored at the moment the price is used.",{"type":136,"content":393},[394],"Store an order-time price snapshot on every order line: the resolved net price, the rule that won and the candidates it beat, the price list and contract ids with their versions, the quantity tier, the currency, the tax rate and the rounding mode. The snapshot costs a few hundred bytes per line and turns a dispute into a lookup. The example below is illustrative, with amounts in cents.",{"type":396,"code":397},"code","{\n  \"orderLine\": \"SO-2026-104233\u002F3\",\n  \"sku\": \"VLV-200-DN50\",\n  \"quantity\": 24,\n  \"resolvedAt\": \"2026-10-07T09:14:03Z\",\n  \"currency\": \"EUR\",\n  \"unitNet\": 18350,\n  \"winningRule\": { \"type\": \"contract\", \"id\": \"CT-7781\", \"version\": 4, \"tier\": \"20+\" },\n  \"candidates\": [\n    { \"type\": \"customerGroup\", \"id\": \"PL-INSTALLERS\", \"unitNet\": 19100 },\n    { \"type\": \"base\", \"id\": \"PL-BASE-EUR\", \"unitNet\": 22400 }\n  ],\n  \"taxRate\": \"19.00\",\n  \"rounding\": \"HALF_UP per line\",\n  \"priceSource\": \"erp-read-model\",\n  \"priceAgeSeconds\": 412\n}",{"type":136,"content":399},[400,401,405],"The platforms give you some of the raw material. commercetools emits Messages for changes on resources, including dedicated Pricing and Discount Messages for standalone prices, but messages are not persisted by default and have to be enabled (",{"tag":207,"href":402,"children":403},"https:\u002F\u002Fdocs.commercetools.com\u002Fapi\u002Fprojects\u002Fmessages",[404],"commercetools Messages","). Pimcore can list the pricing rules applied to a PriceInfo, and in SAP each document item carries the condition records its access sequences found. None of that replaces a snapshot you control, because those sources change or expire on their own schedule.",{"type":136,"content":407},[408,409,412],"Price anomalies are a good place for automation that knows its limits. A check that flags lines where the charged price differs from the resolved one by more than a tolerance should route the case to a person, not correct it automatically. My ",{"tag":396,"children":410},[411],"jev-decide"," skill follows the same pattern: a decision with low confidence goes to a human instead of being applied.",{"type":159,"level":160,"id":120,"text":121},{"type":136,"content":415},[416,417,420,421,424,425,230],"Price logic is a matrix of customer, contract, region, quantity, currency and date. Pick a small set of representative values for each dimension, compute the resolved price for every combination, and store the result as a golden file in the repository. Playwright writes a golden snapshot on the first run, compares every later run against it and updates it only with ",{"tag":396,"children":418},[419],"--update-snapshots","; ",{"tag":396,"children":422},[423],"toMatchSnapshot()"," works on any text, not just screenshots (",{"tag":207,"href":69,"children":426},[427],"Playwright",{"type":396,"code":429},"import { test, expect } from '@playwright\u002Ftest'\nimport { resolvePrice } from '..\u002Fsrc\u002Fpricing'\nimport { customers, skus, quantities, dates } from '.\u002Fprice-matrix'\n\n\u002F\u002F One row per combination; the golden file is reviewed like code\ntest('price matrix matches the golden file', () => {\n  const rows: string[] = []\n  for (const c of customers) for (const sku of skus) for (const qty of quantities) for (const date of dates) {\n    const p = resolvePrice({ customer: c, sku, quantity: qty, date, currency: 'EUR' })\n    rows.push([c.id, sku, qty, date, p.unitNet, p.winningRule.id].join(';'))\n  }\n  expect(rows.join('\\n')).toMatchSnapshot('price-matrix.txt')\n})",{"type":136,"content":431},[432,433,437],"Golden files catch regressions in specific cases; property-based tests catch violations of the general rules. With fast-check you state invariants and let the library generate hundreds of inputs and shrink any failure to a minimal case (",{"tag":207,"href":434,"children":435},"https:\u002F\u002Ffast-check.dev\u002Fdocs\u002Fintroduction\u002F",[436],"fast-check","). Useful invariants: the order total equals the sum of the rounded line totals, a higher tier never yields a higher unit price, gross is never below net for a positive tax rate, and a customer-specific price never exceeds the base price where the business rule says it must not.",{"type":136,"content":439},[440],"Run the matrix after every price import and every ERP delta, not only on code changes, because most price bugs come from data rather than code. On the storefront side, I use Playwright MCP to drive the real shop as different logged-in roles and compare the displayed price with the resolved one after a contract change.",{"type":159,"level":160,"id":123,"text":124},{"type":136,"content":443},[444,445,230],"B2B prices are usually maintained and shown net, consumer prices gross, and a platform has to know which one it stores. Spryker keeps the price mode, gross or net, on the quote, and a quote cannot use both at once. Its documentation also shows the consequence: in gross mode, a product priced at €100 net with 20% tax in the store’s region stays €120 gross when sold into a region with 10% tax, so the net amount becomes €109.09 (",{"tag":207,"href":36,"children":446},[10],{"type":448,"variant":449,"title":450,"body":451},"callout","warn","The 19\u002F119 trap",[452,457],[453,454,230],"Extracting VAT from a gross price means multiplying by 19\u002F119 for 19% VAT, and rounding that small amount per unit distorts the tax. The Advocate General’s opinion in Case C-484\u002F06 gives the example: €0.28 including 19% VAT contains €0.0447 VAT; rounded to €0.04, the implied net is €0.24, whose 19% VAT is really €0.0456 (",{"tag":207,"href":63,"children":455},[456],"C-484\u002F06",[458],"Decide whether you round per unit, per line or per invoice, write the rule down, and use the same rule in the shop, the ERP and the invoice.",{"type":136,"content":460},[461,462,465,466,230],"The same opinion notes that no provision in the EU VAT directives explicitly regulates rounding, so the detail is a matter of national law within EU principles, and that with net invoicing between businesses the distortions either do not arise or are greatly reduced. HMRC’s rule is a concrete example: VAT under 0.5p rounds down, 0.5p or more rounds up, and other methods are allowed if they give a fair and reasonable result (",{"tag":207,"href":66,"children":463},[464],"HMRC","). For consumers, the Price Indication Directive requires the selling price including VAT, which is one reason B2C shops are gross-first and B2B shops are net-first (",{"tag":207,"href":467,"children":468},"https:\u002F\u002Feur-lex.europa.eu\u002Flegal-content\u002FEN\u002FTXT\u002F?uri=CELEX%3A31998L0006",[469],"Directive 98\u002F6\u002FEC",{"type":136,"content":471},[472,473,476,477,481],"Make rounding explicit in code. commercetools exposes rounding as configuration, with HalfUp, HalfDown and HalfEven modes and separate cart settings for price rounding, tax rounding and tax calculation, and notes that B2B prices often need sub-cent precision. In PHP, moneyphp keeps amounts as strings and its ",{"tag":396,"children":474},[475],"allocate()"," splits €10.00 into 334, 333 and 333 cents deterministically (",{"tag":207,"href":478,"children":479},"https:\u002F\u002Fgithub.com\u002Fmoneyphp\u002Fmoney",[480],"moneyphp","). Store prices as integers in the smallest unit or through a money library, never as floats.",{"type":159,"level":160,"id":126,"text":127},{"type":136,"content":484},[485],"Most B2B shops should not build a pricing engine; they should build a pricing adapter. If the ERP already resolves conditions correctly, duplicating that logic in the shop creates two engines that drift apart. The hybrid pattern keeps the ERP authoritative: replicate resolved prices into a read model for browsing, revalidate at checkout, and show staleness instead of hiding it.",{"type":136,"content":487},[488],"Build resolution in the shop when the ERP cannot answer fast enough for browsing at your catalog size, when the shop sells things the ERP does not price, such as configured options or marketplace offers, or when the ERP’s rules are simple enough that a price-list import covers them. Even then, keep one owner per price rule and treat the other system as a copy with a known maximum age.",{"type":136,"content":490},[491,492,157],"“Price on request” deserves to be a business state with its own flow, not an error. If no rule matches, a request-a-quote action is better than a base price the customer is not entitled to, and better than a missing price that looks like a bug. Quote handling then takes over, as described in ",{"tag":148,"to":493,"children":494},"\u002Fblog\u002Fllm-rfq-quote-automation",[495],"RFQ automation with LLMs",{"type":159,"level":160,"id":129,"text":130},{"type":136,"content":498},[499],"In my Spryker and Pimcore work, where prices live is the first question to settle. This is the list to go through before changing indexes, caches or importers that touch prices.",{"type":165,"ordered":222,"items":501},[502,504,506,508,510,512,514,516,518],[503],"Write down the dimensions and the resolution order, including the tie-break inside a step.",[505],"Name one owner for each price rule and a maximum age for every copy.",[507],"Put every price-changing dimension in the cache key, and nothing per user.",[509],"Tag cached prices by SKU and price list, and purge on contract change, not only by TTL.",[511],"Revalidate at checkout and show the price age when it is not live.",[513],"Store an order-time price snapshot with rule ids, versions, tax rate and rounding mode.",[515],"Keep a golden price matrix and property tests, and run them after every import.",[517],"Document one rounding rule for shop, ERP and invoice, and use integers or a money library.",[519],"Treat “price on request” as a state with its own flow.",{"type":136,"content":521},[522,523,527,528,157],"If you are untangling customer-specific prices across a shop and an ERP, see my ",{"tag":148,"to":524,"children":525},"\u002Fexpertise\u002Fb2b-ecommerce-developer",[526],"B2B e-commerce work",". Sales reps who have to explain a price to a customer benefit from the same snapshot data, see ",{"tag":148,"to":529,"children":530},"\u002Fblog\u002Fsales-rep-ai-copilot-b2b",[531],"an AI copilot for B2B sales reps",{"type":159,"level":160,"id":132,"text":133},{"type":165,"ordered":222,"items":534},[535,538,541,544,547,550,553,556,559,562,565,568],[536],{"tag":207,"href":36,"children":537},[35],[539],{"tag":207,"href":39,"children":540},[38],[542],{"tag":207,"href":42,"children":543},[41],[545],{"tag":207,"href":45,"children":546},[44],[548],{"tag":207,"href":48,"children":549},[47],[551],{"tag":207,"href":51,"children":552},[50],[554],{"tag":207,"href":54,"children":555},[53],[557],{"tag":207,"href":57,"children":558},[56],[560],{"tag":207,"href":60,"children":561},[59],[563],{"tag":207,"href":63,"children":564},[62],[566],{"tag":207,"href":66,"children":567},[65],[569],{"tag":207,"href":69,"children":570},[68],[572,646,702,776],{"slug":573,"published":574,"minutes":575,"category":7,"tags":576,"keywords":582,"about":594,"sources":603,"cover":640,"og":641,"expertise":72,"locales":642,"lang":74,"title":643,"description":644,"coverAlt":645},"schema-org-structured-data-b2b-shop","2026-10-17",10,[577,578,579,580,581],"Schema.org","Structured data","B2B e-commerce","Technical SEO","Nuxt",[583,584,585,586,587,588,589,590,591,592,593],"schema.org b2b shop","product structured data b2b","merchant listing structured data","product snippet vs merchant listing","unitpricespecification referencequantity","productgroup variants structured data","organization schema vatid","b2b prices behind login seo","json-ld nuxt schema org","merchant return policy structured data","mpn gtin structured data",[595,597,600],{"name":577,"url":596},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSchema.org",{"name":598,"url":599},"JSON-LD","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FJSON-LD",{"name":601,"url":602},"Business-to-business","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FBusiness-to-business",[604,607,610,613,616,619,622,625,628,631,634,637],{"title":605,"url":606},"Google Search Central: Introduction to Product structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct",{"title":608,"url":609},"Google Search Central: Merchant listing (Product, Offer) structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fmerchant-listing",{"title":611,"url":612},"Google Search Central: Product snippet structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-snippet",{"title":614,"url":615},"Google Search Central: Product variant structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-variants",{"title":617,"url":618},"Google Search Central: Organization structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Forganization",{"title":620,"url":621},"Google Search Central: Merchant return policy structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Freturn-policy",{"title":623,"url":624},"Google Search Central: General structured data guidelines","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fsd-policies",{"title":626,"url":627},"Google Search Central: Generate structured data with JavaScript","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fgenerate-structured-data-with-javascript",{"title":629,"url":630},"Schema.org: Offer","https:\u002F\u002Fschema.org\u002FOffer",{"title":632,"url":633},"Schema.org: UnitPriceSpecification","https:\u002F\u002Fschema.org\u002FUnitPriceSpecification",{"title":635,"url":636},"Unhead: Product schema (defineProduct)","https:\u002F\u002Funhead.unjs.io\u002Fdocs\u002Fschema-org\u002Fapi\u002Fschema\u002Fproduct",{"title":638,"url":639},"Nuxt SEO: Schema.org identity setup","https:\u002F\u002Fnuxtseo.com\u002Fdocs\u002Fschema-org\u002Fguides\u002Fsetup-identity","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fog.jpg",[74,75,76],"Schema.org structured data for B2B shops: prices behind logins, units and variants","Product markup for B2B shops: snippet or merchant listing, which price to publish, part numbers, unit pricing, variants, Organization and SSR in Nuxt.","A product rectangle with a framed outline and a small price slot hidden behind a low wall.",{"slug":647,"published":574,"minutes":648,"category":7,"tags":649,"keywords":654,"about":666,"sources":674,"cover":696,"og":697,"expertise":72,"locales":698,"lang":74,"title":699,"description":700,"coverAlt":701},"ai-assistant-b2b-shop-customer-support",9,[579,650,651,652,653],"AI assistant","Customer support","LLM agents","EU AI Act",[655,656,657,658,659,660,661,662,663,664,665],"ai assistant b2b shop","ai customer support b2b ecommerce","llm chatbot order status","ai agent customer service tools","chatbot liability air canada","klarna ai assistant results","human handoff chatbot design","eu ai act chatbot disclosure","owasp excessive agency","ai support resolution rate","b2b customer portal chatbot",[667,670,673],{"name":668,"url":669},"Customer service","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCustomer_service",{"name":671,"url":672},"Chatbot","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FChatbot",{"name":601,"url":602},[675,678,681,684,687,690,693],{"title":676,"url":677},"Klarna: AI assistant handles two-thirds of customer service chats in its first month (press release, 2024)","https:\u002F\u002Fwww.klarna.com\u002Finternational\u002Fpress\u002Fklarna-ai-assistant-handles-two-thirds-of-customer-service-chats-in-its-first-month\u002F",{"title":679,"url":680},"Entrepreneur: Klarna is hiring customer service agents again, according to its CEO (May 2025)","https:\u002F\u002Fwww.entrepreneur.com\u002Fbusiness-news\u002Fklarna-ceo-reverses-course-by-hiring-more-humans-not-ai\u002F491396",{"title":682,"url":683},"ABA Business Law Today: BC tribunal confirms companies remain liable for information provided by AI chatbot","https:\u002F\u002Fwww.americanbar.org\u002Fgroups\u002Fbusiness_law\u002Fresources\u002Fbusiness-law-today\u002F2024-february\u002Fbc-tribunal-confirms-companies-remain-liable-information-provided-ai-chatbot\u002F",{"title":685,"url":686},"OWASP GenAI Security Project: LLM06:2025 Excessive Agency","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm062025-excessive-agency\u002F",{"title":688,"url":689},"OWASP GenAI Security Project: LLM05:2025 Improper Output Handling","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm052025-improper-output-handling\u002F",{"title":691,"url":692},"EU AI Act, Article 50: Transparency obligations","https:\u002F\u002Fartificialintelligenceact.eu\u002Farticle\u002F50\u002F",{"title":694,"url":695},"Intercom: Fin pricing and billable outcomes","https:\u002F\u002Ffin.ai\u002Fpricing","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fog.jpg",[74,75,76],"An AI assistant for B2B shop support: tools, permissions and the human handoff","How to build an AI support assistant for a B2B shop: which requests to automate, tools that run in the customer context, handoff, AI Act and metrics.","A tall wall with one narrow gap; an arrow passes through it into a boxed enclosure of three squares, and another leads down to a tray.",{"slug":703,"published":574,"minutes":704,"category":7,"tags":705,"keywords":710,"about":722,"sources":732,"cover":769,"og":770,"expertise":771,"locales":772,"lang":74,"title":773,"description":774,"coverAlt":775},"vue-ai-chat-ui-streaming-accessibility",11,[706,581,707,708,709],"Vue","Accessibility","Streaming","AI chat",[711,712,713,714,715,716,717,718,719,720,721],"vue ai chat ui","nuxt llm streaming","sse vs fetch streaming","aria-live streaming text","accessible chat interface","abort fetch stream vue","dompurify markdown llm","role log chat","reduced motion chat ui","wcag 2.2 chat","screen reader streaming",[723,726,729],{"name":724,"url":725},"Web accessibility","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWeb_accessibility",{"name":727,"url":728},"Server-sent events","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FServer-sent_events",{"name":730,"url":731},"Vue.js","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FVue.js",[733,736,739,742,745,748,751,754,757,760,763,766],{"title":734,"url":735},"MDN: Using server-sent events","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FServer-sent_events\u002FUsing_server-sent_events",{"title":737,"url":738},"MDN: AbortController","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FAbortController",{"title":740,"url":741},"MDN: ReadableStream","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FReadableStream",{"title":743,"url":744},"MDN: TextDecoderStream","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FTextDecoderStream",{"title":746,"url":747},"MDN: ARIA log role","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAccessibility\u002FARIA\u002FReference\u002FRoles\u002Flog_role",{"title":749,"url":750},"W3C: Understanding SC 4.1.3 Status Messages","https:\u002F\u002Fwww.w3.org\u002FWAI\u002FWCAG22\u002FUnderstanding\u002Fstatus-messages.html",{"title":752,"url":753},"W3C WAI: What's new in WCAG 2.2","https:\u002F\u002Fwww.w3.org\u002FWAI\u002Fstandards-guidelines\u002Fwcag\u002Fnew-in-22\u002F",{"title":755,"url":756},"MDN: prefers-reduced-motion","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F@media\u002Fprefers-reduced-motion",{"title":758,"url":759},"DOMPurify README","https:\u002F\u002Fgithub.com\u002Fcure53\u002FDOMPurify",{"title":761,"url":762},"markdown-it README","https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it",{"title":764,"url":765},"AI SDK docs: useChat","https:\u002F\u002Fai-sdk.dev\u002Fdocs\u002Fai-sdk-ui\u002Fchatbot",{"title":767,"url":768},"Vue docs: nextTick","https:\u002F\u002Fvuejs.org\u002Fapi\u002Fgeneral.html#nexttick","\u002Fimages\u002Fblog\u002Fvue-ai-chat-ui-streaming-accessibility\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fvue-ai-chat-ui-streaming-accessibility\u002Fog.jpg","vue-nuxt-developer",[74,75,76],"Streaming AI chat in Vue and Nuxt: accessible by design","How to build a Vue or Nuxt AI chat UI: fetch streams, abort and retry, safe markdown rendering, aria-live announcements, keyboard support and reduced motion.","A message card with a careful caret, a dotted line opening into ripples and a check.",{"slug":777,"published":574,"minutes":648,"category":7,"tags":778,"keywords":783,"about":795,"sources":805,"cover":838,"og":839,"expertise":771,"locales":840,"lang":74,"title":841,"description":842,"coverAlt":843},"llm-translation-pipeline-i18n",[779,780,781,706,782],"Translation","i18n","LLM","Localization",[784,785,786,787,788,789,790,791,792,793,794],"llm translation pipeline","icu messageformat placeholders","vue i18n llm translation","translation memory llm","glossary enforcement translation","german informal sie du","hungarian translation quality llm","structured output json translation","machine translation post-editing","locale file validation","cldr plural categories",[796,799,802],{"name":797,"url":798},"Internationalization and localization","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FInternationalization_and_localization",{"name":800,"url":801},"ICU MessageFormat","https:\u002F\u002Funicode-org.github.io\u002Ficu\u002Fuserguide\u002Fformat_parse\u002Fmessages\u002F",{"name":803,"url":804},"Vue I18n","https:\u002F\u002Fvue-i18n.intlify.dev\u002F",[806,808,811,814,817,820,823,826,829,832,835],{"title":807,"url":801},"Unicode ICU User Guide: Formatting Messages",{"title":809,"url":810},"Vue I18n: Message Format Syntax","https:\u002F\u002Fvue-i18n.intlify.dev\u002Fguide\u002Fessentials\u002Fsyntax",{"title":812,"url":813},"Nuxt i18n: Getting started","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fgetting-started",{"title":815,"url":816},"Unicode CLDR: Plural rules","https:\u002F\u002Fcldr.unicode.org\u002Findex\u002Fcldr-spec\u002Fplural-rules",{"title":818,"url":819},"OpenAI: Structured outputs","https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fstructured-outputs",{"title":821,"url":822},"DeepL API: Glossaries","https:\u002F\u002Fdevelopers.deepl.com\u002Fdocs\u002Fapi-reference\u002Fglossaries",{"title":824,"url":825},"W3C: Localization vs. internationalization","https:\u002F\u002Fwww.w3.org\u002FInternational\u002Fquestions\u002Fqa-i18n",{"title":827,"url":828},"GNU gettext manual","https:\u002F\u002Fwww.gnu.org\u002Fsoftware\u002Fgettext\u002Fmanual\u002Fgettext.html",{"title":830,"url":831},"RFC 5646: Tags for Identifying Languages","https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc5646",{"title":833,"url":834},"Rat für deutsche Rechtschreibung","https:\u002F\u002Fwww.rechtschreibrat.com\u002F",{"title":836,"url":837},"Microsoft Writing Style Guide","https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fstyle-guide\u002Fwelcome\u002F","\u002Fimages\u002Fblog\u002Fllm-translation-pipeline-i18n\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fllm-translation-pipeline-i18n\u002Fog.jpg",[74,75,76],"An LLM translation pipeline for i18n: glossary, placeholders and review","How to translate product and website strings with LLMs without breaking placeholders, plurals or tone: glossary, ICU messages, QA checks and review gates.","Three source cards, one translated card with a glossary ribbon, then a checked clipboard.",1792337027560]