Handling Trial Periods with the Rapyd Collect API

By James Olaogun

A free trial is a lead generation technique that enables users to try out your product at no cost to them. Giving potential customers an opportunity to use your product and experience its value risk-free is a powerful marketing strategy, especially for SaaS products.

SaaS providers are encouraged to adopt this technique as it has the potential to increase the product’s conversion rate, customer satisfaction, and customer confidence in the product, as well as the benefit of influencing users before they make their purchasing decision. Free trials can be integrated seamlessly into any SaaS application with the help of a robust payment gateway system like Rapyd Collect.

In this article, you’ll learn how to implement a subscription model with the free trial technique for your SaaS product using the Rapyd Collect API. Rapyd Collect is a global payment acceptance platform that helps businesses transact with their locally-preferred payment methods. Rapyd is on a mission to liberate global commerce with all the tools you need for payments, payouts, and business everywhere.

Implementing a Free Trial in Your Node.js Application

In the following step-by-step tutorial, you’ll be developing a new application using Node.js for the backend, EJS templating engine for the frontend, and PostgreSQL for the database. For ease of understanding, the tutorial is divided into three main sections:

  • Setting up Node.js
  • Setting up the database
  • Developing the application

Each of these processes will come with detailed instructions. Before jumping in, here are a few prerequisites. To follow along with the tutorial, you’ll need:

  • Basic knowledge of Node.js (and Node installed on your local machine).
  • Basic knowledge of ExpressJS.
  • Basic knowledge of a database (this tutorial will make use of PostgreSQL—though you may use any database of your choice, installed on your local computer).
  • Knowledge of HTML, CSS, JS, and Ajax
  • A Rapyd account (you can sign up here if you don’t already have one).

Let’s get started!

Setting Up Node.js

The steps in this section focus on creating a new Node.js application, setting up the directory structures, installing the required node packages, and setting up the developmental server using nodemon.

Step 1: Initialize a New Node Application

Open your terminal or command line interface in your preferred directory. Create a new directory for the application called rapyd-trial-app and change the directory into the rapyd-trial-app:

mkdir rapyd-trial-app && cd rapyd-trial-app

Now initialize a new node application in the rapyd-trial-app directory. Ensure you use node version 16.13.0 and npm version 8.1.0. You can make use of Node Version Manager (NVM) to manage and use the node and npm versions mentioned above. Follow this guide for a step-by-step tutorial on how to use NVM.

npm init

Running this command will also walk you through creating a package.json file. It will ask you for the package name, version, description, entry point, test command, git repository, keywords, author, and license. Some of these requests come with a default adoption in brackets. Press Enter to go with the default option. For the blank options, you can either add a value and press Enter or simply press Enter to leave it blank, since the package.json file can always be edited at a later time.

Step 2: Install the Required Node Packages

The next step is to install the required dependencies. These include express, dotenv, moment, pg, body-parser, ejs, and https.

Run the following command from the root directory of the application to install the packages:

npm install express dotenv moment pg body-parser ejs https

You can proceed to set up the application directory structure by creating the following directories in the root the rapyd-trial-app directory:

  • /Model
  • /Controller
  • /Config
  • /Views
  • /Routes/index.js
  • /Helpers
  • /Public/css
  • /Public/js

Also, create .env and index.js files in the rapyd-trial-app folder. Your application directories should now look like this:

Step 3: Set Up the Basic Configuration

Go to the .env file you created in the root directory of the application and add the PORT and NODE_ENV variables:

NODE_ENV=development
PORT={Any_port_number _of_your_choice}

Next, create a server.js file in the /Config folder and add the following block of code:

const express = require('express');
const app = express();
app.use('/Public', express.static('public'));

module.exports = app;

Then add the block of code below to the index.js file in the root directory. This code uses express to expose your application to the defined port number:

const app = require('./Config/server');
const dotEnv = require('dotenv')
 
dotEnv.config()
 
const port = process.env.PORT || {YOUR_PORT_NUMBER};
app.listen(port, () => {console.log(`App listening on port ${port}`)})

Step 4: Set Up the Development Server

Finally, install nodemon to monitor and reload the application when any changes are made. Run the following command to install nodemon as a dev dependency:

npm install --save-dev nodemon

Then proceed to the package.json file and add the code block below to the "scripts": {} section—if you are following the tutorial to this point, it should be in line number six.

"start": "node index.js",
"dev": "nodemon index.js",

Now run the npm run dev command to start the application in dev mode. You should see something like this:

Setting Up the Database

The steps in this section describe setting up and configuring the database, creating all the required tables, and testing the database connection. As noted previously, this article will make use of PostgreSQL, so be sure you have it installed on your local computer.

