Discounting at Square

Modeling discounts at Square

Reddit
LinkedIn

Square recently launched a set of flexible APIs to create auto-apply discounts. I wanted to take some time to explain how they work, why we designed them the way we did, and how they can be used to implement a wide variety of discounting schemes.

The Model

Square’s discount model is built into our Catalog, which means that discounts can be associated with a seller’s products and are synced to mobile devices to allow off-line application. It is built around three object types: Pricing Rules, Product Sets, and Time Periods. These three objects allow discounts to be configured in a wide variety of ways.

Discounts

A discount specifies an amount or percentage price reduction to apply to a line item or order. Historically, these were applied manually by the cashier in Square Point of Sale products. Automatic discounts provide the ability to apply them automatically, via rules.

Product Sets

A Product Set is a rule that matches products in a customer’s cart. It has a number of attributes that make it possible to match various combinations of products. Let’s look at a few of them.

One of the simplest cases is to apply a discount to any purchases from a single category. Let’s suppose a grocery store wants to discount fruit. Assuming that the store already had a category called fruit, that could be modeled with a product set referencing the fruit category:

{
    "product_ids_any": fruit_category_id
}

If berries were in a separate category, but were part of the sale, multiple IDs could be used:

{
    "product_ids_any": [
    fruit_category_id,
    berry_category_id
  ]
}

Now suppose the store wanted to offer a sale on peanut butter and jelly. To get the discount, a customer needs to purchase both a jar of peanut butter and a jar of jelly. Here, the product_ids_all field is useful.

{
    "product_ids_all": [
    peanut_butter_category_id, 
    jelly_category_id
  ]
}

Other combinations are possible, as well. A discount on exactly 10 apples could be represented using quantity_exact:

{
    "product_ids_any": apple_category_id,
    "quantity_exact": 10
}

This would match apples in groups of 10. If instead the discount would apply once a minimum threshold was met, 10 or more apples could be matched using quantity_min.

Let’s take a more complicated case. Suppose the grocery store wants to offer a discount on cookies or donuts when purchased with a gallon of milk. We can represent this using nested product sets:

{
    "id": sweets_id,
  "product_ids_any": [
    cookie_category_id,
    donut_category_id
  ]
}
{
    "product_ids_all": [
    sweets_id,
    milk_id
  ]
}

Arbitrarily complex boolean combinations of products can be represented in this way. Representing a product set for exactly 3 gallons of milk or a cup of coffee and 6 or more donuts will be left as an exercise to the reader. The same product set can be reused across multiple pricing rules to allow for flexibility in constructing sales.

Time Periods

A second building block of automatic discounts is the time period. Time periods leverage the flexibility of the iCal event format to represent dates and times. This makes it simple to represent a happy hour that occurs every Monday and Wednesday from 4-5 PM starting on August 1st:

DTSTART:20190801T160000
DURATION:P1H
RRULE:FREQ=WEEKLY;BYDAY=MO,WE

Numerous libraries exist to parse the iCal format, which makes it easy to construct rules for more complicated use cases. Within Square, times are always interpreted in the time zone of the unit, so if a seller had two locations, one in Atlanta, and one in Los Angeles, the happy hour would start at 4 PM local time at each location. Of course, the seller could construct separate rules if they wanted the Los Angeles happy hour to start earlier.

The ICal RRule format allows very granular control over repeating events. Rules can repeat on a daily, weekly, monthly or annual basis, starting on a specific date, and ending on a date or after a number of occurrences

The same time period can be reused for multiple pricing rules, making it easy to have a variety of discounts on offer during the same period.

Pricing Rules

The core object in the automatic discount model is the Pricing Rule. A Pricing Rule configures the application of a discount to specific products, combining the functionality of the various objects that we have already described. In particular, it combines product sets to apply discounts to specific products. For example, when we were discussing Product Sets, we spoke about a sale on sweets when purchased with milk. We built a product set that would match donuts or cookies, when purchased together with milk. However, we don’t want to discount all of those products, just the sweets. To do so, we need to exclude the milk from the discount. That would create a pricing rule like the following:

{
    "discount_id": one_dollar_off_discount_id,
    "match_products_id": sweets_with_milk_product_set_id,
    "exclude_products_id": milk_product_set_id
}

