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 Java and Servlets 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:
For this workflow to work, we need to have a user model and allow logins.
src/main/java/org/twilio/airtng/models/User.java
_116package org.twilio.airtng.models;_116_116import javax.persistence.*;_116import java.util.ArrayList;_116import java.util.List;_116_116@Entity_116@Table(name = "users")_116public class User {_116 @Id_116 @GeneratedValue(strategy = GenerationType.IDENTITY)_116 @Column(name = "id")_116 private long id;_116_116 @Column(name = "name")_116 private String name;_116_116 @Column(name = "email")_116 private String email;_116_116 @Column(name = "password")_116 private String password;_116_116 @Column(name = "phone_number")_116 private String phoneNumber;_116_116 @OneToMany(mappedBy="user")_116 private List<Reservation> reservations;_116_116 @OneToMany(mappedBy="user")_116 private List<VacationProperty> vacationProperties;_116_116 public User() {_116 this.reservations = new ArrayList<>();_116 this.vacationProperties = new ArrayList<>();_116 }_116_116 public User(_116 String name,_116 String email,_116 String password,_116 String phoneNumber) {_116 this();_116 this.name = name;_116 this.email = email;_116 this.password = password;_116 this.phoneNumber = phoneNumber;_116 }_116_116 public long getId() {_116 return id;_116 }_116_116 public void setId(long id) {_116 this.id = id;_116 }_116_116 public String getName() {_116 return name;_116 }_116_116 public void setName(String name) {_116 this.name = name;_116 }_116_116 public String getEmail() {_116 return email;_116 }_116_116 public void setEmail(String email) {_116 this.email = email;_116 }_116_116 public String getPassword() {_116 return password;_116 }_116_116 public void setPassword(String password) {_116 this.password = password;_116 }_116_116 public String getPhoneNumber() {_116 return phoneNumber;_116 }_116_116 public void setPhoneNumber(String phoneNumber) {_116 this.phoneNumber = phoneNumber;_116 }_116_116 public void addReservation(Reservation reservation) {_116_116 if (this.reservations.add(reservation) && reservation.getUser() != this) {_116 reservation.setUser(this);_116 }_116 }_116_116 public void removeReservation(Reservation reservation) {_116 if (this.reservations.remove(reservation) && reservation.getUser() == this) {_116 reservation.setUser(null);_116 }_116 }_116_116 public void addVacationProperty(VacationProperty vacationProperty) {_116_116 if (this.vacationProperties.add(vacationProperty) && vacationProperty.getUser() != this) {_116 vacationProperty.setUser(this);_116 }_116 }_116_116 public void removeVacationProperty(VacationProperty vacationProperty) {_116 if (this.reservations.remove(vacationProperty) && vacationProperty.getUser() == this) {_116 vacationProperty.setUser(null);_116 }_116 }_116_116}
Next, let's look at the Vacation Property model.
In order to build a true vacation rental company we'll need a way to create a property rental listing.
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 imageUrl
.
The model has two associations implemented: there are many reservations in it and many users can make those reservations.
src/main/java/org/twilio/airtng/models/VacationProperty.java
_83package org.twilio.airtng.models;_83_83import javax.persistence.*;_83import java.util.ArrayList;_83import java.util.List;_83_83@Entity_83@Table(name = "vacation_properties")_83public class VacationProperty {_83_83 @javax.persistence.Id_83 @GeneratedValue(strategy = GenerationType.IDENTITY)_83 @Column(name = "id")_83 private long id;_83_83 @Column(name = "description")_83 private String description;_83_83 @Column(name = "image_url")_83 private String imageUrl;_83_83 @OneToMany(mappedBy = "vacationProperty")_83 private List<Reservation> reservations;_83_83 @ManyToOne(fetch = FetchType.LAZY)_83 @JoinColumn(name = "user_id")_83 private User user;_83_83 public VacationProperty() {_83 this.reservations = new ArrayList<Reservation>();_83 }_83_83 public VacationProperty(String description, String imageUrl, User user) {_83 this.description = description;_83 this.imageUrl = imageUrl;_83 this.user = user;_83 }_83_83 public long getId() {_83 return id;_83 }_83_83 public void setId(long id) {_83 this.id = id;_83 }_83_83 public String getDescription() {_83 return description;_83 }_83_83 public void setDescription(String description) {_83 this.description = description;_83 }_83_83 public String getImageUrl() {_83 return imageUrl;_83 }_83_83 public void setImageUrl(String imageUrl) {_83 this.imageUrl = imageUrl;_83 }_83_83 public User getUser() {_83 return this.user;_83 }_83_83 public void setUser(User user) {_83 this.user = user;_83 }_83_83 public void addReservation(Reservation reservation) {_83_83 if (this.reservations.add(reservation) && reservation.getVacationProperty() != this) {_83 reservation.setVacationProperty(this);_83 }_83 }_83_83 public void removeReservation(Reservation reservation) {_83 if (this.reservations.remove(reservation) && reservation.getVacationProperty() == this) {_83 reservation.setVacationProperty(null);_83 }_83 }_83}
Next, let's look at the all-important Reservation model.
The Reservation
model is at the center of the workflow for this application. It is responsible for keeping track of:
guest
who performed the reservation
vacation property
the guest is requesting (and associated
host
)
status
of the reservation:
pending
,
confirmed
, or
rejected
src/main/java/org/twilio/airtng/models/Reservation.java
_133package org.twilio.airtng.models;_133_133import javax.persistence.*;_133_133@Entity_133@Table(name = "reservations")_133public class Reservation {_133_133 @javax.persistence.Id_133 @GeneratedValue(strategy = GenerationType.IDENTITY)_133 @Column(name = "id")_133 private long id;_133_133 @Column(name = "message")_133 private String message;_133_133 @Column(name = "status")_133 private Integer status;_133_133 @ManyToOne(fetch = FetchType.LAZY)_133 @JoinColumn(name = "user_id")_133 private User user;_133_133 @ManyToOne(fetch = FetchType.LAZY)_133 @JoinColumn(name = "vacation_property_id")_133 private VacationProperty vacationProperty;_133_133 public Reservation() {_133 }_133_133 public Reservation(String message, VacationProperty vacationProperty, User user) {_133 this();_133 this.message = message;_133 this.vacationProperty = vacationProperty;_133 this.user = user;_133 this.setStatus(Status.Pending);_133 }_133_133 public long getId() {_133 return id;_133 }_133_133 public void setId(long id) {_133 this.id = id;_133 }_133_133 public String getMessage() {_133 return message;_133 }_133_133 public void setMessage(String message) {_133 this.message = message;_133 }_133_133 public Status getStatus() {_133 return Status.getType(this.status);_133 }_133_133 public void setStatus(Status status) {_133 if (status == null) {_133 this.status = null;_133 } else {_133 this.status = status.getValue();_133 }_133 }_133_133 public VacationProperty getVacationProperty() {_133 return vacationProperty;_133 }_133_133 public void setVacationProperty(VacationProperty vacationProperty) {_133 this.vacationProperty = vacationProperty;_133 }_133_133 public User getUser() {_133 return this.user;_133 }_133_133 public void setUser(User user) {_133 this.user = user;_133 }_133_133 public void reject() {_133 this.setStatus(Status.Rejected);_133 }_133_133 public void confirm() {_133 this.setStatus(Status.Confirmed);_133 }_133_133 public enum Status {_133_133 Pending(0), Confirmed(1), Rejected(2);_133_133 private int value;_133_133 Status(int value) {_133 this.value = value;_133 }_133_133 public int getValue() {_133 return value;_133 }_133_133 @Override_133 public String toString() {_133 switch (value) {_133 case 0:_133 return "pending";_133 case 1:_133 return "confirmed";_133 case 2:_133 return "rejected";_133 }_133 return "Value out of range";_133 }_133_133_133 public static Status getType(Integer id) {_133_133 if (id == null) {_133 return null;_133 }_133_133 for (Status status : Status.values()) {_133 if (id.equals(status.getValue())) {_133 return status;_133 }_133 }_133 throw new IllegalArgumentException("No matching type for value " + id);_133 }_133 }_133}
Next up, we'll show how our new application will create a new reservation.
The reservation creation form holds only one field: the message that will be sent to the host when reserving one of her properties.
The rest of the information necessary to create a reservation is taken from the logged in user and the relationship between a property and its owner. Our base generic Repository
is in charge of handling entity insertion.
A reservation is created with a default status pending.
This lets our application easily react to a host rejecting or accepting a reservation request.
src/main/java/org/twilio/airtng/servlets/ReservationServlet.java
_81package org.twilio.airtng.servlets;_81_81import org.twilio.airtng.lib.notifications.SmsNotifier;_81import org.twilio.airtng.lib.servlets.WebAppServlet;_81import org.twilio.airtng.lib.web.request.validators.RequestParametersValidator;_81import org.twilio.airtng.models.Reservation;_81import org.twilio.airtng.models.User;_81import org.twilio.airtng.models.VacationProperty;_81import org.twilio.airtng.repositories.ReservationRepository;_81import org.twilio.airtng.repositories.UserRepository;_81import org.twilio.airtng.repositories.VacationPropertiesRepository;_81_81import javax.servlet.ServletException;_81import javax.servlet.http.HttpServletRequest;_81import javax.servlet.http.HttpServletResponse;_81import java.io.IOException;_81_81public class ReservationServlet extends WebAppServlet {_81_81 private final VacationPropertiesRepository vacationPropertiesRepository;_81 private final ReservationRepository reservationRepository;_81 private UserRepository userRepository;_81 private SmsNotifier smsNotifier;_81_81 public ReservationServlet() {_81 this(new VacationPropertiesRepository(), new ReservationRepository(), new UserRepository(), new SmsNotifier());_81 }_81_81 public ReservationServlet(VacationPropertiesRepository vacationPropertiesRepository, ReservationRepository reservationRepository, UserRepository userRepository, SmsNotifier smsNotifier) {_81 super();_81 this.vacationPropertiesRepository = vacationPropertiesRepository;_81 this.reservationRepository = reservationRepository;_81 this.userRepository = userRepository;_81 this.smsNotifier = smsNotifier;_81 }_81_81 @Override_81 public void doGet(HttpServletRequest request, HttpServletResponse response)_81 throws ServletException, IOException {_81_81 VacationProperty vacationProperty = vacationPropertiesRepository.find(Long.parseLong(request.getParameter("id")));_81 request.setAttribute("vacationProperty", vacationProperty);_81 request.getRequestDispatcher("/reservation.jsp").forward(request, response);_81 }_81_81 @Override_81 public void doPost(HttpServletRequest request, HttpServletResponse response)_81 throws ServletException, IOException {_81_81 super.doPost(request, response);_81_81 String message = null;_81 VacationProperty vacationProperty = null;_81_81 if (isValidRequest()) {_81 message = request.getParameter("message");_81 String propertyId = request.getParameter("propertyId");_81 vacationProperty = vacationPropertiesRepository.find(Long.parseLong(propertyId));_81_81 User currentUser = userRepository.find(sessionManager.get().getLoggedUserId(request));_81 Reservation reservation = reservationRepository.create(new Reservation(message, vacationProperty, currentUser));_81 smsNotifier.notifyHost(reservation);_81 response.sendRedirect("/properties");_81 }_81 preserveStatusRequest(request, message, vacationProperty);_81 request.getRequestDispatcher("/reservation.jsp").forward(request, response);_81 }_81_81 @Override_81 protected boolean isValidRequest(RequestParametersValidator validator) {_81_81 return validator.validatePresence("message");_81 }_81_81 private void preserveStatusRequest(_81 HttpServletRequest request,_81 String message, Object vacationProperty) {_81 request.setAttribute("message", message);_81 request.setAttribute("vacationProperty", vacationProperty);_81 }_81}
Now let's look at how the system will notify a host
when a new reservation request is submitted.
When a reservation is created for a property we want to notify the owner that someone has made a reservation.
We use an abstraction called SmsNotifier
which under the hood uses another abstraction called Sender
. Here is where we use Twilio's Rest API to send an SMS message to the host using your Twilio phone number. That's right - it's as simple as that to send a SMS with Twilio.
src/main/java/org/twilio/airtng/lib/sms/Sender.java
_28package org.twilio.airtng.lib.sms;_28_28import com.twilio.http.TwilioRestClient;_28import com.twilio.rest.api.v2010.account.MessageCreator;_28import com.twilio.type.PhoneNumber;_28import org.twilio.airtng.lib.Config;_28_28public class Sender {_28_28 private final TwilioRestClient client;_28_28 public Sender() {_28 client = new TwilioRestClient.Builder(Config.getAccountSid(), Config.getAuthToken()).build();_28 }_28_28 public Sender(TwilioRestClient client) {_28 this.client = client;_28 }_28_28 public void send(String to, String message) {_28 new MessageCreator(_28 new PhoneNumber(to),_28 new PhoneNumber(Config.getPhoneNumber()),_28 message_28 ).create(client);_28 }_28_28}
The next step shows how to handle a host accepting or rejecting a request - let's continue.
Let's take a closer look at the ReservationConfirmation
servlet. This servlet handles our incoming Twilio request and does three things:
In the Twilio console, you should change the 'A Message Comes In' webhook to call your application's endpoint in the route /reservation-confirmation:
One way to expose your machine to the world during development is to use ngrok. Your URL for the SMS web hook on your phone number should look something like this:
_10http://<subdomain>.ngrok.io/reservation-confirmation
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 he/she has any pending reservations. If he/she does, we'll use the message body to check for an 'accept' or 'reject'.
Finally, we update the reservation status and use the SmsNotifier
abstraction to send an SMS to the guest with the news.
Webhook for handling Host's decision
_70package org.twilio.airtng.servlets;_70_70import com.twilio.twiml.MessagingResponse;_70import com.twilio.twiml.TwiMLException;_70import org.twilio.airtng.lib.helpers.TwiMLHelper;_70import org.twilio.airtng.lib.notifications.SmsNotifier;_70import org.twilio.airtng.lib.servlets.WebAppServlet;_70import org.twilio.airtng.models.Reservation;_70import org.twilio.airtng.models.User;_70import org.twilio.airtng.repositories.ReservationRepository;_70import org.twilio.airtng.repositories.UserRepository;_70_70import javax.servlet.ServletException;_70import javax.servlet.http.HttpServletRequest;_70import javax.servlet.http.HttpServletResponse;_70import java.io.IOException;_70_70public class ReservationConfirmationServlet extends WebAppServlet {_70_70 private UserRepository userRepository;_70 private ReservationRepository reservationRepository;_70 private SmsNotifier smsNotifier;_70_70 public ReservationConfirmationServlet() {_70 this(new UserRepository(), new ReservationRepository(), new SmsNotifier());_70 }_70_70 public ReservationConfirmationServlet(UserRepository userRepository, ReservationRepository reservationRepository, SmsNotifier smsNotifier) {_70 super();_70 this.userRepository = userRepository;_70 this.reservationRepository = reservationRepository;_70 this.smsNotifier = smsNotifier;_70 }_70_70 @Override_70 public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {_70_70 String phone = request.getParameter("From");_70 String smsContent = request.getParameter("Body");_70_70 String smsResponseText = "Sorry, it looks like you don't have any reservations to respond to.";_70_70 try {_70 User user = userRepository.findByPhoneNumber(phone);_70 Reservation reservation = reservationRepository.findFirstPendantReservationsByUser(user.getId());_70 if (reservation != null) {_70 if (smsContent.contains("yes") || smsContent.contains("accept"))_70 reservation.confirm();_70 else_70 reservation.reject();_70 reservationRepository.update(reservation);_70_70 smsResponseText = String.format("You have successfully %s the reservation", reservation.getStatus().toString());_70 smsNotifier.notifyGuest(reservation);_70 }_70_70 respondSms(response, smsResponseText);_70_70 } catch (Exception e) {_70 throw new RuntimeException(e);_70 }_70 }_70_70 private void respondSms(HttpServletResponse response, String message)_70 throws IOException, TwiMLException {_70 MessagingResponse twiMLResponse = TwiMLHelper.buildSmsRespond(message);_70 response.setContentType("text/xml");_70 response.getWriter().write(twiMLResponse.toXml());_70 }_70}
Next up, let's see how we will respond to Twilio.
Finally, we'll use Twilio's TwiML as a response to Twilio's server instructing it to send SMS notification message to the host.
We'll be using the Message
verb to instruct Twilio's server that it should send the message.
src/main/java/org/twilio/airtng/servlets/ReservationConfirmationServlet.java
_70package org.twilio.airtng.servlets;_70_70import com.twilio.twiml.MessagingResponse;_70import com.twilio.twiml.TwiMLException;_70import org.twilio.airtng.lib.helpers.TwiMLHelper;_70import org.twilio.airtng.lib.notifications.SmsNotifier;_70import org.twilio.airtng.lib.servlets.WebAppServlet;_70import org.twilio.airtng.models.Reservation;_70import org.twilio.airtng.models.User;_70import org.twilio.airtng.repositories.ReservationRepository;_70import org.twilio.airtng.repositories.UserRepository;_70_70import javax.servlet.ServletException;_70import javax.servlet.http.HttpServletRequest;_70import javax.servlet.http.HttpServletResponse;_70import java.io.IOException;_70_70public class ReservationConfirmationServlet extends WebAppServlet {_70_70 private UserRepository userRepository;_70 private ReservationRepository reservationRepository;_70 private SmsNotifier smsNotifier;_70_70 public ReservationConfirmationServlet() {_70 this(new UserRepository(), new ReservationRepository(), new SmsNotifier());_70 }_70_70 public ReservationConfirmationServlet(UserRepository userRepository, ReservationRepository reservationRepository, SmsNotifier smsNotifier) {_70 super();_70 this.userRepository = userRepository;_70 this.reservationRepository = reservationRepository;_70 this.smsNotifier = smsNotifier;_70 }_70_70 @Override_70 public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {_70_70 String phone = request.getParameter("From");_70 String smsContent = request.getParameter("Body");_70_70 String smsResponseText = "Sorry, it looks like you don't have any reservations to respond to.";_70_70 try {_70 User user = userRepository.findByPhoneNumber(phone);_70 Reservation reservation = reservationRepository.findFirstPendantReservationsByUser(user.getId());_70 if (reservation != null) {_70 if (smsContent.contains("yes") || smsContent.contains("accept"))_70 reservation.confirm();_70 else_70 reservation.reject();_70 reservationRepository.update(reservation);_70_70 smsResponseText = String.format("You have successfully %s the reservation", reservation.getStatus().toString());_70 smsNotifier.notifyGuest(reservation);_70 }_70_70 respondSms(response, smsResponseText);_70_70 } catch (Exception e) {_70 throw new RuntimeException(e);_70 }_70 }_70_70 private void respondSms(HttpServletResponse response, String message)_70 throws IOException, TwiMLException {_70 MessagingResponse twiMLResponse = TwiMLHelper.buildSmsRespond(message);_70 response.setContentType("text/xml");_70 response.getWriter().write(twiMLResponse.toXml());_70 }_70}
Congrats! You've just learned how to automate your workflow with Twilio SMS.
Next, let's look at some other interesting features you might want to add to your application.
Like Twilio? Like Java? You're in the right place. Here are a couple other excellent tutorials:
Build a server notification system that will alert all administrators via SMS when a server outage occurs.
Convert web traffic into phone calls with the click of a button.
Thanks for checking this tutorial out! Tweet to us @twilio with what you're building!