NAV
javascript json

0. Introduction

Occasion Javascript SDK Getting Start Guide

The Occasion Javascript SDK enables you to sell bookings simply and securely through your web application, providing services to help you make use of many of the resources of Occasion. Such resources include merchants, products, venues, customers, orders, coupons, gift cards, payment methods, and calendars.

This guide will show you how to easily create a fully functioning order widget for booking products in your application. Every merchant in Occasion operates venues, and each venue has many products that it sells. Customers can buy these products through the order widget you will build today.

The process lets your customer browse products, select one for purchase, enter their personal information (or sign in to load their saved information), select one or more time slots, answer any number of questions relevant to the product, include a coupon or any number of gift cards, as well as enter credit card information through our PCI compliant interface.

There is also an advanced section at the end that demonstrates how to make full use of the SDK’s underlying technology to customize your application.

1. Installation

npm i occasion-sdk --save

We use NPM to distribute our SDK. Use the command at right to download and save it to your package.json

You can also use the CDN address https://unpkg.com/occasion-sdk to add it to your AMD loader or into your page:

<script type='text/javascript' src='https://unpkg.com/occasion-sdk'></script>

2. Initialization

let occsnClient = new Occasion.Client('[API_TOKEN]');

Once you’ve added the occasion-sdk module to your application, initialize it with your public API token, which can be found in your account settings on Occasion.

3. Browsing Merchants and Products

This chapter will set the foundation for building an order widget by demonstrating how to query merchants and their products, perhaps to provide customers with the ability to view all of a merchant’s products before selecting one to book.

It will also highlight the DSL for the SDK’s query interface, so it is useful for you to read this chapter completely.

Getting All Merchants

occsnClient.Merchant.all()
   .then(function(merchants) {
     merchants.each((merchant) => ...) // do something with each merchant
   });

If your API token permits you to access all of Occasion’s merchants, you can use Merchant.all() to receive a promise for a Collection of all merchants, and then process each one individually when you receive them.

Getting a Specific Merchant


occsnClient.Merchant.find('712h3as')
  .then(function(merchant) {
    // do something with Merchant(id: '712h3as')
  })
  .catch(function(errors) {
    console.log(errors)
    // =>
    //  [
    //    {
    //      parameter: 'id',
    //      code: 'notFound',
    //      detail: 'Could not find merchant with id: "712h3as"'
    //    } 
    //  ]
  });

You can use Merchant.find(id) to load a specific merchant by ID. The promise returned will yield either a resource of class Merchant or errors.

Getting Your Individual Merchant Account

occsnClient.Merchant.first()
  .then(function(merchant) {
    // do something with your individual merchant
  });

If your API token is for a single merchant using Occasion, rather than an Occasion API partner, and you want to retrieve your merchant account’s information, you can use Merchant.first().

Getting A Merchant’s Venues and Products

occsnClient.Merchant.find('712h3as')
  .then(function(merchant) {
    // do something with merchant

    // Retrieves merchant's venues from the server
    merchant.venues().all()
      .then(function(venues) {
        venues.each((venue) => ...) // do something with each venue
      });

    // Retrieves merchant's products from the server
    merchant.products().all()
      .then(function(products) {
        products.each((product) => ...) // do something with each product
      });

    // Loads merchant's venues from the server and saves them to
    // merchant.venues().target()
    merchant.venues().load()
      .then(function(venues) {

        // Load each venue's products separately
        venues.each(function(venue) {
          venue.products().load()
        });
      });

    // Retrieves merchant's product ID 'n2as78' from the server
    merchant.products().find('n2as78')
      .then(function(product) {
        // do something with Product(id: 'n2as78')
      })
      .catch(function(errors) {
        console.log(errors)
        // =>
        //  [
        //    {
        //      parameter: 'id',
        //      code: 'notFound',
        //      detail: 'Could not find product with id: "n2as78"'
        //    } 
        //  ]
      });

    // Retrieves merchant's first product from the server
    merchant.products().first()
      .then(function(product) {
        // do something with merchant's first product
      });

    // Retrieves merchant's last 3 products from the server
    merchant.products().last(3)
      .then(function(products) {
        products.each((product) => ...) // do something with each product
      });
  })

The Occasion SDK allows you to easily navigate any resource and any relationship of any resource with the exact same relational format. In the way that we retrieve all merchants or find a specific merchant by ID, we can also retrieve all of a specific merchant’s venues and products, as well as find a specific venue or product by ID amongst the merchant’s.

Note the difference between using merchant.venues().all() and merchant.venues().load(). The former retrieves the venues from the server and responds with them, the latter does the same but also stores them to merchant.venues().target().

You can also call .first(n) and .last(n) on any resource or relationship, as well as include any relationship

Eager Loading Venues and Products

occsnClient.Merchant.includes({ venues: 'products' }).first()
  .then(function(merchant) {
    // show merchant, with pages for venues and each of
    // the venues' products (all already loaded)

    // get array of venues loaded to target()
    let venues = merchant.venues().target.all();

    // gets the first venue loaded to target()
    let venue = merchant.venues().target().first();

    // get the first venue's last product
    let product = venue.products().target().last();

    // true, because relationships are constructed between resources
    venue === product.venue();
  });

You can also make use of eager loading relationships by using includes. This will load any relationships along with the primary resource(s) you are retrieving, and allow you to explore local copies of all of these resources linked together seamlessly all in one query.

It is not recommended you make this query on larger merchants because it could be slow responding with large numbers of venues and products. Instead, think consciously about where it makes sense to eager load vs. other methods.

Conclusion