Step 1: Create the Database Config File

Go to the /Config folder and create a new file called db-config.js. Add the following code block:

const dotEnv = require('dotenv');
const postgres = require('pg');
const { Pool } = postgres;
 
dotEnv.config()
 
const dbConfig = {
    user: process.env.DB_USER,
    host: process.env.DB_HOST,
    database: process.env.DB_NAME,
    password: process.env.DB_PASSWORD,
    port: process.env.DB_PORT
}
 
const pool = new Pool(dbConfig)
module.exports = pool;

Step 2: Create a New Database in PostgreSQL

Create a new database called rapyd_trial_app and set its user and password. Ensure you create the tables named customers, products, and subscriptions.

The customers table should have the following columns and constraints:

CREATE TABLE customers(
    id                                  SERIAL PRIMARY KEY,
    name                            varchar(255) NOT NULL,
    email                            varchar(255) NOT NULL,
    payment_method         varchar(255) NOT NULL,
    created_at                    TIMESTAMP NOT NULL
);

The products table should have the following columns and constraints:

CREATE TABLE products(
    id                                  SERIAL PRIMARY KEY,
    rapyd_product_id         varchar(255) NOT NULL,
    name                            varchar(255) NOT NULL,
    type                              varchar(11) NOT NULL,
    price	                  DOUBLE PRECISION NOT NULL,
    description                    varchar(255) NOT NULL,
    created_at                    TIMESTAMP NOT NULL
);

The subscriptions table should have the following columns and constraints:


CREATE TABLE subscriptions(
    id                                 SERIAL PRIMARY KEY,
    customer_id                 INT NOT NULL,
    product_id                  INT NOT NULL,
    rapyd_sub_id              varchar(255) NOT NULL,
    created_at                  TIMESTAMP NOT NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(id),
    FOREIGN KEY (product_id) REFERENCES products(id)
);

Now, head back to the .env and add the database variable. See the code block below:

DB_USER='postgres'
DB_HOST='localhost'
DB_NAME={database_name}
DB_PASSWORD={db_password}
DB_PORT={postgres_db_port}

Here, the {database_name} is rapyd_trial_app, {db_password} is the password set for the database user (the default password is most times an empty string), and {postgres_db_port} is the database port (the default port number is 5432).

Step 3: Test the Database Connection

Add the following code block to the index.js file to test the database connection:

const pool = require('./Config/db-config');
var sql = `SELECT * FROM subscriptions`
pool.query(sql, (error, response) => {
        if (error) return console.log(error);
        console.log(response.rows);
      }
    )

Save all files and run npm run start if you have deactivated the nodemon server. It should output an empty array to the terminal.

Developing the Application

The steps in this section focus on building the major components of the demo application. The components include product creation, customer creation, and subscription management.

Step 1: Create Subscription Products

First, go to /Helpers and create a new file called rapydUtilities.js. Add the code block below, which contains all the core functions used to integrate (make requests to) the Rapyd API:

const https = require('https');
const crypto = require('crypto');
const dotEnv = require('dotenv');
dotEnv.config()

const secretKey = process.env.SECRETE_KEY;
const accessKey = process.env.ACCESS_KEY;
const log = false;

async function makeRequest(method, urlPath, body = null) {

    try {
        httpMethod = method;
        httpBaseURL = "sandboxapi.rapyd.net";
        httpURLPath = urlPath;
        salt = generateRandomString(8);
        idempotency = new Date().getTime().toString();
        timestamp = Math.round(new Date().getTime() / 1000);
        signature = sign(httpMethod, httpURLPath, salt, timestamp, body)

        const options = {
            hostname: httpBaseURL,
            port: 443,
            path: httpURLPath,
            method: httpMethod,
            headers: {
                'Content-Type': 'application/json',
                salt: salt,
                timestamp: timestamp,
                signature: signature,
                access_key: accessKey,
                idempotency: idempotency
            }
        }

        return await httpRequest(options, body, log);
    }
    catch (error) {
        console.error("Error generating request options");
        throw error;
    }
}

function sign(method, urlPath, salt, timestamp, body) {

    try {
        let bodyString = "";
        if (body) {
            bodyString = JSON.stringify(body);
            bodyString = bodyString == "{}" ? "" : bodyString;
        }

        let toSign = method.toLowerCase() + urlPath + salt + timestamp + accessKey + secretKey + bodyString;
        log && console.log(`toSign: ${toSign}`);

        let hash = crypto.createHmac('sha256', secretKey);
        hash.update(toSign);
        const signature = Buffer.from(hash.digest("hex")).toString("base64")
        log && console.log(`signature: ${signature}`);

        return signature;
    }
    catch (error) {
        console.error("Error generating signature");
        throw error;
    }
}

