Shopify B2B Catalog Pricing Integrity: Stop Null Compare-at Prices and Tier Rule Drift

By Milan Dhameliya · · 6 min read

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:

  1. Product compare_at_price is configured at base product level.
  2. Catalog fixed prices are added for B2B contexts.
  3. Compare-at isn’t explicitly defined in catalog behavior, so contextual compare-at returns null.
  4. 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:

  1. Base retail price.
  2. B2B fixed price per catalog.
  3. B2B quantity rule and price breaks.
  4. 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:

  1. Query contextual price.
  2. Compare against expected ERP/export values.
  3. Validate quantity rule and break display.

Publish by location cohort

Roll out to a low-risk company cohort first, then full catalog.

Technical Checklist

SEO Checklist