This chapter demonstrates some of the DSL used to query resources in the Occasion SDK. To explore the entire interface for querying resources, see the Advanced Section under Querying and Relations.

4. Laying Out The Widget For a Product

Once your customer has selected a product to be purchased, you should show them a widget / form to create an order for that product. The remaining chapters will demonstrate how to create such a form, with this section dedicated to outlining the basic structure and components of a widget.

A great widget will provide the customer with a booking experience similar to a conversation. All of the chapters that come after this one will be structured like such a conversation, with the merchant speaking to the customer and the customer responding with user interaction.

But first, load the product with relevant data

This example shows the basic gist of the structure of an order widget, where each comment represents a section of the HTML form. The product can customize the title that comes before each section. The remaining chapters of this guide assume you display these titles as you see fit, and will show you how to implement the @todo of each section.

let productId = 'ashw762j';

occsnClient.Product.includes('merchant', 'venue').find(productId)
  .then(function(product) {
    // do something with the product, with its merchant and venue loaded

    window.product = product;
    console.log(product.title);
    console.log(product.venue().attributes());

    //
    // Header
    //

    console.log(product.title);
    // --------------------------
    // @todo Implement Chapter 5
    // --------------------------

    //
    // Order Form
    //

    // --------------------------
    // @todo Implement Chapter 6
    // --------------------------

      //
      // Contact Information
      //

      console.log(product.widgetContactsTitle);
      // --------------------------
      // @todo Implement Chapter 7
      // --------------------------

      //
      // Time Slot Information
      //

      console.log(product.widgetTimeSlotsTitle);
      // --------------------------
      // @todo Implement Chapter 8
      // --------------------------

      //
      // Additional Questions
      //

      console.log(product.widgetQuestionsTitle);
      // --------------------------
      // @todo Implement Chapter 9
      // --------------------------

      //
      // Coupon and Gift Card Redemption
      //

      // --------------------------
      // @todo Implement Chapter 10
      // --------------------------

      //
      // Payment Information
      //

      console.log(product.widgetsPaymentTitle);
      // --------------------------
      // @todo Implement Chapter 11
      // --------------------------

      //
      // Price Information
      //

      console.log(product.widgetsTotalDueTitle);
      // --------------------------
      // @todo Implement Chapter 12
      // --------------------------

      //
      // Submit Order Button
      //

      // --------------------------
      // @todo Implement Chapter 13
      // --------------------------

      //
      // After Submit (Success, Errors)
      //

      // --------------------------
      // @todo Implement Chapter 14
      // --------------------------
  });

Assuming you only know the ID of the product you want to load, you can use Product.find(id) to retrieve the product you want to sell, including its merchant and venue so you can display their information too.

You can then access any attribute of any resource as if it were a normal property of a Javascript object. Relationships are always defined as functions, rather than properties.

Using the function attributes(), you can quickly get an Object of useful information for each resource. We’ve limited the output in each example to relevant attributes only.

The remainder of this guide will assume that you’ve set window.product equal to the loaded product that you are selling.

Always make sure you are selling an active product

switch(window.product.status) {
  case 'active':
    // display order widget
  case 'inactive':
    // not accepting orders
  case 'expired':
    // has time slots, but all have passed
  case 'sold_out':
    // fully booked
}

A product can have four statuses. Customers can only book active products, so you should always check that a product is active before allowing someone to book it.

You could also limit the products you display for browsing to those you know a customer can book. See this in the Advanced Section, under Querying and Relations.



With that, let’s start the conversation…

5. Hi! Let me tell you about who we are, what this product offers you, and where we’re located

console.log(window.product.merchant().name); // Chicago's Art Studio

console.log(window.product.merchant().attributes()); // =>
// {
//   name: 'Chicago\'s Art Studio',
//   profilePicture: 'https://s3.amazonaws.com/development.assets.playoccasion/uploads/profile.jpg',
//   timeZone: 'America/Chicago',
//   facebookPage: 'https://www.facebook.com/xxxxxxx'
// }

console.log(window.product.attributes()); // =>
// {
//   title: 'Paint Party',
//   description: 'You are here to paint, and you are here to party',
//   image: {
//     url: 'https://s3.amazonaws.com/development.assets.playoccasion/uploads/product/image/xxxxx/xxxxx.jpg',
//     thumb: {
//       url: 'https://s3.amazonaws.com/development.assets.playoccasion/uploads/product/image/xxxxx/xxxxx.jpg'
//     }
//   },
//   status: 'active',
//   taxPercentage: '3.5'
// }

console.log(window.product.venue().attributes()); // =>
// {
//   name: 'Lakeview Campus',
//   address: '123 Main Street',
//   city: 'Chicago',
//   website: 'www.example.com',
//   email: 'admin@example.com',
//   phone: '(999) 999-9999',
//   timeZone: 'America/Chicago'
// }

The header of the widget is about introducing the customer to the merchant and the product they’re selling.

Use any of the snippets or properties in this example to display relevant product, venue, and merchant information in the order widget.

6. Complete this form to book this product

occsnClient.Order.construct({ product: window.product })
.then(function(order) {
  window.order = order;
});

Time to create the order form. How do we display an order form with all of its requirements, when those requirements may change depending on what product you are selling?

To answer this, the SDK provides the function Order.construct shown in this example to initialize an order with the specifics required of the product it is selling. The exact functionality of the construct method will be covered in each of the remaining sections where it is relevant.

The remainder of this guide will assume that you’ve set window.order equal to the constructed order for window.product.

Tracking sessions

console.log(window.order.sessionIdentifier)
// => "jo7xer-1504906476059"

construct instantiates each order with a sessionIdentifier that is unique. This session identifier is used by Occasion for different purposes related to tracking sessions.

