Appointment Reminders with Ruby and Rails

Ready to implement appointment reminders in your application? Here's how it works at a high level:

  1. An administrator creates an appointment for a future date and time, and stores a customer's phone number in the database for that appointment
  2. A background process checks the database on a regular interval, looking for appointments that require a reminder to be sent out
  3. At a configured time in advance of the appointment, an SMS reminder is sent out to the customer to remind them of their appointment

Building Blocks

Here are the technologies we'll use to get this done:

How To Read This Tutorial

To implement appointment reminders, we will be working through a series of user stories that describe how to fully implement appointment reminders in a web application. We'll walk through the code required to satisfy each story, and explore what we needed to add at each step.

All this can be done with the help of Twilio in under half an hour.

Creating an Appointment

As a user, I want to create an appointment with a name, guest phone numbers, and a time in the future.

In order to build an automated appointment reminder app, we probably should start with an appointment. This story requires that we create a bit of UI and a model object to create and save a new Appointment in our system. At a high level, here's what we will need to add:

  • A form to enter details about the appointment
  • A route and controller function on the server to render the form
  • A route and controller function on the server to handle the form POST request
  • A persistent Appointment model object to store information about the user

Let's start by looking at the model, where we decide what information we want to store with the appointment.

The Rails Generator

Usually at this point in the tutorial we would build our model, view and controller from scratch (see account verification as an example). But since the appointment model is so straight-forward, and we really just want the basic CRUD scaffolding, we're going to use the Rails generator for once.

A Note about Tools

In this app we're using Rails 4, but it will be very similar for 3 and below. We will also be using the twilio-ruby helper library. Lastly we use bootstrap to simplify design, and in this case there is a gem that will generate bootstrap-themed views called twitter-bootstrap-rails. Please check out these tools when you have a chance, now let's move on to generating our scaffolding.

Generate a Model, View and Controller

Rails generate is a command-line tool that generates rails components like models, views, tests and more. For our purposes we are going to use the big kahuna generator, scaffold to generate everything at once.

Here's how we did it. From inside our rails app, we ran:

$ bin/rails generate scaffold Appointment name:string phone_number:string time:datetime

This tells our generator to create the 'scaffolding' for a resource called Appointment, which has the properties name, phone_number and time.

Now let's go to the model that was generated and add some stuff to it.

Appointment Model

The appointment model is pretty simple out of the box, but since humans will be interacting with it let's make sure we add some data validation.

Data Validation

Validations are important since we want to make sure only accurate data is being saved into our database. In this case, we only want to validate that all of our required fields are present. We can do this by creating a validates statement with presence: true.

It is likely that our Appointment Model would be created by an admin person at the site of the appointment. Well it would be great if we could give our admin user some feedback when they create the appointment. Luckily in Rails if we add validations to our models we get error reporting for free with the session's flash object.

One note: in order to run this demo you would need to run rake db:migrate which would run the migrations in our db/migrate folder. For this tutorial we're gonna focus on the core concepts but if you want to learn more about migrations you can read the Rails guide on the subject.

Loading Code Samples...
Language
class Appointment < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true
  validates :time, presence: true

  after_create :reminder

  @@REMINDER_TIME = 30.minutes # minutes before appointment

  # Notify our appointment attendee X minutes before the appointment time
  def reminder
    @twilio_number = ENV['TWILIO_NUMBER']
    @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
    time_str = ((self.time).localtime).strftime("%I:%M%p on %b. %d, %Y")
    reminder = "Hi #{self.name}. Just a reminder that you have an appointment coming up at #{time_str}."
    message = @client.account.messages.create(
      :from => @twilio_number,
      :to => self.phone_number,
      :body => reminder,
    )
    puts message.to
  end

  def when_to_run
    time - @@REMINDER_TIME
  end

  handle_asynchronously :reminder, :run_at => Proc.new { |i| i.when_to_run }
end
app/models/appointment.rb
Appointment Model

app/models/appointment.rb

Now we're ready to move up to the controller level of the application, starting with the HTTP request routes we'll need.

Routes

In a Rails application, Resource Routing automatically maps a resource's CRUD capabilities to its controller. Since our Appointment is an ActiveRecord resource, we can simply tell Rails that we want to use these routes, which will save us some lines of code.

This means that in this one line of code we automatically have an appointment/new route which will automatically render our appointment/new.html.erb file.

Loading Code Samples...
Language
Rails.application.routes.draw do
  resources :appointments
  
  # You can have the root of your site routed with "root"
  root 'appointments#welcome'
end
config/routes.rb
Routes

config/routes.rb

Let's take a look at this form up close.

New Appointment Form

When we create a new appointment, we need a guest name, a phone number and a time. By using the rails form_for tag we can bind the form to the model object. This will generate the necessary html markup that will create a new Appointment on submit.