It is easy to represent other cases, such as a BOGO (buy one get one) discount on apples:

{
    "discount_id": one_hundred_percent_off_discount_id,
    "match_products_id": two_apples_product_set_id,
    "exclude_products_id": one_apple_product_set_id
}

We chose to build a flexible model to give Square’s sellers full control over how they discount their products. While no one is likely to offer a discount on one to three apples when purchased with a donut, but no cookies and no more than 12 gallons of milk, we are giving sellers flexibility in creating discounting schemes that are best for their business. This flexibility also makes it easy for us to launch first party support for new discounting structures needed by our sellers.

Rule Application

Given a solid data model, one might think that we have answered all of the questions needed to apply discounts to carts. However, there are still some complexities that have to be addressed in the application algorithm, itself. In the future, we may add support to configure some of these options at the rule level.

Stackable Discounts

One of the key questions when applying discounts is what happens when multiple discounts apply to the same item? In the interest of minimizing confusion, the first release of our APIs only supports non-stackable discounts. That means that when two discounts could potentially apply to the same product, our pricing engine is responsible for selecting which discount to apply. Initially, we only support one method of resolving that conflict: we always select the discount that is best for the customer.

Best for Customer

There are a number of cases where the pricing engine needs to decide which discount to apply to which item. Square has always prospered by doing what is best for our sellers, and we want to help them treat their customers right, as well. For that reason, we always resolve conflicts and ambiguities by selecting the discount that is best for the customer.

For example, suppose our grocer offers two discounts: 10% off all baked goods, and buy one, get one free on donuts. Since we only apply one automatic discount to any given item, we have to decide which of these discounts to apply to a given cart. If a customer has three donuts in their cart, the pricing engine will apply the buy one, get one discount to the first two, and the 10% off discount to the remaining donut, ensuring that they get the best possible treatment from the seller.

Developers using our APIs should keep this behavior in mind to avoid any unexpected side effects. For example, let’s suppose that the seller created a buy one, get one discount on baked goods, and a customer checked out with a cake and a donut in their cart. The customer would pay full price for the donut, and get the cake for free! Other ways of configuring the discount that might be easier on the bottom line would include creating two separate BOGO discounts, one for cakes, and one for donuts, or creating a 50% off discount for baked goods when purchasing two or more.

We will be launching an “equal or lesser value” constraint soon, making it possible to create “Buy one, get one of equal or lesser value” discounts. Within the confines of that constraint, we will continue to apply best for customer conflict resolution. That means that a cart with two cakes and two donuts would get one cake and one donut free if there were a BOGO discount on baked goods.

Fixed Amount Discounts on a Line Item

Automatic discount application includes new functionality for Square: the ability to apply fixed amount discounts to a line item. This behavior is not supported directly in the POS because of the potential to cause confusion. When you apply a 20% off discount to 5 bananas, the amount discounted is the same whether you take 20% off of each banana, or 20% off of the total. When applying a $1 discount, however, the results are quite different. Applying $1 off each banana yields a $5 total discount, while applying the discount to the total only takes $1 off the total cart.

Based on our product research into sellers’ needs, we now automatically apply fixed amount discounts to each individual item. That means that the $1 discount would apply to each banana, not to the total collection in the cart. The possibility of confusion around questions like this make it important to thoroughly test discounting rules before enabling them for customers to make sure that they have the desired behavior in all cases.

Rule Search

It turns out that picking the best discounts for a customer is a computationally hard problem - it is a variation of the famous knapsack problem. Because this problem can not be solved in reasonable time, we use a heuristic to approximate the best solution quickly. In our tests, the heuristic returns the optimal result in nearly all cases, and allows a nearly instantaneous checkout experience on today’s mobile devices. However, it is possible to construct certain sets of rules and carts for which our heuristic doesn’t return the best result for the customer. We believe that these cases will be almost non-existent in ordinary business usage.

Using the APIs

Documentation on using our public APIs, including examples for common discount configurations, can be found on our website. We encourage developers to experiment with our APIs and share feature requests using our developer slack channel and online forms. Rules currently apply automatically in the latest version of all of Square’s point of sale products, and application via API will be available soon. We hope that our work will help many businesses deliver the right pricing to their customers, so that they can continue to grow and prosper, and we continue to work on new solutions to make their experience even better. Happy discounting!