You can use sessionIdentifier for your own application too.

7. What is your contact information?

Guest Checkout

window.order.customer().attributes() // =>
// {
//   email: null,
//   firstName: null,
//   lastName: null,
//   zip: null
// }

The first part of the order form is to ask the customer for their email, first name, last name, and zip code. Order.construct will build a blank customer for you. Bind each of the customer’s attributes to form inputs using your frontend library of choice.

This is called “Guest Checkout,” because it enables a customer to book any product without signing into any account, enabling faster purchase.

Account Sign In

occsnClient.Customer.signIn({
  email: 'customer@example.com',
  password: '********'
})
  .then(function(existingCustomer) {

    // Replace the guest customer with the existing customer
    window.order.assignCustomer(existingCustomer);
  })
  .catch(function(errors) {
    console.log(errors) // =>
    //  [
    //    {
    //      parameter: 'email',
    //      code: 'notFound',
    //      detail: 'Could not find customer with email: "customer@example.com"'
    //    },
    //    {
    //      parameter: 'email',
    //      code: 'unconfirmed',
    //      detail: 'We need to verify your email address first. Please check your inbox and verify your email address.'
    //    },
    //    {
    //      parameter: 'password',
    //      code: 'invalid',
    //      detail: 'Incorrect password'
    //    },
    //  ]
  });

All possible errors of this method are displayed in the example.

You can also allow customers to sign into accounts that are persisted in Occasion’s database, all they have to do is enter their email and password. Display a sign in form somewhere on your page and when the form is submitted, call Customer.signIn.

A customer can choose to save their account information on completion of purchase, which we will cover in the next section.

Account Creation

Coming soon…

8. What time slot(s) do you want to book?

Displaying pages of bookable or upcoming time slots

window.product.timeSlots().where({ status: 'bookable' }).perPage(10).all()
  .then(function(timeSlotsPage) {

    // do something with mapped version of time slots page
    timeSlotsPage.map(function(timeSlot) {
      console.log(timeSlot.attributes()) // =>
      // {
      //   duration: 7200, // seconds
      //   spotsAvailable: 5,
      //   startsAt: '2015-10-24T11:00:44.539-05:00'
      // }
    });

    if(timeSlotsPage.hasNextPage()) {
      timeSlotsPage.nextPage()
      .then(function(nextTimeSlotsPage) {
        // do something with next page of 10 time slots
      });
    }
  })

The standard product in Occasion enables a customer to select a single time slot to book an order for. To allow a customer to book one, you should show them lists of time slots to choose from.

Product time slots are loaded in pages, and you can specify the page size when you load the first page using product.timeSlots().first(n). Every page thereafter can be loaded using nextPage().

Products choose how to display individual time slots

timeSlotsPage.map(function(timeSlot) {
  // Always display startsAt
  console.log(timeSlot.startsAt);

  // Only display spotsAvailable if product.showOccurrenceAvailability
  if(window.product.showOccurrenceAvailability) {
    console.log(timeSlot.spotsAvailable);
  }

  // Only display duration if product.showTimeSlotDuration
  if(window.product.showTimeSlotDuration) {
    console.log(timeSlot.duration);
  }
});

Each product has a specific way of displaying individual time slots, based on two properties: showOccurrenceAvailability and showTimeSlotDuration. The example demonstrates how these two properties affect output.

Note that timeSlot.duration is in seconds, and should be converted to a more meaningful time metric for the customer before display.

Managing selected time slots

// Add selected time slot
window.order.timeSlots().target().push(selectedTimeSlot);

// Remove selected time slot
window.order.timeSlots().target().delete(selectedTimeSlot);

// Clear selected time slots
window.order.timeSlots().target().clear();

order.timeSlots() is a collection relationship, but for now, orders require that only a single time slot be selected. Future releases will allow the ability to select multiple timeSlots.

The example shows some basic ways to manipulate selected time slots.

Sessions

Coming soon…

9. Please answer some additional questions to customize your experience

One of the richest features of Occasion is that it enables merchants to add additional questions to each product they sell, selecting from a wide range of:

Categories

Form Control Types

The first two sections of this chapter will show how Order.construct assists in creating this section of the order form, as well as some SDK helpers to provide your customer with instant price updates any time a priceCalculating question changes.

Each section thereafter will be dedicated to showing you how to display a specific form control type, for every possible category that form control type can be (informational, price changing, view component, special). By learning how to display each form control type, you will be able to successfully build the additional questions section of the order widget.

Create blank answer inputs for all the questions

window.order.answers().target().map(function(answer) {
  console.log(answer.attributes()); // =>
  // {
  //   id: 'bg35ajhe',
  //   value: null
  // }

  console.log(answer.question().attributes()); // =>
  // {
  //   id: 'k127s2y',
  //   required: false,
  //   title: 'The first optional question',
  //   formControl: 'drop_down',
  //   priceCalculating: false,
  //   operation: null
  // }

  console.log(answer.question().options()); // =>
  // @see Drop Downs and Option Lists
});

Order.construct called in Chapter 4 initializes the order with blank answers() for each question in window.product.questions(). You can iterate over every one of order.answers() and use each of their question()’s properties to display the answer input for the question on the order widget.

We’ve listed these properties in the example using attributes().

The configuration of these properties will affect how you display each input and how you respond when the input changes.

answer.question().formControl

The formControl attribute of each question will determine the type of input needed to be displayed in order to answer it. Possible values are:

Individual sections of this chapter will show you how to map these values to HTML form inputs.

answer.question().title

The title of each question should be used as a <label></label> for each answer input.