Loading Code Samples...
Language
<%= form_for @appointment, :html => { :class => "form-horizontal appointment" } do |f| %>

  <% if @appointment.errors.any? %>
    <div id="error_expl" class="panel panel-danger">
      <div class="panel-heading">
        <h3 class="panel-title"><%= pluralize(@appointment.errors.count, "error") %> prohibited this appointment from being saved:</h3>
      </div>
      <div class="panel-body">
        <ul>
        <% @appointment.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
        </ul>
      </div>
    </div>
  <% end %>

  <div class="form-group">
    <%= f.label :name, :class => 'control-label col-lg-2' %>
    <div class="col-lg-10">
      <%= f.text_field :name, :class => 'form-control' %>
    </div>
    <%=f.error_span(:name) %>
  </div>
  <div class="form-group">
    <%= f.label :phone_number, :class => 'control-label col-lg-2' %>
    <div class="col-lg-10">
      <%= f.text_field :phone_number, :class => 'form-control' %>
    </div>
    <%=f.error_span(:phone_number) %>
  </div>
  <div class="form-group">
    <%= f.label :time, "Time and Date", :class => 'control-label col-lg-2' %>
    <!-- Rails expects time_select when dealing with ActiveRecord forms -->
    <div class="col-lg-2">
      <%= time_select :appointment, :time, {:class => "form-control" } %>
    </div>
    <div class="col-lg-4">
      <%= date_select :appointment, :time, {:class => "form-control" } %>
    </div>
    <div class="col-lg-2">
      <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.all.sort, default: "Pacific Time (US & Canada)" %>
    </div>
    <%=f.error_span(:time) %>
  </div>

  <div class="form-group">
    <div class="col-lg-offset-2 col-lg-10">
      <%= f.submit nil, :class => 'btn btn-primary' %>
      <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
                appointments_path, :class => 'btn btn-default' %>
    </div>
  </div>

<% end %>
app/views/appointments/_form.html.erb
Appointment Form

app/views/appointments/_form.html.erb

Let's point out one specific helper tag that Rails gives us for model-bound forms.

Date and Time

One potential time-suck is figuring out how to handle the date and time of the appointment. In reality this is two separate user inputs, one for the day and one for the time of the appointment. We need a way to combine these two separate inputs into one paramater on the server-side. Again Rails handles this by giving us the data_select and time_select tags which the server automatically gathers into one paramater that maps to the appointment.time property.

Loading Code Samples...
Language
<%= form_for @appointment, :html => { :class => "form-horizontal appointment" } do |f| %>

  <% if @appointment.errors.any? %>
    <div id="error_expl" class="panel panel-danger">
      <div class="panel-heading">
        <h3 class="panel-title"><%= pluralize(@appointment.errors.count, "error") %> prohibited this appointment from being saved:</h3>
      </div>
      <div class="panel-body">
        <ul>
        <% @appointment.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
        </ul>
      </div>
    </div>
  <% end %>

  <div class="form-group">
    <%= f.label :name, :class => 'control-label col-lg-2' %>
    <div class="col-lg-10">
      <%= f.text_field :name, :class => 'form-control' %>
    </div>
    <%=f.error_span(:name) %>
  </div>
  <div class="form-group">
    <%= f.label :phone_number, :class => 'control-label col-lg-2' %>
    <div class="col-lg-10">
      <%= f.text_field :phone_number, :class => 'form-control' %>
    </div>
    <%=f.error_span(:phone_number) %>
  </div>
  <div class="form-group">
    <%= f.label :time, "Time and Date", :class => 'control-label col-lg-2' %>
    <!-- Rails expects time_select when dealing with ActiveRecord forms -->
    <div class="col-lg-2">
      <%= time_select :appointment, :time, {:class => "form-control" } %>
    </div>
    <div class="col-lg-4">
      <%= date_select :appointment, :time, {:class => "form-control" } %>
    </div>
    <div class="col-lg-2">
      <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.all.sort, default: "Pacific Time (US & Canada)" %>
    </div>
    <%=f.error_span(:time) %>
  </div>

  <div class="form-group">
    <div class="col-lg-offset-2 col-lg-10">
      <%= f.submit nil, :class => 'btn btn-primary' %>
      <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
                appointments_path, :class => 'btn btn-default' %>
    </div>
  </div>

<% end %>
app/views/appointments/_form.html.erb
Date and Time inputs

app/views/appointments/_form.html.erb

Let's jump back over to the controller to see what happens when we create this appointment.

Handle the Form POST

One of the other handy controllers created by our Appointment resource route was appointment/create which handles the POST from our form.

In our controller we take the input from our form and create a new Appointment model. If the appointment is saved to the database successfully, we redirect to the appointment details view which will show the creator the new appointment and allow them to edit or delete it.

