Implementation Example Using Metering API
Overview
Let's consider a SaaS with plans set up for metered billing based on the number of users and active users.
We will create a feature that allows tenant administrators to check the usage based on the date and metering units.
Creating the Pricing Plan for This Implementation Example
Creating the Pricing Plan
Creating Meter Units
We will create Meter units for the number of users and active users.
-
Go to Pricing Plan -> Metering Units from the development console.
-
Click on "Metering Unit" at the top right.
-
Create meter units for the number of users and active users.
Creating Pricing Units
We will create pricing units for the number of users and active users.
In this example, we are creating usage units, but when actually operating, consider creating tiered units or tiered usage units as referenced in the link below.
About the Difference in Billing Calculation between Tiered Units and Tiered Usage Units
-
Go to Pricing Plan -> Pricing Unit Settings from the development console.
-
In the Pricing Unit Settings screen, click the "▼" next to "Create Fixed Unit Pricing" and select "Create Usage Unit Pricing", then click the "Create Usage Unit Pricing" button.
-
Set the pricing units for the number of users and active users.
Creating Feature Menus
Create feature menus to set the created pricing units.
-
Go to Pricing Plan -> Feature Menu Settings from the development console.
-
Click on "Create Feature Menu" at the top right.
-
Create a feature menu with the pricing units for the number of users and active users set.
Creating the Pricing Plan
Create a pricing plan with the created feature menus set.
-
Go to Pricing Plan -> Pricing Plan Settings from the development console.
-
Click on "Create Pricing Plan" at the top right.
-
Create a pricing plan with the created feature menus set.
Applying the Pricing Plan
Apply the pricing plan from the SaaSus operation console.
-
Click on Pricing Plan Settings in the SaaSus operation console.
-
Click the "Plan Settings" button in the row of the tenant you want to apply the pricing plan to.
-
Set the created pricing plan and save it with the application date set to immediate effect.
After this, the pricing plan will be applied in 5 minutes.
Implementation Sample
In the Laravel implementation sample, APIs for obtaining login user information and tenant information are omitted.
React Implementation Sample
- App.tsx
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Auth from "./components/route/Auth.tsx";
import Tenant from "./components/route/Tenant.tsx";
import Callback from "./pages/Callback";
import UsageCheck from "./pages/UsageCheck.tsx";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/callback" element={<Callback />} />
<Route path="/" element={<Auth />}>
<Route path="/tenants/:tenantId" element={<Tenant />}>
<Route path="usage-check" element={<UsageCheck />} />
</Route>
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;
- Auth.tsx
import axios from "axios";
import React, { useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
const LOGIN_URL = import.meta.env.VITE_LOGIN_URL;
const API_ENDPOINT = import.meta.env.VITE_API_URL;
const Auth: React.FC = () => {
const [authUser, setAuthUser] = useState<User>()
const [authUserFetching, setAuthUserFetching] = useState(true);
// Fetching information of the logged-in user
const getUserInfo = async () => {
try {
const jwtToken = window.localStorage.getItem("SaaSusIdToken");
const res = await axios.get<User>(`${API_ENDPOINT}/api/userinfo`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
Authorization: `Bearer ${jwtToken}`,
},
withCredentials: true,
});
// If unable to fetch logged-in user information (login not confirmed), redirect to login screen
if (!res) {
window.location.href = LOGIN_URL;
} else {
setAuthUser(res.data)
setAuthUserFetching(false);
}
} catch {
window.location.href = LOGIN_URL;
}
};
useEffect(() => {
(async () => {
await getUserInfo();
})()
}, []);
// Passing logged-in user information and user info update function as Outlet Context
return authUserFetching ? <></> : <Outlet context={{ authUser, getUserInfo }} />;
};
export default Auth;
- UsageCheck.tsx
import React, { useEffect, useState } from 'react';
import axios, { isAxiosError } from "axios";
import { useOutletContext } from "react-router-dom";
import { DateTime } from 'luxon';
import styled from "styled-components";
import { SubmitHandler, useForm, useWatch } from "react-hook-form";
// Defining interface for usage counts
interface UsageCount {
count: number;
date: string | undefined;
month: string | undefined;
timestamp: number | undefined;
meteringUnitName: 'user_count' | 'active_user_count'; // Target meter names defined in the pricing plan creation
}
// Defining form schemas for different search types
interface MonthSearchFormSchema {
type: 'month';
month: string;
date: null | undefined;
meteringUnitName: null | undefined;
}
interface MonthMeteringUnitNameSearchFormSchema {
type: 'month';
month: string;
meteringUnitName: string;
date: null | undefined;
}
interface DateSearchFormSchema {
type: 'date';
date: string;
month: null | undefined;
meteringUnitName: null | undefined;
}
interface DateMeteringUnitNameSearchFormSchema {
type: 'date';
date: string;
meteringUnitName: string;
month: null | undefined;
}
type SearchFormSchema = MonthMeteringUnitNameSearchFormSchema | MonthSearchFormSchema | DateMeteringUnitNameSearchFormSchema | DateSearchFormSchema;
// Dictionary for metering unit names
const METERING_UNIT_NAME_DICT_ARRAY = [
{
key: 'user_count', // Target meter name for user count defined in the pricing plan creation
displayName: 'Number of Users', // Display name for user count
},
{
key: 'active_user_count', // Target meter name for active user count defined in the pricing plan creation
displayName: 'Number of Active Users', // Display name for active user count
},
]
const LOGIN_URL = import.meta.env.VITE_LOGIN_URL;
const API_ENDPOINT = import.meta.env.VITE_API_URL ?? "";
const UsageCheck: React.FC = () => {
const { tenant } = useOutletContext<UserRouterContext & TenantRouterContext>();
const [meteringUnitCountList, setMeteringUnitCountList] = useState<Array<UsageCount>>([]);
const [targetMonthOrDate, setTargetMonthOrDate] = useState('');
const jwtToken = window.localStorage.getItem("SaaSusIdToken");
const { register, control, handleSubmit, formState: { isDirty, isValid }, getValues } = useForm<SearchFormSchema>();
const searchType = useWatch({ control, name: 'type' });
/**
* Retrieve the metering unit count for the specified date
* @param date
* @param meteringUnitName
*/
const fetchUsageCountOfSpecifiedDate = async (date: string, meteringUnitName: string) => {
try {
const res = await axios.get<UsageCount>(`${API_ENDPOINT}/api/tenants/${tenant.id}/metering/${meteringUnitName}/date/${date}/count`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/json",
Authorization: `Bearer ${jwtToken}`,
},
})
setMeteringUnitCountList([res.data]);
} catch (e) {
if (isAxiosError<{type: string, message: string}>(e) && e.response && e.response.data.type === 'token_validation_error_expired') {
window.location.href = LOGIN_URL;
}
}
}
/**
* Retrieve all metering unit counts for the specified month
* @param month
*/
const fetchAllUsageCountOfSpecifiedMonth = async (month: string) => {
try {
const res = await axios.get<Array<UsageCount>>(`${API_ENDPOINT}/api/tenants/${tenant.id}/metering/month/${month}/count`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/json",
Authorization: `Bearer ${jwtToken}`,
},
})
setMeteringUnitCountList(res.data);
} catch (e) {
if (isAxiosError<{type: string, message: string}>(e) && e.response && e.response.data.type === 'token_validation_error_expired') {
window.location.href = LOGIN_URL;
}
}
}
/**
* Retrieve all metering unit counts for the specified date
* @param date
*/
const fetchAllUsageCountOfSpecifiedDate = async (date: string) => {
try {
const res = await axios.get<Array<UsageCount>>(`${API_ENDPOINT}/api/tenants/${tenant.id}/metering/date/${date}/count`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/json",
Authorization: `Bearer ${jwtToken}`,
},
})
setMeteringUnitCountList(res.data);
} catch (e) {
if (isAxiosError<{type: string, message: string}>(e) && e.response && e.response.data.type === 'token_validation_error_expired') {
window.location.href = LOGIN_URL;
}
}
}
/**
* Retrieve the metering unit count for the specified month
* @param month
* @param meteringUnitName
*/
const fetchUsageCountOfSpecifiedMonth = async (month: string, meteringUnitName: string) => {
try {
const res = await axios.get<UsageCount>(`${API_ENDPOINT}/api/tenants/${tenant.id}/metering/${meteringUnitName}/month/${month}/count`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/json",
Authorization: `Bearer ${jwtToken}`,
},
})
setMeteringUnitCountList([res.data]);
} catch (e) {
if (isAxiosError<{type: string, message: string}>(e) && e.response && e.response.data.type === 'token_validation_error_expired') {
window.location.href = LOGIN_URL;
}
}
}
useEffect(() => {
const month = DateTime.now().toFormat('yyyy-MM');
setTargetMonthOrDate(month);
fetchAllUsageCountOfSpecifiedMonth(month);
}, []);
const onSearchSubmit: SubmitHandler<SearchFormSchema> = async (data) => {
if (data.type === 'month') {
if (!data.meteringUnitName) {
await fetchAllUsageCountOfSpecifiedMonth(data.month);
} else {
await fetchUsageCountOfSpecifiedMonth(data.month, data.meteringUnitName);
}
setTargetMonthOrDate(data.month);
} else {
if (!data.meteringUnitName) {
await fetchAllUsageCountOfSpecifiedDate(data.date);
} else {
await fetchUsageCountOfSpecifiedDate(data.date, data.meteringUnitName);
}
setTargetMonthOrDate(data.date);
}
}
return (
<div>
<h2>Usage Check</h2>
<form onSubmit={handleSubmit(onSearchSubmit)}>
<label>Method of Specifying Search Target</label>
<select {...register('type', {required: 'Please select.'})}>
<option></option>
<option value="month">Year-Month</option>
<option value="date">Year-Month-Day</option>
</select>
{searchType && (
<>
<label>{searchType === 'month' ? 'Target Year-Month' : 'Target Year-Month-Day'}</label>
<input {...register(searchType, { required: 'Required' })} />
<label>Metering Unit Name</label>
<select {...register('meteringUnitName')}>
<option></option>
{METERING_UNIT_NAME_DICT_ARRAY.map(({ key, displayName }) => (
<option key={key} value={key}>{displayName}</option>
))}
</select>
</>
)}
<button type="submit" disabled={!isDirty || !isValid}>Search</button>
</form>
<h3>Search Target Date: {targetMonthOrDate}</h3>
{METERING_UNIT_NAME_DICT_ARRAY.map(({key, displayName}) => {
const formMeteringUnitName = getValues('meteringUnitName')
return (!formMeteringUnitName || formMeteringUnitName && formMeteringUnitName === key) && (
<UsageCountWrapper key={key}>
<p>{displayName}:{meteringUnitCountList.find(({meteringUnitName}) => key === meteringUnitName)?.count ?? 0} cases</p>
</UsageCountWrapper>
);
})}
</div>
)
}
const UsageCountWrapper = styled.div`
border-bottom: 1px solid #333;
`
export default UsageCheck;
Laravel Implementation Sample
- routes/api.php
Route::middleware(\AntiPatternInc\Saasus\Laravel\Middleware\Auth::class)->group(function () {
Route::get('/userinfo', 'App\Http\Controllers\UserinfoController@index')->name('userinfo.index');
Route::get('/tenant/{tenantId}', 'App\Http\Controllers\TenantController@show')->name('tenant.index');
Route::get('/tenants/{tenantId}/metering/{meteringUnitName}/date/{date}/count', 'App\Http\Controllers\MeteringController@getCountOfSpecifiedDate')->name('metering.get-count-of-specified-date');
Route::get('/tenants/{tenantId}/metering/{meteringUnitName}/month/{month}/count', 'App\Http\Controllers\MeteringController@getCountOfSpecifiedMonth')->name('metering.get-count-of-specified-month');
Route::get('/tenants/{tenantId}/metering/date/{date}/count', 'App\Http\Controllers\MeteringController@getAllMeteringCountOfSpecifiedDate')->name('metering.get-all-unit-count-of-specified-date');
Route::get('/tenants/{tenantId}/metering/month/{month}/count', 'App\Http\Controllers\MeteringController@getAllMeteringCountOfSpecifiedMonth')->name('metering.get-all-unit-count-of-specified-month');
}
- MeteringController.php
<?php
namespace App\Http\Controllers;
use AntiPatternInc\Saasus\Sdk\Pricing\Model\MeteringUnitMonthCount;
class MeteringController extends Controller
{
/**
* Retrieve the unit count for the specified date
*
* @see https://docs.saasus.io/reference/getmeteringunitdatecountbytenantidandunitnameanddate
*
* @param string $tenantId
* @param string $meteringUnitName
* @param string $date
* @return array
*/
public function getCountOfSpecifiedDate(string $tenantId, string $meteringUnitName, string $date)
{
$client = new \AntiPatternInc\Saasus\Api\Client();
$pricingApi = $client->getPricingClient();
$unitCount = $pricingApi->getMeteringUnitDateCountByTenantIdAndUnitNameAndDate(
$tenantId,
$meteringUnitName,
$date
);
return [
'count' => $unitCount->getCount(),
'date' => $unitCount->getDate(),
'meteringUnitName' => $unitCount->getMeteringUnitName(),
];
}
/**
* Retrieve the unit count for the specified month
*
* @see https://docs.saasus.io/reference/getmeteringunitmonthcountbytenantidandunitnameandmonth
*
* @param string $tenantId
* @param string $meteringUnitName
* @param string $month
* @return array
*/
public function getCountOfSpecifiedMonth(string $tenantId, string $meteringUnitName, string $month)
{
$client = new \AntiPatternInc\Saasus\Api\Client();
$pricingApi = $client->getPricingClient();
$unitCount = $pricingApi->getMeteringUnitMonthCountByTenantIdAndUnitNameAndMonth(
$tenantId, $meteringUnitName, $month
);
return [
'count' => $unitCount->getCount(),
'month' => $unitCount->getMonth(),
'meteringUnitName' => $unitCount->getMeteringUnitName(),
];
}
/**
* Retrieve all unit counts for the specified date
*
* @param string $tenantId
* @param string $date
* @return \AntiPatternInc\Saasus\Sdk\Pricing\Model\MeteringUnitDateCount[]
*/
public function getAllMeteringCountOfSpecifiedDate(string $tenantId, string $date)
{
$client = new \AntiPatternInc\Saasus\Api\Client();
$pricingApi = $client->getPricingClient();
$allMeteringCount = $pricingApi->getMeteringUnitDateCountsByTenantIdAndDate(
$tenantId, $date
);
return array_map(function ($unitCount) {
return [
'count' => $unitCount->getCount(),
'date' => $unitCount->getDate(),
'meteringUnitName' => $unitCount->getMeteringUnitName(),
];
}, $allMeteringCount->getCounts());
}
/**
* Retrieve all unit counts for the specified month
*
* @param string $tenantId
* @param string $month
* @return MeteringUnitMonthCount[]
*/
public function getAllMeteringCountOfSpecifiedMonth(string $tenantId, string $month)
{
$client = new \AntiPatternInc\Saasus\Api\Client();
$pricingApi = $client->getPricingClient();
$allMeteringCount = $pricingApi->getMeteringUnitMonthCountsByTenantIdAndMonth(
$tenantId, $month
);
return array_map(function ($unitCount) {
return [
'count' => $unitCount->getCount(),
'month' => $unitCount->getMonth(),
'meteringUnitName' => $unitCount->getMeteringUnitName(),
];
}, $allMeteringCount->getCounts());
}
}