function generateRandomString(size) {
    try {
        return crypto.randomBytes(size).toString('hex');
    }
    catch (error) {
        console.error("Error generating salt");
        throw error;
    }
}

async function httpRequest(options, body) {

    return new Promise((resolve, reject) => {

        try {
            
            let bodyString = "";
            if (body) {
                bodyString = JSON.stringify(body);
                bodyString = bodyString == "{}" ? "" : bodyString;
            }

            log && console.log(`httpRequest options: ${JSON.stringify(options)}`);
            const req = https.request(options, (res) => {
                let response = {
                    statusCode: res.statusCode,
                    headers: res.headers,
                    body: ''
                };

                res.on('data', (data) => {
                    response.body += data;
                });

                res.on('end', () => {

                    response.body = response.body ? JSON.parse(response.body) : {}
                    log && console.log(`httpRequest response: ${JSON.stringify(response)}`);

                    if (response.statusCode !== 200) {
                        return reject(response);
                    }

                    return resolve(response);
                });
            })
            
            req.on('error', (error) => {
                return reject(error);
            })
            
            req.write(bodyString)
            req.end();
        }
        catch(err) {
            return reject(err);
        }
    })

}

exports.makeRequest = makeRequest;

Next, grab your API Keys from your Rapyd account by logging in to your Rapyd dashboard, going to the sidebar, and clicking Developers > Credentials Details.

Remember to switch to the sandbox environment before you copy your API keys.

Then open your .env file and add your Rapyd keys:

SECRETE_KEY={YOUR_SECRET_KEY}
ACCESS_KEY={YOUR_ACCESS_KEY}

Then, you’ll need to create a seeder file. To do that, go to the /Config folder and create a new file called dataSeeder.js. Add the code block below, which will create three products in Rapyd and save them—including the Rapyd product id—in the database.

const pool = require("./db-config");
const {makeRequest} = require('../Helpers/rapydUtilities')

const products = [
    {
        name: 'Product 1',
        type:'services',
        price: 30000,
        description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' 

    },
    {
        name: 'Product 2',
        type:'services',
        price: 50000,
        description: 'Etiam id dictum neque. Etiam at risus ac libero' 

    },
    {
        name: 'Product 3',
        type:'services',
        price: 90000,
        description: 'Pellentesque id nisi id ligula convallis scelerisque facilisis a dolor' 
    }
]

products.forEach(product => {

    const body = {
        name: product.name,
        type: product.type
    };

    makeRequest('POST', '/v1/products', body).then((data)=>{

        return new Promise((resolve, reject) => {
            pool.query(
                `INSERT INTO products(rapyd_product_id, name, type, price, description, created_at) 
                VALUES ($1, $2, $3, $4, $5, $6)`, 
                [data.body.data.id, product.name, product.type, product.price, product.description, new Date(Date.now())],
                (error, response) => {
        
                if (error) return reject(error);
        
                resolve(response.rows[0])
            }
            
            );
        }).catch(error => console.log(error))

    })
    .catch((error)=>{
        console.log('error: ',error);
    });

});

Finally, to run the dataSeeder, open the package.json file and add "seed": "node ./Config/dataSeeder.js" to the script section. Your script section should look like the example below:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js",
    "dev": "nodemon index.js",
    "seed": "node ./Config/dataSeeder.js"
  },

Then run the following command to execute the script that will create the products:

npm run seed

Step 2: Develop the Application and Integrate Rapyd

This step involves creating the application interface to display the products, the form to collect subscription details, and the form to cancel the subscription. To do that, first change the directory to the /Views folder and create an ejs file named subscription.ejs. Paste the code block below into the file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Products</title>
    <link rel="stylesheet" href="public/css/style.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
