# Step 6 - Monetize your application

By
The Quable Team

Requirements
Quable Partner JS
Create a payment
Check customer payment
Billing middleware


# Monetize your application

In this tutorial we'll guide you on how to monetize with your app! Whether you're considering a single-purchase approach or a recurring revenue, this guide will show you how to effectively implement these monetization methods.

# Requirements

We'll use the quable-partner-js package for handling financial operations :

yarn add @quable/quable-partner-js
or
npm install @quable/quable-partner-js

We'll also use the uuid package to generate unique ids :

yarn add uuid
yarn add -D @types/uuid
or
npm install uuid
npm install -D @types/uuid

# Quable Partner JS

The payment module of the Quable Partner JS package provides the necessary tools to manage financial operations. This includes generating payment links and retrieving payment information, giving us control over financial transactions.


# Setup billing configuration

# Create billing data structure

Add theses new types interfaces in helper/types.ts:

interface ProductPriceInput {
  name: string;
  description?: string;
  images?: string[];
}

interface PriceDataInput {
  product: ProductPriceInput;
  amount: number;
  currency: string;
}

export interface PriceInput {
  priceId?: string;
  priceData?: PriceDataInput;
}

export interface BillingConfig {
  quantity: number;
  appId: string;
  price: PriceInput;
  isTest?: boolean;
  successUrl?: string;
  interval?: "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";
  trialPeriod?: number;
}

# Create billing config file

Create a directory in the helper folder named billing. Add insid configs.ts with the following code:

import { BillingConfig } from "../types";

export const appId = process.env.QUABLE_PARTNER_APP_ID ?? "quable-app-template";
export const billingConfigs: BillingConfig[] = [
  {
    appId,
    price: {
      priceData: {
        product: {
          name: "app-pay-monthly",
          description: "Application pay subscription test",
          images: [],
        },
        amount: 15,
        currency: "EUR",
      },
    },
    quantity: 1,
    isTest: true,
    interval: "MONTHLY",
  },
  {
    appId,
    price: {
      priceData: {
        product: {
          name: "app-pay-onetime",
          description: "Application pay onetime payment test",
          images: [],
        },
        amount: 1500,
        currency: "EUR",
      },
    },
    quantity: 1,
    isTest: true,
  },
];

With this code, we define two payment options: one for a monthly subscription called app-pay-monthly priced at 15 euros per month, and another for a one-time payment option named app-pay-onetime priced at 1500 euros. Both options share the same application ID for identification purpose.


# Display billing price

# Create billing controller

We're now creating a controller to manage incoming requests.
Create a file named billing.controller.ts in the controllers folder and paste the given code:

import { Request, Response } from "express";

class BillingController {
  public index = async (_req: Request, res: Response) => {
    return res.render("pages/billing");
  };
}
export const billingController = new BillingController();

# Create billing routes

Create a new file named billing.routes.ts in the routes directory and paste the following code:

import { billingController } from "src/controllers/billing.controller";
import { Router } from "express";

const billingRouter = Router();

billingRouter.get("/", billingController.index);

export default billingRouter;

After defining the routes, you must register the route in index.ts.

import billingRouter from "./routes/billing.routes";
/*
... code
*/
app.use("/:customer/billing", billingRouter);

# Create billing view

With the backend now configured, let's create a view to bill our user. Create a file called index.ejs inside a new folder views/pages/billing and insert the given code:

<div id="billing-page">
  <h5>Choose your Pricing plan</h5>
  <div class="row justify-content-center">
    <div class="col-md-6 col-xl-4">
      <div class="plan-box card">
        <div class="p-4 card-body price-item">
          <h5>Pay Monthly</h5>
          <div class="py-4">
            <h2>
              <sup>
                <small>€</small>
              </sup>
              15 / <span class="font-size-13">Month</span>
            </h2>
          </div>
          <div class="text-center plan-btn">
            <button
              class="btn btn-primary btn-sm waves-effect waves-light"
              priceid="app-pay-monthly"
            >
              Subscribe
            </button>
          </div>
        </div>
        <div class="p-4 card-body price-item">
          <h5>Onetime Payment</h5>
          <div class="py-4">
            <h2>
              <sup>
                <small>€</small>
              </sup>
              1500/ <span class="font-size-13">Live time</span>
            </h2>
          </div>
          <div class="text-center plan-btn">
            <button
              class="btn btn-primary btn-sm waves-effect waves-light"
              priceid="app-pay-onetime"
            >
              Subscribe
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Finally, update the index.ejs template with a link to the document page. You should also include the authenticated customer variable the URL.

