Shopify B2B Catalog Pricing Integrity: Stop Null Compare-at Prices and Tier Rule Drift
Fix Shopify B2B catalog price drift, null compare-at values, and tier break mismatches with GraphQL pricing controls and audit routines.
Your B2B store is showing wrong strike-through prices and inconsistent tier pricing because catalog pricing precedence is misconfigured. The storefront is rendering one price model while the catalog engine applies another.
Failure pattern in scaling B2B stores
The recurring production failure looks like this:
- Product
compare_at_priceis configured at base product level. - Catalog fixed prices are added for B2B contexts.
- Compare-at isn’t explicitly defined in catalog behavior, so contextual compare-at returns
null. - Tier breaks are added manually and drift from fixed prices over time.
In 2025, Shopify added direct compare-at price controls in catalogs, which removed the old dependency on clumsy CSV workarounds.
Why this happens technically
Shopify catalogs use Catalog -> PriceList -> PriceListPrice precedence. A fixed PriceListPrice wins over relative adjustments. If you don’t design compare-at handling in the same layer, frontend logic can render misleading strike-through behavior.
Build a pricing source-of-truth map first
Define one source of truth per field:
- Base retail price.
- B2B fixed price per catalog.
- B2B quantity rule and price breaks.
- Compare-at behavior for the same context.
If one field is authored in admin and another via API without governance, price drift is guaranteed.
Audit contextual pricing before changing code
Use contextual pricing queries against the B2B company location. This exposes where your rendered value and catalog value diverge.
query B2BContextPricingAudit($companyLocationId: ID!, $first: Int!, $after: String) {
productVariants(first: $first, after: $after) {
nodes {
id
sku
price
compareAtPrice
contextualPricing(context: { companyLocationId: $companyLocationId }) {
price {
amount
currencyCode
}
compareAtPrice {
amount
currencyCode
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Any variant where base compare-at exists but contextual compare-at is null should be flagged.
Apply fixed price + quantity rules atomically
Use quantityPricingByVariantUpdate so fixed pricing, rules, and breaks update in one operation.
mutation quantityPricingByVariantUpdate(
$priceListId: ID!
$companyLocationId: ID!
$input: QuantityPricingByVariantUpdateInput!
) {
quantityPricingByVariantUpdate(priceListId: $priceListId, input: $input) {
productVariants {
id
contextualPricing(context: { companyLocationId: $companyLocationId }) {
quantityRule {
minimum
maximum
increment
}
quantityPriceBreaks(first: 10) {
nodes {
minimumQuantity
price {
amount
currencyCode
}
}
}
}
}
userErrors {
field
code
message
}
}
}
Variables:
{
"priceListId": "gid://shopify/PriceList/467640202",
"companyLocationId": "gid://shopify/CompanyLocation/441870438",
"input": {
"pricesToAdd": [
{
"variantId": "gid://shopify/ProductVariant/113711323",
"price": { "amount": 40, "currencyCode": "USD" }
}
],
"quantityRulesToAdd": [
{
"variantId": "gid://shopify/ProductVariant/113711323",
"minimum": 10,
"maximum": 100,
"increment": 5
}
],
"quantityPriceBreaksToAdd": [
{
"variantId": "gid://shopify/ProductVariant/113711323",
"minimumQuantity": 25,
"price": { "amount": 35, "currencyCode": "USD" }
}
],
"pricesToDeleteByVariantId": [],
"quantityRulesToDeleteByVariantId": [],
"quantityPriceBreaksToDelete": []
}
}
Treat userErrors as blocking deployment signals.
Theme rendering guardrails for B2B compare-at
If you render compare-at blindly, you’ll surface false discount states. Guard on contextual data availability first.
{%- assign show_compare = false -%}
{%- if product.selected_or_first_available_variant.compare_at_price
and product.selected_or_first_available_variant.compare_at_price
> product.selected_or_first_available_variant.price -%}
{%- assign show_compare = true -%}
{%- endif -%}
<div class="price-block" data-b2b-context="{{ customer.current_company.id }}">
{% if show_compare %}
<span class="price-compare">
{{ product.selected_or_first_available_variant.compare_at_price | money }}
</span>
{% endif %}
<span class="price-current">
{{ product.selected_or_first_available_variant.price | money }}
</span>
</div>
For B2B-heavy builds, enrich this with app-provided contextual pricing rather than relying only on base variant fields.
Rollout sequence that avoids pricing incidents
Freeze ad-hoc edits during migration
Disable manual B2B price editing while you rebase pricing data.
Run parity validation by company location
For each top B2B account:
- Query contextual price.
- Compare against expected ERP/export values.
- Validate quantity rule and break display.
Publish by location cohort
Roll out to a low-risk company cohort first, then full catalog.
Technical Checklist
- Catalog-to-price list mapping documented by company location and market.
- Contextual pricing audit query run for all high-volume variants.
- Fixed prices, quantity rules, and breaks updated atomically with
quantityPricingByVariantUpdate. - Compare-at behavior validated inside the same pricing context.
-
userErrorsare blocking and logged to deployment telemetry. - Merchant editing permissions locked during migration window.
SEO Checklist
- Public DTC pages never expose B2B-only pricing states.
- JSON-LD product price matches rendered price for the active context.
- No indexable page renders contradictory compare-at and current price.
- Canonical product URLs remain stable during B2B pricing migration.
- Price-driven rich result eligibility tested after catalog rollout.