</head>
<body>
    <div class="wrapper">
		<h2>Product List</h2>
		<p>Click the subscribe button to subscribe to your preferred product and enjoy one week free trial.</p>

        <% for(var i=0; i < products.length; i++) { %>
		<div class="product">

            <input type="hidden" id="name<%= products[i].id %>" name="name" value="<%= products[i].name %>">
            <input type="hidden" id="type<%= products[i].id %>" name="type" value="<%= products[i].type %>">
            <input type="hidden" id="description<%= products[i].id %>" name="description" value="<%= products[i].description %>">
            <input type="hidden" id="rapyd_product_id<%= products[i].id %>" name="rapyd_product_id" value="<%= products[i].rapyd_product_id %>">
            <input type="hidden" id="price<%= products[i].id %>" name="price" value="<%= products[i].price %>">

			<div class="title">
				<h3><%= products[i].name %></h3>
			</div>
			<div class="content">

                <p><%= products[i].description %></p>
                <br>
                <p class="product_price">$<%= products[i].price / 100 %>.00</p>

			</div> 
				
			<div class="cart" onclick="showEmailModal('<%= products[i].id %>')">
				<p>Subscribe</p>
			</div>
		</div>
        <% } %>

	</div>

  <div class="cancel-subscription">
    <button type="button" class="btn btn-danger" onclick="showCancelSubModal()">Cancel Subscription</button>
  </div>

  <div class="modal fade" id="signupModal" tabindex="-1" role="dialog" aria-labelledby="signupModalLabel" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="signupModalLabel">Subscribe</h5>
          </div>
          <div class="modal-body">
              <p class="text text-primary">Free trial will be offered for a week before you are charged.</p>
              <form id="subscription-form">
                  <div class="form-group">
                      <input type="hidden" name="form_product_id" id="form_product_id">
                      <input type="hidden" name="form_rapyd_product_id" id="form_rapyd_product_id">
                      <input type="hidden" name="form_price" id="form_price">
                      <input type="hidden" name="form_product_type" id="form_product_type">
                      <label for="cust_full_name">Full Name</label>
                      <input type="name" class="form-control" id="cust_full_name" placeholder="Full name">
                      <label for="cust_email_address">Email address</label>
                      <input type="email" class="form-control" id="cust_email_address" aria-describedby="emailHelp" placeholder="Enter email">
                      <small id="emailHelp" class="form-text text-muted">Enter your email to sign up</small>
                      <div class="form-group">
                          <label for="cust_payment_method">Select payment method</label>
                          <select class="form-control" id="cust_payment_method">
                              <option value="">Select payment method</option>
                              <% for(var i=0; i < paymentMethods.length; i++) { %>
                                  <% if (paymentMethods[i].category == 'card') { %>
                                  <option value="<%= paymentMethods[i].type %>"><%= paymentMethods[i].name %></option>
                                  <% } %>
                              <% } %>
                          </select>
                        </div>
                        <br>
                      <button type="button" class="btn btn-secondary btn-sm" id="getPaymentMethodFields">Get Fields</button>
                      <p id="getPaymentMethodFieldsLoading" style="display: none;">Loading.....</p>
                      <div id="paymentMethodFieldsDiv">
                        <!-- // -->
                      </div>
                  </div>
              </form>
              <br>
              <div class="alert alert-success" id="subscription-created-success" style="display: none;">
                Subscription created with one week trial. Feel free to cancel the subscription before your trial ends. <br>
	    Note: The page will reload in 5 seconds.
              </div>
              <div class="alert alert-danger" id="subscription-created-error" style="display: none;">
                <!-- // -->
              </div>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" onclick="closeEmailModal()">Close</button>
            <button type="button" class="btn btn-primary" id="confirmSubscription">Submit</button>
            <button type="button" class="btn btn-info" id="confirmSubscriptionLoading" style="display: none;">Loading</button>
          </div>
        </div>
      </div>
  </div>

  <div class="modal fade" id="CancelSubModal" tabindex="-1" role="dialog" aria-labelledby="CancelSubModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="CancelSubModalLabel">Cancel Subscription</h5>
        </div>
        <div class="modal-body">
            <p class="text text-primary">Enter your email address to cancel subscription.</p>
            <form id="cancel-subscription-form">
                <div class="form-group">
                    <label for="cust_email_address_cs">Email address</label>
                    <input type="email" class="form-control" id="cust_email_address_cs" aria-describedby="emailHelp" placeholder="Enter email">
                </div>
            </form>
            <br>
            <div class="alert alert-success" id="subscription-canceled-success" style="display: none;">
              Subscription canceled
            </div>
            <div class="alert alert-danger" id="subscription-canceled-error" style="display: none;">
              <!-- // -->
            </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" onclick="closeCancelSubModal()">Close</button>
          <button type="button" class="btn btn-danger" id="cancelSubscriptionSubmit">Submit</button>
          <button type="button" class="btn btn-info" id="cancelSubscriptionLoading" style="display: none;">Loading</button>
        </div>
      </div>
    </div>
</div>

</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"></script>
<script src="public/js/script.js"></script>
</html>

Next, you’ll add the required style and script to the ejs file. To do so, change the directory to the /Public folder. In the /css folder, create a new css file named style.css and paste in the code block below:

* {
	margin: 0;
	padding: 0;
	list-style-type: none;
}

body {
	background-color: #eee;
}

.wrapper {
	margin: 0 auto;
	text-align: center;
}