Depending on the form control, the question may have additional attributes that are useful or necessary when displaying a label and input. We’ll cover these in each of the individual form control sections.

answer.question().required

Answers with question().required equal to true must have a value or an option() specified. Make sure to set the required attribute of the HTML input to this value for easier validation.

answer.question().position

The order that questions should be displayed in is indicated by their position, from 1 to n.

answer.question().priceCalculating

If priceCalculating equals true, you should add a callback so that any time the input changes, the price of the order displayed to the customer updates. The next section will cover this functionality.

answer.question().operation

Additionally, if the question is priceCalculating, it will also have an operation equal to one of the following: add, subtract, multiply, divide, and permute. The value of operation will affect the attributes associated with a question and how the customer will understand the question.

answer.question().options()

If a question has formControl equal to drop_down or option_list, it will also have options().target() loaded with options for the customer to choose from.

See the section on Drop Downs and Option Lists for more details.

answer.option() or answer.value

In general, you will bind answer.value to the value of the input displayed for each question. Drop downs and option lists will set answer.option() equal to one of their options.

Calculating and updating the order’s price

window.afterPriceCalculatingItemChange = function() {
  // called after the answer on `window.order` has already
  // been updated to reflect the new option or value specified
  // in the DOM

  window.order.calculatePrice()
  .then(function(order) {
    // @todo Implement Chapter 12
  });
};

Every time the answer to a priceCalculating question changes, you must update the price displayed on the order widget so the customer can see real-time how their choices affect the order’s price.

After change event has been processed and the answer itself has been changed on window.order, you can call window.order.calculatePrice(), which will automatically update window.order to have various pricing attributes useful to the customer.

The implementation of this method is completed in Chapter 12, we’re showing it here so you can hook up this callback as you follow along with building views for the different form control types.

Text Inputs and Text Areas

window.order.answers().target().map(function(answer) {
  console.log(answer.question().attributes()); // =>
  // {
  //   id: 'k127s2y',
  //   required: true,
  //   title: 'What is your favorite color?',
  //   formControl: 'text_input'
  // }
  //
  // *or*
  //
  // {
  //   id: '6gdt3aw',
  //   required: true,
  //   title: 'What is your favorite color? Elaborate.',
  //   formControl: 'text_area'
  // }
});

Text inputs and text areas are the simplest form control types.

A text_input corresponds to an <input type='text'></input>.

A text_area corresponds to a <textarea></textarea>.

// Assign new value on input change
window.onInputChanged = function(e) {
  answer.value = e.target.value;
};

The second example in this section shows a basic version of an onChange callback for any answer input without options(). Any time the input changes, read the value of the input and assign it to answer.value.

This is the way answers are completed for every form control type except those with options() (drop downs and option lists). This guide assumes you will use this snippet when answering such form controls.

Checkboxes

window.order.answers().target().map(function(answer) {
  console.log(answer.question().attributes()); // =>
  // {
  //   id: 'mjyw4t5',
  //   must_be_checked: false,
  //   title: 'Would you like to enable this free feature?',
  //   formControl: 'checkbox',
  //   priceCalculating: false
  // }
  //
  // *or*
  //
  // {
  //   id: 'mjyw4t5',
  //   must_be_checked: false,
  //   title: 'Would you like to add this specific item?',
  //   formControl: 'checkbox',
  //   priceCalculating: true,
  //   operation: 'add',
  //   price: '5.0'
  // }
  //
  // *or*
  //
  // {
  //   id: 'mjyw4t5',
  //   must_be_checked: false,
  //   title: 'Would you like to a fixed discount based on this?',
  //   formControl: 'checkbox',
  //   priceCalculating: true,
  //   operation: 'subtract',
  //   price: '5.0'
  // }
  //
  // *or*
  //
  // {
  //   id: 'mjyw4t5',
  //   must_be_checked: false,
  //   title: 'Want a premium upgrade?',
  //   formControl: 'checkbox',
  //   priceCalculating: true,
  //   operation: 'multiply',
  //   percentage: '25.0'
  // }
  //
  // *or*
  //
  // {
  //   id: 'mjyw4t5',
  //   must_be_checked: false,
  //   title: 'Are you a student?',
  //   formControl: 'checkbox',
  //   priceCalculating: true,
  //   operation: 'divide',
  //   percentage: '10.0'
  // }
  //
  // *or*
  //
  // {
  //   id: 'mjyw4t5',
  //   must_be_checked: false,
  //   title: 'Combine this with other package deals',
  //   formControl: 'checkbox',
  //   priceCalculating: true,
  //   operation: 'permute',
  //   price: null
  // }
});

Checkboxes can collect information or change the price of the order. The example shows the types of checkbox questions you can expect to see on a product.

If the checkbox is checked, set answer.value equal to true. Otherwise, set it equal to false.

First, note that all of them use must_be_checked rather required to indicate if they must be checked. This goes along with HTML5, which allows all inputs except [type=checkbox] to use the required attribute to validate presence of a value. Use answer.value to validate on your own that any checkbox that must_be_checked is in fact so.

The first object in the example is an informational checkbox question, priceCalculating is false.

The rest of them are price changing questions, and each of them has a price operation as well as an extra property regarding its impact on price.

Operation Property Description
add price The checkbox will add a fixed value price to the order total if checked.
subtract price The checkbox will subtract a fixed value price from the order total if checked.
multiply percentage The checkbox will add a percentage markup to the order total if checked.
divide percentage The checkbox will subtract a percentage discount from the order total if checked.
permute N/A The specific value of this checkbox combined with the specific value of other questions will determine the price change, based on what is set by the merchant. As such, these will not have a price or percentage attribute. This calculation is done for you in the SDK, so think of these as price changing questions with no price or percentage needed for display.

