One of the more abstract concepts you'll handle when building your business is what the workflow will look like.
At its core, setting up a standardized workflow is about enabling your service providers (agents, hosts, customer service reps, administrators, and the rest of the gang) to better serve your customers.
To illustrate a very real-world example, today we'll build a Ruby on Rails webapp for finding and booking vacation properties — tentatively called Airtng.
Here's how it'll work:
We'll be using the Twilio REST API to send our users messages at important junctures. Here's a bit more on our API:
config/routes.rb
_16Rails.application.routes.draw do_16_16 get "login/", to: "sessions#login", as: 'login'_16 get "logout/", to: "sessions#logout"_16 post "login_attempt/", to: "sessions#login_attempt"_16_16 resources :users, only: [:new, :create, :show]_16_16 resources :vacation_properties, path: "/properties"_16 resources :reservations, only: [:new, :create]_16 post "reservations/incoming", to: 'reservations#accept_or_reject', as: 'incoming'_16_16 # Home page_16 root 'main#index', as: 'home'_16_16end
The VacationProperty
model belongs to the User
who created it (we'll call this user the host moving forward) and contains only two properties, a description
and an image_url
.
It has two associations in that it has many reservations and therefore many users through those reservations.
The best way to generate the model and all of the basic CRUD scaffolding we'll need is to use the Rails command line tool:
_10bin/rails generate scaffold VacationProperty_10description:string image_url:string
One of the benefits of using the Rails generator is that it creates all of our routes, controllers and views so that we have a fully functional CRUD interface out of the box. Nifty!
app/models/vacation_property.rb
_10class VacationProperty < ActiveRecord::Base_10 belongs_to :user # host_10 has_many :reservations_10 has_many :users, through: :reservations #guests_10end
Let's jump into the stew and look next at the Reservation mode.
The Reservation
model is at the center of the workflow for this application. It is responsible for keeping track of:
VacationProperty
it is associated with
User
who owns that vacation property (the
host
)
app/models/reservation.rb
_45class Reservation < ActiveRecord::Base_45 validates :name, presence: true_45 validates :phone_number, presence: true_45_45 enum status: [ :pending, :confirmed, :rejected ]_45_45 belongs_to :vacation_property_45 belongs_to :user_45_45 def notify_host(force = true)_45 @host = User.find(self.vacation_property[:user_id])_45_45 # Don't send the message if we have more than one or we aren't being forced_45 if @host.pending_reservations.length > 1 or !force_45 return_45 else_45 message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:_45_45 '#{self.message}'_45_45 Reply [accept] or [reject]."_45_45 @host.send_message_via_sms(message)_45 end_45 end_45_45 def confirm!_45 self.status = "confirmed"_45 self.save!_45 end_45_45 def reject!_45 self.status = "rejected"_45 self.save!_45 end_45_45 def notify_guest_45 @guest = User.find_by(phone_number: self.phone_number)_45_45 if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")_45 message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."_45 @guest.send_message_via_sms(message)_45 end_45 end_45end
Since the reservation can only have one guest for our example we simplified the model by assigning a name
and phone_number
directly.
We'll cover how we did this later. Next, however, we'll zoom in on the reservation status.
First we validate some key properties and define the associations so that we can later lookup those relationships through the model. (If you'd like more context, the Rails guide explains models and associations quite well.)
The main property we need to enable a reservation workflow is some sort of status
that we can monitor. This is a perfect candidate for an enumerated status
attribute.
Enumerated attributes allow us to store a simple integer in the table, while giving each status a searchable name. Here is an example:
_10# reservation.pending! status: 0_10reservation.status = "confirmed"_10reservation.confirmed? # => true
app/models/reservation.rb
_45class Reservation < ActiveRecord::Base_45 validates :name, presence: true_45 validates :phone_number, presence: true_45_45 enum status: [ :pending, :confirmed, :rejected ]_45_45 belongs_to :vacation_property_45 belongs_to :user_45_45 def notify_host(force = true)_45 @host = User.find(self.vacation_property[:user_id])_45_45 # Don't send the message if we have more than one or we aren't being forced_45 if @host.pending_reservations.length > 1 or !force_45 return_45 else_45 message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:_45_45 '#{self.message}'_45_45 Reply [accept] or [reject]."_45_45 @host.send_message_via_sms(message)_45 end_45 end_45_45 def confirm!_45 self.status = "confirmed"_45 self.save!_45 end_45_45 def reject!_45 self.status = "rejected"_45 self.save!_45 end_45_45 def notify_guest_45 @guest = User.find_by(phone_number: self.phone_number)_45_45 if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")_45 message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."_45 @guest.send_message_via_sms(message)_45 end_45 end_45end
Once we have an attribute that can trigger our workflow events, it's time to write some callbacks. Let's look there next.
We'll be posting our reservation details to the create
route from the vacation property page.
After we create the reservation we want to notify the host that she has a request pending. After she accepts or rejects it we want to notify the guest of the news.
While the Reservation
model handles the notification, we want to keep all these actions in the controller to show our intentions.
Create a new reservation
_59class ReservationsController < ApplicationController_59_59 # GET /vacation_properties/new_59 def new_59 @reservation = Reservation.new_59 end_59_59 def create_59 @vacation_property = VacationProperty.find(params[:reservation][:property_id])_59 @reservation = @vacation_property.reservations.create(reservation_params)_59_59 if @reservation.save_59 flash[:notice] = "Sending your reservation request now."_59 @reservation.notify_host_59 redirect_to @vacation_property_59 else_59 flast[:danger] = @reservation.errors_59 end_59 end_59_59 # webhook for twilio incoming message from host_59 def accept_or_reject_59 incoming = Sanitize.clean(params[:From]).gsub(/^\+\d/, '')_59 sms_input = params[:Body].downcase_59 begin_59 @host = User.find_by(phone_number: incoming)_59 @reservation = @host.pending_reservation_59_59 if sms_input == "accept" || sms_input == "yes"_59 @reservation.confirm!_59 else_59 @reservation.reject!_59 end_59_59 @host.check_for_reservations_pending_59_59 sms_reponse = "You have successfully #{@reservation.status} the reservation."_59 respond(sms_reponse)_59 rescue_59 sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."_59 respond(sms_reponse)_59 end_59 end_59_59 private_59 # Send an SMS back to the Subscriber_59 def respond(message)_59 response = Twilio::TwiML::Response.new do |r|_59 r.Message message_59 end_59 render text: response.text_59 end_59_59 # Never trust parameters from the scary internet, only allow the white list through._59 def reservation_params_59 params.require(:reservation).permit(:name, :phone_number, :message)_59 end_59_59end
Next up, let's take a look at how exactly we'll notify the lucky host.
In theory, notifying the host should be as simple as looking up the host User
and send her an SMS. However:
How do we ensure our hosts are: a) responding to the correct reservation inquiry and b) not getting spammed?
Our simple solution to both problems:
The easiest way to surface pending_reservations
for a user is to create a helper method on the User
model. We'll go over that in the next step.
If in fact the host only has one pending reservation, we're going to fire an SMS off to the host immediately.
app/models/reservation.rb
_45class Reservation < ActiveRecord::Base_45 validates :name, presence: true_45 validates :phone_number, presence: true_45_45 enum status: [ :pending, :confirmed, :rejected ]_45_45 belongs_to :vacation_property_45 belongs_to :user_45_45 def notify_host(force = true)_45 @host = User.find(self.vacation_property[:user_id])_45_45 # Don't send the message if we have more than one or we aren't being forced_45 if @host.pending_reservations.length > 1 or !force_45 return_45 else_45 message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:_45_45 '#{self.message}'_45_45 Reply [accept] or [reject]."_45_45 @host.send_message_via_sms(message)_45 end_45 end_45_45 def confirm!_45 self.status = "confirmed"_45 self.save!_45 end_45_45 def reject!_45 self.status = "rejected"_45 self.save!_45 end_45_45 def notify_guest_45 @guest = User.find_by(phone_number: self.phone_number)_45_45 if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")_45 message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."_45 @guest.send_message_via_sms(message)_45 end_45 end_45end
Let's now take a look at the User
model.
We have one model for both the guests and the hosts who are using Airtng.
When Airtng takes off it will merit creating two more classes that inherit from the base User
class. Since we're still on the ground this should suit us fine (to boldly stay...).
First, we validate the 'uniqueness' of our user - they should be unique, just like everyone else. Specifically, it is important we ensure that the phone_number
attribute is unique since we will use this to look up User
records on incoming SMSes.
After that, we set up our associations for when we need to query for reservations.
app/models/user.rb
_38class User < ActiveRecord::Base_38 has_secure_password_38_38 validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true_38 validates :name, presence: true_38 validates :country_code, presence: true_38 validates :phone_number, presence: true, uniqueness: true_38 validates_length_of :password, in: 6..20, on: :create_38_38 has_many :vacation_properties_38 has_many :reservations, through: :vacation_properties_38_38 def send_message_via_sms(message)_38 @app_number = ENV['TWILIO_NUMBER']_38 @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']_38 phone_number = "+#{country_code}#{self.phone_number}"_38 sms_message = @client.account.messages.create(_38 from: @app_number,_38 to: phone_number,_38 body: message,_38 )_38 end_38_38 def check_for_reservations_pending_38 if pending_reservation_38 pending_reservation.notify_host(true)_38 end_38 end_38_38 def pending_reservation_38 self.reservations.where(status: "pending").first_38 end_38_38 def pending_reservations_38 self.reservations.where(status: "pending")_38 end_38_38end
Arguably the most important task delegated to our User
model is to send an SMS to the user when our app requests it, let's take a look.
Since we only send text messages in our application when we're communicating with specific users, it makes sense to create this function on the User
class. And yes: these 7 lines are all you need to send SMSes with Ruby and Twilio! It's really just two steps:
Now whenever we need to communicate with a user, whether host or guest, we can pass a message to this user method and... Voilà! We've sent them a text.
(If you peek below this method you'll see the helper methods for finding pending_reservations
that we mentioned previously.)
app/models/user.rb
_38class User < ActiveRecord::Base_38 has_secure_password_38_38 validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true_38 validates :name, presence: true_38 validates :country_code, presence: true_38 validates :phone_number, presence: true, uniqueness: true_38 validates_length_of :password, in: 6..20, on: :create_38_38 has_many :vacation_properties_38 has_many :reservations, through: :vacation_properties_38_38 def send_message_via_sms(message)_38 @app_number = ENV['TWILIO_NUMBER']_38 @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']_38 phone_number = "+#{country_code}#{self.phone_number}"_38 sms_message = @client.account.messages.create(_38 from: @app_number,_38 to: phone_number,_38 body: message,_38 )_38 end_38_38 def check_for_reservations_pending_38 if pending_reservation_38 pending_reservation.notify_host(true)_38 end_38 end_38_38 def pending_reservation_38 self.reservations.where(status: "pending").first_38 end_38_38 def pending_reservations_38 self.reservations.where(status: "pending")_38 end_38_38end
Now we need a way to handle incoming texts so our lucky host can accept or reject a request. Let's look there next.
The accept_or_reject
controller handles our incoming Twilio request and does three things:
An incoming request from Twilio comes with some helpful parameters including the From
phone number and the message Body
.
We'll use the From
parameter to lookup the host and check if she has any pending reservations. If she does, we'll use the message body to check if she accepted or rejected the reservation.
Then we'll redirect the request to a TwiML response to send a message back to the user.
Usually a Rails controller has a template associated with it that renders a webpage. In our case, the only request being made will be by Twilio's API so we don't need a public page. Instead we're using Twilio's Ruby API to render a custom TwiML response as raw XML on the page.
app/controllers/reservations_controller.rb
_59class ReservationsController < ApplicationController_59_59 # GET /vacation_properties/new_59 def new_59 @reservation = Reservation.new_59 end_59_59 def create_59 @vacation_property = VacationProperty.find(params[:reservation][:property_id])_59 @reservation = @vacation_property.reservations.create(reservation_params)_59_59 if @reservation.save_59 flash[:notice] = "Sending your reservation request now."_59 @reservation.notify_host_59 redirect_to @vacation_property_59 else_59 flast[:danger] = @reservation.errors_59 end_59 end_59_59 # webhook for twilio incoming message from host_59 def accept_or_reject_59 incoming = Sanitize.clean(params[:From]).gsub(/^\+\d/, '')_59 sms_input = params[:Body].downcase_59 begin_59 @host = User.find_by(phone_number: incoming)_59 @reservation = @host.pending_reservation_59_59 if sms_input == "accept" || sms_input == "yes"_59 @reservation.confirm!_59 else_59 @reservation.reject!_59 end_59_59 @host.check_for_reservations_pending_59_59 sms_reponse = "You have successfully #{@reservation.status} the reservation."_59 respond(sms_reponse)_59 rescue_59 sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."_59 respond(sms_reponse)_59 end_59 end_59_59 private_59 # Send an SMS back to the Subscriber_59 def respond(message)_59 response = Twilio::TwiML::Response.new do |r|_59 r.Message message_59 end_59 render text: response.text_59 end_59_59 # Never trust parameters from the scary internet, only allow the white list through._59 def reservation_params_59 params.require(:reservation).permit(:name, :phone_number, :message)_59 end_59_59end
Next up, let's see how to notify the guest.
The final step in our workflow is to notify the guest that their reservation has been booked (or, ahem, rejected).
We called this method earlier from the reservations_controller
when we updated the reservation status. Here's what it does:
reservation.phone_number
And of course all we need to do to send the SMS message to the guest is call the send_message_via_sms
method that is present on all users.
app/models/reservation.rb
_45class Reservation < ActiveRecord::Base_45 validates :name, presence: true_45 validates :phone_number, presence: true_45_45 enum status: [ :pending, :confirmed, :rejected ]_45_45 belongs_to :vacation_property_45 belongs_to :user_45_45 def notify_host(force = true)_45 @host = User.find(self.vacation_property[:user_id])_45_45 # Don't send the message if we have more than one or we aren't being forced_45 if @host.pending_reservations.length > 1 or !force_45 return_45 else_45 message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:_45_45 '#{self.message}'_45_45 Reply [accept] or [reject]."_45_45 @host.send_message_via_sms(message)_45 end_45 end_45_45 def confirm!_45 self.status = "confirmed"_45 self.save!_45 end_45_45 def reject!_45 self.status = "rejected"_45 self.save!_45 end_45_45 def notify_guest_45 @guest = User.find_by(phone_number: self.phone_number)_45_45 if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")_45 message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."_45 @guest.send_message_via_sms(message)_45 end_45 end_45end
Thank you so much for your help! Airtng now has a nice SMS based workflow in place and you're ready to add a workflow to your own application.
Let's look at some other features you might enjoy adding for your use cases.
Ruby and Rails and Twilio: what an excellent combo. Here are a couple other ideas you might pursue:
Protect your users' privacy by anonymously connecting them with Twilio Voice and SMS.
Collect instant feedback from your customers with SMS or Voice.
Thanks for checking this tutorial out! Tweet to us @twilio with what you're building!