.wrapper h2 {
	margin: 20px 0 10px 0;
}

.product{
	width: 300px;
    height: 320px;
	position: relative;
	top: 50px;
	left: 0px;
	border-radius: 10px;
	box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
	margin: 20px 10px 0 60px;
	background-color: #fff;
	display: inline-block;
	transform-origin: 100% 0;
}

.product_price {
    color: #1e611e;
}

.product {
	margin-left: 0;
}

.product .title  {
	width: 100%;
	height: 90px;
	line-height: 25px;
	padding: 25px 25px;
	box-sizing: border-box;
	color: #346;
	margin: 0;
	z-index: 20;
}

.product .content {
	width: 260px;
    min-height: 150px;
	margin-left: 20px;
	position: relative;
	left: 0;
	top: 0;
	overflow: hidden;
}

.product .cart {
	text-align: center;
	padding: 5px 10px;
	width: 230px;
	border-radius: 5px;
	margin: 15px 0 0 23px;
	background-color: #000;
	cursor: pointer
}
.product .cart p {
	color:#fff;
    margin: 0px;
}

.product .cart:hover {
	background-color: #346;
	color: #fff;
	transition: 1s;
}

.cancel-subscription{
	display: flex;
    justify-content: center;
    margin-top: 100px;
}

Then go to the /js folder, create a new JS file named script.js, and add the code block below:

function showEmailModal(product_id){
    $("#signupModalLabel").text($("#name"+product_id).val())
    $("#form_product_id").val(product_id)
    $("#form_rapyd_product_id").val($("#rapyd_product_id"+product_id).val())
    $("#form_product_type").val($("#type"+product_id).val())
    $("#form_price").val($("#price"+product_id).val())
    $('#signupModal').modal('show')
}

function closeEmailModal(){
    $('#signupModal').modal('hide')
}

function showCancelSubModal(){
    $('#CancelSubModal').modal('show')
}

function closeCancelSubModal(){
    $('#CancelSubModal').modal('hide')
}

$(document).ready(function() {

    $("#getPaymentMethodFields").click(function(){
        $("#getPaymentMethodFieldsLoading").show()
        $("#getPaymentMethodFields").hide()

        $.ajax('/payment-method-fields', {
            type: 'POST',
            data: {
                'product_id': $("#form_product_id").val(),
                'rapyd_product_id': $("#form_rapyd_product_id").val(),
                'price': $("#form_price").val(),
                'customer_email': $("#cust_email_address").val(),
                'full_name': $("#cust_full_name").val(),
                'product_type': $("#form_product_type").val(),
                'payment_method': $("#cust_payment_method").val()
            }, 
            success: async function (data, status, xhr) {
                var fields = data.data.fields
                var html = ''

                await fields.forEach(field => {
if (field.instructions) {
                    		html += `<div class="form-group">
                                	<label for="field${field.name}">${field.instructions}</label>
                                	<input type="text" class="form-control" id="field${field.name}" placeholder="${field.instructions}">
                            	</div>`
		}
                });

                $("#paymentMethodFieldsDiv").append(html)
                $("#getPaymentMethodFieldsLoading").hide()
                $("#getPaymentMethodFields").hide()

            },
            error: function (jqXhr, textStatus, errorMessage) {
                $("#getPaymentMethodFieldsLoading").hide()
                $("#getPaymentMethodFields").show()
                console.log('Error' + errorMessage);
            }
        });
    });


    $("#confirmSubscription").click(function(){
        $("#confirmSubscriptionLoading").show()
        $("#confirmSubscription").hide()

        $.ajax('/create-subscription', {
            type: 'POST',
            data: {
                'product_id': $("#form_product_id").val(),
                'rapyd_product_id': $("#form_rapyd_product_id").val(),
                'price': $("#form_price").val(),
                'customer_email': $("#cust_email_address").val(),
                'full_name': $("#cust_full_name").val(),
                'product_type': $("#form_product_type").val(),
                'payment_method': $("#cust_payment_method").val(),

                'fieldnumber': $("#fieldnumber").val(),
                'fieldexpiration_month': $("#fieldexpiration_month").val(),
                'fieldexpiration_year': $("#fieldexpiration_year").val(),
                'fieldname': $("#fieldname").val(),
                'fieldcvv': $("#fieldcvv").val()
            }, 
            success: function (data, status, xhr) {

                $("#subscription-form").hide()
                $("#confirmSubscriptionLoading").hide()
                $("#subscription-created-success").show()

	    setTimeout(location.reload(), 5000);

            },
            error: function (data, textStatus, errorMessage) {
                $("#confirmSubscriptionLoading").hide()
                $("#confirmSubscription").show()
                $("#subscription-created-error").text(data.responseJSON.message)
                $("#subscription-created-error").show()
            }
        });
    });

    $("#cancelSubscriptionSubmit").click(function(){
        $("#cancelSubscriptionLoading").show()
        $("#cancelSubscriptionSubmit").hide()

        $.ajax('/cancel-subscription', {
            type: 'POST',
            data: {
                'customer_email': $("#cust_email_address_cs").val(),
            }, 
            success: function (data, status, xhr) {

                $("#cancel-subscription-form").hide()
                $("#cancelSubscriptionLoading").hide()
                $("#subscription-canceled-success").show()

            },
            error: function (data, textStatus, errorMessage) {
                $("#cancelSubscriptionLoading").hide()
                $("#cancelSubscriptionSubmit").show()
                $("#subscription-canceled-error").text(data.responseJSON.message)
                $("#subscription-canceled-error").show()
            }
        });
    });

});