Remember to bind the change event for priceCalculating checkbox inputs to update the order price using afterPriceCalculatingItemChange. See section 2 of this chapter.

Merchants usually do not put the price or percentage that applies to any individual question in its title, so you should use operation and these attributes to modify the label of the question accordingly.

For example, if the question had operation == 'divide', an appropriate modification might be: <label>[TITLE] ([PERCENTAGE]% off)</label>

window.order.answers().target().map(function(answer) {
  console.log(answer.question().attributes()); // =>
  // {
  //   id: 'j1ng3s4',
  //   required: true,
  //   title: 'Which of these items is your favorite?',
  //   formControl: 'drop_down',
  //   priceCalculating: false,
  //   operation: null
  // }
  //
  // *or*
  //
  // {
  //   id: 'bsg36hy',
  //   required: true,
  //   title: 'Pick one of the following items',
  //   formControl: 'option_list',
  //   priceCalculating: true,
  //   operation: 'add'
  // }
  //
  // *or*
  //
  // {
  //   id: 'j1ng3s4',
  //   required: true,
  //   title: 'Pick one of the following items',
  //   formControl: 'drop_down',
  //   priceCalculating: true,
  //   operation: 'permute'
  // }

  console.log(answer.question().options().target().toArray()); // =>
  // [
  //   {
  //     id: 'j47ash',
  //     title: 'The first option.',
  //     position: 0,
  //     default: false,
  //     price: null
  //   }, {
  //     id: 'bt5a9yt',
  //     title: 'The second option.',
  //     position: 1,
  //     default: true,
  //     price: null
  //   }
  // ]
  //
  // *or*
  //
  // [
  //   {
  //     id: 'j47ash',
  //     title: 'The first option.',
  //     position: 0,
  //     default: false,
  //     price: '5.0'
  //   }, {
  //     id: 'bt5a9yt',
  //     title: 'The second option.',
  //     position: 1,
  //     default: true,
  //     price: '20.0'
  //   }
  // ]
});

Drop downs and option lists are the two types of questions that have options().

A drop down corresponds to a <select></select> tag with an <option></option> for each option in question().options().

An option list corresponds to a group of <input type='radio'></input>s for each option in question().options().

For each option:

If the question is priceCalculating, then the drop down or option list will perform one of two operations:

Operation Property Description
add price Each option will add a fixed value option.price to the order total if checked.
permute N/A The specific value of each option combined with the specific value of other questions will determine the price change, based on what is set by the merchant. As such, no options will have a price attribute.

Merchants usually do not put the price that applies to any individual option in its title, so you should add this to each option’s label so customers know the price of each option.

// Directly assign answer.option
answer.assignOption(answer.question().options().target().first());
// Find option by ID then assign answer.option
let selectedOptionId = 'yt5mfkg';

let selectedOption = answer.questions.options()
                     .target().detect(function(o) {
                       return o.id == selectedOptionId;
                     });

answer.assignOption(selectedOption);


The second and third examples in this section show:

Once an option has been selected, you can set the answer.option() to the selected option using assignOption(option).

If you only know the ID of the option the customer selected, you can find the option itself amongst answer.question().options() using detect(predicate), which takes in a function that iterates over every option and returns the first option where the function returns true.

In this case, we check to see if the option’s ID is equal to the selectedOptionId.

Remember to bind the change event for priceCalculating drop down and option list inputs to update the order price using afterPriceCalculatingItemChange. See section 2 of this chapter.

Spin Buttons

window.order.answers().target().map(function(answer) {
  console.log(answer.question().attributes()); // =>
  // {
  //   id: 'j1ng3s4',
  //   required: true,
  //   title: 'How many free items do you want?',
  //   formControl: 'spin_button',
  //   priceCalculating: false,
  //   operation: null,
  //   max: 10,
  //   exclude_zero: false
  // }
  //
  // *or*
  //
  // {
  //   id: 'bsg36hy',
  //   required: true,
  //   title: 'How many priced items do you want?',
  //   formControl: 'spin_button',
  //   priceCalculating: true,
  //   operation: 'add',
  //   max: 5,
  //   exclude_zero: false,
  //   price: '10.0'
  // }
  //
  // *or*
  //
  // {
  //   id: 'bsg36hy',
  //   required: true,
  //   title: 'Do you want more than one of this order?',
  //   formControl: 'spin_button',
  //   priceCalculating: true,
  //   operation: 'multiply',
  //   max: 5,
  //   exclude_zero: false
  // }
});

Spin buttons count incrementally up or down.

A spin button corresponds to an <input type='number'></input> tag.

Attributes specific to this form control type:

If the question is priceCalculating, then the spin button will perform one of two operations:

Operation Property Description
add price Each time the spin button is incremented, the fixed value price will be added to the order total.
multiply N/A The order total is multiplied by the value of the spin button.

Merchants usually do not put the price that applies to any individual increment of the spin button, so you should add this to the label so customers know the price of each increment.

Remember to bind the change event for priceCalculating spin button inputs to update the order price using afterPriceCalculatingItemChange. See section 2 of this chapter.

View Components

window.order.answers().target().map(function(answer) {
  console.log(answer.question().attributes()); // =>
  // {
  //   id: 'j1ng3s4',
  //   title: '',
  //   formControl: 'separator',
  //   priceCalculating: false
  // }
  //
  // *or*
  //
  // {
  //   id: 'j1ng3s4',
  //   title: 'This is some text to display for the customer.',
  //   formControl: 'text_output',
  //   displayAsTitle: true,
  //   priceCalculating: false
  // }
});