<div class="center">
  <img
    src="/images/quable-logo.png"
    alt="quable_logo"
    height="100"
    class="mb-2"
  />
  <h1 class="mb-4 text-bold">Quable App Template</h1>
  <a class="mx-1" href="<%- customer %>/translation">Translation</a>
  <a class="mx-1" href="<%- customer %>/document">Document</a>
  <a class="mx-1" href="<%- customer %>/billing">Billing</a>
</div>

You should now see the following result

Billing Page
Billing Page


# Create a payment

# Create billing utilities

For the rest of the tutorial we'll be using these utility functions to keep our code more organized.
Create a file in the billing folder named utils.ts and paste the following code:

import {
  QuablePartnerAPI,
  SubscriptionPaymentModel,
  OneTimePaymentModel,
} from "@quable/quable-partner-js";
import { BillingConfig, PriceInput } from "../types";
import { v4 as uuidv4 } from "uuid";

export const validateOneTimePayment = async (
  quableInstanceName: string,
  billingConfig: BillingConfig,
  appId: string
) => {
  const partnerClient = new QuablePartnerAPI({
    accessToken: process.env.QUABLE_PARTNER_SECRET || "",
    partnerId: process.env.QUABLE_PARTNER_ID || "",
  });

  let hasPayment = false;
  const payments = await partnerClient.Payment.OneTimePayment.getPayments({
    customer: quableInstanceName,
    appId,
    status: "paid",
  });

  if (payments.length > 0) {
    for (const payment of payments) {
      hasPayment = verifyPayment(payment, billingConfig);

      if (hasPayment) {
        break;
      }
    }
  }

  return hasPayment;
};

export const validateSubscriptionPayment = async (
  quableInstanceName: string,
  billingConfigs: BillingConfig[],
  appId: string
) => {
  const partnerClient = new QuablePartnerAPI({
    accessToken: process.env.QUABLE_PARTNER_SECRET!,
    partnerId: process.env.QUABLE_PARTNER_ID!,
  });

  let hasPayment = false;
  let paymentUptoDate = false;

  let payments: SubscriptionPaymentModel[];
  try {
    payments = await partnerClient.Payment.SubscriptionPayment.getPayments({
      customer: quableInstanceName,
      appId,
    });
  } catch (error) {
    payments = [];
  }

  if (payments.length > 0) {
    for (const billingConfig of billingConfigs) {
      hasPayment = verifyPayment(payments[0], billingConfig);
      if (hasPayment) {
        paymentUptoDate = isDateInIntervalType(
          new Date(payments[0].createdAt),
          billingConfig?.interval
        );
        break;
      }
    }
  }

  if (!hasPayment || !paymentUptoDate) {
    return false;
  }

  return true;
};

const isDateInIntervalType = (dateToCheck: Date, intervalType?: string) => {
  const currentDate = new Date();

  switch (intervalType) {
    case "DAILY":
      return dateToCheck.getDate() === currentDate.getDate();
    case "WEEKLY":
      return (
        dateToCheck.getFullYear() === currentDate.getFullYear() &&
        dateToCheck.getMonth() === currentDate.getMonth() &&
        dateToCheck.getDate() >= currentDate.getDate() - currentDate.getDay() &&
        dateToCheck.getDate() <=
          currentDate.getDate() + (6 - currentDate.getDay())
      );
    case "MONTHLY":
      return (
        dateToCheck.getFullYear() === currentDate.getFullYear() &&
        dateToCheck.getMonth() === currentDate.getMonth()
      );
    case "YEARLY":
      return dateToCheck.getFullYear() === currentDate.getFullYear();
    default:
      return false;
  }
};

export const formatPriceId = (price: PriceInput) => {
  if (price.priceId) {
    return price.priceId;
  } else if (price.priceData && price.priceData?.product?.name) {
    return price.priceData.product.name;
  } else {
    throw new Error("Price id not found");
  }
};

export const verifyPayment = (
  payment: OneTimePaymentModel | SubscriptionPaymentModel,
  billingConfig: BillingConfig
) => {
  return formatPriceId(payment.price) === formatPriceId(billingConfig.price);
};

