How To Build A Booking System with Ruby On Rails: Part 2

Tressa Sanders
10 min readSep 3, 2018

--

At Stack English, we offer Technical and Business English lessons to IT professionals. The first step in the booking process is to book an assessment. Once the assessment is complete, an assessment code is sent via email. New clients must use this assessment code to book their first lesson with an English trainer. During this booking process, we will create, a new user and a new booking.

PART 1: How To Build A Booking System with Ruby On Rails: Part 1

In part 2 of this tutorial, I will cover the actual booking process and walk you through handling different time zones, creating the booking object, explaining the views and outlining limitations.

TIME ZONES:

If there is even a slim chance that the user and service provider will be in different time zones, then you need to deal with this first thing. I initially dreaded having to address this issue but it was so easy, I thought I might have missed something. But no, it was just easy.

You need two Gems:

You need the local_time gem for your frontend. Install the local_time gem according to the instructions on github.

You only want the local_time gem to work on the frontend because we are using the timezone gem to deal with the backend(user/admin control panel) times.

Include the following in /app/helpers/application_helper.rb

include LocalTimeHelper

That’s it. When we get to the /app/views/users/new.html.erb view, you will notice that all we have to do is use the following syntax to display the time in the client’s local time (before they login). You can format the time however you like.

<%= local_time(schedule.start, format: ‘%e %b, %Y — %H:%M %P’) %>

You need the timezone gem for your backend. Install the gem according to the instructions on github. You will also need to add a “time_zone” field to your USER table. Then, in your /app/controllers/application_controller.rb add the following:

before_action :set_time_zone, if: :logged_in?private
def set_time_zone
Time.zone = current_user.time_zone
end

That’s it. When the user is logged in, they will see times in the time zone they have set in their user account. If they move to a new location, they only have to edit their user account with a new time zone.

FRONTEND BOOKING:

For booking the first lesson, we are going to use the NEW and CREATE actions of app/controllers/users_controller.rb to do everything that we need. Future lessons must be booked using the backend user admin panel.

Before we get started, we need to create the booking feature:

rails g scaffold Booking status:string title:string cost:integer start:datetime cancellation_reason:text refunded:boolean trainer_id:integer:index schedule_id:integer:index lesson_id:integer:index account_id:integer:indexrake db:migrate

These are basic fields. You need to decide what you need for your booking model. The “status” field is to set the booking as “Booked”, “Completed”, “No Show”, “No Show - Refunded”, “Canceled” and “Canceled - Refunded”.

Then we need to create the lesson payment scaffold:

rails g scaffold LessonPayment payment_number:string status:string date:date cost:integer service:string booking_id:integer:index account_id:integer:indexrake db:migrate

The “service” field should contain the title of the lesson or service being purchased. You can also move the lesson payment views wherever you like in the backend. They are not used at all on the frontend.

In /app/models/booking.rb add:

  belongs_to :account, :inverse_of => :bookings
accepts_nested_attributes_for :account

belongs_to :lesson, :inverse_of => :bookings
accepts_nested_attributes_for :lesson

belongs_to :schedule, :inverse_of => :bookings
accepts_nested_attributes_for :schedule

belongs_to :trainer, :inverse_of => :bookings
accepts_nested_attributes_for :trainer

belongs_to :client, :inverse_of => :bookings
accepts_nested_attributes_for :client

has_many :lesson_payments, dependent: :destroy, :inverse_of => :booking
accepts_nested_attributes_for :lesson_payments

validates :schedule_id, presence: true

In /app/models/lesson_payment add:

 belongs_to :account, :inverse_of => :lesson_payments
accepts_nested_attributes_for :account

belongs_to :booking, :inverse_of => :lesson_payments
accepts_nested_attributes_for :booking

USER CONTROLLER:

The USER controller we are dealing with is for the FRONTEND and only has one job; create new bookings and user accounts for first time users.

In app/controllers/users_controller.rb:

class UsersController < ApplicationController
[...]
before_action :set_lesson, only: [:new]
def new
@account = Account.new
@user = User.new
@client = Client.new
@booking = Booking.new
@lesson_payment = LessonPayment.new
@schedules = Schedule.where('start >= ? and start <= ?', Date.today + 1.day, Date.today + 2.weeks).where(title: 'Available').order('start ASC').all
end
def create// I have not included the code for "create_client_charge" because you need to figure out how you will process the charge. I have left this here though because I wanted to show that I try to charge the user first because if the charge fails, I don't want anything else created.
create_client_charge
create_client_account @user = User.new(user_params) @user.account_id = @account.id