Some questions in Occasion are not questions at all

Instead, some are just helpful view components like separators and titles that make the widget more helpful and aesthetic.

There are two view component form control types right now: separator, and text_output.

separator corresponds to an <hr/> tag. That’s about it.

text_output corresponds to a <p></p> tag with title as the content. However, if question().displayAsTitle is true, switch to a <h3></h3> or similar.

Special Fields

window.order.answers().target().map(function(answer) {
  console.log(answer.question().attributes()); // =>
  // {
  //   id: 'j1ng3s4',
  //   title: 'Do you accept the terms and conditions?',
  //   formControl: 'waiver',
  //   priceCalculating: false,
  //   waiverText: 'By agreeing to these terms, you accept that ...'
  // }
});

Waiver Fields

The first special field form control type is the Waiver Field, which is an upgraded checkbox that allows for an additional waiverText attribute on top of the standard title. These fields will not have a must_be_checked attribute, but waiver fields must always be checked for successful purchase.

Attendees Fields

Coming soon…

10. Do you have a coupon or any gift cards you’d like to use?

You can provide customers with the ability to add a coupon or any number of gift cards to their order. This could be implemented as a single text input that takes in a code, checks that it’s valid, and then displays the coupon or gift card information on the order widget, indicating that it has been added to the order while updating the order price.

Finding coupons and gift cards by code

window.product.redeemables().findBy({ code: 'HOLIDAYS45' })
.then(function(redeemable) {
  if(redeemable.isA(occsnClient.Coupon)) {

    console.log(redeemable.attributes()) // =>
    // {
    //   id: 'bh7ag0zn',
    //   code: 'HOLIDAYS45',
    //   name: 'Holiday Coupon',
    //   discount_fixed: '5.0',
    //   discount_percentage: null
    // }
    //
    // *or*
    //
    // {
    //   id: 'bh7ag0zn',
    //   code: 'HOLIDAYS45',
    //   name: 'Holiday Coupon',
    //   discount_fixed: null,
    //   discount_percentage: '10.0'
    // }

  } else if(redeemable.isA(occsnClient.GiftCard)) {

    console.log(redeemable.attributes()) // =>
    // {
    //   id: '56bvcf4e',
    //   code: 'HOLIDAYS45',
    //   initial_value: '50.0',
    //   value: '10.0'
    // }
  }

})
.catch(function(errors) {
  console.log(errors) // =>
  //
  // 404 Not Found
  //
  // {
  //   parameter: 'filter/code',
  //   code: 'notFound',
  //   detail: 'Could not find redeemable with code: "HOLIDAYS45"'
  // }
  //
  // *or*
  //
  // 409 Conflict
  //
  // {
  //   parameter: 'filter/code',
  //   code: 'unavailable',
  //   detail: 'A coupon or gift card was found, but is no longer available for use.'
  // }
})

All possible error responses are displayed in the example

The relationship product.redeemables() enables you to search for any redeemables (coupon or gift card) that can be redeemed for the product being ordered, using findBy() to find the distinct redeemable for the code the customer entered.

Every resource returned from a promise in the SDK has a function isA(class), which will return true if the resource is an instance of the class provided.

Once we’ve found a redeemable, we can use isA to see if it is an occsnClient.Coupon, or an occsnClient.GiftCard.

Coupons and gift cards will have different attributes(), as displayed in the example. These attributes are:

Coupons

Attribute Description
code The coupon code
name The name of the coupon
discount_fixed The fixed monetary discount provided by the coupon. Will be null if discount_percentage exists.
discount_percentage The percentage discount provided by the coupon. Will be null if discount_fixed exists.

Gift Cards

Attribute Description
code The gift card code
initial_value The initial value that was loaded onto the gift card.
value The remaining value on the gift card that can be applied to the order.

Adding the redeemable to the order

window.product.redeemables().findBy({ code: 'HOLIDAYS45' })
.then(function(redeemable) {
  if(redeemable.isA(occsnClient.Coupon)) {
    // Assign the coupon to the order if the redeemable is a coupon
    window.order.assignCoupon(redeemable);

  } else if(redeemable.isA(occsnClient.GiftCard)) {
    // Charge gift card for its entire value if the redeemable is a gift card
    // @see Chapter 11 for more information on `charge`
    window.order.charge(redeemable, redeemable.value);
  }
})

Once we’ve found a redeemable, we check to see if it is an occsnClient.Coupon, and if it is, we window.order.assignCoupon(coupon).

If it is a occsnClient.GiftCard, use order.charge to charge the gift card for the value specified in the second argument, in this case its entire value.

Removing redeemables from the order

// Remove coupon by setting order.coupon equal to null
window.order.assignCoupon(null);

// Remove gift card using order.removeCharge
window.order.removeCharge(giftCardToRemove);

You can easily remove redeemables using the functions displayed in this example.

Updating order price

If we add a coupon or a gift card to the order, the price is going to change (unless it is already free, of course).

To reflect any discount provided by a redeemable, you should call the window.afterPriceCalculatingItemChange callback created in Chapter 9 Section 2 after adding the redeemable to the order.

Remember to bind the event for successfully finding a redeemable to update the order price using afterPriceCalculatingItemChange. See Chapter 9 Section 2.

11. Your total today is…

Displaying the result of a customer’s choices on the price of their order in real time is important to the experience. The special method order.calculatePrice() can give you the subtotal, coupon discount, tax, and total price of the order.

It can also calculate the contribution of gift cards toward the order’s outstanding balance, to be paid using the payment method information collected in the next chapter.

Displaying useful price information