export const clientHasPayment = async (
  quableInstanceName: string,
  billingConfigs: BillingConfig[],
  appId: string
) => {
  let hasOneTimePayment = false;
  let hasSubscriptionPayment = false;
  try {
    if (billingConfigs && billingConfigs.length > 0) {
      const subscriptionBillings = billingConfigs.filter((el) => el.interval);
      const onetimeBillings = billingConfigs.filter((el) => !el.interval);

      if (subscriptionBillings.length > 0) {
        hasSubscriptionPayment = await validateSubscriptionPayment(
          quableInstanceName,
          subscriptionBillings,
          appId
        );
      }

      if (onetimeBillings.length > 0) {
        hasOneTimePayment = await validateOneTimePayment(
          quableInstanceName,
          onetimeBillings[0],
          appId
        );
      }

      if (!hasSubscriptionPayment && !hasOneTimePayment) {
        return false;
      }
    }

    return true;
  } catch (error) {
    return false;
  }
};

export const generateUUID = () => {
  return uuidv4();
};
  • validateOneTimePayment: This function takes a Quable instance name, a billing configuration object, and an app ID as parameters. It checks if the customer has made a one-time payment for the app that matches the billing configuration. It returns true if a matching payment is found, and false otherwise.

  • validateSubscriptionPayment: This function takes a Quable instance name, an array of billing configuration objects, and an app ID as parameters. It checks if the customer has an active subscription for the app that matches one of the billing configurations. It returns true if a matching subscription is found, and false otherwise.

  • isDateInIntervalType: This function takes a date and an interval type as parameters and returns true if the date falls within the specified interval type, and false otherwise. The supported interval types are "DAILY", "WEEKLY", "MONTHLY", and "YEARLY".

  • formatPriceId: This function takes a price object as a parameter and returns the price ID. If the price object has a priceId property, it returns the priceId. Otherwise, it tries to extract the price ID from the price data. If no price ID can be found, it throws an error.

  • verifyPayment: This function takes a payment object and a billing configuration object as parameters and returns true if the payment matches the billing configuration, and false otherwise. It checks if the price ID of the payment matches the price ID of the billing configuration.

  • clientHasPayment: This function takes a Quable instance name, an array of billing configuration objects, and an app ID as parameters. It checks if the customer has made a one-time payment or has an active subscription for the app that matches one of the billing configurations. It returns true if a matching payment or subscription is found, and false otherwise.

  • generateUUID: This function generates a UUID using the uuid package.


# Billing service

Let's create a service to manage billing-related tasks. Create a file named billing.service.ts within the services directory and paste the provided code:

import { QuablePartnerAPI } from "@quable/quable-partner-js";
import { appId, billingConfigs } from "src/helper/billing/configs";
import {
  formatPriceId,
  clientHasPayment,
  generateUUID,
} from "src/helper/billing/utils";

class BillingService {
  private partnerClient = new QuablePartnerAPI({
    partnerId: process.env.QUABLE_PARTNER_ID!,
    accessToken: process.env.QUABLE_PARTNER_SECRET!,
  });

  public generateCheckoutLink = async (
    instanceName: string,
    priceId: string
  ) => {
    const response: any = {
      statusCode: 201,
      message: "OK",
      data: null,
    };

    try {
      if (billingConfigs.length === 0) {
        response.data = { url: null };
        return response;
      }

      const validPrices = billingConfigs.filter((el) => {
        return formatPriceId(el.price) === priceId;
      });

      if (validPrices.length < 1) {
        response.message = "Price not found";
        response.statusCode = 404;
        return response;
      }

      const hasPayment = await clientHasPayment(
        instanceName,
        validPrices,
        appId
      );

      if (hasPayment) {
        response.data = { url: null };
        return response;
      }

      let checkoutUrl = "";
      const billingConfig = validPrices[0];
      const checkoutData: any = {
        appCheckoutId: generateUUID(),
        customer: instanceName,
        ...billingConfig,
      };

      if (billingConfig.interval) {
        const checkout =
          await this.partnerClient.Payment.SubscriptionPayment.generatePaymentLink(
            checkoutData
          );
        checkoutUrl = checkout.url;
      } else {
        const checkout =
          await this.partnerClient.Payment.OneTimePayment.generatePaymentLink(
            checkoutData
          );
        checkoutUrl = checkout.url;
      }
      response.data = { url: checkoutUrl };
    } catch (error) {
      response.statusCode = 500;
      response.message = "Something went wrong, please try again later";
    }

    return response;
  };
}