Next, you will set up the routes: one GET route to display the application page and three POST routes to get payment fields, create a subscription, and cancel a subscription.

First, import the route script into the index.js file by adding require('./Routes/index') to the root folder index.file. Then go to the /Routes folder, and add the code block below to the index.js file:

const app = require('../Config/server')
const path = require('path');
const dotEnv = require('dotenv');
const bodyParser = require('body-parser');
const subscriptionController = require('../Controller/subscriptionController')

dotEnv.config()

app.set('views', path.join(__dirname, '../Views'))
app.set('view engine', 'ejs')

app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());

app.get('/', (req, res, next) => {subscriptionController.viewProducts(req, res, next)})
app.post('/payment-method-fields', (req, res, next) => {subscriptionController.getPaymentMethodFields(req, res, next)})
app.post('/create-subscription', (req, res, next) => {subscriptionController.createSubscription(req, res, next)})
app.post('/cancel-subscription', (req, res, next) => {subscriptionController.cancelSubscription(req, res, next)})

Now you’ll add the subscription controller. Go to the /Controller folder, create a new JS file named subscriptionController.js, and add the code block below to the file. The code block contains all the functions that communicate with Rapyd APIs via the rapydAPICalls.js helper class (which you will create later in this tutorial). It also renders the subscription.ejs view and manages data (customers and subscription data) in the database via the DBQueries.js model class (which you will create later in this tutorial).

const rapydAPICalls = require('../Helpers/rapydAPICalls')
const DBQueries = require('../Model/DBQueries')

class subscriptionController{
    constructor(){

    }

    static viewProducts(req, res, next){
        
         DBQueries.getDataFromBDTable('products').then( async (data)=>{

                const paymentMethod = await rapydAPICalls.getPaymentMethods('USD', 'US');
                res.render('Subscription', {
                    products: data,
                    paymentMethods:  paymentMethod
                })

        }).catch((error) =>  console.log(error))

    }

    static async getPaymentMethodFields(req, res, next){

        const getRequiredFields = await rapydAPICalls.getRequiredFields(req.body.payment_method);

        return res.json({status: 200, message: 'action successful', data: getRequiredFields});

    }

