Skip to main content
All CollectionsCustomizing Experiences App to your theme
Prevent overbooking by editing your cart-template.liquid file
Prevent overbooking by editing your cart-template.liquid file

It's possible to oversell an experience in certain cases. Here's how to fix that.

Updated over 4 months ago

PLEASE NOTE: Every theme is slightly different, and this customization has proven challenging for our customers. Because of the differences between themes, making this article general enough to cover every theme is nearly impossible. Please get in touch with us via chat if you'd like help with this.

For experiences with limited availability, it's possible that two people are purchasing at roughly the same time or someone has placed the slot in their /cart page but not yet checked out. Here's how it can play out:

--------
Joe wants to purchase an experience at 10 a.m. on Tuesday, the 21st. He adds it to the cart but doesn't check out all the way. (Note: At this time, we don't remove tickets from inventory until the final checkout step.)

Around the same time, Susan adds all the remaining inventory for this same day and time to her cart and completes the checkout by paying for her experiences.

Joe returns and finalizes his purchase even though the inventory is no longer available because Susan purchased it all while he was away.

Both Joe and Susan made their purchase even though there was not enough inventory, resulting in overselling that day and time's experience.
--------

The Current Fix

We have created a fix that limits overselling in these scenarios. Here's a quick video showing how it works.

How to update your store

Please follow these instructions. If you find this difficult, please contact us with your store URL and a request to have us help via chat.

Step 1: Open Shopify's Code Editor

This will allow you to tweak the theme of the cart template liquid file. Follow steps 1-3 here to find this file »

Step 2: Make the following changes to the cart-template.liquid file. 

Please note that your cart file may exist as another name depending on your theme creator. Also, you may need to preserve any style changes you made to your theme. (e.g. color values or other customizations).

Add this code at the top of the file

<style type="text/css" rel="stylesheet">
  #availability-errors > p {
    color: #ED6347;
  }
</style>

It should look like this


And then add a new form tag

Here's the code to add

<form id="cart-form" action="#" method="post" novalidate class="cart">


Add this code wherever you want the error to pop up for your customer
The default error says, "[Title of event] has sold out for your selected timeslot. To proceed please remove it from your cart and select a new timeslot."

<div id="availability-errors"></div>

It should look something like this

Find the checkout button and change the attribute called type from type="submit"  to type="button".

You must also ensure that it has the name="checkout"  attribute. Here's an example of what that might look like depending on your theme.

Here's some sample code

<input type="button" name="checkout" class="btn btn--small-wide" value="{{ 'cart.general.checkout' | t }}">

And finally, put this script right under the closing form tag.
That is the </form>  element

<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.11/moment-timezone-with-data.js"></script>
  <script>
    function loadScript(url, callback){
     
        var script = document.createElement("script")
        script.type = "text/javascript";
     
        if (script.readyState){  //IE
            script.onreadystatechange = function(){
                if (script.readyState == "loaded" ||
                        script.readyState == "complete"){
                    script.onreadystatechange = null;
                    callback();
                }
            };
        } else {  //Others
            script.onload = function(){
                callback();
            };
        }
     
        script.src = url;
        document.getElementsByTagName("head")[0].appendChild(script);
    }
   
    var myAppJavaScript = function($){
      console.log('Your app is using jQuery version '+$.fn.jquery);
      $('[name="checkout"]').bind("click", handleSubmit($));
    };

    if ((typeof jQuery === 'undefined') || (parseFloat(jQuery.fn.jquery) < 1.7)) {
      loadScript('//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', function(){
        jQuery191 = jQuery.noConflict(true);
        myAppJavaScript(jQuery191);
      });
    } else {
      myAppJavaScript(jQuery);
    }
   
    function handleSubmit($) {
      return function(e) {
        e.preventDefault();
        e.stopPropagation();
        const experienceQuantities = calculateProductQuantities();
        const availabilityPromises = [];
        Object.keys(experienceQuantities).forEach(productId => {
          const expQuant = experienceQuantities[productId];
          availabilityPromises.push(getAvailabilityPromise($, productId, expQuant.startsAt, expQuant.endsAt));
        });
        Promise.all(availabilityPromises).then(products => {
          const errors = [];
          if(products && Array.isArray(products)) {

            products.forEach(product => {
              const productId = product.productId;

              // should only ever be one availability
              const title = experienceQuantities[product.productId].productName;
              const cartQuantity = experienceQuantities[product.productId].quantity;
             
              const firstAvailability = getFirstDayAvailabilities(product.result);
              const availableQuantity = firstAvailability && firstAvailability[0] ? firstAvailability[0].unitsLeft : 0;
              if(cartQuantity > availableQuantity) {
                errors.push("<p class=\"error\">" + title + " has sold out for your selected timeslot. To proceed please remove it from your cart and select a new timeslot.</p>");
              }
            });
          }
         
          if(errors.length) {
            $("#availability-errors").empty().append(errors.join("\n"));
          } else {
            window.location.href = "/checkout";
          }
        }).catch(e => {
          console.error(e);
        });
      }
    }

    function calculateProductQuantities() {
      var line_items = {{cart.items | json}};
      return line_items.reduce((quantities, item) => {
        if(item && item.properties && item.properties.When) {
          const startDates = dateRangeStringToDates(item.properties.When);
          if(!quantities[item.product_id]) {
            quantities[item.product_id] = {
              quantity: item.quantity,
              startsAt: startDates.startsAt,
              endsAt: startDates.endsAt,
              productName: item.title.split(" - ")[0],
            };
          } else {
            quantities[item.product_id].quantity += item.quantity;
          }
        }
        return quantities;
      }, {});
    }
   
    function dateRangeStringToDates(dateString) {
      var [startsAt, endsAt] = dateString
        .split(" to ");
      return {
        startsAt: moment(startsAt, "MMM D, YYYY [at] h:mma z[(]ZZ[)]").toDate(),
        endsAt: moment(endsAt, "MMM D, YYYY [at] h:mma z[(]ZZ[)]").toDate(),
      };
    }
   
    function getAvailabilityPromise($, productId, startsAt, endsAt) {
      return new Promise((res, rej) => {
        $.ajax({
          url: "https://prod-v2-api.experiencesapp.services/rest/firstAvailability?shop={{ shop.permanent_domain }}",
          type: "POST",
          dataType: 'json',
          contentType: 'application/json',
          data: JSON.stringify({
            productId,
            startingFrom: startsAt.toISOString(),
            timespanInSeconds: 60,
          })
        }).done((result) => {
          res({
            productId,
            result
          });
        }).fail((err) => {
          rej(err);
        });
      });
    }
   
    function getFirstDayAvailabilities(data) {
      const firstYear = data[Object.keys(data)[0]] || {};
      const firstMonth = firstYear[Object.keys(firstYear)[0]] || {};
      const firstWeek = firstMonth[Object.keys(firstMonth)[0]] || {};

      return (firstWeek[Object.keys(firstWeek)[0]] || []).map(fixAvailability);
    }
   
    function fixAvailability(availability) {
      return {
        ...availability,
        endsAt: new Date(availability.endsAt),
        startsAt: new Date(availability.startsAt),
      };
    }
</script>
Did this answer your question?