respond_to do |format|
if @user.save
create_client_profile create_client_booking create_client_lesson_payment auto_login(@user) UserMailer.new_signup_booking_admin(@user, @booking).deliver_later UserMailer.new_signup_booking_client(@user, @booking).deliver_later format.html { redirect_to dashboard_url, notice: 'Your account was successfully created.' }
format.json { render :show, status: :created, location: @user }
else
format.html { redirect_back fallback_location: root_path, alert: 'An error occurred while sending this request.' }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
endprivate

def set_lesson
@lesson = Lesson.find(params[:lesson_id])
end
def user_params
params.require(:user).permit(:email, :password, :time_zone)
end

def create_client_account
@account = Account.new()
@account.status = 'active'
@account.account = 'prefix-' + SecureRandom.hex(4)
@account.account_type = 'client'
@account.save
end
def create_client_profile
@client = Client.new()
@client.firstname = params[:user][:client][:firstname]
@client.lastname = params[:user][:client][:lastname]
@client.phone = params[:user][:client][:phone]
@client.user_id = @user.id
@client.account_id = @user.account_id
@client.save
end

def create_client_booking
@lesson = Lesson.find(params[:user][:booking][:lesson_id])
@booking = Booking.new()
@booking.lesson_id = params[:user][:booking][:lesson_id]
@booking.schedule_id = params[:user][:booking][:schedule_id]
@booking.client_id = @client.id
@booking.account_id = @user.account_id
@booking.title = @lesson.title
@booking.cost = @lesson.cost
@booking.status = 'Booked'
@booking.save
@schedule = Schedule.find(params[:user][:booking][:schedule_id])
@booking.trainer_id = @schedule.trainer_id
@booking.start = @schedule.start
@booking.refunded = 0
@booking.save
@schedule.title = 'Booked'
@schedule.save
end
def create_client_lesson_payment
@lesson_payment = LessonPayment.new()
@lesson_payment.status = 'Paid'
@lesson_payment.date = Date.today
@lesson_payment.cost = @lesson.cost
@lesson_payment.service = @lesson.title
@lesson_payment.booking_id = @booking.id
@lesson_payment.account_id = @user.account_id
@lesson_payment.save
end

end

NOTE: When we want to show the available slots, we want to start 1 day ahead (we don’t want clients to be able to book a lesson on the same day they are making the booking). This would be “Date.today + 1.day”. You can adjust this to whatever you like. Additionally, we did not want clients to be able to book a lesson more than two weeks ahead of the day they are making the booking. So we set this limit with “Date.today + 2.weeks”. So when we show a list of available time slots, clients will not see any dates for the current day or earlier nor will they see any dates more than two weeks out.

NEW USER VIEW:

Our /app/views/users/new.html.erb view is pretty straight forward but has multiple steps. We will be creating several objects during this process. In order to visually manage the process in a useful way, I decided to use a three step wizard.

We will use forms for other controllers nested within the form for the USER controller and the USER form must be bound to the payment processing javascript. We must begin the form as follows:

<%= simple_form_for @user, :html => {:class => 'step-form', :id => "payment-form"} do |f| %>

The FIRST STEP allows the user to pick a date and time. The times that the user sees are for their time zone only. When the trainer views the booking in their admin panel, the time will be listed in their time zone and NOT the time zone of the user who booked the lesson. When the client views the booking in their admin panel, they time will be listed in their time zone and NOT the time zone of the trainer with whom the lesson is booked. This way, everyone sees the correct time in their own time zone and you can avoid any time zone related mishaps.

Our first nested form is the booking form. We must nest it as follows:

<%= f.fields_for [@user, @booking] do |b| %>

Within the booking form, we must list the available time slots. You only want to show time slots where the title is NOT “Booked”. The slots can be displayed in a table as follows:

<table class="table table-bordered">
<thead>
<tr>
<th>Slots In The User's Time Zone</th>
<th>Price</th>
<th>Service Provider</th>
<th>Booking Button</th>
</tr>
</thead>
<tbody>
<% @schedules.each do |schedule| %>
<% unless schedule.title == 'Booked' %>
<tr>
<td><%= local_time(schedule.start, format: '%e %b, %Y - %H:%M %P') %></td>
<td>$<%= @lesson.cost %> USD</td>
<td><%= schedule.trainer.name %></td>
<td><%= b.radio_button :schedule_id, schedule.id, required: true %> <i class="fa fa-calendar-plus-o"></i> Book This!</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>