Loading Code Samples...
Language
class AppointmentsController < ApplicationController
  before_action :set_appointment, only: [:show, :edit, :update, :destroy]

  # GET /appointments
  # GET /appointments.json
  def index
    @appointments = Appointment.all
    if @appointments.length == 0
      flash[:alert] = "You have no appointments. Create one now to get started."
    end
  end

  # GET /appointments/1
  # GET /appointments/1.json
  def show
  end

  # GET /appointments/new
  def new
    @appointment = Appointment.new
    @min_date = DateTime.now
  end

  # GET /appointments/1/edit
  def edit
  end

  # POST /appointments
  # POST /appointments.json
  def create
    Time.zone = appointment_params[:time_zone]
    @appointment = Appointment.new(appointment_params)
    
    respond_to do |format|
      if @appointment.save
        format.html { redirect_to @appointment, notice: 'Appointment was successfully created.' }
        format.json { render :show, status: :created, location: @appointment }
      else
        format.html { render :new }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /appointments/1
  # PATCH/PUT /appointments/1.json
  def update
    respond_to do |format|
      if @appointment.update(appointment_params)
        format.html { redirect_to @appointment, notice: 'Appointment was successfully updated.' }
        format.json { render :show, status: :ok, location: @appointment }
      else
        format.html { render :edit }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /appointments/1
  # DELETE /appointments/1.json
  def destroy
    @appointment.destroy
    respond_to do |format|
      format.html { redirect_to appointments_url, notice: 'Appointment was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    # See above ---> before_action :set_appointment, only: [:show, :edit, :update, :destroy]
    def set_appointment
      @appointment = Appointment.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def appointment_params
      params.require(:appointment).permit(:name, :phone_number, :time, :time_zone)
    end
end
app/controllers/appointments_controller.rb
Handle the Form POST, create an Appointment

app/controllers/appointments_controller.rb

Next we're going to take a look at the generated controllers for edit and delete.

Interacting with Appointments

As a user, I want to view a list of all future appointments, and be able to delete those appointments.

If you're an organization that handles a lot of appointments, you probably want to be able to view and manage them in a single interface. That's what we'll tackle in this user story. We'll create a UI to:

  • Show all appointments
  • Delete individual appoinments

Let's start by looking at the controller.

Show a List of Appointments

At the controller level, all we'll do is get a list of all the appointments in the database and rendering them with a view. We should also add a prompt if there aren't any appointments, since this demo relies on there being at least one appointment in the future.

Loading Code Samples...
Language
class AppointmentsController < ApplicationController
  before_action :set_appointment, only: [:show, :edit, :update, :destroy]

  # GET /appointments
  # GET /appointments.json
  def index
    @appointments = Appointment.all
    if @appointments.length == 0
      flash[:alert] = "You have no appointments. Create one now to get started."
    end
  end

  # GET /appointments/1
  # GET /appointments/1.json
  def show
  end

  # GET /appointments/new
  def new
    @appointment = Appointment.new
    @min_date = DateTime.now
  end

  # GET /appointments/1/edit
  def edit
  end

  # POST /appointments
  # POST /appointments.json
  def create
    Time.zone = appointment_params[:time_zone]
    @appointment = Appointment.new(appointment_params)
    
    respond_to do |format|
      if @appointment.save
        format.html { redirect_to @appointment, notice: 'Appointment was successfully created.' }
        format.json { render :show, status: :created, location: @appointment }
      else
        format.html { render :new }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /appointments/1
  # PATCH/PUT /appointments/1.json
  def update
    respond_to do |format|
      if @appointment.update(appointment_params)
        format.html { redirect_to @appointment, notice: 'Appointment was successfully updated.' }
        format.json { render :show, status: :ok, location: @appointment }
      else
        format.html { render :edit }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /appointments/1
  # DELETE /appointments/1.json
  def destroy
    @appointment.destroy
    respond_to do |format|
      format.html { redirect_to appointments_url, notice: 'Appointment was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    # See above ---> before_action :set_appointment, only: [:show, :edit, :update, :destroy]
    def set_appointment
      @appointment = Appointment.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def appointment_params
      params.require(:appointment).permit(:name, :phone_number, :time, :time_zone)
    end
end
app/controllers/appointments_controller.rb
Show a List of Appointments

app/controllers/appointments_controller.rb

Let's go back to the template to see our list of appointments.

View All Appointments

The index view lists all of the appointments which are automatically ordered by date_created. The only thing we need to add to fulfil our user story is a delete button. We'll add the edit button just for kicks.

URL Helpers

You may notice that instead of hard-coding the urls for Edit and Delete we are using a Rails URL helper. If you view the rendered markup you will see these paths:

  • /appointments/ID/edit for edit
  • /appointments/ID for delete, with an HTTP DELETE method appended to the query

These URL helpers can take either an appointment object, or an ID.

There are some other helpers in this code that Rails generates for us. The one I want to point out is the :confirm tag. The confirm tag is a data attribute that interrupts the actual DELETE request with a javascript alert. This is best-practices when calling DELETE on an object. If the user confirms we process the request normally, otherwise no action will be taken.

Loading Code Samples...
Language
<%- model_class = Appointment -%>
<div class="page-header">
  <h1><%=t '.title', :default => model_class.model_name.human.pluralize.titleize %></h1>
</div>
<table class="table table-striped">
  <thead>
    <tr>
      <th><%= model_class.human_attribute_name(:id) %></th>
      <th><%= model_class.human_attribute_name(:name) %></th>
      <th><%= model_class.human_attribute_name(:phone_number) %></th>
      <th><%= model_class.human_attribute_name(:time) %></th>
      <th><%= model_class.human_attribute_name(:created_at) %></th>
      <th><%=t '.actions', :default => t("helpers.actions") %></th>
    </tr>
  </thead>
  <tbody>
    <% @appointments.each do |appointment| %>
      <tr>
        <td><%= link_to appointment.id, appointment_path(appointment) %></td>
        <td><%= appointment.name %></td>
        <td><%= appointment.phone_number %></td>
        <td><%= appointment.time %></td>
        <td><%=l appointment.created_at %></td>
        <td>
          <%= link_to "Edit", 
                      edit_appointment_path(appointment), :class => 'btn btn-default btn-xs' %>
          <%= link_to "Delete",
                      appointment_path(appointment),
                      :method => :delete,
                      :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
                      :class => 'btn btn-xs btn-danger' %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

<%= link_to "New",
            new_appointment_path,
            :class => 'btn btn-primary' %>
app/views/appointments/index.html.erb
View All Appointments

app/views/appointments/index.html.erb

Now let's take a look at what happens in the controller when we ask to delete an appointment.

Look up an Appointment

In this controller we need to pull up an appointment record and then delete it. Let's take a look at how we're grabbing the appointment first.

Since we're probably going to need an appointment record in most of our views, we should just create a private instance method that can be shared across multiple controllers.

In set_appointment we use the id paramater, passed through from the route, to look up the appointment. Then at the top of our controller we use the before_action filter like so:

before_action :set_appointment, only: [:show, :edit, :update, :destroy]

This tells our application which controllers to apply this filter to. In this case we only need an appointment when the controller deals with a single appointment.

Loading Code Samples...
Language
class AppointmentsController < ApplicationController
  before_action :set_appointment, only: [:show, :edit, :update, :destroy]

  # GET /appointments
  # GET /appointments.json
  def index
    @appointments = Appointment.all
    if @appointments.length == 0
      flash[:alert] = "You have no appointments. Create one now to get started."
    end
  end

  # GET /appointments/1
  # GET /appointments/1.json
  def show
  end

  # GET /appointments/new
  def new
    @appointment = Appointment.new
    @min_date = DateTime.now
  end

  # GET /appointments/1/edit
  def edit
  end

  # POST /appointments
  # POST /appointments.json
  def create
    Time.zone = appointment_params[:time_zone]
    @appointment = Appointment.new(appointment_params)
    
    respond_to do |format|
      if @appointment.save
        format.html { redirect_to @appointment, notice: 'Appointment was successfully created.' }
        format.json { render :show, status: :created, location: @appointment }
      else
        format.html { render :new }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /appointments/1
  # PATCH/PUT /appointments/1.json
  def update
    respond_to do |format|
      if @appointment.update(appointment_params)
        format.html { redirect_to @appointment, notice: 'Appointment was successfully updated.' }
        format.json { render :show, status: :ok, location: @appointment }
      else
        format.html { render :edit }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /appointments/1
  # DELETE /appointments/1.json
  def destroy
    @appointment.destroy
    respond_to do |format|
      format.html { redirect_to appointments_url, notice: 'Appointment was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    # See above ---> before_action :set_appointment, only: [:show, :edit, :update, :destroy]
    def set_appointment
      @appointment = Appointment.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def appointment_params
      params.require(:appointment).permit(:name, :phone_number, :time, :time_zone)
    end
end
app/controllers/appointments_controller.rb
Look up an Appointment

app/controllers/appointments_controller.rb

Now let's take a look at what happens when an appointment is actually deleted.

Delete an Appointment

Now that we have the appointment, we simply need to call .destroy on it. In a production app you may want to evaluate whether to use .delete instead of destroy, since both are valid ways to delete a database row in Rails. For our purposes we will use the less-eficient destroy for two reasons:

  1. It handles database clean-up
  2. It keeps the Appointment in memory, so that we can flash a success message to the user
Loading Code Samples...
Language
class AppointmentsController < ApplicationController
  before_action :set_appointment, only: [:show, :edit, :update, :destroy]

  # GET /appointments
  # GET /appointments.json
  def index
    @appointments = Appointment.all
    if @appointments.length == 0
      flash[:alert] = "You have no appointments. Create one now to get started."
    end
  end

  # GET /appointments/1
  # GET /appointments/1.json
  def show
  end

  # GET /appointments/new
  def new
    @appointment = Appointment.new
    @min_date = DateTime.now
  end

  # GET /appointments/1/edit
  def edit
  end

  # POST /appointments
  # POST /appointments.json
  def create
    Time.zone = appointment_params[:time_zone]
    @appointment = Appointment.new(appointment_params)
    
    respond_to do |format|
      if @appointment.save
        format.html { redirect_to @appointment, notice: 'Appointment was successfully created.' }
        format.json { render :show, status: :created, location: @appointment }
      else
        format.html { render :new }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /appointments/1
  # PATCH/PUT /appointments/1.json
  def update
    respond_to do |format|
      if @appointment.update(appointment_params)
        format.html { redirect_to @appointment, notice: 'Appointment was successfully updated.' }
        format.json { render :show, status: :ok, location: @appointment }
      else
        format.html { render :edit }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /appointments/1
  # DELETE /appointments/1.json
  def destroy
    @appointment.destroy
    respond_to do |format|
      format.html { redirect_to appointments_url, notice: 'Appointment was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    # See above ---> before_action :set_appointment, only: [:show, :edit, :update, :destroy]
    def set_appointment
      @appointment = Appointment.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def appointment_params
      params.require(:appointment).permit(:name, :phone_number, :time, :time_zone)
    end
end
app/controllers/appointments_controller.rb
Delete an Appointment

app/controllers/appointments_controller.rb

Now that we can interact with our appointments, let's dive into sending out reminders when one of these appointments is coming up.

Send the Reminder

As an appointment system, I want to notify a user via SMS an arbitrary interval before a future appointment.

There are a lot of ways to build this part of our application, but no matter how you implement it there should be two moving parts:

  • A script that checks the database for any appointment that is upcoming, then sends an sms
  • A worker that runs that script continuously
Loading Code Samples...
Language
class Appointment < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true
  validates :time, presence: true

  after_create :reminder

  @@REMINDER_TIME = 30.minutes # minutes before appointment

  # Notify our appointment attendee X minutes before the appointment time
  def reminder
    @twilio_number = ENV['TWILIO_NUMBER']
    @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
    time_str = ((self.time).localtime).strftime("%I:%M%p on %b. %d, %Y")
    reminder = "Hi #{self.name}. Just a reminder that you have an appointment coming up at #{time_str}."
    message = @client.account.messages.create(
      :from => @twilio_number,
      :to => self.phone_number,
      :body => reminder,
    )
    puts message.to
  end

  def when_to_run
    time - @@REMINDER_TIME
  end

  handle_asynchronously :reminder, :run_at => Proc.new { |i| i.when_to_run }
end
app/models/appointment.rb
Send the Reminder

app/models/appointment.rb

Let's take a look at how we decided to implement the latter with Delayed::Job.

Working with Delayed::Job

As we mentioned before, there are a lot of ways to implement a scheduler/worker, but in Rails Delayed::Job is the most established. 

Delayed Job needs a backend of some kind to queue the upcoming jobs. Here we have added the ActiveRecord adapter for delayed_job, which uses our database to store the 'Jobs' database. There are plenty of backends supported, so use the correct gem for your application.

Once we included the gem, we need to run bundle install and run the rake task to create the database.

rails g delayed_job:active_record

You can see all of these steps in the github repo for this project.

Loading Code Samples...
Language
source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.0'
# Use postgresql as the database for Active Record
gem 'pg'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.1.0'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby

# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc

# Use Twilio
gem 'twilio-ruby'

# Use bootstrap themes
gem 'twitter-bootstrap-rails', :git => 'git://github.com/seyhunak/twitter-bootstrap-rails.git'

# Use delayed job for running background jobs
gem 'delayed_job_active_record'

# Need daemons to start delayed_job
gem 'daemons'

# Use workless to use less workers on heroku
gem "workless", "~> 1.2.2"

# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Unicorn as the app server
# gem 'unicorn'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug'

  # Access an IRB console on exception pages or by using <%= console %> in views
  gem 'web-console', '~> 2.0'

  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
end
The Rails Generator
Gemfile for Appointment Reminders

The Rails Generator

Now we're ready to create the actual job.

Send a Reminder

The next step in sending a reminder to our user is creating the script that we'll fire at some interval before the appointment time. We will end up wanting to schedule this reminder when the appointment is created, so it makes sense to write it as a method on the Appointment model.

The first thing we do is create a Twilio client that will send our SMS via the Twilio REST API. We'll need three things to create the Twilio client:

  • Our Twilio account SID
  • Our Twilio auth token
  • A Twilio number in our account that can send text messages

All of these can be found in your console.

Then all we need to do to send an sms is use the built in messages.create() to send an SMS to the user's phone.

Model Callback

Because we made our reminder script a method on the model we get one very handy tool; a callback. By using the before_create callback we ensure that the :reminder gets called whenever an Appointment is created.

Loading Code Samples...
Language
class Appointment < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true
  validates :time, presence: true

  after_create :reminder

  @@REMINDER_TIME = 30.minutes # minutes before appointment

  # Notify our appointment attendee X minutes before the appointment time
  def reminder
    @twilio_number = ENV['TWILIO_NUMBER']
    @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
    time_str = ((self.time).localtime).strftime("%I:%M%p on %b. %d, %Y")
    reminder = "Hi #{self.name}. Just a reminder that you have an appointment coming up at #{time_str}."
    message = @client.account.messages.create(
      :from => @twilio_number,
      :to => self.phone_number,
      :body => reminder,
    )
    puts message.to
  end

  def when_to_run
    time - @@REMINDER_TIME
  end

  handle_asynchronously :reminder, :run_at => Proc.new { |i| i.when_to_run }
end
app/models/appointment.rb
Send a Reminder

app/models/appointment.rb

The last step is making sure this callback always ends up being scheduled by Delayed Job.

Schedule a Reminder

Well we're almost done, now all we need to do is write an extremely complicated schedule controller that does the following:

  • Look up each future appointment
  • Add it to a Jobs table
  • Check whether it is within the minutes_before_appointment interval
  • Fire the reminder method

Oh wait, Delayed Job does this for free in one handy method called handle_asynchronously which tells Delayed Job to schedule this job whenever this method is fired. Since our job time is dependent on the individual Appointment instance we need to pass the handle_asynchronously method a function that will calculate the time. In this case minutes_before_appointment is set to 30 minutes, but you can use any Time interval here.

Now when we create an appointment we will see a new row in the Jobs table, with a time and a method that needs to be fired. Additionally, delayed_job saves errors, and attempts so we can debug any weirdness before it fails. Once it fires a job it removes it from the database, so on a good day we should see an empty database.

Loading Code Samples...
Language
class Appointment < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true
  validates :time, presence: true

  after_create :reminder

  @@REMINDER_TIME = 30.minutes # minutes before appointment

  # Notify our appointment attendee X minutes before the appointment time
  def reminder
    @twilio_number = ENV['TWILIO_NUMBER']
    @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
    time_str = ((self.time).localtime).strftime("%I:%M%p on %b. %d, %Y")
    reminder = "Hi #{self.name}. Just a reminder that you have an appointment coming up at #{time_str}."
    message = @client.account.messages.create(
      :from => @twilio_number,
      :to => self.phone_number,
      :body => reminder,
    )
    puts message.to
  end

  def when_to_run
    time - @@REMINDER_TIME
  end

  handle_asynchronously :reminder, :run_at => Proc.new { |i| i.when_to_run }
end
app/models/appointment.rb
Schedule a Reminder

app/models/appointment.rb

All Done

Wow, that was quite an undertaking, but in reality we had to write very little code to get automated appointment reminders firing with Twilio.

Where to Next?

And with a little code and a dash of configuration, we're ready to get automated appointment reminders firing in our application. Good work!

If you are a Ruby developer working with Twilio, you might want to check out other tutorials in Ruby:

Click to Call

Put a button on your web page that connects visitors to live support or sales people via telephone.

Two-Factor Authentication

Improve the security of your Ruby app's login functionality by adding two-factor authentication via text message.

Did this help?

Thanks for checking out this tutorial! If you have any feedback to share with us, please reach out on Twitter... we'd love to hear your thoughts, and know what you're building!

Jarod Reyes
David Prothero
Agustin Camino
Jose Oliveros

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
class Appointment < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true
  validates :time, presence: true

  after_create :reminder

  @@REMINDER_TIME = 30.minutes # minutes before appointment

  # Notify our appointment attendee X minutes before the appointment time
  def reminder
    @twilio_number = ENV['TWILIO_NUMBER']
    @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
    time_str = ((self.time).localtime).strftime("%I:%M%p on %b. %d, %Y")
    reminder = "Hi #{self.name}. Just a reminder that you have an appointment coming up at #{time_str}."
    message = @client.account.messages.create(
      :from => @twilio_number,
      :to => self.phone_number,
      :body => reminder,
    )
    puts message.to
  end

  def when_to_run
    time - @@REMINDER_TIME
  end

  handle_asynchronously :reminder, :run_at => Proc.new { |i| i.when_to_run }
end
Rails.application.routes.draw do
  resources :appointments
  
  # You can have the root of your site routed with "root"
  root 'appointments#welcome'
end
<%= form_for @appointment, :html => { :class => "form-horizontal appointment" } do |f| %>

  <% if @appointment.errors.any? %>
    <div id="error_expl" class="panel panel-danger">
      <div class="panel-heading">
        <h3 class="panel-title"><%= pluralize(@appointment.errors.count, "error") %> prohibited this appointment from being saved:</h3>
      </div>
      <div class="panel-body">
        <ul>
        <% @appointment.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
        </ul>
      </div>
    </div>
  <% end %>

  <div class="form-group">
    <%= f.label :name, :class => 'control-label col-lg-2' %>
    <div class="col-lg-10">
      <%= f.text_field :name, :class => 'form-control' %>
    </div>
    <%=f.error_span(:name) %>
  </div>
  <div class="form-group">
    <%= f.label :phone_number, :class => 'control-label col-lg-2' %>
    <div class="col-lg-10">
      <%= f.text_field :phone_number, :class => 'form-control' %>
    </div>
    <%=f.error_span(:phone_number) %>
  </div>
  <div class="form-group">
    <%= f.label :time, "Time and Date", :class => 'control-label col-lg-2' %>
    <!-- Rails expects time_select when dealing with ActiveRecord forms -->
    <div class="col-lg-2">
      <%= time_select :appointment, :time, {:class => "form-control" } %>
    </div>
    <div class="col-lg-4">
      <%= date_select :appointment, :time, {:class => "form-control" } %>
    </div>
    <div class="col-lg-2">
      <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.all.sort, default: "Pacific Time (US & Canada)" %>
    </div>
    <%=f.error_span(:time) %>
  </div>

  <div class="form-group">
    <div class="col-lg-offset-2 col-lg-10">
      <%= f.submit nil, :class => 'btn btn-primary' %>
      <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
                appointments_path, :class => 'btn btn-default' %>
    </div>
  </div>

<% end %>
<%= form_for @appointment, :html => { :class => "form-horizontal appointment" } do |f| %>

  <% if @appointment.errors.any? %>
    <div id="error_expl" class="panel panel-danger">
      <div class="panel-heading">
        <h3 class="panel-title"><%= pluralize(@appointment.errors.count, "error") %> prohibited this appointment from being saved:</h3>
      </div>
      <div class="panel-body">
        <ul>
        <% @appointment.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
        </ul>
      </div>
    </div>
  <% end %>

  <div class="form-group">
    <%= f.label :name, :class => 'control-label col-lg-2' %>
    <div class="col-lg-10">
      <%= f.text_field :name, :class => 'form-control' %>
    </div>
    <%=f.error_span(:name) %>
  </div>
  <div class="form-group">
    <%= f.label :phone_number, :class => 'control-label col-lg-2' %>
    <div class="col-lg-10">
      <%= f.text_field :phone_number, :class => 'form-control' %>
    </div>
    <%=f.error_span(:phone_number) %>
  </div>
  <div class="form-group">
    <%= f.label :time, "Time and Date", :class => 'control-label col-lg-2' %>
    <!-- Rails expects time_select when dealing with ActiveRecord forms -->
    <div class="col-lg-2">
      <%= time_select :appointment, :time, {:class => "form-control" } %>
    </div>
    <div class="col-lg-4">
      <%= date_select :appointment, :time, {:class => "form-control" } %>
    </div>
    <div class="col-lg-2">
      <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.all.sort, default: "Pacific Time (US & Canada)" %>
    </div>
    <%=f.error_span(:time) %>
  </div>

  <div class="form-group">
    <div class="col-lg-offset-2 col-lg-10">
      <%= f.submit nil, :class => 'btn btn-primary' %>
      <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
                appointments_path, :class => 'btn btn-default' %>
    </div>
  </div>

<% end %>
class AppointmentsController < ApplicationController
  before_action :set_appointment, only: [:show, :edit, :update, :destroy]

  # GET /appointments
  # GET /appointments.json
  def index
    @appointments = Appointment.all
    if @appointments.length == 0
      flash[:alert] = "You have no appointments. Create one now to get started."
    end
  end

  # GET /appointments/1
  # GET /appointments/1.json
  def show
  end

  # GET /appointments/new
  def new
    @appointment = Appointment.new
    @min_date = DateTime.now
  end

  # GET /appointments/1/edit
  def edit
  end

  # POST /appointments
  # POST /appointments.json
  def create
    Time.zone = appointment_params[:time_zone]
    @appointment = Appointment.new(appointment_params)
    
    respond_to do |format|
      if @appointment.save
        format.html { redirect_to @appointment, notice: 'Appointment was successfully created.' }
        format.json { render :show, status: :created, location: @appointment }
      else
        format.html { render :new }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /appointments/1
  # PATCH/PUT /appointments/1.json
  def update
    respond_to do |format|
      if @appointment.update(appointment_params)
        format.html { redirect_to @appointment, notice: 'Appointment was successfully updated.' }
        format.json { render :show, status: :ok, location: @appointment }
      else
        format.html { render :edit }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /appointments/1
  # DELETE /appointments/1.json
  def destroy
    @appointment.destroy
    respond_to do |format|
      format.html { redirect_to appointments_url, notice: 'Appointment was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    # See above ---> before_action :set_appointment, only: [:show, :edit, :update, :destroy]
    def set_appointment
      @appointment = Appointment.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def appointment_params
      params.require(:appointment).permit(:name, :phone_number, :time, :time_zone)
    end
end
class AppointmentsController < ApplicationController
  before_action :set_appointment, only: [:show, :edit, :update, :destroy]

  # GET /appointments
  # GET /appointments.json
  def index
    @appointments = Appointment.all
    if @appointments.length == 0
      flash[:alert] = "You have no appointments. Create one now to get started."
    end
  end

  # GET /appointments/1
  # GET /appointments/1.json
  def show
  end

  # GET /appointments/new
  def new
    @appointment = Appointment.new
    @min_date = DateTime.now
  end

  # GET /appointments/1/edit
  def edit
  end

  # POST /appointments
  # POST /appointments.json
  def create
    Time.zone = appointment_params[:time_zone]
    @appointment = Appointment.new(appointment_params)
    
    respond_to do |format|
      if @appointment.save
        format.html { redirect_to @appointment, notice: 'Appointment was successfully created.' }
        format.json { render :show, status: :created, location: @appointment }
      else
        format.html { render :new }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /appointments/1
  # PATCH/PUT /appointments/1.json
  def update
    respond_to do |format|
      if @appointment.update(appointment_params)
        format.html { redirect_to @appointment, notice: 'Appointment was successfully updated.' }
        format.json { render :show, status: :ok, location: @appointment }
      else
        format.html { render :edit }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /appointments/1
  # DELETE /appointments/1.json
  def destroy
    @appointment.destroy
    respond_to do |format|
      format.html { redirect_to appointments_url, notice: 'Appointment was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    # See above ---> before_action :set_appointment, only: [:show, :edit, :update, :destroy]
    def set_appointment
      @appointment = Appointment.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def appointment_params
      params.require(:appointment).permit(:name, :phone_number, :time, :time_zone)
    end
end
<%- model_class = Appointment -%>
<div class="page-header">
  <h1><%=t '.title', :default => model_class.model_name.human.pluralize.titleize %></h1>
</div>
<table class="table table-striped">
  <thead>
    <tr>
      <th><%= model_class.human_attribute_name(:id) %></th>
      <th><%= model_class.human_attribute_name(:name) %></th>
      <th><%= model_class.human_attribute_name(:phone_number) %></th>
      <th><%= model_class.human_attribute_name(:time) %></th>
      <th><%= model_class.human_attribute_name(:created_at) %></th>
      <th><%=t '.actions', :default => t("helpers.actions") %></th>
    </tr>
  </thead>
  <tbody>
    <% @appointments.each do |appointment| %>
      <tr>
        <td><%= link_to appointment.id, appointment_path(appointment) %></td>
        <td><%= appointment.name %></td>
        <td><%= appointment.phone_number %></td>
        <td><%= appointment.time %></td>
        <td><%=l appointment.created_at %></td>
        <td>
          <%= link_to "Edit", 
                      edit_appointment_path(appointment), :class => 'btn btn-default btn-xs' %>
          <%= link_to "Delete",
                      appointment_path(appointment),
                      :method => :delete,
                      :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
                      :class => 'btn btn-xs btn-danger' %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

<%= link_to "New",
            new_appointment_path,
            :class => 'btn btn-primary' %>
class AppointmentsController < ApplicationController
  before_action :set_appointment, only: [:show, :edit, :update, :destroy]

  # GET /appointments
  # GET /appointments.json
  def index
    @appointments = Appointment.all
    if @appointments.length == 0
      flash[:alert] = "You have no appointments. Create one now to get started."
    end
  end

  # GET /appointments/1
  # GET /appointments/1.json
  def show
  end

  # GET /appointments/new
  def new
    @appointment = Appointment.new
    @min_date = DateTime.now
  end

  # GET /appointments/1/edit
  def edit
  end

  # POST /appointments
  # POST /appointments.json
  def create
    Time.zone = appointment_params[:time_zone]
    @appointment = Appointment.new(appointment_params)
    
    respond_to do |format|
      if @appointment.save
        format.html { redirect_to @appointment, notice: 'Appointment was successfully created.' }
        format.json { render :show, status: :created, location: @appointment }
      else
        format.html { render :new }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /appointments/1
  # PATCH/PUT /appointments/1.json
  def update
    respond_to do |format|
      if @appointment.update(appointment_params)
        format.html { redirect_to @appointment, notice: 'Appointment was successfully updated.' }
        format.json { render :show, status: :ok, location: @appointment }
      else
        format.html { render :edit }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /appointments/1
  # DELETE /appointments/1.json
  def destroy
    @appointment.destroy
    respond_to do |format|
      format.html { redirect_to appointments_url, notice: 'Appointment was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    # See above ---> before_action :set_appointment, only: [:show, :edit, :update, :destroy]
    def set_appointment
      @appointment = Appointment.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def appointment_params
      params.require(:appointment).permit(:name, :phone_number, :time, :time_zone)
    end
end
class AppointmentsController < ApplicationController
  before_action :set_appointment, only: [:show, :edit, :update, :destroy]

  # GET /appointments
  # GET /appointments.json
  def index
    @appointments = Appointment.all
    if @appointments.length == 0
      flash[:alert] = "You have no appointments. Create one now to get started."
    end
  end

  # GET /appointments/1
  # GET /appointments/1.json
  def show
  end

  # GET /appointments/new
  def new
    @appointment = Appointment.new
    @min_date = DateTime.now
  end

  # GET /appointments/1/edit
  def edit
  end

  # POST /appointments
  # POST /appointments.json
  def create
    Time.zone = appointment_params[:time_zone]
    @appointment = Appointment.new(appointment_params)
    
    respond_to do |format|
      if @appointment.save
        format.html { redirect_to @appointment, notice: 'Appointment was successfully created.' }
        format.json { render :show, status: :created, location: @appointment }
      else
        format.html { render :new }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /appointments/1
  # PATCH/PUT /appointments/1.json
  def update
    respond_to do |format|
      if @appointment.update(appointment_params)
        format.html { redirect_to @appointment, notice: 'Appointment was successfully updated.' }
        format.json { render :show, status: :ok, location: @appointment }
      else
        format.html { render :edit }
        format.json { render json: @appointment.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /appointments/1
  # DELETE /appointments/1.json
  def destroy
    @appointment.destroy
    respond_to do |format|
      format.html { redirect_to appointments_url, notice: 'Appointment was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    # See above ---> before_action :set_appointment, only: [:show, :edit, :update, :destroy]
    def set_appointment
      @appointment = Appointment.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def appointment_params
      params.require(:appointment).permit(:name, :phone_number, :time, :time_zone)
    end
end
class Appointment < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true
  validates :time, presence: true

  after_create :reminder

  @@REMINDER_TIME = 30.minutes # minutes before appointment

  # Notify our appointment attendee X minutes before the appointment time
  def reminder
    @twilio_number = ENV['TWILIO_NUMBER']
    @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
    time_str = ((self.time).localtime).strftime("%I:%M%p on %b. %d, %Y")
    reminder = "Hi #{self.name}. Just a reminder that you have an appointment coming up at #{time_str}."
    message = @client.account.messages.create(
      :from => @twilio_number,
      :to => self.phone_number,
      :body => reminder,
    )
    puts message.to
  end

  def when_to_run
    time - @@REMINDER_TIME
  end

  handle_asynchronously :reminder, :run_at => Proc.new { |i| i.when_to_run }
end
source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.0'
# Use postgresql as the database for Active Record
gem 'pg'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.1.0'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby

# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc

# Use Twilio
gem 'twilio-ruby'

# Use bootstrap themes
gem 'twitter-bootstrap-rails', :git => 'git://github.com/seyhunak/twitter-bootstrap-rails.git'

# Use delayed job for running background jobs
gem 'delayed_job_active_record'

# Need daemons to start delayed_job
gem 'daemons'

# Use workless to use less workers on heroku
gem "workless", "~> 1.2.2"

# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Unicorn as the app server
# gem 'unicorn'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug'

  # Access an IRB console on exception pages or by using <%= console %> in views
  gem 'web-console', '~> 2.0'

  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
end
class Appointment < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true
  validates :time, presence: true

  after_create :reminder

  @@REMINDER_TIME = 30.minutes # minutes before appointment

  # Notify our appointment attendee X minutes before the appointment time
  def reminder
    @twilio_number = ENV['TWILIO_NUMBER']
    @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
    time_str = ((self.time).localtime).strftime("%I:%M%p on %b. %d, %Y")
    reminder = "Hi #{self.name}. Just a reminder that you have an appointment coming up at #{time_str}."
    message = @client.account.messages.create(
      :from => @twilio_number,
      :to => self.phone_number,
      :body => reminder,
    )
    puts message.to
  end

  def when_to_run
    time - @@REMINDER_TIME
  end

  handle_asynchronously :reminder, :run_at => Proc.new { |i| i.when_to_run }
end
class Appointment < ActiveRecord::Base
  validates :name, presence: true
  validates :phone_number, presence: true
  validates :time, presence: true

  after_create :reminder

  @@REMINDER_TIME = 30.minutes # minutes before appointment

  # Notify our appointment attendee X minutes before the appointment time
  def reminder
    @twilio_number = ENV['TWILIO_NUMBER']
    @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
    time_str = ((self.time).localtime).strftime("%I:%M%p on %b. %d, %Y")
    reminder = "Hi #{self.name}. Just a reminder that you have an appointment coming up at #{time_str}."
    message = @client.account.messages.create(
      :from => @twilio_number,
      :to => self.phone_number,
      :body => reminder,
    )
    puts message.to
  end

  def when_to_run
    time - @@REMINDER_TIME
  end

  handle_asynchronously :reminder, :run_at => Proc.new { |i| i.when_to_run }
end