export const billingService = new BillingService();

generateCheckoutLink method generates a checkout link for a specified price ID and instance name. It first checks if there are any matching billing configurations and if the customer has not already made a payment. If both conditions are met, it generates a checkout link.


# Update billing controller

We're now going to update our billing.controller.ts and add the following method:

import { billingService } from "src/services/billing.service";
import { Request, Response } from "express";

class BillingController {
  /*
... code
*/
  public generateCheckoutLink = async (req: any, res: Response) => {
    const response = await billingService.generateCheckoutLink(
      req.quableInstance.name,
      req.params.id
    );

    return res.status(response.statusCode).send(response);
  };
}

export const billingController = new BillingController();

# Update billing routes

Update the billing.routes.ts file with the following routes:

billingRouter.post("/pricing/:id", billingController.generateCheckoutLink);

The router defines route for generating a checkout link.


# Payment link generation with Javascript

We'll now add the ability to respond to button click events and send a request to generate the payment link. Let's create inside public/scripts folder, a new file called billing.js and paste the provided code below:

import { createLoader } from "./ui/loader.js";
import { customFetch } from "./utils/custom-fetch.js";
const toast = new Notyf({ position: { x: "center", y: "bottom" } });

document.addEventListener("DOMContentLoaded", () => {
  const billingPageContainer = document.querySelector("#billing-page");
  if (billingPageContainer) {
    const priceItems = billingPageContainer.querySelectorAll(".price-item");
    handleItemClick(priceItems);
  }
});

const handleItemClick = (priceItems) => {
  for (const priceItem of priceItems) {
    const payBtn = priceItem.querySelector("button");
    payBtn.addEventListener("click", async (event) => {
      event.preventDefault();
      const priceId = event.target.getAttribute("priceId");

      const loader = createLoader();
      event.target.replaceWith(loader);
      const url = `billing/pricing/${priceId}`;
      const { data, error } = await customFetch(url, { method: "POST" });

      if (error) {
        toast.error(error);
        loader.replaceWith(event.target);
        return;
      }

      if (data.url) {
        window.location.href = data.url;
      } else {
        loader.replaceWith(event.target);
        toast.success("You have already subscribed");
      }
    });
  }
};

The provided code add click event listeners on price items. When a price item is clicked, it sends a request to the backend to generate payment link. If they haven't already paid, it takes them to the checkout page to pay for the plan, otherwise it lets them know with a message.


# Update billing view

We'll now update the view to include our script so that the buttons can respond to click events. Update the billing/index.ejs and add the following code:

<script type="module" defer src="/scripts/billing.js"></script>

Congratulation ! You can now make a payment for a product and collect the money directly from your stripe account.


# Check customer payment

In this section we'll add a method to check the status of a customer's payment.

# Update billing service

Update our billing.service.ts with the following method:

import { formatPriceId, clientHasPayment } from 'src/helper/billing/utils';
/*
.. code ..
*/

public checkPaymentStatus = async (instanceName: string, priceId: string) => {
    const response: any = {
        statusCode: 200,
        message: 'OK',
        data: null,
    };

    try {
        if (billingConfigs.length === 0) {
            response.data = true;
            return response;
        }

        const validPrices = billingConfigs.filter((el) => {
            return formatPriceId(el.price) === priceId;
        });

        if (validPrices.length < 1) {
            response.message = 'Price not found';
            response.statusCode = 404;
            return response;
        }


        const hasPayment = await clientHasPayment(
            instanceName,
            validPrices,
            appId,
        );

        response.data = hasPayment;
    } catch (error) {
        response.statusCode = 500;
        response.message = 'Something went wrong, please try again later';
    }

    return response;
};

The checkPaymentStatus function checks if a user has an active payment for a specified pricing plan by first determining the existence of billing configurations. If no configurations are found, it assumes active payment. Otherwise, it filters these configurations for the specified plan, returning an error if none match, or use clientHasPayment to verify active payment for matching plans.


# Update billing controller

Update billing.controller.ts with the following method :

