Workflow Automation with Ruby and Rails
A workflow defines how your service providers (agents, hosts, customer service reps, and administrators) interact with your customers.
This tutorial builds a Ruby on Rails web app for finding and booking vacation properties, called Airtng.
Here's how it'll work:
- A host creates a vacation property listing
- A guest requests a reservation for a property
- The host receives an SMS notifying them of the reservation request. The host can either Accept or Reject the reservation
- The guest is notified whether a request was rejected or accepted
This app uses the Twilio REST API to send users messages at important junctures, such as sending an SMS when a reservation request arrives.
config/routes.rb
1Rails.application.routes.draw do23get "login/", to: "sessions#login", as: 'login'4get "logout/", to: "sessions#logout"5post "login_attempt/", to: "sessions#login_attempt"67resources :users, only: [:new, :create, :show]89resources :vacation_properties, path: "/properties"10resources :reservations, only: [:new, :create]11post "reservations/incoming", to: 'reservations#accept_or_reject', as: 'incoming'1213# Home page14root 'main#index', as: 'home'1516end
The VacationProperty model belongs to the User who created it (the host) 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:
1bin/rails generate scaffold VacationProperty2
The Rails generator creates all of the routes, controllers, and views, giving you a fully functional CRUD interface out of the box.
app/models/vacation_property.rb
1class VacationProperty < ActiveRecord::Base2belongs_to :user # host3has_many :reservations4has_many :users, through: :reservations #guests5end
The Reservation model is at the center of the workflow for this application. It keeps track of:
- the
VacationPropertyit is associated with - the
Userwho owns that vacation property (the host) - the guest name and phone number
app/models/reservation.rb
1class Reservation < ActiveRecord::Base2validates :name, presence: true3validates :phone_number, presence: true45enum status: [ :pending, :confirmed, :rejected ]67belongs_to :vacation_property8belongs_to :user910def notify_host(force = true)11@host = User.find(self.vacation_property[:user_id])1213# Don't send the message if we have more than one or we aren't being forced14if @host.pending_reservations.length > 1 or !force15return16else17message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:1819'#{self.message}'2021Reply [accept] or [reject]."2223@host.send_message_via_sms(message)24end25end2627def confirm!28self.status = "confirmed"29self.save!30end3132def reject!33self.status = "rejected"34self.save!35end3637def notify_guest38@guest = User.find_by(phone_number: self.phone_number)3940if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")41message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."42@guest.send_message_via_sms(message)43end44end45end
Since the reservation can only have one guest in this example, the model assigns a name and phone_number directly.
First, validate some key properties and define the associations so that you can later look up those relationships through the model. (For more context, the Rails guide explains models and associations.)
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 an integer in the table, while giving each status a searchable name. Here is an example:
1# reservation.pending! status: 02reservation.status = "confirmed"3reservation.confirmed? # => true4
app/models/reservation.rb
1class Reservation < ActiveRecord::Base2validates :name, presence: true3validates :phone_number, presence: true45enum status: [ :pending, :confirmed, :rejected ]67belongs_to :vacation_property8belongs_to :user910def notify_host(force = true)11@host = User.find(self.vacation_property[:user_id])1213# Don't send the message if we have more than one or we aren't being forced14if @host.pending_reservations.length > 1 or !force15return16else17message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:1819'#{self.message}'2021Reply [accept] or [reject]."2223@host.send_message_via_sms(message)24end25end2627def confirm!28self.status = "confirmed"29self.save!30end3132def reject!33self.status = "rejected"34self.save!35end3637def notify_guest38@guest = User.find_by(phone_number: self.phone_number)3940if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")41message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."42@guest.send_message_via_sms(message)43end44end45end
The reservations controller posts reservation details to the create route from the vacation property page.
After creating the reservation, the controller notifies the host that a request is pending. After the host accepts or rejects it, the controller notifies the guest of the result.
While the Reservation model handles the notification, keep these actions in the controller to make the intent clear.
Create a new reservation
1class ReservationsController < ApplicationController23# GET /vacation_properties/new4def new5@reservation = Reservation.new6end78def create9@vacation_property = VacationProperty.find(params[:reservation][:property_id])10@reservation = @vacation_property.reservations.create(reservation_params)1112if @reservation.save13flash[:notice] = "Sending your reservation request now."14@reservation.notify_host15redirect_to @vacation_property16else17flast[:danger] = @reservation.errors18end19end2021# webhook for twilio incoming message from host22def accept_or_reject23incoming = Sanitize.clean(params[:From]).gsub(/^\+\d/, '')24sms_input = params[:Body].downcase25begin26@host = User.find_by(phone_number: incoming)27@reservation = @host.pending_reservation2829if sms_input == "accept" || sms_input == "yes"30@reservation.confirm!31else32@reservation.reject!33end3435@host.check_for_reservations_pending3637sms_reponse = "You have successfully #{@reservation.status} the reservation."38respond(sms_reponse)39rescue40sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."41respond(sms_reponse)42end43end4445private46# Send an SMS back to the Subscriber47def respond(message)48response = Twilio::TwiML::Response.new do |r|49r.Message message50end51render text: response.text52end5354# Never trust parameters from the scary internet, only allow the white list through.55def reservation_params56params.require(:reservation).permit(:name, :phone_number, :message)57end5859end
To notify the host, look them up and send them an SMS message. To ensure hosts respond to the correct reservation inquiry and don't get spammed, the app handles pending reservations carefully.
The app solves both problems as follows:
- We only notify the host of the oldest pending reservation.
- We don't send another SMS until the host has dealt with the last reservation.
To do this, create a helper method on the User model to surface the pending_reservations method on a user.
If the host has only one pending reservation, the app sends an SMS to the host immediately.
app/models/reservation.rb
1class Reservation < ActiveRecord::Base2validates :name, presence: true3validates :phone_number, presence: true45enum status: [ :pending, :confirmed, :rejected ]67belongs_to :vacation_property8belongs_to :user910def notify_host(force = true)11@host = User.find(self.vacation_property[:user_id])1213# Don't send the message if we have more than one or we aren't being forced14if @host.pending_reservations.length > 1 or !force15return16else17message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:1819'#{self.message}'2021Reply [accept] or [reject]."2223@host.send_message_via_sms(message)24end25end2627def confirm!28self.status = "confirmed"29self.save!30end3132def reject!33self.status = "rejected"34self.save!35end3637def notify_guest38@guest = User.find_by(phone_number: self.phone_number)3940if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")41message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."42@guest.send_message_via_sms(message)43end44end45end
A single User model represents both the guests and the hosts who use Airtng.
First, validate the uniqueness of the user. The phone_number attribute must be unique because the app uses it to look up User records on incoming SMS messages.
Then, set up the associations for querying reservations.
app/models/user.rb
1class User < ActiveRecord::Base2has_secure_password34validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true5validates :name, presence: true6validates :country_code, presence: true7validates :phone_number, presence: true, uniqueness: true8validates_length_of :password, in: 6..20, on: :create910has_many :vacation_properties11has_many :reservations, through: :vacation_properties1213def send_message_via_sms(message)14@app_number = ENV['TWILIO_NUMBER']15@client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']16phone_number = "+#{country_code}#{self.phone_number}"17sms_message = @client.account.messages.create(18from: @app_number,19to: phone_number,20body: message,21)22end2324def check_for_reservations_pending25if pending_reservation26pending_reservation.notify_host(true)27end28end2930def pending_reservation31self.reservations.where(status: "pending").first32end3334def pending_reservations35self.reservations.where(status: "pending")36end3738end
Because the app only sends text messages when communicating with specific users, the send_message_via_sms method lives on the User class. These seven lines are all you need to send an SMS with Ruby and Twilio, in two steps:
- Look up the app's phone number.
- Initialize the Twilio client and build the message.
Whenever the app needs to communicate with a user, whether host or guest, it passes a message to this method to send a text.
The helper methods for finding pending_reservations appear below this method.
app/models/user.rb
1class User < ActiveRecord::Base2has_secure_password34validates :email, presence: true, format: { with: /\A.+@.+$\Z/ }, uniqueness: true5validates :name, presence: true6validates :country_code, presence: true7validates :phone_number, presence: true, uniqueness: true8validates_length_of :password, in: 6..20, on: :create910has_many :vacation_properties11has_many :reservations, through: :vacation_properties1213def send_message_via_sms(message)14@app_number = ENV['TWILIO_NUMBER']15@client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']16phone_number = "+#{country_code}#{self.phone_number}"17sms_message = @client.account.messages.create(18from: @app_number,19to: phone_number,20body: message,21)22end2324def check_for_reservations_pending25if pending_reservation26pending_reservation.notify_host(true)27end28end2930def pending_reservation31self.reservations.where(status: "pending").first32end3334def pending_reservations35self.reservations.where(status: "pending")36end3738end
The accept_or_reject controller handles the incoming Twilio request and does three things, so the host can accept or reject a request by SMS:
- Check for a pending reservation the user owns
- Update the status of the reservation
- Respond to the host (and guest)
An incoming request from Twilio includes parameters such as the From phone number and the message Body.
Use the From parameter to look up the host and check for pending reservations. If a pending reservation exists, use the message body to check whether the host accepted or rejected it.
Then, return a TwiML response to send a message back to the user.
A Rails controller usually has a template that renders a webpage. Here, the only request comes from Twilio's API, so no public page is needed. Instead, the controller uses Twilio's Ruby library to render a custom TwiML response as raw XML.
app/controllers/reservations_controller.rb
1class ReservationsController < ApplicationController23# GET /vacation_properties/new4def new5@reservation = Reservation.new6end78def create9@vacation_property = VacationProperty.find(params[:reservation][:property_id])10@reservation = @vacation_property.reservations.create(reservation_params)1112if @reservation.save13flash[:notice] = "Sending your reservation request now."14@reservation.notify_host15redirect_to @vacation_property16else17flast[:danger] = @reservation.errors18end19end2021# webhook for twilio incoming message from host22def accept_or_reject23incoming = Sanitize.clean(params[:From]).gsub(/^\+\d/, '')24sms_input = params[:Body].downcase25begin26@host = User.find_by(phone_number: incoming)27@reservation = @host.pending_reservation2829if sms_input == "accept" || sms_input == "yes"30@reservation.confirm!31else32@reservation.reject!33end3435@host.check_for_reservations_pending3637sms_reponse = "You have successfully #{@reservation.status} the reservation."38respond(sms_reponse)39rescue40sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."41respond(sms_reponse)42end43end4445private46# Send an SMS back to the Subscriber47def respond(message)48response = Twilio::TwiML::Response.new do |r|49r.Message message50end51render text: response.text52end5354# Never trust parameters from the scary internet, only allow the white list through.55def reservation_params56params.require(:reservation).permit(:name, :phone_number, :message)57end5859end
The final step in the workflow notifies the guest that their reservation was booked or rejected.
The reservations_controller calls this method when it updates the reservation status. The method does the following:
- Looks up the guest with the
reservation.phone_number. - If the status changed to an expected result, notifies the guest of the change.
To send the SMS message to the guest, the method calls the send_message_via_sms method that is present on all users.
app/models/reservation.rb
1class Reservation < ActiveRecord::Base2validates :name, presence: true3validates :phone_number, presence: true45enum status: [ :pending, :confirmed, :rejected ]67belongs_to :vacation_property8belongs_to :user910def notify_host(force = true)11@host = User.find(self.vacation_property[:user_id])1213# Don't send the message if we have more than one or we aren't being forced14if @host.pending_reservations.length > 1 or !force15return16else17message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:1819'#{self.message}'2021Reply [accept] or [reject]."2223@host.send_message_via_sms(message)24end25end2627def confirm!28self.status = "confirmed"29self.save!30end3132def reject!33self.status = "rejected"34self.save!35end3637def notify_guest38@guest = User.find_by(phone_number: self.phone_number)3940if self.status_changed? && (self.status == "confirmed" || self.status == "rejected")41message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."42@guest.send_message_via_sms(message)43end44end45end
Airtng now has an SMS-based workflow in place, and you're ready to add a workflow to your own application.
Here are other tutorials you might pursue with Ruby, Rails, and Twilio:
Protect your users' privacy by anonymously connecting them with Twilio Voice and SMS.
Collect instant feedback from your customers with SMS or Voice.