How-To: Determine Product Prices
How Shopfront Stores Prices
Shopfront is exceptionally customisable for pricing products. This can lead to some confusion when developing applications as the price displayed on the register may not be the same as the one initially developed.
If you're building a generic application for a lot of Vendors, care should be taken to match (or at least advise stores) of the settings and how your application will calculate prices.
If you're building for a single Vendor, you should discuss with that store how they would like their prices to come out.
Your application doesn't need to support every possible combination of pricing, but you should make it clear to your users how your application will calculate prices.
Prices for products should be loaded from the Product GraphQL object.
Specifically, you should use the prices field, which returns a list of
ProductPrice objects.
quantity: The quantity required to trigger this price,price: The price set by the store for the quantity and price set,priceSet: The Price Set this price belongs to
For example, you might have a product like Pure Blonde 375ml cans, this item is typically sold as a single, a six-pack, and a carton. So it might have prices set as 1 for $4, 6 for $16, and 24 for $50.
We call the lowest quantity the base price and the price being used (e.g. 1 for $4, 2 for $8, or 6 for $16) the everyday price.
There are other fields available on the
ProductPriceobject, consult the documentation page to find out more
Example Prices Query
query GetProducts {
products {
edges {
node {
prices {
quantity,
price,
priceSet {
id,
name,
},
},
},
},
},
}
query GetProducts {
products {
edges {
node {
prices {
quantity,
price,
priceSet {
id,
name,
},
},
},
},
},
}
Alternatives to Everyday Prices
Stores have a number of other options other than just setting the everyday price, these include:
- Promotions: Discounts or overrides to the everyday price which will be displayed to the customer as savings. These must be lower than the everyday price to trigger,
- Price Lists: Customer specific pricing, these can be a price adjustment (e.g. discount 10%) or can be something like a price override. These are not displayed to customers as a savings as we treat them as specific pricing for those customers,
- Surcharging: Whilst not directly the setting of the price, Shopfront supports public-holiday surcharging where a fixed percentage or amount can be applied to items to increase the price,
- Family Pricing: Shopfront has a special category called families, these allow for mixing and matching items as if they were the same product. Typically you'd group together products that share the same branding and size. Prices must be the same for the prices within a family,
- Future Prices: More a feature than a specific way of affecting the price, Shopfront supports the ability to set prices to change in the future
Price Sets
Shopfront supports a concept called Price Sets which allows stores to sell products at different prices in different locations. These locations can be something like having a bar price vs a bottleshop price, location A vs location B, in-store vs online or whatever the store would prefer to support.
If you're building an eCommerce store or a ticketing system, we would highly suggest allowing the store to select which price set they would like to use within your settings. This can potentially be per-Outlet or it may be a global selection for your application.
All stores internally support price sets, but being able to define new price sets is only available on the premium plan.
Every store has a price set called "Default Prices", in the API this is indicated by the priceSet field for the
ProductPrice type returning null.
Price Sets completely override their parent item. For example if you have a Default Price Set of 1 for $4 and 4 for $10 plus a second price set with prices set as 1 for $5, 2 for $7, buying 4 is not expected to provide you with a $10 price as they don't interleave.
Inheritance
Price Sets support inheritance where the store can select which price set they would like to override, this is done on a per-price-set level (not per-product). You might have a setup which looks something like:
+-----------------+
| Default Price |
+-----------------+
/ \
/ \
v v
+-----------------+ +-----------------+
| Store Group #1 | | Store Group #2 |
| (Price Level A) | | (Price Level A) |
+-----------------+ +-----------------+
| |
v v
+-----------------+ +-----------------+
| Expanded Prices | | My Second Group |
| (Price Level B) | | Moonee Ponds |
+-----------------+ +-----------------+
|
v
+-----------------+
| My Banner Group |
| Brunswick |
+-----------------+
You might then have a product setup something like this:
| Price Set | Price |
|---|---|
| Default Price | $10 |
| Store Group #1 | $11 |
| Expanded Prices | $12 |
| Moonee Ponds | $14 |
| Brunswick | $9 |
Selling this item at Brunswick would cost $9. Selling it at Moonee Ponds would cost $14, and if you had another store which inherited from Store Group #2, this would cost $10 (as Store Group #2 isn't overriding the default price).
In the code examples on the right-hand side of the page (on desktop) you can view a basic version of the algorithm we use internally to determine the price to use.
Price Set Inheritance
const prices = [{
quantity: 1,
price: 4,
priceSet: null,
}, {
quantity: 4,
price: 10,
priceSet: null,
}, {
quantity: 1,
price: 5,
priceSet: "11e6..."
}, {
quantity: 2,
price: 7,
priceSet: "11e6...",
}];
const parents = {
"11e6...": null,
"11e7...": "11e6...",
};
let wantedPriceSet = "11e7...";
const pricesToUse = [];
while(wantedPriceSet !== false) {
for(let i = 0, l = prices.length; i < l; i++) {
if(prices[i].priceSet === wantedPriceSet) {
pricesToUse.push(prices[i]);
}
}
if(pricesToUse.length) {
break;
}
if(wantedPriceSet === null) {
wantedPriceSet = false;
} else {
wantedPriceSet = parents[wantedPriceSet] ?? null;
}
}
// `pricesToUse` will now contain the prices that match the
// originally wanted price set
<?php
$prices = [[
"quantity" => 1,
"price" => 4,
"priceSet" => null,
], [
"quantity" => 4,
"price" => 10,
"priceSet" => null,
], [
"quantity" => 1,
"price" => 5,
"priceSet" => "11e6...",
], [
"quantity" => 2,
"price" => 7,
"priceSet" => "11e6...",
]];
$parents = [
"11e6..." => null,
"11e7..." => "11e6...",
];
$wantedPriceSet = "11e7...";
$pricesToUse = [];
while($wantedPriceSet !== false) {
for($i = 0, $l = count($prices); $i < $l; $i++) {
if($prices[$i]["priceSet"] === $wantedPriceSet) {
$pricesToUse[] = $prices[$i];
}
}
if(count($pricesToUse) > 0) {
break;
}
if($wantedPriceSet === null) {
$wantedPriceSet = false;
} else {
$wantedPriceSet = $parents[$wantedPriceSet] ?? null;
}
}
// `$pricesToUse` now contains the prices that match the originally
// wanted price set (or its first ancestor with prices)
Settings That Affect Pricing
As mentioned earlier, Shopfront has a number of settings that affect how prices are both calculated and distributed.
General information and in-depth examples of how these work can be found in our support centre's documentation.
The two main settings that may affect how your integration works are Quantity Rate and Cross Promotion Count. The most up-to-date information on how Cross Promotion Count works can be found in our support centre.
Quantity Rate / High Mix Price
If a store is set to use Quantity Rate (in the general settings, the Use Quantity Rate toggle is enabled) products that are sold after they have passed the previous price point will continue selling at the same rate.
If a store is set to use High Mix Price (in the general settings, when the Use Quantity Rate toggle is disabled) products that are sold after they have passed the previous price point will recalculate from the base price (as if the products were sold separately).
Imagine you have a product with the following price points:
| Quantity | Price |
|---|---|
| 1 | $3.50 |
| 6 | $15.00 |
| 24 | $50.00 |
If Quantity Rate is enabled, the price for the quantity of 7 will be $17.50
- $15 divided by 6 which equals $2.50. Multiply $2.50 by the quantity of 7
If High Mix is enabled, the price for the quantity of 7 will be $18.50
- $15 for the quantity of 6 plus $3.50 for the quantity of 1
On the right-hand side of the page (desktop) you can view basic examples of algorithms we use to calculate the price.
Shopfront's calculation is significantly more complex than the examples to the right, however, for most applications the algorithm should be enough to cover 99% of cases.
Price Calculation
const calculateQuantityRate = (prices, quantity) => {
if(!prices.length) {
return 0;
}
let currentRate = prices[0].price / prices[0].quantity;
for(let j = 1, m = prices.length; j < m; j++) {
if(prices[j].quantity <= quantity) {
const newRate = prices[j].price / prices[j].quantity;
if(newRate < currentRate) {
currentRate = newRate;
}
} else {
break;
}
}
return currentRate * quantity;
};
const calculateHighMixPrice = (prices, quantity) => {
// NOTE: Internally Shopfront uses a dynamic Knapsack calculation
// for this as it produces the best result, however the concept
// can be completed by the below
let priceIndex = prices.length - 1;
if(priceIndex === -1) {
return 0;
}
let tempQuantity = quantity;
let currentPrice = 0;
while(tempQuantity > 0 && priceIndex >= 0) {
if(prices[priceIndex].quantity <= tempQuantity) {
currentPrice += prices[priceIndex].price;
tempQuantity -= prices[priceIndex].quantity;
} else {
priceIndex -= 1;
}
}
if(tempQuantity > 0) {
currentPrice += prices[0].price / prices[0].quantity * tempQuantity;
}
return currentPrice;
};
<?php
function calculateQuantityRate(
array $prices,
int|float $quantity
): float {
if(count($prices) === 0) {
return 0.0;
}
$currentRate = $prices[0]["price"] / $prices[0]["quantity"];
for($j = 1, $m = count($prices); $j < $m; $j++) {
if($prices[$j]["quantity"] <= $quantity) {
$newRate = $prices[$j]["price"] / $prices[$j]["quantity"];
if($newRate < $currentRate) {
$currentRate = $newRate;
}
} else {
break;
}
}
return $currentRate * $quantity;
}
function calculateHighMixPrice(
array $prices,
int|float $quantity
): float {
// NOTE: Internally Shopfront uses a dynamic Knapsack calculation for
// this as it produces the best result, however the concept can be
// completed by the below
$priceIndex = count($prices) - 1;
if($priceIndex === -1) {
return 0.0;
}
$tempQuantity = $quantity;
$currentPrice = 0.0;
while($tempQuantity > 0 && $priceIndex >= 0) {
if($prices[$priceIndex]["quantity"] <= $tempQuantity) {
$currentPrice += $prices[$priceIndex]["price"];
$tempQuantity -= $prices[$priceIndex]["quantity"];
} else {
$priceIndex -= 1;
}
}
if($tempQuantity > 0) {
$currentPrice += $prices[0]["price"] / $prices[0]["quantity"] * $tempQuantity;
}
return $currentPrice;
}