public checkPaymentStatus = async (req: any, res: Response) => {
  const response = await billingService.checkPaymentStatus(
    req.quableInstance.name,
    req.params.id
  );

  return res.status(response.statusCode).send(response);
};

# Update billing routes

Update the billing.routes.ts file with the following routes:

billingRouter.get("/pricing/:id/status", billingController.checkPaymentStatus);

# Update billing view

We'll now update billing.ejs as follows so that all buttons are disabled by default until their payment status is checked:

<div id="billing-page">
  <h5>Choose your Pricing plan</h5>
  <div class="row justify-content-center">
    <div class="col-md-6 col-xl-4">
      <div class="plan-box card">
        <div class="p-4 card-body price-item">
          <h5>Pay Monthly</h5>
          <div class="py-4">
            <h2>
              <sup><small>€</small></sup> 15 /
              <span class="font-size-13">Month</span>
            </h2>
          </div>
          <div class="text-center plan-btn">
            <button
              class="btn btn-primary btn-sm waves-effect waves-light"
              priceid="app-pay-monthly"
              disabled
            >
              <i
                style="margin-right: -4px;"
                class="bx bx-loader-alt bx-spin"
              ></i>
            </button>
          </div>
        </div>
        <div class="p-4 card-body price-item">
          <h5>Onetime Payment</h5>
          <div class="py-4">
            <h2>
              <sup><small>€</small></sup> 1500/
              <span class="font-size-13">Live time</span>
            </h2>
          </div>
          <div class="text-center plan-btn">
            <button
              class="btn btn-primary btn-sm waves-effect waves-light"
              priceid="app-pay-onetime"
              disabled
            >
              <i
                style="margin-right: -4px;"
                class="bx bx-loader-alt bx-spin"
              ></i>
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
<script type="module" defer src="/scripts/billing.js"></script>

# Updating billing script

Let's update our js script to disable/enable the subscribe button based on the payment status as follows :

document.addEventListener("DOMContentLoaded", () => {
  const billingPageContainer = document.querySelector("#billing-page");
  if (billingPageContainer) {
    const priceItems = billingPageContainer.querySelectorAll(".price-item");
    handleItemClick(priceItems);
    checkPaymentStatus(priceItems);
  }
});

/*
... code
*/

const checkPaymentStatus = async (priceItems) => {
  for (const priceItem of priceItems) {
    const payBtn = priceItem.querySelector("button");

    const priceId = payBtn.getAttribute("priceId");

    const loader = payBtn.querySelector("i");
    const url = `billing/pricing/${priceId}/status`;
    const { data: result, error } = await customFetch(url, { method: "GET" });

    if (error) {
      toast.error(error);
      payBtn.disabled = false;
      payBtn.textContent = "Subscribe";
      loader.remove();
      return;
    }

    loader.remove();
    if (result === true || result.data === true) {
      payBtn.textContent = "Subscribed";
    } else {
      payBtn.disabled = false;
      payBtn.textContent = "Subscribe";
    }
  }
};

If you have already paid for a pricing plan the button should now be disabled.

Billing Page
Billing Page


# Billing middleware

In this section, we'll set up a billing middleware to block access to translation routes and authorize only customers with valid payments to access translation resources.

# Create billing middleware

Create a new file named billing.middleware.ts and paste the following the code :

import { NextFunction, Response } from "express";
import { appId, billingConfigs } from "src/helper/billing/configs";
import { clientHasPayment, formatPriceId } from "src/helper/billing/utils";

export const billingMiddleware = async (
  req: any,
  res: Response,
  next: NextFunction
) => {
  try {
    const validPrice = billingConfigs.filter((el) => {
      return formatPriceId(el.price) === "app-pay-onetime";
    });

    const hasPayment = await clientHasPayment(req.customer, validPrice, appId);

    if (!hasPayment) {
      throw null;
    }

    next();
  } catch (error) {
    res.redirect(`/${req.customer}/billing`);
  }
};

This middleware, checks if a user has paid for app-pay-onetime before they can proceed to the next step. If the payment is not valid, users are redirected to the billing page.


# Update translation routes

Open the index.ts file and modify the translation route to use our middleware as follows :

app.use("/:customer/translation", billingMiddleware, translationRouter);

This ensures that any request to access translation resource will pass through our middleware which will then verify the customer translation payment status. If you try to access translation page again you should now be redirected back to billing if you haven't made any payment for translation.