The broken carton dilemma: why loose eaches destroy wholesale margins
When merchants migrate wholesale operations to Shopify Plus, they frequently treat product variants as simple quantities with discounted price lists. In doing so, they assume wholesale buyers will naturally order in sensible volumes.
They do not. Unless constrained by software, buyers will order whatever number fits their immediate shelf restock: 7, 13, 19, or 31 units. On the factory floor and distribution centre, this causes four acute operational breakdowns:
In our work on wholesale supplier workflows for Weldaad Supplier QC Workflow and automated packaging documentation for Weldaad Artwork Generator, we found that carton specifications must be treated as first-class constraints in the commerce layer, not downstream warehouse guidelines.
- Manual Case Cracking: Warehouse pickers must stop picking flow, retrieve utility knives, cut open factory-sealed corrugated cartons, count loose units, and re-pack fragile items in custom void-fill cartons.
- Lost Barcode Traceability: Factory master cases carry scannable GS1-128 or ITF-14 carton barcodes. When a case is cracked, loose inner units often lack individual consumer barcodes, triggering manual labeling or inventory miscounts at customer receiving docks.
- Transit Damage & Freight Volumetrics: Full master cartons are engineered to withstand pallet stacking compression. Half-empty or split cartons collapse under load inside freight containers, driving up transit damage claims.
- Inventory Discrepancies in ERPs: Exact Online, SAP, and NetSuite track inventory across packaging units (Pallet, Master, Inner, Unit). When Shopify writes sales orders with arbitrary odd numbers, ERP warehouse modules report partial stock allocations that freeze automated pick lists.
Carton hierarchies: mapping eaches, inner boxes, and master cases
A resilient packaging data model operates across four nested physical tiers:
1. The Consumer Unit (Each): The physical sellable item (unitsPerInnerPack = 1). Carries standard EAN-13 / UPC barcode.
2. The Inner Pack (Sub-carton): Intermediate bundle holding a defined count of eaches (e.g. 6 or 12 units), packaged in chipboard or shrink wrap. Carries an inner ITF-14 barcode.
3. The Master Shipping Case (Carton): Heavyweight corrugated shipping carton holding multiple inner packs (e.g. 4 inners × 6 units = 24 units). Carries a master carton barcode.
4. The Pallet Layer (Tie / High): A discrete layer of master cases interlocked on a standard EUR (1200×800mm) or GMA (48×40 inch) pallet (e.g. 6 cases per layer, 4 layers high = 24 cases = 576 units).
If your supplier database stores this hierarchy, passing it into Shopify requires translating physical carton units into mathematical ordering rules.
Shopify Plus B2B QuantityRule mechanics and mathematical constraints
Shopify Plus B2B provides native QuantityRule objects attached to variants and B2B Catalogs (PriceList). The GraphQL Admin API defines QuantityRuleInput with three core scalar integers: minimum (Int), maximum (Int, optional), and increment (Int).
While conceptually straightforward, Shopify enforces a strict, unforgiving mathematical invariant: minimum must be greater than or equal to increment, and minimum % increment === 0.
If an automated ERP sync script attempts to push minimum: 10 and increment: 6 (a merchant attempting a 10-unit minimum with 6-unit case increments), Shopify's GraphQL API immediately rejects the entire mutation with UserError: Minimum must be an exact multiple of increment.
Furthermore, increment must be strictly greater than or equal to 1. If an unconfigured supplier SKU carries case_pack: 0 or null in an upstream database feed, passing that raw zero crashes the sync pipeline. The calculation engine must clean, validate, and snap all values to valid integer multiples before communicating with the Shopify Admin API.
import assert from "node:assert/strict";
export interface PackagingHierarchy {
unitsPerInnerPack: number;
innersPerMasterCase: number;
casesPerPalletLayer: number;
layersPerPallet: number;
itemBarcode?: string;
innerBarcode?: string;
masterBarcode?: string;
}
export type B2BTradeTier = "RETAIL_SAMPLE" | "BOUTIQUE_WHOLESALE" | "MASTER_DISTRIBUTOR" | "FULL_CONTAINER";
export interface B2BQuantityRuleConfig {
variantId: string;
sku: string;
packaging: PackagingHierarchy;
tradeTier: B2BTradeTier;
allowBrokenCase?: boolean;
customMaxUnits?: number;
}
export interface ShopifyQuantityRulePayload {
minimum: number;
maximum: number | null;
increment: number;
}
export interface QuantityRuleDerivationOutput {
status: "VALID" | "INVALID_PACKAGING";
errorMessage?: string;
unitsPerMasterCase: number;
unitsPerPallet: number;
calculatedRule: ShopifyQuantityRulePayload;
cartonHierarchySummary: {
packagingLevel: "EACH" | "INNER_PACK" | "MASTER_CASE" | "PALLET";
totalCasesPerOrderFloor: number;
brokenCasePermitted: boolean;
};
graphqlMutationInput: {
variantId: string;
quantityRule: ShopifyQuantityRulePayload;
};
}
export function deriveB2BQuantityRule(config: B2BQuantityRuleConfig): QuantityRuleDerivationOutput {
const { packaging, tradeTier, allowBrokenCase = false, customMaxUnits } = config;
if (
packaging.unitsPerInnerPack <= 0 ||
packaging.innersPerMasterCase <= 0 ||
packaging.casesPerPalletLayer <= 0 ||
packaging.layersPerPallet <= 0 ||
!Number.isInteger(packaging.unitsPerInnerPack) ||
!Number.isInteger(packaging.innersPerMasterCase)
) {
return {
status: "INVALID_PACKAGING",
errorMessage: "Packaging hierarchy must contain strictly positive integer units for inner and master cartons",
unitsPerMasterCase: 0,
unitsPerPallet: 0,
calculatedRule: { minimum: 1, maximum: null, increment: 1 },
cartonHierarchySummary: {
packagingLevel: "EACH",
totalCasesPerOrderFloor: 0,
brokenCasePermitted: false,
},
graphqlMutationInput: {
variantId: config.variantId,
quantityRule: { minimum: 1, maximum: null, increment: 1 },
},
};
}
const unitsPerMasterCase = packaging.unitsPerInnerPack * packaging.innersPerMasterCase;
const unitsPerPallet = unitsPerMasterCase * packaging.casesPerPalletLayer * packaging.layersPerPallet;
let minimum: number;
let increment: number;
let packagingLevel: "EACH" | "INNER_PACK" | "MASTER_CASE" | "PALLET";
switch (tradeTier) {
case "RETAIL_SAMPLE":
if (allowBrokenCase) {
minimum = 1;
increment = 1;
packagingLevel = "EACH";
} else {
minimum = packaging.unitsPerInnerPack;
increment = packaging.unitsPerInnerPack;
packagingLevel = "INNER_PACK";
}
break;
case "BOUTIQUE_WHOLESALE":
if (allowBrokenCase) {
minimum = unitsPerMasterCase;
increment = packaging.unitsPerInnerPack;
packagingLevel = "INNER_PACK";
} else {
minimum = unitsPerMasterCase;
increment = unitsPerMasterCase;
packagingLevel = "MASTER_CASE";
}
break;
case "MASTER_DISTRIBUTOR":
minimum = unitsPerMasterCase;
increment = unitsPerMasterCase;
packagingLevel = "MASTER_CASE";
break;
case "FULL_CONTAINER":
minimum = unitsPerPallet;
increment = unitsPerMasterCase;
packagingLevel = "PALLET";
break;
default:
minimum = unitsPerMasterCase;
increment = unitsPerMasterCase;
packagingLevel = "MASTER_CASE";
}
if (minimum % increment !== 0) {
minimum = Math.ceil(minimum / increment) * increment;
}
let maximum: number | null = null;
if (typeof customMaxUnits === "number" && customMaxUnits > 0) {
const snappedMax = Math.floor(customMaxUnits / increment) * increment;
maximum = Math.max(minimum, snappedMax);
}
const calculatedRule: ShopifyQuantityRulePayload = {
minimum,
maximum,
increment,
};
const totalCasesPerOrderFloor = Math.ceil(minimum / unitsPerMasterCase);
return {
status: "VALID",
unitsPerMasterCase,
unitsPerPallet,
calculatedRule,
cartonHierarchySummary: {
packagingLevel,
totalCasesPerOrderFloor,
brokenCasePermitted: allowBrokenCase,
},
graphqlMutationInput: {
variantId: config.variantId,
quantityRule: calculatedRule,
},
};
}Trade tier partitioning: boutique wholesale vs master distributors
In enterprise B2B operations, a single quantity rule cannot serve every customer segment. A local gift shop buying candles cannot absorb a 576-unit pallet, whereas a regional supermarket distributor will not process shipments smaller than full master cases.
Shopify Plus models this distinction through B2B Catalogs:
1. Global Variant Default Rules: Configured directly on the ProductVariant record via productVariantUpdate. This serves as the default rule for unassigned or standard trade customers (typically enforcing minimum 1 master carton, increment 1 master carton).
2. Catalog Price List Overrides: Configured per PriceList via the priceListFixedPricesAdd mutation. Each PriceListPriceInput accepts an explicit quantityRule object.
By assigning distinct Catalogs to distinct B2B CompanyLocation entities (as explored in our analysis of B2B trade account onboarding), merchants allow key distributors to order in pallet increments with aggressive discounts, while boutique retailers are granted inner-pack access at standard wholesale margins.
Storefront theme stepping and Storefront API boundaries
Configuring quantity rules on the backend solves only half the problem. If your storefront theme does not respect the quantity rule schema, customer experience deteriorates rapidly:
Theme Steppers Must Read variant.quantity_rule: Standard Shopify Liquid themes include JavaScript stepper buttons (+ and -). Theme developers must bind these buttons to variant.quantity_rule.increment and variant.quantity_rule.min. If a user clicks +, the input must jump from 24 to 48, not 25.
Cart Line Validation: If a buyer bypasses theme controls and submits an invalid quantity (e.g. typing 25 directly via keyboard), Shopify's checkout engine will block checkout with a native error message. However, validating the input client-side before cart submission prevents user frustration.
Storefront API Headless Queries: Headless Hydrogen or Next.js storefronts must explicitly query quantityRule on the ProductVariant fragment. Omitting quantityRule results in client-side state managers defaulting to step: 1, triggering validation failures when the cart mutation reaches Shopify's checkout engine.
When this needs an engineer
You do not need a custom quantity rule engine if you sell direct-to-consumer goods in single units, or if your wholesale catalogue consists entirely of simple variants with no carton packing constraints.
You need an engineer when your products ship in complex physical packaging tiers: when supplier feeds specify inner boxes, master cartons, and pallet layers that must translate into automated Shopify Plus B2B rules; when minimum order quantities must mathematically snap to case-pack increments across thousands of SKUs; when distinct wholesale trade tiers require segregated quantity rules via B2B Catalogs; and when ERP systems (Exact Online, SAP, NetSuite) require orders to arrive strictly aligned with warehouse carton pick lists.
Explore our engineering work in Shopify B2B development (/services/shopify-b2b), inspect our B2B operations case study collection (/work/collections/b2b-operations), or review our supplier data validation in the Weldaad Supplier QC Workflow case study (/work/weldaad-supplier-qc-workflow).
Send us your packaging hierarchy specifications, master case counts, and wholesale tier rules. We will design an automated quantity rule synchronization pipeline that eliminates broken cartons and protects warehouse fulfillment.