    static async createSubscription(req, res, next){

        const customerDetails = {
            name: req.body.full_name,
            email: req.body.customer_email,
            payment_method: {
                type: req.body.payment_method,
                fields: {
                  number: req.body.fieldnumber,
                  expiration_month: req.body.fieldexpiration_month,
                  expiration_year: req.body.fieldexpiration_year,
                  cvv: req.body.fieldcvv,
                  name: req.body.fieldname,
                },
                complete_payment_url: 'https://complete.rapyd.net/',
                error_payment_url: 'https://error.rapyd.net/'
              }
        };

        //check if customer exist before subscription.
        const checkCustomer = await DBQueries.findOne('customers', 'email', req.body.customer_email);

        if (checkCustomer.length > 0) {
            return res.status(400).json({status: 400, message: 'Customer email already exist'});
        }

        //created customer in the DB
        const createCustomerInDB = await DBQueries.createCustomer(customerDetails);

        if (createCustomerInDB.status == 'success') {
            // create customer in Rapyd
            const customerDetailsResponse = await rapydAPICalls.createCustomerProfile(customerDetails);

            if (customerDetailsResponse.status.status == 'SUCCESS') {
                // console.log('customerDetailsResponse: ', customerDetailsResponse.data)

                const planDetails = {
                    currency: 'USD',
                    interval: 'month',
                    product: req.body.rapyd_product_id,
                    amount: req.body.price / 100,
                    nickname: 'Monthly Subscription',
                    usage_type: 'licensed'
                };

                //create plan
                const planDetailsResponse = await rapydAPICalls.createPlan(planDetails);

                if (customerDetailsResponse.status.status == 'SUCCESS') {
                    // console.log('planDetailsResponse: ', planDetailsResponse.data)

                    const subscriptionDetails = {
                        customer: customerDetailsResponse.data.id,
                        billing: 'pay_automatically',
                        billing_cycle_anchor: '',
                        cancel_at_period_end: true,
                        coupon: '',
                        days_until_due: null,
                        payment_method: customerDetailsResponse.data.default_payment_method,
                        subscription_items: [
                          {
                            plan: planDetailsResponse.data.id,
                            quantity: 1
                          }
                        ],
                        metadata: {
                          merchant_defined: true
                        },
                        tax_percent: 10.5,
                        trial_start: parseInt((new Date(Date.now()).getTime() / 1000).toFixed(0)),
                        trial_period_days: 7,
                        plan_token: ''
                    };
                    
                    // create subscription in Rapyd
                    const subscriptionResponse = await rapydAPICalls.subscribe(subscriptionDetails);

                    if (subscriptionResponse.status.status == 'SUCCESS') {
                        // console.log('subscriptionResponse: ', subscriptionResponse.data)

                        const subscriptionInDbDetails = {
                            customer_id: createCustomerInDB.response.rows[0].id,
                            rapyd_sub_id: subscriptionResponse.data.id,
                            product_id: req.body.product_id
                        };

                        //create subscription in DB
                        const createSubscriptionInDB = await DBQueries.createSubscription(subscriptionInDbDetails);

                        if (createSubscriptionInDB.status == 'success') {
                            return res.json({status: 200, message: 'subscription successful'});
                        }
                    }


                }


            }   
        }

        
    }

    static async cancelSubscription(req, res, next){

        const customerDetails = {
            name: req.body.full_name,
            email: req.body.customer_email,
            payment_method: {
                type: req.body.payment_method,
                fields: {
                  number: req.body.fieldnumber,
                  expiration_month: req.body.fieldexpiration_month,
                  expiration_year: req.body.fieldexpiration_year,
                  cvv: req.body.fieldcvv,
                  name: req.body.fieldname,
                },
                complete_payment_url: 'https://complete.rapyd.net/',
                error_payment_url: 'https://error.rapyd.net/'
              }
        };

        //check if customer exist before subscription.
        const checkSubscription = await DBQueries.getSubscriptionId(req.body.customer_email);

        if (checkSubscription.length == 0) {
            return res.status(400).json({status: 400, message: 'You presently do not have a subscription'});
        }

        const cancelResponse = await rapydAPICalls.cancelSubscription(checkSubscription[0].rapyd_sub_id);

        if (cancelResponse.status.status == 'SUCCESS') {
            // console.log(cancelResponse)
            return res.json({status: 200, message: 'action successful'});
        }
        
    }

} 

module.exports = subscriptionController;

Next, create the rapydAPICalls.js helper class by changing the directory to the /Helpers folder, creating a new JS file called rapydAPICalls.js, and adding the code block below into the file. This code block is a class containing all the functions that communicate with Rapyd APIs.

const {makeRequest} = require('../Helpers/rapydUtilities')

class rapydAPICalls{
    constructor(){

    }

    static async getPaymentMethods(currency, country){

        var paymentMethod;
        await makeRequest('GET', `/v1/payment_methods/country?country=${country}&currency=${currency}`).then((data)=>{

            paymentMethod = data

        }).catch((error) => console.log(error))

        return paymentMethod.body.data

    }

    static async getRequiredFields(payment_type){

        var requiredFields;
        await makeRequest('GET', `/v1/payment_methods/required_fields/${payment_type}`).then((data)=>{

            requiredFields = data

        }).catch((error) => console.log(error))

        return requiredFields.body.data

    }

    static async createCustomerProfile(body){
        
        var customerDetails;
        await makeRequest('POST', `/v1/customers`, body).then((data)=>{

            customerDetails = data

        }).catch((error) => console.log(error))

        return customerDetails.body

    }

    static async createPlan(body){
        
        var planDetails;
        await makeRequest('POST', `/v1/plans`, body).then((data)=>{

            planDetails = data

        }).catch((error) => console.log(error))

        return planDetails.body

    }


    static async subscribe(body){
        
        var subscription;
        await makeRequest('POST', `/v1/payments/subscriptions`, body).then((data)=>{

            subscription = data

        }).catch((error) => console.log(error))

        return subscription.body

    }