The SECOND STEP collects typical user data needed to create the user account. If you have any booking restrictions, this is a great place to put them. For example, the client can not book a lesson without an assessment code. A valid assessment code has to be entered during this step or the booking will not continue. It is also critical to collect time zone data during this step as well.This time zone data is what we will use in the user admin panel to display the correct times for their time zone.

This form is a combination of the USER form and the CLIENT profile form and needs to start as follows:

<%= f.fields_for [@user, @client] do |c| %>

NOTE: Be careful with this nested form because some of your fields will need to start with “c.input” for the client profile fields and the other fields need to start with “f.input” for the user fields.

The FINAL STEP of this booking process is payment processing. I use Stripe to process payments and all it takes is the Stripe gem, a simple credit card form and a little bit of javascript.

Follow the instructions for how to setup the Stripe gem on github. You will need to add the Stripe javascript links to both your frontend and backend layout files since we will also cover allowing users to book lessons from the user admin panel once logged in. Also use the following resource to help you with the javascript for the form:

You will need to decide if you will add any Stripe fields to an existing model or create a new model for it. You don’t need to save much data but you might want to save the customer ID and/or the Payment ID.

The form is plain HTML. I didn’t need or use any rails form code for it. It does need to be within the top-level form however. Make sure you check the Stripe links provided to figure out how to markup the credit card form for submission.

Once the client submits the booking form, they will be immediately taken to the user admin dashboard where they will see a calendar with their booking listed.

That’s it! We went through all of that rigmarole and it all comes down to one controller and one view. Can you believe it?!

The only controller we’ll need is /app/controllers/users_controller.rb and the only view we are going to use for the actual lesson booking is the /app/views/users/new.html.rb view. We do have a booking button on our lesson description pages but you could very easily just add the lessons as a drop down in the /app/views/users/new.html.rb view.

In our case, it made sense to display the details of the lesson in its own SHOW view so viewers can know what they will learn in the lesson and then click a button if they want to book it. The button would then pass on the lesson ID we will save to our lesson_id field in our booking object.

BACKEND BOOKING:

I won’t do a detailed write-up on the backend booking because how you handle the backend setup is specific to your application. BUT once you have the frontend booking feature setup, you only have to adjust it a bit for the backend. For example, you can just use the BOOKING controller on the backend because you aren’t creating a new account, user or client profile. You are only just creating a one-off booking.

You do need to setup backend booking however, because the frontend booking is only for allowing users to book their very first lesson. For the backend, you just need to setup a list of lessons, each with a display of available time slots (these time slots are global and apply to all lessons) and a button to book that particular lesson. When clicking the button navigates to the BOOKING/NEW template, it can be setup exactly like the frontend for picking a time. Since you already have the user data, you don’t need a step to collect it and you may add a final step for credit card data depending on how you are charging in the backend.

LIMITATIONS:

Surely, you can tell right away that displaying multiple trainers and schedules will become unruly. I’m already working on a new way to handle this issue for this particular use case. However, it’s still important to point out that this system would be perfect for a single service provider (i.e. Psychologist, Life or Career Coach, Music or Yoga Instructor, etc.) For a single service provider you would need to limit the amount of days or weeks shown during booking or again, it would become unruly.

I’m in the process of implementing this same system for a single full-time travel coach (me) over at https://bounceplan.com.

FINAL NOTE:

We would be here all month if I included every little thing in this article. Namespace your controllers, views and routes anywhere separation is needed. Only include the controller actions, views and routes that you actually need. You will also want to address any issues you may have with timezones. If you are only dealing with clients in your own timezone, then you can use the system as is. However, if you are dealing with any clients in a different timezone, you will want to display the booking times in their timezone on the frontend and the backend.

Rails version: 5.1.3

Have you built a booking system with Rails? How? Let me know in the comments, chat with me on Twitter, LinkedIn or if you are looking to improve your Technical or Business English, subscribe to my newsletter for video mini-courses!

--

--

Tressa Sanders
Tressa Sanders

Written by Tressa Sanders

Technical Writer, Senior English Trainer and Ruby on Rails Developer with over 20 years working in the Information Technology industry.

Responses (1)