Skip to main content

By Philip Zaengle

Reservation Booking with Craft Commerce

Craft Commerce is a welcome addition to the Craft CMS ecosystem, offering native functionality for full-fledged e-commerce sites built right within the same powerful system we’ve grown to love.

It currently offers product variants, discounts, coupons, customer address management, and detailed order customization, with recurring payments and memberships on the roadmap. As is the case with most e-commerce development, however, custom code is frequently necessary to accommodate specific business rules. This was the case with a project the ZC team has been working on.

The challenge

The business owner was using an external booking service to manage reservations for their multiple vacation/holiday properties. They had a Craft site developed for them that implemented the following flow:

  1. Customer used a search form to select location and dates
  2. The booking API would return available options along with the base price
  3. Customer would select an option and provide guest numbers
  4. The booking API would return the adjusted price
  5. Customer would submit contact details
  6. Contact details would be emailed to the business’ reception
  7. Reception would contact the customer via phone to accept payment

In chart form, the flow looked something like this:

Zaengle Craft Reservations Basic

While this approach worked well for the business in the interim, they wanted to modify it to accept payment without requiring the reception to directly contact each customer.

Enter Craft Commerce

Before I get into how we addressed this requirement, let me set the stage on a few “givens” relating to Craft Commerce.

Craft Commerce is a young product (released December, 2015)

There is an exciting buzz surrounding Commerce. It is in active development, but the landscape is still forming and “best practices” are fluid, which means the solution we’ve implemented may not be exactly how you would have handled it. Fortunately it’s a flexible product, and since it’s first-party we can expect the same quality and support we’ve seen from the Pixel & Tonic team in the past.

Craft Commerce essentially sells “products” out of the box

When you first install Commerce you see that it expects you to provide product types and then products. Granted, there is a lot of flexibility in what can be considered a “product” due to custom fields and product variants, nonetheless a product is required in order to sell anything. (Fortunately we will see that almost anything can be turned into a “product” by implementing the “Purchasable” interface on a custom element type.)

Potential approaches

Given the fact that Commerce requires some kind of a product, or purchasable, we talked about two main options for selling these reservations.

Option 1: Create a base “reservation” product and sell variants of it, utilizing custom fields to collect the booking data from the external API.

Option 2: Utilize the Purchasable php interface to create a custom product unique to each reservation.

Honestly, we probably could have made either approach work. It essentially came down to there not being a structured way of creating dynamic product variants, and the fact that there IS a way of creating custom Element Types (more on that shortly).

Proposed reservation flow

Once we decided on an approach, we built out the following chart to help visualize what steps would be necessary.

Zaengle Craft Reservations Complex

As you can see, the initial portion of the flow remains the same. The external API is still responsible for providing the availability and pricing data. The key difference starts right before the customer details form. A custom purchasable is created based on the response of the API, and is fed into the customer’s cart in the background.

Custom purchasable element type

Unfortunately, the process of creating custom Element Types in Craft is not well documented (yet), although there are a few resources available . The only difference between a regular Element Type and an Element Type that may be purchased is the implementation of the Purchasable interface on top of the Element Type’s model. Essentially, this means you need to add a handful of methods that give Commerce the necessary information about your Element Type required to sell it, such as pricing, tax categories, shipping rules, and SKU generation.

The nitty gritty on custom purchasable element types (feel free to skip)

Although there is a lack of documentation on creating custom element types, I was able to gather the basic steps necessary to create a custom Purchasable Element Type (These steps were shared with me by Luke Holder, the primary dev on Commerce, and an all-around great guy.):

  1. Create a model and populate its data (may or may not have an ID yet).
  2. Populate the record from the model.
  3. Validate the record and add any errors found to the model.
  4. If the model and record are valid, save the model with the craft()->commerce_purchasables>saveElement($model) which, if successful, will place an ID on the model if there was none.
  5. If the model saved successfully with the element service, add the model's ID to the record and save the record.
  6. This results in the same ID being shared in the main craft_elements table, and your own records table.

We have something to purchase

Now that the customer has selected the reservation he or she wants to make, and we have a custom purchasable element created, we continue the process of checking out by adding it to the cart in the background and accepting the customer contact details.

Confirming reservation availability

Once the payment form is submitted, we jump in again with some more logic using the Commerce_payments.Commerce_payments.onBeforeGatewayRequestSend event which will allow us to run a few checks before actually hitting the payment gateway. We need to check if the reservation is still available, so we send a request to the booking API using details from the custom purchasable element. If the booking API responds that the desired reservation is still available, then we send the credit card on to the payment gateway.

/**
 * Need to run some logic once the credit card information has been submitted,
 * but before hitting the gateway. We need to make sure the reservation
 * is still available and that the prices match.
 */

craft()->on('commerce_payments.onBeforeGatewayRequestSend', function ($event) {
    if($event->params['type'] == 'purchase') {
        $transaction = $event->params['transaction'];

        $lineItems = craft()->commerce_lineItems->getAllLineItemsByOrderId(
            $transaction->getAttribute('orderId')
        );

        // They should have only 1 item in their cart, so we grab the first row
        if ( ! craft()->reservation->confirmReservationAvailability($lineItems[0])) {
            $transaction->message = "Booking API: Reservation unavailable";
            $event->performAction = false;
        }
    }
});

Booking the reservation

Assuming the credit card information is good, the payment gateway should respond with a success status, which is our trigger to actually book the reservation with the booking API. Using the purchasable element, which contains all the reservation information, we construct a request to send to the booking API requesting the reservation be made.

If there are any errors here, we catch them and report to a site admin. If not, we display the reservation confirmation screen.

/**
 * Process the order once it has been completed.
 */
       
craft()->on('commerce_orders.onOrderComplete', function ($event) {
     craft()->reservation->processCompletedOrder($event->params['order']);
});

Tidying up

After the order is completed, we update the order status with a confirmation number from the booking API and update the status on the purchasable element, indicating it is associated with a completed order.

You probably gathered that we will be generating custom purchasables that may be “orphaned”. Meaning, when a customer selects a booking, but doesn’t complete the order process, the custom purchasable that was created for that booking isn’t associated with any order. The way we’ve dealt with that scenario is to automatically assign the “disabled” status to the purchasables upon creation. Once the order is complete, we switch the status to “enabled”. Therefore, any of the purchasables in the system that are “disabled” should be safe to delete.

In summary

By coupling the tight integration of Craft CMS with Commerce, and the ability to create custom purchasable element types, we’ve been able to sell one-off, individual products.

Want to read more tips and insights on working with a Craft CMS development team that wants to help your organization grow for good? Sign up for our bimonthly newsletter.

By Philip Zaengle

Founder & CEO

Philip’s greatest childhood loves were LEGO and earning money (by selling soda at baseball games) – two foundational traits of entrepreneurs everywhere.