    static async cancelSubscription(sub_id){

        var response;
        await makeRequest('DELETE', `/v1/payments/subscriptions/${sub_id}`).then((data)=>{

            response = data

        }).catch((error) => console.log(error))

        return response.body

    }

} 

module.exports = rapydAPICalls;

Finally, create the DBQueries.js model class by changing the directory to the /Model folder, creating a new JS file named DBQueries.js, and adding the code block below to the file. This code block is a class that contains functions, with each function containing a database query.

const pool = require('../Config/db-config');


class DBQueries {
    constructor(){

    }

    static getDataFromBDTable(table_name){

        return new Promise((resolve, reject) => {
            pool.query(
                `SELECT * FROM ${table_name} ORDER BY name ASC`, 
                (error, response) => {

                if (error) return reject(error);

                resolve(response.rows)
            });
        })
    }

    static findOne(table_name, column_name, value){
        var sql = `SELECT * FROM ${table_name} WHERE ${column_name} = $1`
        var sqlValue = [value]
  
        return new Promise((resolve, reject) => {
            pool.query(
              sql,
              sqlValue,
              (error, response) => {
                if (error) return reject(error);
  
                resolve(response.rows);
              }
            )
          });
    }

    static createCustomer(customer){

        return new Promise((resolve, reject) => {
            pool.query(
                `INSERT INTO customers(name, email, payment_method, created_at) 
                VALUES ($1, $2, $3, $4) RETURNING id`, 
                [customer.name, customer.email, customer.payment_method.type, new Date(Date.now())],
                (error, response) => {
        
                if (error) return reject(error);
        
                resolve({'status': 'success', 'response': response})
            }
            
            );
        })
    }

    static createSubscription(subscription){

        return new Promise((resolve, reject) => {
            pool.query(
                `INSERT INTO subscriptions(customer_id, product_id, rapyd_sub_id, created_at) 
                VALUES ($1, $2, $3, $4) RETURNING id`, 
                [subscription.customer_id, subscription.product_id, subscription.rapyd_sub_id, new Date(Date.now())],
                (error, response) => {
        
                if (error) return reject(error);
        
                resolve({'status': 'success', 'response': response})
            }
            
            );
        })
    }

    static getSubscriptionId(email){

        var sql = `SELECT * FROM customers c
        JOIN subscriptions s ON s.customer_id = c.id 
        WHERE c.email = $1 `
        var sqlValue = [email]
  
        return new Promise((resolve, reject) => {
            pool.query(
              sql,
              sqlValue,
              (error, response) => {
                if (error) return reject(error);
  
                resolve(response.rows);
              }
            )
          });
    }
    
}

module.exports = DBQueries;

You can now proceed to save all the files and run npm run dev to start the nodemon server. In the next section, you’ll see the final result. If you’ve followed the tutorial using the same technology (database and templating engine), you should get the same results.

Demonstrating the Application

Congratulations for making it this far in the tutorial! Now go to your browser and load the application via http://127.0.0.1:{YOUR_PORT_NUMBER}.

You should see a page displaying all the products, with each product having a “Subscribe” button. Below the products, you should see a “Cancel Subscription” button.

Click on any of the products, and you should see a modal requesting your customer’s full name, email address, preferred payment method, and a “Get Fields” button.

Fill out the form appropriately and click Get Fields. You should see more fields asking for card details. Since it is for testing purposes, you can use the test card below:

  • Card number: 4111111111111111
  • Card expiration month: 12
  • Card expiration year: 24
  • Card holder name: {Your preferred name}
  • Card CVV: 123

When you are done filling out the form, click the Submit button. A new subscription should be created for the customer, and you will see a success message:

Click the Close button to close the modal.

Presently, you might be unable to view the subscriptions on your Rapyd dashboard because the feature is in closed beta. Feel free to contact the Rapyd Support team if you wish to join the closed beta program. However, you can call the List Subscriptions API to view the subscriptions and their status. The subscription you have just created should have a “trialing” status:

To cancel the subscription, click the Cancel Subscription button. A modal will pop up asking for the user’s email:

Enter the email address of the customer that just subscribed, and click Submit. You should see a success message if the email address entered has a subscription:

You can call the List Subscriptions API to view the subscriptions, and it should have a “canceled” status:

Finally, click the Close button to close the modal.

Conclusion

This article provided a quick overview of the free trial subscription feature and a step-by-step tutorial on how Rapyd, a digital payment system, can enable your company to integrate free trials into your Node.js application. Rapyd is a robust payment gateway solution that empowers businesses to accept both local and international payments seamlessly.

You can access the complete code for this tutorial on GitHub.

Questions? You can connect with other developers and ask anything fintech here in the Rapyd Developer Community.

1 Like