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 Node.js and Express 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:
Ready to go? Boldly click the button right after this sentence.
For this workflow to work, we need to handle user authentication. We're going to rely on Passport for Node.js.
Each User
will need to have a countryCode
and a phoneNumber
which will be required to send SMS notifications later.
models/user.js
_17var mongoose = require('mongoose');_17var passportLocalMongoose = require('passport-local-mongoose');_17_17var userSchema = new mongoose.Schema({_17 email: { type: String, required: true },_17 username: { type: String, required: true },_17 password: { type: String },_17 countryCode: { type: String, required: true },_17 phoneNumber: { type: String, required: true },_17 date: { type: Date, default: Date.now },_17});_17_17userSchema.plugin(passportLocalMongoose);_17_17var user = mongoose.model('user', userSchema);_17_17module.exports = user;
Next let's model the vacation properties.
In order to build our rentals company we'll need a way to create the property listings.
The Property
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
.
models/property.js
_15var mongoose = require('mongoose');_15_15var propertySchema = new mongoose.Schema({_15 description: { type: String, required: true },_15 imageUrl: { type: String, required: true },_15 date: { type: Date, default: Date.now },_15 owner: {_15 type: mongoose.Schema.Types.ObjectId,_15 ref: 'user'_15 }_15});_15_15var property = mongoose.model('property', propertySchema);_15_15module.exports = property;
Next up, the 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
models/reservation.js
_22var mongoose = require('mongoose');_22var deepPopulate = require('mongoose-deep-populate')(mongoose);_22_22var reservationSchema = new mongoose.Schema({_22 message: { type: String, required: true },_22 status: { type: String, default: 'pending' },_22 date: { type: Date, default: Date.now },_22 property: {_22 type: mongoose.Schema.Types.ObjectId,_22 ref: 'property'_22 },_22 guest: {_22 type: mongoose.Schema.Types.ObjectId,_22 ref: 'user'_22 }_22});_22_22reservationSchema.plugin(deepPopulate, {});_22_22var reservation = mongoose.model('reservation', reservationSchema);_22_22module.exports = reservation;
Next, let's look at triggering the creation of a new reservation.
The reservation creation form holds only a single field 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 vacation property.
A reservation is created with a default status pending
, so when the host replies with an accept
or reject
response our application knows which reservation to update.
routes/reservations.js
_70var MessagingResponse = require('twilio').twiml.MessagingResponse;_70var twilio = require('twilio');_70var express = require('express');_70var router = express.Router();_70var Property = require('../models/property');_70var Reservation = require('../models/reservation');_70var User = require('../models/user');_70var notifier = require('../lib/notifier');_70_70// POST: /reservations_70router.post('/', function (req, res) {_70 var propertyId = req.body.propertyId;_70 var user = req.user;_70_70 Property.findOne({ _id: propertyId })_70 .then(function (property) {_70 var reservation = new Reservation({_70 message: req.body.message,_70 property: propertyId,_70 guest: user.id_70 });_70_70 return reservation.save();_70 })_70 .then(function () {_70 notifier.sendNotification();_70 res.redirect('/properties');_70 })_70 .catch(function(err) {_70 console.log(err);_70 });_70});_70_70// POST: /reservations/handle_70router.post('/handle', twilio.webhook({validate: false}), function (req, res) {_70 var from = req.body.From;_70 var smsRequest = req.body.Body;_70_70 var smsResponse;_70_70 User.findOne({phoneNumber: from})_70 .then(function (host) {_70 return Reservation.findOne({status: 'pending'});_70 })_70 .then(function (reservation) {_70 if (reservation === null) {_70 throw 'No pending reservations';_70 }_70 reservation.status = smsRequest.toLowerCase() === "accept" ? "confirmed" : "rejected";_70 return reservation.save();_70 })_70 .then(function (reservation) {_70 var message = "You have successfully " + reservation.status + " the reservation";_70 respond(res, message);_70 })_70 .catch(function (err) {_70 var message = "Sorry, it looks like you do not have any reservations to respond to";_70 respond(res, message);_70 });_70});_70_70var respond = function(res, message) {_70 var messagingResponse = new MessagingResponse();_70 messagingResponse.message({}, message);_70_70 res.type('text/xml');_70 res.send(messagingResponse.toString());_70}_70_70module.exports = router;
In the next step, we'll take a look at how the SMS notification is sent to the host when the reservation is created.
When a reservation is created, we want to notify the owner of said property that someone has made a reservation.
This is where we use Twilio Node Helper Library to send an SMS message to the host, using our Twilio phone number. As you can see, sending SMS messages using Twilio is just a few lines of code.
Now we just have to wait for the host to send an SMS response accepting or rejecting the reservation so we can notify the guest and host that the reservation information is updated.
Notify through SMS the host of the new reservation
_44var config = require('../config');_44var client = require('twilio')(config.accountSid, config.authToken);_44var Reservation = require('../models/reservation');_44_44var sendNotification = function() {_44 Reservation.find({status: 'pending'})_44 .deepPopulate('property property.owner guest')_44 .then(function (reservations) {_44 if (reservations.length > 1) {_44 return;_44 }_44_44 var reservation = reservations[0];_44 var owner = reservation.property.owner;_44_44 // Send the notification_44 client.messages.create({_44 to: phoneNumber(owner),_44 from: config.phoneNumber,_44 body: buildMessage(reservation)_44 })_44 .then(function(res) {_44 console.log(res.body);_44 })_44 .catch(function(err) {_44 console.log(err);_44 });_44 });_44};_44_44var phoneNumber = function(owner) {_44 return "+" + owner.countryCode + owner.phoneNumber;_44};_44_44var buildMessage = function(reservation) {_44 var message = "You have a new reservation request from " + reservation.guest.username +_44 " for " + reservation.property.description + ":\n" +_44 reservation.message + "\n" +_44 "Reply accept or reject";_44_44 return message;_44};_44_44exports.sendNotification = sendNotification;
The next step shows how to handle and configure the host's SMS response.
The reservations/handle
endpoint 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 /handle:
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/handle
An incoming request from Twilio comes with some helpful including the From
phone number and the message Body
.
We'll use the From
parameter to lookup the host and check if she has any pending reservations. If she does, we'll use the message body to check if she accepted or rejected the reservation.
routes/reservations.js
_70var MessagingResponse = require('twilio').twiml.MessagingResponse;_70var twilio = require('twilio');_70var express = require('express');_70var router = express.Router();_70var Property = require('../models/property');_70var Reservation = require('../models/reservation');_70var User = require('../models/user');_70var notifier = require('../lib/notifier');_70_70// POST: /reservations_70router.post('/', function (req, res) {_70 var propertyId = req.body.propertyId;_70 var user = req.user;_70_70 Property.findOne({ _id: propertyId })_70 .then(function (property) {_70 var reservation = new Reservation({_70 message: req.body.message,_70 property: propertyId,_70 guest: user.id_70 });_70_70 return reservation.save();_70 })_70 .then(function () {_70 notifier.sendNotification();_70 res.redirect('/properties');_70 })_70 .catch(function(err) {_70 console.log(err);_70 });_70});_70_70// POST: /reservations/handle_70router.post('/handle', twilio.webhook({validate: false}), function (req, res) {_70 var from = req.body.From;_70 var smsRequest = req.body.Body;_70_70 var smsResponse;_70_70 User.findOne({phoneNumber: from})_70 .then(function (host) {_70 return Reservation.findOne({status: 'pending'});_70 })_70 .then(function (reservation) {_70 if (reservation === null) {_70 throw 'No pending reservations';_70 }_70 reservation.status = smsRequest.toLowerCase() === "accept" ? "confirmed" : "rejected";_70 return reservation.save();_70 })_70 .then(function (reservation) {_70 var message = "You have successfully " + reservation.status + " the reservation";_70 respond(res, message);_70 })_70 .catch(function (err) {_70 var message = "Sorry, it looks like you do not have any reservations to respond to";_70 respond(res, message);_70 });_70});_70_70var respond = function(res, message) {_70 var messagingResponse = new MessagingResponse();_70 messagingResponse.message({}, message);_70_70 res.type('text/xml');_70 res.send(messagingResponse.toString());_70}_70_70module.exports = router;
In the last step, we'll use Twilio's TwiML and instruct Twilio to send SMS messages to the guest.
After updating the reservation status, we must notify the host that he/she has successfully confirmed or rejected the reservation. If no reservation is found, we send an error message instead.
If a reservation is confirmed or rejected we send an additional SMS to the guest to pass along the news.
We use the verb Message from TwiML to instruct Twilio's server that it should send SMS messages.
routes/reservations.js
_70var MessagingResponse = require('twilio').twiml.MessagingResponse;_70var twilio = require('twilio');_70var express = require('express');_70var router = express.Router();_70var Property = require('../models/property');_70var Reservation = require('../models/reservation');_70var User = require('../models/user');_70var notifier = require('../lib/notifier');_70_70// POST: /reservations_70router.post('/', function (req, res) {_70 var propertyId = req.body.propertyId;_70 var user = req.user;_70_70 Property.findOne({ _id: propertyId })_70 .then(function (property) {_70 var reservation = new Reservation({_70 message: req.body.message,_70 property: propertyId,_70 guest: user.id_70 });_70_70 return reservation.save();_70 })_70 .then(function () {_70 notifier.sendNotification();_70 res.redirect('/properties');_70 })_70 .catch(function(err) {_70 console.log(err);_70 });_70});_70_70// POST: /reservations/handle_70router.post('/handle', twilio.webhook({validate: false}), function (req, res) {_70 var from = req.body.From;_70 var smsRequest = req.body.Body;_70_70 var smsResponse;_70_70 User.findOne({phoneNumber: from})_70 .then(function (host) {_70 return Reservation.findOne({status: 'pending'});_70 })_70 .then(function (reservation) {_70 if (reservation === null) {_70 throw 'No pending reservations';_70 }_70 reservation.status = smsRequest.toLowerCase() === "accept" ? "confirmed" : "rejected";_70 return reservation.save();_70 })_70 .then(function (reservation) {_70 var message = "You have successfully " + reservation.status + " the reservation";_70 respond(res, message);_70 })_70 .catch(function (err) {_70 var message = "Sorry, it looks like you do not have any reservations to respond to";_70 respond(res, message);_70 });_70});_70_70var respond = function(res, message) {_70 var messagingResponse = new MessagingResponse();_70 messagingResponse.message({}, message);_70_70 res.type('text/xml');_70 res.send(messagingResponse.toString());_70}_70_70module.exports = router;
And that's a wrap! Next let's take a look at other features you might enjoy in your application.
Node.js goes great with a helping of Twilio... let us prove it:
Easily route callers to the right people and information with an IVR (interactive voice response) system.
Instantly collect structured data from your users with a survey conducted over a voice call or SMS text messages.
Thanks for checking this tutorial out! Tweet to us @twilio with what you're building!