window.afterPriceCalculatingItemChange = function() {
  // called after the answer on `window.order` has already
  // been updated to reflect the new option or value specified
  // in the DOM

  window.order.calculatePrice()
  .then(function(order) {
    console.log(window.order.attributes()) // =>
    // {
    //   subtotal: '10.0',
    //   couponAmount: '1.0',
    //   tax: '2.0',
    //   giftCardAmount: '5.0',
    //   price: '11.0',
    //   outstandingBalance: '6.0'
    // }
  });
};

Every time:

You must update the price displayed on the order widget so the customer can see real-time how their choices affect the order’s price. We’ve already covered in previous sections how to bind this callback appropriately - now we will explain how to implement the remaining functionality.

Each time the window.afterPriceCalculatingItemChange callback shown is executed, it will call window.order.calculatePrice(), which will automatically update window.order to have the following attributes:

Attribute Description
subtotal The price of the order before coupon discount and tax have been added
couponAmount The monetary value of the coupon that may have been applied to the order. If there is no coupon, this will equal null
tax The monetary value of product.tax_percentage applied to order.subtotal - order.couponAmount
giftCardAmount The monetary value of all of the gift cards charged to the order. If no gift cards, this will equal null.
price The total price of the order after tax has been applied to the subtotal - discounts
outstandingBalance The remaining balance after giftCardAmount has been deducted from price. Other payment methods like credit cards will have to pay this amount for the order to be completed paid for.

You should display these attributes to the customer accordingly, so that they have a full understanding of exactly what charges they are receiving on their payment method.

The numbers must add up

In the next chapter we will make use of order.outstandingBalance to charge a credit card for the remaining balance left on the order.

An order with payment methods (credit cards and gift cards), saved to Occasion, is only valid if all of the charges to those payment methods add up to order.price, otherwise your order will fail to save.

Note, this does not apply to orders that are paid for using cash, since no charges are created with that type of order.

12. Please enter your payment information

window.merchant.pspName // => 'cash'

After the customer has selected any coupons or gift cards, it is time for them to enter their payment information. Right now only cash and credit cards are supported, but other platforms may be featured in future releases.

Each merchant on Occasion accepts payments using a payment service provider (PSP): Cash, Spreedly, or Square, and they can only use one. The latter two process credit cards. The following sections will show you how to handle each individual case.

First off, though, you must determine which PSP the merchant uses. To do this, read the merchant.pspName attribute, as shown in the example. Possible names include:

PSP Name
Cash cash
Spreedly spreedly
Square square

Accept cash only

For merchants with pspName == 'Cash', payment for an order is collected in cash when the customer comes in for their reservation. For these merchants, no additional measures must be taken to collect payment information to use to pay for the order.

Build and charge a credit card using a payment processor payment method token

Occasion does not process payment information through our own application, we use PCI compliant payment processing services to do this for us, and there are two options. Each provider offers an iFrame script that can used to securely send credit card information to their server, and receive a token back that Occasion uses to process payment for the order.

Spreedly

Spreedly.init('UnQhm0g7l3nOIz2hmAoV3eqm26k',
  {} // ... additional config required, see Spreedly docs
);

window.onOrderFormSubmit = function(e) {
  var creditCardData = {
    // see documentation
  };

  Spreedly.tokenizeCreditCard(creditCardData)

  Spreedly.on('paymentMethod', function(token) {
    var creditCard = occsnClient.CreditCard.build({ id: token });

    window.order.charge(creditCard, window.order.outstandingBalance);

    // @todo Implement Chapter 13
    // window.order.save
  });

  Spreedly.on('errors', function(errors) {
    // do something with errors
  });
};

Spreedly supports hundreds of payment gateway services like Stripe, Braintree, Authorize.net, and more. Note: At this time, Paypal support through Spreedly is not enabled in the Occasion SDK, though a future release will add support.

To add a Spreedly payment form to your application, you must use their iFrame payment method script, which you can find a guide on here.

The example shows a simplified version of the basic steps you’ll have to take to implement this form in your application once the script has been added:

  1. Initialize Spreedly using Occasion’s Spreedly environment key
  2. Create a payment method token from user-input credit card information once the order form is submitted
  3. Submit order to Occasion with the payment method token using the Occasion SDK

Use our environment key with the Spreedly form:

Name API Key
Spreedly UnQhm0g7l3nOIz2hmAoV3eqm26k

Square

window.squareForm = new SqPaymentForm({
    applicationId: "sq0idp-kKdgouNdlT2lj08V0tSJ3g",
    // ... additional config required, see Square docs
    callbacks: {
      cardNonceResponseReceived: function(errors, nonce) {
        if(errors) {
          // do something with errors
        } else {
          var creditCard = occsnClient.CreditCard.build({ id: nonce });

          window.order.charge(creditCard, window.order.outstandingBalance);

          // @todo Implement Chapter 13
          // window.order.save
        }
      }
    }
});

window.onOrderFormSubmit = function(e) {
  window.squareForm.requestCardNonce();
};

Square is a payment gateway much like the hundreds supported under Spreedly, but their closed platform must be implemented individually.

To add a Square payment form to your application, you must use their iFrame payment method script, which you can find a guide on here.

The example shows a simplified version of the basic steps you’ll have to take to implement this form in your application once the script has been added:

  1. Initialize Square using Occasion’s Square application ID
  2. Request a payment method nonce from user-input credit card information once the order form is submitted
  3. Submit order to Occasion with the payment method nonce using the Occasion SDK

Use our key with the Square form:

Name Application ID
Square sq0idp-kKdgouNdlT2lj08V0tSJ3g

