All Collections
Customizing 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 a week ago

PLEASE NOTE: Every theme is a little bit different and this customization has proven to be challenging for our customers. It's close to impossible to get this article general enough to cover every theme. Please reach out to us via chat if you'd like help with this.

For experiences that have limited availability it's possible that two people are purchasing at roughly the same time or that 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 am on Tuesday the 21st. He adds it to the cart but he 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 comes back 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 were able to make their purchase even though there's not enough inventory resulting in overselling that day and time's experience.
--------

The Current Fix

We have created a fix you can use that limits overselling from these scenarios. Here's a quick video to show how it works.

How to update your store

Please follow these instructions. If you find this difficult, please reach out to use 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 make the theme tweaks to 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 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 also need to ensure that it has the name="checkout"  attribute. Here's an example of what that might end up looking 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?