Pay by Phone with Voice Calling
Use the Say
PerCL command and privacymode
to process PCI compliant payments over voice call
In this how-to guide, we will guide you through implementing a pay by phone IVR system using FreeClimb. This system will perform the following actions:
- Receive an incoming call via a FreeClimb application
- Route users via DTMF (e.g. keypad) input
- Collect and process credit card payment information using
privacymode
- Use PerCL to send an SMS payment confirmation message to user
You're ready for this how-to guide if you have the following:
A FreeClimb account
A registered application with a named alias
A configured FreeClimb number
Trial accounts: A verified number
Your language and tools:
Step 1: App set up
Make your server locally accessible
The fastest way to start testing your FreeClimb application is to temporarily make your local server publicly accessible through a tunneling service. We'll use ngrok to do this.
Once you have downloaded ngrok and unzipped the file to install it, open your terminal and navigate to the directory where you've unzipped ngrok. Use the following command to start a HTTP tunnel on port 3000:
ngrok http 3000
Once you run ngrok using the above command, you should receive a response with a public URL that looks something like this:
ngrok by @inconshreveable
Tunnel Status online
Version 2.0/2.0
Web Interface http://127.0.0.1:4040
Forwarding http://92832de0.ngrok.io -> localhost:3000
Forwarding https://92832de0.ngrok.io -> localhost:3000
Connnections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
The forwarding URLs provided point to your local server. Save the URLs and move on to the next step. Make sure to keep ngrok open as your continue to build and run your app.
Configure your application's endpoints
Now that you've got a public URL, you're ready to configure your application's endpoints. We'll be configuring the voiceUrl using your ngrok URL and the route reference /incomingCall
.
Go to the Apps page in your dashboard. You should see your registered FreeClimb app.
Click Edit Config to enter the ngrok forwarding URL into your registered app's voiceUrl field and add the route /incomingCall
to the end of it. When you're done, the App Config should look something like this:
Save your updated App Config and proceed with the next steps.
Get dependencies
To start, create a new directory for the project. Within this directory create a file named package.json
and add the following content:
{
"name": "Node_Pay_By_Phone",
"version": "1.0.0",
"description": "Simple pay-by-phone application tutorial",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@freeclimb/sdk": "^1.1.1",
"body-parser": "^1.19.0",
"dotenv": "^8.2.0",
"dotenv-safe": "^8.2.0",
"express": "^4.17.1"
},
"scripts": {
"start": "DEBUG=express:* node ."
}
}
These dependencies can be installed from the command line by using the following command:
yarn install
Set environment variables
In order to authenticate your requests with the FreeClimb platform, we need to include your API credentials (account ID and API key) in every request. Your account ID and API key can be found in the FreeClimb dashboard.
Why put API credentials in environment variables?
If you were to push your code to a public repository, those credentials would be public and an attacker could steal your account. To prevent this, we create environment variables with your API credentials and import them into your code using dotenv-safe.
This sample app uses dotenv-safe to read in your credentials, and anything else you save, as environment variables. As required by dotenv-safe, before creating our .env
file we must create a .env.example
file that outlines the environment variables that will need to be present in the actual .env
file.
You must include both a
.env.example
file and a.env
file in order for dotenv-safe to work properly.
To do so, create a file in your project's directory named .env.example
and add the following content:
ACCOUNT_ID=
API_KEY=
HOST=
PORT=
FC_NUMBER=
Once you've created your .env.example
file, create a .env
file in the root directory of your repo. Make sure to add the file to your .gitignore
file. Add your account ID and API key to your .env
file.
We'll also include the host, port, and FreeClimb number.
If you've set up ngrok to expose a specific port from your local machine, use the public facing link provided by ngrok as your HOST
variable.
The port number will allow you to specify the port on which the app will run. For example, if you were to enter PORT=3000
to view the app, you would direct your browser to http://localhost:3000
.
If you haven't already, make sure to assign the registered application you configured with your ngrok URL to the configured FreeClimb number you will be using for this sample application. Use this phone number as your FC_NUMBER
variable.
Your .env
file should look as follows:
ACCOUNT_ID="YOUR-ACCOUNT-ID"
API_KEY="YOUR-API-KEY"
HOST="YOUR-HOST"
PORT="YOUR-PORT-NUMBER"
FC_NUMBER="+15555550010" // make sure your FreeClimb number is in E.164 format
Step 2: Create and share a FreeClimb SDK instance
Throughout this application, we will be using a FreeClimb SDK object to generate our PerCL responses. Rather than re-create this object in every file that needs it, we will instread create one instance and pass it using require
where needed.
To do this, create a new file in your project directory called freeclimb.js
and add the following:
const freeclimbSDK = require('@freeclimb/sdk')
const accountId = process.env.ACCOUNT_ID
const apiKey = process.env.API_KEY
const freeclimb = freeclimbSDK(accountId, apiKey)
module.exports = freeclimb
Step 3: Handle incoming calls
The first step in our application will be handling incoming calls and redirecting them to the appropriate endpoint. Additionally, we also need to define behavior for ending a call. To do so, create a file called index.js
in your project directory and add the following:
require('dotenv-safe').config()
const express = require('express')
const bodyParser = require('body-parser')
const freeclimb = require('./freeclimb')
const app = express()
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
const port = process.env.PORT || 3000
const host = process.env.HOST
app.post('/incomingCall', (req, res) => {
res.status(200).json(
freeclimb.percl.build( // return PerCL commands for greeting and redirect to the main menu endpoint
freeclimb.percl.say('Welcome to the Node Pay By Phone Sample App.'),
freeclimb.percl.pause(100),
freeclimb.percl.redirect(`${host}/mainMenuPrompt`)
)
)
})
app.post('/transfer', (req, res) => { // mimic transfer to an operator
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say('There are no operators available at this time'),
freeclimb.percl.redirect(`${host}/endCall`)
)
)
})
app.post('/endCall', (req, res) => { // end call with script + hangup
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'Thank you for calling the Node Pay By Phone sample app , have a nice day!'
),
freeclimb.percl.hangup()
)
)
})
if (process.env.NODE_ENV !== 'test') {
app.listen(port, () => {
console.log(`Starting server on port ${port}`)
})
}
module.exports = { app }
Next, we'll create a main menu that will give callers the option to proceed with their payment or transfer to an operator using the /transfer
endpoint defined in index.js
. To do this, create a file called mainMenu.js
in your project directory and add the following:
const express = require('express')
const freeclimb = require('./freeclimb')
const host = process.env.HOST
const router = express.Router()
let mainMenuErrCount = 0
router.post('/mainMenuPrompt', (req, res) => {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.getDigits(`${host}/mainMenu`, { // once dtmf input is collected redirect to main menu for further routing
prompts: [
freeclimb.percl.say(
'To make a one time payment please press 1, or press 0 to speak with an operator'
)
],
maxDigits: 1,
minDigits: 1,
flushBuffer: true
})
)
)
})
router.post('/mainMenu', (req, res) => {
const getDigitsResponse = req.body
const digits = getDigitsResponse.digits // get digit input from request body
const menuOpts = new Map([
[
'1',
{
redirect: `${host}/ccAmountPrompt`
}
],
[
'0',
{
redirect: `${host}/transfer`
}
]
])
if ((!digits || !menuOpts.get(digits)) && mainMenuErrCount < 3) {
mainMenuErrCount++
res.status(200).json(
freeclimb.percl.build( // if no or incorrect input and errors below max allowed limit return PerCL commands for redirect back to getDigits collection at /mainMenuPrompt
freeclimb.percl.say('Error, please try again'),
freeclimb.percl.redirect(`${host}/mainMenuPrompt`)
)
)
} else if (mainMenuErrCount >= 3) { // creating a max error limit stops good actors from getting stuck and bad actors from circling endlessly in your IVR
mainMenuErrCount = 0
res.status(200).json(
freeclimb.percl.build( // if max error limit reached return PerCL commands for redirect to /endCall
freeclimb.percl.say('Max retry limit reached'),
freeclimb.percl.pause(100),
freeclimb.percl.redirect(`${host}/endCall`)
)
)
} else {
mainMenuErrCount = 0
res.status(200).json( // on successful input redirect return redirect PerCL commands based on option selected by the user
freeclimb.percl.build(freeclimb.percl.redirect(menuOpts.get(digits).redirect))
)
}
})
module.exports = router
Interested in routing callers via speech input?
Check out our Call Routing Using Voice Input how-to guide to learn more about how to collect speech input from callers and route them accordingly.
Step 4: Handle customer data
Throughout this application, we'll be prompting the caller with various scripts in order to collect information from them. Unlike our other IVR how-to guides, this guide also tracks whether or not the caller has already heard certain prompts and will play shorter, more succint messages if the caller is hearing them a second time. This aligns with industry best practices for IVRs and VUIs (Voice User Interfaces).
Adding customer.js
and caller.js
files to our project allows us to handle customer data within the IVR system for this purpose.
Why have secondary prompts in your IVR system?
Customers sometimes make mistakes or need to re-enter information via an IVR. When they do so, they don't necessarily need to hear the same lengthy prompts again. To provide a more efficient user experience, we play shorter, more succinct prompts for repeat interactions with the system in the same call (e.g. re-entering credit card information after making a mistake).
In order to track callers, we'll create and use a Set
and add the phone number of each incoming caller to it. Create a new file called customers.js
and add the following:
const customers = new Set()
module.exports = customers
We'll also need a data structure to capture important information about the caller. We'll do this by creating a caller object and passing it to our various routing files as needed. To do this, create another file called caller.js
and add the following:
let caller = {}
module.exports = caller
Step 5: Collect and confirm payment amount from caller
Next, we'll
- create routes to prompt the caller for their desired payment amount
- handle their DTMF input
- confirm their DTMF input
- handle any potential errors
To do this, create a new file called ccAmount.js
and add the following:
const express = require('express')
const customers = require('./customers')
const freeclimb = require('./freeclimb')
const host = process.env.HOST
const router = express.Router()
let errCount = 0
router.post('/ccAmountPrompt', (req, res) => {
const incoming = req.body.from
let script
if (customers.has(incoming)) {
script = 'So how much would you like to pay?' // this prompt is for customers interacting with a prompt for the second time
} else {
script =
'How much would you like to pay? Just key in the payment amount in US Dollars. For example, to make a payment of twenty dollars press two zero. To speak to an agent, press zero.'
}
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.getDigits(`${host}/ccAmount`, {
prompts: [
freeclimb.percl.say(script),
freeclimb.percl.say('The maximum amount is 100 dollars.') // limit the amount callers can choose to pay via phone
],
maxDigits: 3,
minDigits: 1,
flushBuffer: true
})
)
)
})
router.post('/ccAmount', (req, res) => {
const getDigitsResponse = req.body
const digits = getDigitsResponse.digits
const price = parseInt(digits)
if (digits == '0') {
res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/transfer`)))
} else if (price < 100) {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.redirect(`${host}/ccAmountConfirmationPrompt?amt=${price}`)
)
)
} else if (errCount >= 3) {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'You have exceeded the maximum number of retries allowed. Please wait while we connect you to an operator'
),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else {
errCount++
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say('Sorry the number you entered was invalid please try again'),
freeclimb.percl.redirect(`${host}/ccAmountPrompt`)
)
)
}
})
module.exports = router
To confirm the DTMF input from the caller and route them to based on their confirmation, create a new file called ccAmountConfirmation.js
and add the following:
const express = require('express')
const freeclimb = require('./freeclimb')
const caller = require('./caller')
const host = process.env.HOST
const router = express.Router()
let errCount = 0
let retries = 0
router.post('/ccAmountConfirmationPrompt', (req, res) => {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.getDigits(`${host}/ccAmountConfirmation?amt=${req.param('amt')}`, {
prompts: [
freeclimb.percl.say(
`Just to be sure thats ${req.param(
'amt'
)} dollars is that correct? Press 1 for yes and 2 for no`
)
],
maxDigits: 1,
minDigits: 1,
flushBuffer: true
})
)
)
})
router.post('/ccAmountConfirmation', (req, res) => {
const getDigitsResponse = req.body
const digits = getDigitsResponse.digits
const menuOpts = new Map([
[
'1',
{
script: 'Thank you',
redirect: `${host}/ccNumberPrompt`
}
],
[
'2',
{
script: 'Ok',
redirect: `${host}/ccAmountPrompt`
}
],
['0', { script: 'Redirecting you to an operator', redirect: `${host}/transfer` }]
])
if ((!digits || !menuOpts.get(digits)) && errCount < 3) {
errCount++
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say('Error'),
freeclimb.percl.redirect(
`${host}/ccAmountConfirmationPrompt?amt=${req.param('amt')}`
)
)
)
} else if (errCount >= 3 || retries >= 2) {
errCount = 0
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say('Please wait while we connect you to an operator'),
freeclimb.percl.pause(100),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else {
errCount = 0
if (digits === '2') {
retries++ // retries tracked separately from input errors
} else if (digits === '1') {
retries = 0
caller.paymentAmt = req.param('amt')
}
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(menuOpts.get(digits).script),
freeclimb.percl.redirect(menuOpts.get(digits).redirect)
)
)
}
})
module.exports = router
Step 6: Collect credit card information from caller
Next we'll need to create several files to collect all the credit card information we will need from the caller in order to properly process the payment. In this step we will collect:
- credit card number
- card expiry date
- card CVV
- card ZIP code
Make sure to secure your applications when collecting sensistive user data
Use
privacymode
to collect sensitive information, such as credit card data or health information, in order to secure customers' data and maintain PCI compliance.
Collect a credit card number
In this step, we'll prompt the caller for a credit card number and verify it. To do this, create a new file called ccNumber.js
and add the following:
const express = require('express')
const cardValidator = require('card-validator')
const customers = require('./customers')
const caller = require('./caller')
const freeclimb = require('./freeclimb')
const host = process.env.HOST
const router = express.Router()
let errCount = 0
router.post('/ccNumberPrompt', (req, res) => {
const incoming = req.body.from
let script
if (customers.has(incoming)) {
script = 'Okay, whats that card number?' // this prompt is for customers interacting with a prompt for the second time
} else {
script = 'To make a payment with a credit card, please enter the card number'
}
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.getDigits(`${host}/ccNumber`, {
prompts: [freeclimb.percl.say(script)],
maxDigits: 19,
minDigits: 1,
flushBuffer: true,
privacyMode: true // privacymode hides important information to maintain PCI compliance and avoid logging sensitive info
})
)
)
})
router.post('/ccNumber', (req, res) => {
const getDigitsResponse = req.body
const digits = getDigitsResponse.digits
const ccValidation = cardValidator.number(digits)
if (ccValidation.isValid) {
// ccNumber checked against a 3rd party library using the Luhn algorithm
caller.CVVType = ccValidation.card.code.size
caller.ccNum = digits
res.status(200).json(
freeclimb.percl.build(freeclimb.percl.redirect(`${host}/ccExpiryPrompt`))
)
} else if (digits == '0') {
res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/transfer`)))
} else if (errCount > 3) {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else if (errCount >= 3) {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else {
errCount++
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say('Sorry the number you entered was invalid please try again'),
freeclimb.percl.redirect(`${host}/ccNumberPrompt`)
)
)
}
})
module.exports = router
Collect credit card expiry date
In this step, we will prompt the user for their credit card expiry date and validate it. To create the routes that will handle this, create a new file called ccExpiry.js
and add the following:
const express = require('express')
const customers = require('./customers')
const caller = require('./caller')
const freeclimb = require('./freeclimb')
const host = process.env.HOST
const router = express.Router()
let errCount = 0
router.post('/ccExpiryPrompt', (req, res) => {
const incoming = req.body.from
let script
if (customers.has(incoming)) {
script = 'Okay, whats the four digit expiration date?' // this prompt is for customers interacting with a prompt for the second time
} else {
script =
'Enter the expiration date using two digits for the month and two digits for the year'
}
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.getDigits(`${host}/ccExpiry`, {
prompts: [freeclimb.percl.say(script)],
maxDigits: 4,
minDigits: 1,
flushBuffer: true,
privacyMode: true
})
)
)
})
router.post('/ccExpiry', (req, res) => {
const getDigitsResponse = req.body
const digits = getDigitsResponse.digits
if (digits.length == 4 && dateCheck(digits)) {
caller.ccExp = digits
res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/ccCVVPrompt`)))
} else if (digits == '0') {
res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/transfer`)))
} else if (errCount > 3) {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else if (errCount >= 3) {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else {
errCount++
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say('Sorry the date you entered was invalid please try again'),
freeclimb.percl.redirect(`${host}/ccExpiryPrompt`)
)
)
}
})
const dateCheck = digits => { // check that numbers entered are a valid date
const month = 1 + parseInt(digits.substring(0, 2))
const year = 2000 + parseInt(digits.substring(2))
const expDate = new Date(year, month)
const now = new Date()
return expDate - now >= 0
}
module.exports = router
Collect credit card CVV
Then, we will collect the credit card CVV and check to make sure it follows proper CVV format. To do this, create another file called ccCVV.js
and adding the following:
const express = require('express')
const customers = require('./customers')
const caller = require('./caller')
const freeclimb = require('./freeclimb')
const host = process.env.HOST
const router = express.Router()
let errCount = 0
router.post('/ccCVVPrompt', (req, res) => {
const incoming = req.body.from
let script
if (customers.has(incoming) || errCount > 0) {
script = 'Okay, whats the security code?' // this prompt is for customers interacting with a prompt for the second time
} else {
script = 'Almost done. Whats the security code?'
}
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.getDigits(`${host}/ccCVV`, {
prompts: [freeclimb.percl.say(script)],
maxDigits: 4, // most CVVs are 3 digits, with American Express having 4 digits
minDigits: 1,
flushBuffer: true,
privacyMode: true
})
)
)
})
router.post('/ccCVV', (req, res) => {
const getDigitsResponse = req.body
const digits = getDigitsResponse.digits
if (digits.length == caller.CVVType) {
caller.CVV = digits
res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/ccZipPrompt`)))
} else if (digits == '0') {
res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/transfer`)))
} else if (errCount > 3) {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else if (errCount >= 3) {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else {
errCount++
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
`Sorry im looking for the ${caller.CVVType} digit security code, please enter it now`
),
freeclimb.percl.redirect(`${host}/ccCVVPrompt`)
)
)
}
})
module.exports = router
Collect credit card ZIP code
The last piece of credit card authentication information we need to collect from the user is the ZIP code associated with the card. To create the routes that will prompt for and validate this input, create a new file called ccZip.js
and add the following:
const express = require('express')
const customers = require('./customers')
const caller = require('./caller')
const freeclimb = require('./freeclimb')
const host = process.env.HOST
const router = express.Router()
let errCount = 0
router.post('/ccZipPrompt', (req, res) => {
const incoming = req.body.from
let script
if (customers.has(incoming)) {
script = 'Okay, whats the Zip code?' // this prompt is for customers interacting with a prompt for the second time
} else {
script = 'Last thing, Whats the zip code for this card?'
}
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.getDigits(`${host}/ccZip`, {
prompts: [freeclimb.percl.say(script)],
maxDigits: 5,
minDigits: 1,
flushBuffer: true,
privacyMode: true
})
)
)
})
router.post('/ccZip', (req, res) => {
const getDigitsResponse = req.body
const digits = getDigitsResponse.digits
if (digits.length == 5) {
caller.Zip = digits
res.status(200).json(
freeclimb.percl.build(freeclimb.percl.redirect(`${host}/ccRecapPrompt`))
)
} else if (digits == '0') {
res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/transfer`)))
} else if (errCount > 3) {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else if (errCount >= 3) {
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else {
errCount++
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
`Please enter the 5 digit zip code of the billing address for the card you've entered.`
),
freeclimb.percl.redirect(`${host}/ccZipPrompt`)
)
)
}
})
module.exports = router
Recap information received back to caller for verification
Next, we'll create routes that play back information about the payment and collected credit card information—such as the payment amount and last four of the entered credit card number—for the caller to verify.
If the caller finds any issue with the information played back to them, they are able to restart and move through the call flow again starting from when they are prompted to enter the amount they wish to pay.
When they run through the call flow again, the call flow will recognize that they have already tried before thanks to the customer.js
and caller.js
files we created in Step 3: Handle customer data. Therefore, they will hear shorter, more succinct prompts to move them through the call flow faster.
To add the verification routes, create a new file called ccRecap.js
and add the following:
const express = require('express')
const caller = require('./caller')
const customers = require('./customers')
const freeclimb = require('./freeclimb')
const host = process.env.HOST
const router = express.Router()
let errCount = 0
let retries = 0
router.post('/ccRecapPrompt', (req, res) => {
customers.add(req.from)
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.getDigits(`${host}/ccRecap`, {
prompts: [
freeclimb.percl.say(
`Your payment will be ${
caller.paymentAmt
} dollars on the card ending in ${caller.ccNum.substring(
caller.ccNum.length - 4
)}, if thats correct press 1 to confirm if not press 2 to try again`
)
],
maxDigits: 1,
minDigits: 1,
flushBuffer: true
})
)
)
})
router.post('/ccRecap', (req, res) => {
const getDigitsResponse = req.body
const digits = getDigitsResponse.digits
const menuOpts = new Map([
[
'1',
{
script: 'Great',
redirect: `${host}/ccProcess`
}
],
[
'2',
{
script: 'Sorry. To make sure your information is correct, lets start again',
redirect: `${host}/ccAmountPrompt` // will return caller back to ccAmount.js to retry the call flow once more
}
],
['0', { script: 'Redirecting you to an operator', redirect: `${host}/transfer` }]
])
if ((!digits || !menuOpts.get(digits)) && errCount < 1) {
errCount++
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say('Error'),
freeclimb.percl.redirect(`${host}/ccRecapPrompt`)
)
)
} else if (errCount >= 1 || retries >= 1) {
errCount = 0
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say('Please wait while we connect you to an operator'),
freeclimb.percl.pause(100),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
} else {
errCount = 0
if (digits === '2') {
retries++ // retries tracked separately from input errors
} else if (digits === '1') {
retries = 0
}
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(menuOpts.get(digits).script),
freeclimb.percl.redirect(menuOpts.get(digits).redirect)
)
)
}
})
module.exports = router
Step 7: Process the credit card payment
To complete the payment, we'll need to process the credit card. If you were to actually process cards, you would need to integrate your app with the payment processor of your choosing. For testing purposes, we've created a file of test credit card data and used it to simulate payment processing in our ccProcess.js
file below.
Add sample credit card data for testing
In order to simulate a payment being sent to a real processor for authentication, we'll create a new file with test credit card numbers. Name this new file testCards.js
and add the following:
const testCards = new Map([
['4539512795366158', { result: 'success', message: 'Transaction processed successfully' }],
['341814945428581', { result: 'success', message: 'Transaction processed successfully' }],
['4007000000027', { result: 'failure', message: 'Invalid card number' }],
['373543870670161', { result: 'failure', message: 'Invalid card number' }],
['5478670693104129', { result: 'failure', message: 'Invalid card number' }],
['6011592457501696709', { result: 'failure', message: 'Invalid card Number' }],
['4929677130838456', { result: 'failure', message: 'Invalid Expiration Date' }],
['6011844071763663', { result: 'failure', message: 'Invalid CVV' }],
['375804818469449', { result: 'failure', message: 'Invalid CVV' }],
['5113791227910848', { result: 'failure', message: 'Invalid Zip code' }]
])
module.exports = testCards
Process the credit card payment
The processing file we've included in this how-to guide below uses sample data from testCards.js
to process credit card data and return a message to the user. To process the caller's credit card information, create a file called ccProcess.js
and add the the following content:
const express = require('express')
const caller = require('./caller')
const testCards = require('./testCards')
const freeclimb = require('./freeclimb')
const host = process.env.HOST
const router = express.Router()
let retries = 0
router.post('/ccProcess', (req, res) => {
const testResponse = testCards.get(caller.ccNum)
if (testResponse.result === 'success') { // from testCards.js
retries = 0
res.status(200).json(
freeclimb.percl.build(freeclimb.percl.redirect(`${host}/ccConfirmationMessage`))
)
} else if (retries < 1) {
retries++
// console.error(testResponse.message)
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say( // although the test credit card data we've supplied provides reasons a card may fail, we don't supply the explicit reason to callers in order to combat fraud
"We're having trouble processing your payment, please try again. Remember we Accept Visa, Discover, Mastercard and American Express cards"
),
freeclimb.percl.redirect(`${host}/ccNumberPrompt`)
)
)
} else {
retries = 0
// console.error(testResponse.message)
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
'The payment could not be processed at this time please wait while we transfer you to an operator'
),
freeclimb.percl.redirect(`${host}/transfer`)
)
)
}
})
module.exports = router
If you were to actually process cards, you would replace instances of testCards.get
with the payment processor of your choosing.
Step 8: Send payment confirmation to caller via SMS
Next, we'll define routes for the final step: sending a payment confirmation message via SMS to the caller. To do this, we'll first generate a random confirmation code for the SMS response we define later. Create a new file called confirmationCode.js
and add the following:
exports.generate = () => {
return Math.floor(100000 + Math.random() * 900000).toString() // create verification code as random 6 digit number represented with a string
}
Now that we're able to generate a random confirmation code from the helper function above, we can define the routes that send the confirmation code to the caller. To do so, create a new file called ccConfirmationMessage.js
and add the following:
const express = require('express')
const freeclimb = require('./freeclimb')
const codeGenerator = require('./confirmationCode')
const host = process.env.HOST
const fcNumber = process.env.FC_NUMBER
const router = express.Router()
router.post('/ccConfirmationMessage', (req, res) => {
const confirmationNumber = codeGenerator.generate()
res.status(200).json(
freeclimb.percl.build(
freeclimb.percl.say(
`Thank you for your payment, your confirmation number is ${confirmationNumber}, you will receive an sms shortly`
),
freeclimb.percl.sms(
fcNumber,
req.body.from,
`Your confirmation number is ${confirmationNumber}, thank you for using the Node pay-by-phone tutorial`
),
freeclimb.percl.redirect(`${host}/endcall`)
)
)
})
module.exports = router
Step 9: Finish and run your app
Lastly, we need to make all of the routes we've just defined accessible from index.js
. To do so, replace the current head of your index.js
file with the following:
require('dotenv-safe').config()
const bodyParser = require('body-parser')
const express = require('express')
const mainMenuRoutes = require('./mainMenu')
const ccAmtRoutes = require('./ccAmount')
const ccAmountConfirmationRoutes = require('./ccAmountConfirmation')
const ccNumberRoutes = require('./ccNumber')
const ccExpiryRoutes = require('./ccExpiry')
const ccCVVRoutes = require('./ccCVV')
const ccZipRoutes = require('./ccZIP')
const ccRecapRoutes = require('./ccRecap')
const ccProcessRoutes = require('./ccProcess')
const ccConfirmationMessageRoutes = require('./ccConfirmationMessage')
const freeclimb = require('./freeclimb')
const app = express()
const port = process.env.PORT || 3000
const host = process.env.HOST
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use('/', mainMenuRoutes)
app.use('/', ccAmtRoutes)
app.use('/', ccAmountConfirmationRoutes)
app.use('/', ccNumberRoutes)
app.use('/', ccExpiryRoutes)
app.use('/', ccCVVRoutes)
app.use('/', ccZipRoutes)
app.use('/', ccRecapRoutes)
app.use('/', ccProcessRoutes)
app.use('/', ccConfirmationMessageRoutes)
Finally, to hear your pay by phone app in action, run the following command at the command line:
yarn start
Once you do this, call the FreeClimb number associated with your application. From there you should be able to experience your pay by phone application.
If you are trying to run this sample app while using a trial account, make sure that the number you use to call and test your app is a verified number.
Congrats! You can now build your own PCI compliant pay by phone application. 🥳🥳
Updated 2 months ago