Edit or remove a credit card charge

// Update the amount charged to the specified credit card to 9.0
window.order.editCharge(chargedCreditCard, 9.0);

// Remove the charged credit card from the order
window.order.removeCharge(invalidCreditCard);

If your order fails to save because the customer’s credit card information was wrong, they’re going to have to enter new credit card data.

But just because the order doesn’t save doesn’t mean the credit card is removed from it, the invalid credit card will remain on the order even if you call order.charge() with a new credit card. An order can only charge one credit card, valid or not valid.

To prevent this scenario, you should remove invalid credit cards if they cause an order not to save, and then go through the process outlined in the previous section in order to build a new credit card.

If for any reason you need to edit the amount charged to any item that has already been charged, you can do that too.

13. Submit your order

The time has come. We are now ready to add the big “Finalize order” button at the bottom of the widget.

The button

window.product.orderButtonText

When displaying this big beautiful button in all its glory, make sure to do it the way our merchants want!

The product has orderButtonText to shine the way.

Finallyze

window.order.save(function() {
  if(window.order.persisted()) {
    // order was a success, go to the completion page
  } else {
    // order failed for some reason, read the errors

    console.log(window.order.errors().toArray()) // =>
    // @see Chapter 14 Section 2
  }
});

You can attempt to persist any resource in the SDK at any time by calling save(callback) on it.

When save receives a response, the callback will execute regardless of whether or not the attempt was a success or failure, and you can call the original window.order from the callback because save mutates the original object it was called on rather than responding with a new one.

persisted() indicates whether or not any resource in the SDK exists on our server. If an order fails to save, it will not be persisted on our server.

Instead, it will have errors() that you can display for the customer, which will be shown in the next chapter.

14. Thanks…or…There was a problem

After the order form is submitted, your customer will either achieve great success, or experience crushing failure.

To make this experience best, give them a great thank you page, and an effective error display.

Success

console.log(window.product.postTransactionalMessage); // =>
// Thanks for booking our product!

// Attributes after the order is saved to the server
console.log(window.order.attributes()); // =>
// {
//   id: '875ahny6',
//   verificationCode: 'kASn37a',
//   status: 'booked',
//   createdAt: '2015-10-23T11:00:44.539-05:00',
//   updatedAt: '2015-10-23T11:00:44.539-05:00',
//   tax: '0.0',
//   taxPercentage: '0.0',
//   couponAmount: '1.0',
//   couponDescription: '$1.00',
//   price: '10.0',
//   quantity: 1,
//   description: 'The title of the product being ordered.'
// }

Displaying a success screen to the customer is simple. Do away with the order form, and show them product.postTransactionalMessage.

You can also choose to show them any of the attributes() of the saved order, the output of which is shown in this example.

Error

// Errors after the order fails to save to the server

console.log(window.order.errors().forField('customer').toArray()); // =>
// [
//   {
//     field: "customer.firstName"
//     code: "blank"
//     detail: "Customer.First Name cannot be blank."
//   },
//   
//   {
//     field: "customer.lastName"
//     code: "blank"
//     detail: "Customer.Last Name cannot be blank."
//   },
//   
//   {
//     field: "customer.email"
//     code: "invalid"
//     detail: "Customer.Email is invalid."
//   }
// ]


console.log(window.order.errors().detailsForField('timeSlots')); // =>
// {
//   blank: "Orders for product id: '94hn_pte' require at least one time slot."
// }


console.log(window.order.errors().forField('answers').toArray()); // =>
// [
//   {
//     field: "answers.option"
//     code: "blank"
//     detail: "Answer option cannot be blank."
//   },
//
//   {
//     field: "answers.value"
//     code: "invalid"
//     detail: "Answer value is invalid."
//   }
// ]


console.log(window.order.errors().forField('coupon').toArray()); // =>
// [
//   {
//     field: "coupon"
//     code: "invalid"
//     detail: "Coupon is invalid."
//   },
//
//   {
//     field: "coupon"
//     code: "inactive"
//     detail: "Coupon is inactive at this time."
//   },
//
//   {
//     field: "coupon"
//     code: "depleted"
//     detail: "Coupon is no longer available."
//   }
// ]


console.log(window.order.errors().forField('transactions').toArray()); // =>
// [
//   {
//     field: "transactions.amount"
//     code: "invalid"
//     detail: "Transactions.Amount is invalid."
//   },
//   
//   {
//     field: "transactions.paymentMethod"
//     code: "blank"
//     detail: "Transactions.Payment Method cannot be blank."
//   },
//   
//   {
//     field: "transactions.paymentMethod"
//     code: "declined"
//     detail: "Transactions.Payment Method declined."
//   }
// ]

console.log(window.order.errors().forBase().toArray()); // =>
// []

All possible errors are shown in this example.

An efficient error display will let the customer know what mistakes they’ve made on the form or what errors have occurred in processing the order.

You can get a collection of all of the errors for a given attribute or relationship using errors().forField(name). You can also use errors().detailsForField(name) for an object that summarizes the errors. These are useful for displaying errors next to the input(s) that generated them.

You can get all of the errors that are associated with the base order (rather than a specific field) using errors.forBase().

15. Conclusion

We made it! You now have a fully functioning order widget, ready to start booking merchants’ products on Occasion.

We hope this getting started guide has served you well. If you have any questions that you feel are not answered through this documentation, please reach out to nick@getoccasion.com. You can also join our Slack channel.

Also check out the Advanced Section immediately after this, where you can see lists of many of the useful functions that are available for each resource in the Occasion SDK in order to gain a more definitive grasp on our DSL.

Good luck with your future success! Happy booking