Fetch allowed plan configuration use cases
This guide builds on the fetchAllowedPlanConfig reference and its basic example by showing how to build the return value for common scenarios. Each example is a complete fetchAllowedPlanConfig function that you can adapt to your own plans, addons, and business logic. The examples are ordered from simple to complex.
All examples must run in a Node.js v6.0 environment.
Warning
If multi-decimal support is enabled for your site, read the current quantity from subscription_item.quantity_in_decimal (a decimal string) instead of subscription_item.quantity, which is null on these sites. This applies to the examples below that read a subscription item's quantity, such as Lock quantity edits for specific plans and Allow upgrades and block downgrades for a plan and addon.
Auto-attach an addon and set a quantity range
Attach a specific addon to one plan and apply the same quantity range to every per-unit plan. Use this when a plan should always ship with a bundled addon and you want to cap how many units a customer can select.
Custom code
var chargebee = require("chargebee")
exports.fetchAllowedPlanConfig = function ({ subscription, customer, item_prices }, callback, logger) {
const planItemPrice = item_prices.find(({item_price}) => item_price.item_type == "plan")
const addonItemPrices = item_prices.filter(({item_price}) => item_price.item_type == "addon")
const myAddonList = [
{
id: "additional-users-addon-USD-monthly"
}
]
chargebee.item_price.list({
"item_type[is]": "plan"
}).request(function (error, result) {
if (error) {
callback(error, null);
} else {
let selectedPlans = result.list.map((p) => p.item_price);
let output = {};
output.items = [];
selectedPlans.forEach(p => {
let entry = {
plan_id: p.id,
}
if(entry.plan_id === "starter1-USD-monthly") {
entry.addons = myAddonList
}
if (p.pricing_model == "per_unit") {
entry["meta_data"] = {
quantity_meta: {
type: "range",
min: 1,
max: 100,
step: 1
}
}
}
output.items.push(entry);
});
callback(null, output);
}
});
}Sample output
{
"items": [
{
"plan_id": "starter2-USD-monthly",
"meta_data": {
"quantity_meta": {
"type": "range",
"min": 1,
"max": 100,
"step": 1
}
}
},
{
"plan_id": "professional2-USD-monthly",
"meta_data": {
"quantity_meta": {
"type": "range",
"min": 1,
"max": 100,
"step": 1
}
}
},
{
"plan_id": "standard2-USD-monthly",
"meta_data": {
"quantity_meta": {
"type": "range",
"min": 1,
"max": 100,
"step": 1
}
}
},
{
"plan_id": "starter1-USD-monthly",
"addons": [
{
"id": "additional-users-addon-USD-monthly"
}
],
"meta_data": {
"quantity_meta": {
"type": "range",
"min": 1,
"max": 100,
"step": 1
}
}
},
{
"plan_id": "professional1-USD-monthly",
"meta_data": {
"quantity_meta": {
"type": "range",
"min": 1,
"max": 100,
"step": 1
}
}
},
{
"plan_id": "standard1-USD-monthly",
"meta_data": {
"quantity_meta": {
"type": "range",
"min": 1,
"max": 100,
"step": 1
}
}
}
]
}Restrict plan changes to the current billing frequency
Show only the plans whose billing frequency matches the customer's current subscription. This example applies to all plan types and filters plans by period and period unit within the same product family and currency. To restrict addon item prices, add your own logic.
Custom code
let chargebee = require("chargebee");
exports.fetchAllowedPlanConfig = function (
{ subscription, customer, item_prices },
callback,
logger
) {
let productFamilyId;
let actualAddonList = [];
let currentPlan = item_prices.find(({ item_price }) => item_price.item_type == "plan");
item_prices.forEach((el) => {
if (el.item_price.item_type == "plan") {
chargebee.item
.retrieve(el.item_price.item_id)
.request(function (error, result) {
if (error) {
callback(error, null);
} else {
productFamilyId = result.item.item_family_id;
}
});
} else if (el.item_price.item_type == "addon") {
actualAddonList.push({ id: el.item_price.id });
}
});
function filterItemByItemType(list, type) {
return list.filter(function (e) {
return e.item_type == type;
});
}
function filterItemByBillingFrequency(list, currentPlan) {
return list.filter(function (e) {
return e.period == currentPlan.item_price.period && e.period_unit == currentPlan.item_price.period_unit;
});
}
function filterItemByProductFamilyId(list, value) {
return list.filter(function (e) {
return e.item_family_id == value;
});
}
let output = {};
output.items = [];
output.use_existing_addons = true;
function paginatedFetchItem(offset) {
chargebee.item_price
.list({
limit: 100,
offset,
"item_type[in]": "[plan,addon]",
"currency_code[is]": currentPlan.item_price.currency_code
})
.request(function (error, result) {
if (error) {
callback(error, null);
} else {
let itemList = result.list.map((p) => p.item_price);
itemList = filterItemByProductFamilyId(itemList, productFamilyId);
let filterAddon = filterItemByItemType(itemList, "addon"); // return the ones with equal item_type
let filterPlan = filterItemByItemType(itemList, "plan");
filterPlan = filterItemByBillingFrequency(filterPlan, currentPlan);
filterPlan.forEach((p) => {
chargebee.item.retrieve(p.item_id).request(function (error, result) {
if (error) {
logger.debug("Error", error);
} else {
let item = result.item;
let applicableItems = item.applicable_items;
let item_applicability = item.item_applicability;
let resultVal;
let addonCheck;
let objAddons = {};
objAddons.allowed_addons = [];
for (let i in applicableItems) {
if (applicableItems[i].id) {
addonCheck = applicableItems[i];
if (addonCheck !== null || addonCheck !== 0) {
objAddons.allowed_addons.push(addonCheck);
}
}
}
if (item_applicability === "all") {
resultVal = filterAddon; // all addons applicable
} else {
resultVal = filterAddon.filter(function (o1) {
return objAddons.allowed_addons.some(function (o2) {
return o1.item_id === o2.id; // return the ones with equal id
});
});
}
let allowedAddons = [];
let addons = [];
// null check and compare current plan addons with allowed addons
if (resultVal.length !== 0) {
allowedAddons = resultVal.map(({ id }) => ({ id }));
addons = actualAddonList.filter(function (o1) {
return allowedAddons.some(function (o2) {
return o1.id === o2.id; // return the ones with equal id
});
});
}
let entry = {
plan_id: p.id,
allowed_addons: allowedAddons,
addons
};
if (item.status == "active" && item.enabled_for_checkout && item.enabled_in_portal) {
output.items.push(entry);
}
}
});
});
if ("next_offset" in result) {
paginatedFetchItem(result.next_offset);
} else {
callback(null, output);
}
}
});
}
paginatedFetchItem("");
};Sample output
{
"use_existing_addons": true,
"items": [
{
"plan_id": "starter-USD-monthly",
"allowed_addons": [
{ "id": "priority-support-USD-monthly" }
],
"addons": [
{ "id": "priority-support-USD-monthly" }
]
},
{
"plan_id": "professional-USD-monthly",
"allowed_addons": [
{ "id": "priority-support-USD-monthly" }
],
"addons": []
}
]
}Lock quantity edits for specific plans
Allow quantity edits for some plans while locking the quantity for others. In this example, quantity edits are blocked for Plan-1 and Plan-2 (the quantity range is fixed to the current quantity) and allowed for Plan-3. This example applies to quantity-based plans.
Custom code
let chargebee = require("chargebee");
exports.fetchAllowedPlanConfig = function (
{ subscription, customer, item_prices },
callback,
logger
) {
let actualPlanId;
let productFamilyId;
let actualAddonList = [];
let addonItemPriceIds = [];
const editQuantityNotAllowedPlanPricePoint = [
"Plan-1",
"Plan-2"
];
const editQuantityAllowedPlanPricePoint = [
"Plan-3"
];
item_prices.forEach((el) => {
if (el.item_price.item_type == "plan") {
chargebee.item
.retrieve(el.item_price.item_id)
.request(function (error, result) {
if (error) {
callback(error, null);
} else {
actualPlanId = el.item_price.id;
productFamilyId = result.item.item_family_id;
}
});
} else if (el.item_price.item_type == "addon") {
actualAddonList.push({ id: el.item_price.id });
addonItemPriceIds.push(el.item_price.id);
}
});
function filterItemByItemType(list, type) {
return list.filter(function (e) {
return e.item_type == type;
});
}
function filterItemByProductFamilyId(list, value) {
return list.filter(function (e) {
return e.item_family_id == value;
});
}
let output = {};
output.items = [];
output.use_existing_addons = true;
function paginatedFetchItem(offset) {
chargebee.item_price
.list({
limit: 100,
offset,
"item_type[in]": "[plan,addon]",
"enabled_in_portal[is]": "True"
})
.request(function (error, result) {
if (error) {
callback(error, null);
} else {
let itemList = result.list.map((p) => p.item_price);
itemList = filterItemByProductFamilyId(itemList, productFamilyId);
let filterAddon = filterItemByItemType(itemList, "addon"); // return the ones with equal item_type
let filterPlan = filterItemByItemType(itemList, "plan");
filterPlan.forEach((p) => {
chargebee.item.retrieve(p.item_id).request(function (error, result) {
if (error) {
logger.debug("Error", error);
} else {
let item = result.item;
let applicableItems = item.applicable_items;
let item_applicability = item.item_applicability;
let resultVal;
let addonCheck;
let objAddons = {};
objAddons.allowed_addons = [];
for (let i in applicableItems) {
if (applicableItems[i].id) {
addonCheck = applicableItems[i];
if (addonCheck !== null || addonCheck !== 0) {
objAddons.allowed_addons.push(addonCheck);
}
}
}
if (item_applicability === "all") {
resultVal = filterAddon; // all addons applicable
} else {
resultVal = filterAddon.filter(function (o1) {
return objAddons.allowed_addons.some(function (o2) {
return o1.item_id === o2.id; // return the ones with equal id
});
});
}
let allowedAddons = [];
let addons = [];
// null check and compare current plan addons with allowed addons
if (resultVal.length !== 0) {
allowedAddons = resultVal.map(({ id }) => ({ id }));
addons = actualAddonList.filter(function (o1) {
return allowedAddons.some(function (o2) {
return o1.id === o2.id; // return the ones with equal id
});
});
}
let entry = {
plan_id: p.id,
allowed_addons: allowedAddons
};
if (p.id === actualPlanId) {
let subscription_item = subscription.subscription_items.find(items => items.item_price_id === p.id);
let min_quantity = 1;
let max_quantity = 10000000;
if (editQuantityNotAllowedPlanPricePoint.includes(actualPlanId)) {
min_quantity = subscription_item.quantity;
max_quantity = subscription_item.quantity;
}
entry["meta_data"] = {
quantity_meta: {
type: "range",
min: min_quantity,
max: max_quantity,
step: 1
}
};
allowedAddons = allowedAddons.map(addon => {
if (addonItemPriceIds.includes(addon.id)) {
let subscription_item = subscription.subscription_items.find(items => items.item_price_id === addon.id);
let min_quantity = 1;
let max_quantity = 10000000;
addon["meta_data"] = {
quantity_meta: {
type: "range",
min: min_quantity,
max: max_quantity,
step: 1
}
};
}
return addon;
});
entry["allowed_addons"] = allowedAddons;
}
if (item.status == "active") {
output.items.push(entry);
}
}
});
});
if ("next_offset" in result) {
paginatedFetchItem(result.next_offset);
} else {
callback(null, output);
}
}
});
}
paginatedFetchItem("");
};Sample output
{
"use_existing_addons": true,
"items": [
{
"plan_id": "Plan-1",
"allowed_addons": [
{
"id": "sms-credits-USD-monthly",
"meta_data": {
"quantity_meta": {
"type": "range",
"min": 1,
"max": 10000000,
"step": 1
}
}
}
],
"meta_data": {
"quantity_meta": {
"type": "range",
"min": 5,
"max": 5,
"step": 1
}
}
},
{
"plan_id": "Plan-3",
"allowed_addons": [
{ "id": "sms-credits-USD-monthly" }
]
}
]
}Allow upgrades and block downgrades for a plan and addon
Let customers increase the quantity of a plan and an addon but prevent them from reducing it. In this example, the minimum quantity for Plan-a and Addon-a is set to the current quantity, so customers can only move up. To remove the addon condition, pass an empty list to increaseQuantityAllowedAddonPricePoint. This example applies to quantity-based plans and addons.
Custom code
let chargebee = require("chargebee");
exports.fetchAllowedPlanConfig = function (
{ subscription, customer, item_prices },
callback,
logger
) {
let actualPlanId;
let productFamilyId;
let actualAddonList = [];
let addonItemPriceIds = [];
const increaseQuantityAllowedPlanPricePoint = [
"Plan-a"
];
const increaseQuantityAllowedAddonPricePoint = [
"Addon-a"
];
item_prices.forEach((el) => {
if (el.item_price.item_type == "plan") {
chargebee.item
.retrieve(el.item_price.item_id)
.request(function (error, result) {
if (error) {
callback(error, null);
} else {
actualPlanId = el.item_price.id;
productFamilyId = result.item.item_family_id;
}
});
} else if (el.item_price.item_type == "addon") {
actualAddonList.push({ id: el.item_price.id });
addonItemPriceIds.push(el.item_price.id);
}
});
function filterItemByItemType(list, type) {
return list.filter(function (e) {
return e.item_type == type;
});
}
function filterItemByProductFamilyId(list, value) {
return list.filter(function (e) {
return e.item_family_id == value;
});
}
let output = {};
output.items = [];
output.use_existing_addons = true;
function paginatedFetchItem(offset) {
chargebee.item_price
.list({
limit: 100,
offset,
"item_type[in]": "[plan,addon]",
"enabled_in_portal[is]": "True"
})
.request(function (error, result) {
if (error) {
callback(error, null);
} else {
let itemList = result.list.map((p) => p.item_price);
itemList = filterItemByProductFamilyId(itemList, productFamilyId);
let filterAddon = filterItemByItemType(itemList, "addon"); // return the ones with equal item_type
let filterPlan = filterItemByItemType(itemList, "plan");
filterPlan.forEach((p) => {
chargebee.item.retrieve(p.item_id).request(function (error, result) {
if (error) {
logger.debug("Error", error);
} else {
let item = result.item;
let applicableItems = item.applicable_items;
let item_applicability = item.item_applicability;
let resultVal;
let addonCheck;
let objAddons = {};
objAddons.allowed_addons = [];
for (let i in applicableItems) {
if (applicableItems[i].id) {
addonCheck = applicableItems[i];
if (addonCheck !== null || addonCheck !== 0) {
objAddons.allowed_addons.push(addonCheck);
}
}
}
if (item_applicability === "all") {
resultVal = filterAddon; // all addons applicable
} else {
resultVal = filterAddon.filter(function (o1) {
return objAddons.allowed_addons.some(function (o2) {
return o1.item_id === o2.id; // return the ones with equal id
});
});
}
let allowedAddons = [];
let addons = [];
// null check and compare current plan addons with allowed addons
if (resultVal.length !== 0) {
allowedAddons = resultVal.map(({ id }) => ({ id }));
addons = actualAddonList.filter(function (o1) {
return allowedAddons.some(function (o2) {
return o1.id === o2.id; // return the ones with equal id
});
});
}
let entry = {
plan_id: p.id,
allowed_addons: allowedAddons
};
if (p.id === actualPlanId) {
let subscription_item = subscription.subscription_items.find(items => items.item_price_id === p.id);
let min_quantity = 1;
let max_quantity = 10000000;
if (increaseQuantityAllowedPlanPricePoint.includes(actualPlanId)) {
min_quantity = subscription_item.quantity;
}
entry["meta_data"] = {
quantity_meta: {
type: "range",
min: min_quantity,
max: max_quantity,
step: 1
}
};
allowedAddons = allowedAddons.map(addon => {
if (addonItemPriceIds.includes(addon.id)) {
let subscription_item = subscription.subscription_items.find(items => items.item_price_id === addon.id);
let min_quantity = 1;
let max_quantity = 10000000;
if (increaseQuantityAllowedAddonPricePoint.includes(addon.id)) {
min_quantity = subscription_item.quantity;
}
addon["meta_data"] = {
quantity_meta: {
type: "range",
min: min_quantity,
max: max_quantity,
step: 1
}
};
}
return addon;
});
entry["allowed_addons"] = allowedAddons;
}
if (item.status == "active") {
output.items.push(entry);
}
}
});
});
if ("next_offset" in result) {
paginatedFetchItem(result.next_offset);
} else {
callback(null, output);
}
}
});
}
paginatedFetchItem("");
};Sample output
{
"use_existing_addons": true,
"items": [
{
"plan_id": "Plan-a",
"allowed_addons": [
{
"id": "Addon-a",
"meta_data": {
"quantity_meta": {
"type": "range",
"min": 3,
"max": 10000000,
"step": 1
}
}
}
],
"meta_data": {
"quantity_meta": {
"type": "range",
"min": 2,
"max": 10000000,
"step": 1
}
}
},
{
"plan_id": "Plan-b",
"allowed_addons": [
{ "id": "Addon-a" }
]
}
]
}Allow plan upgrades and block downgrades across the catalog
Compare the current plan with every portal-enabled plan and list only the plans whose price is greater than or equal to the current plan. This example normalizes prices to a monthly baseline, so it accounts for different billing frequencies, for example a discounted annual price versus a monthly price. It also allows the portal-enabled addons and charges for each plan, matching addon frequency to the plan frequency.
Custom code
var chargebee = require("chargebee")
function listAll(resourceFn, params, done, offset, acc) {
acc = acc || []
var p = Object.assign({}, params)
if (offset) p.offset = offset
resourceFn(p).request(function (err, result) {
if (err) return done(err)
var rows = result.list || []
for (var i = 0; i < rows.length; i++) acc.push(rows[i])
if (result.next_offset) {
return listAll(resourceFn, params, done, result.next_offset, acc)
}
done(null, acc)
})
}
exports.fetchAllowedPlanConfig = function (
{ subscription, customer, item_prices },
callback,
logger
) {
try {
// ----------------------------------
// CURRENT PLAN
// ----------------------------------
var currentPlanItem = (subscription.subscription_items || []).find(function (si) {
return si.item_type === "plan"
})
if (!currentPlanItem) return callback(null, { items: [] })
var currentPlanPriceId = currentPlanItem.item_price_id
var currentPlanPayload = (item_prices || []).find(function (r) {
return r.item_price && r.item_price.id === currentPlanPriceId
})
if (!currentPlanPayload) return callback(null, { items: [] })
var currentPlanPrice = currentPlanPayload.item_price
// Correct currency source
var currency = subscription.currency_code
var family = currentPlanPrice.item_family_id
var currentCadence = currentPlanPrice.period_unit
// ----------------------------------
// 1. Fetch portal-enabled plan items (same family)
// ----------------------------------
listAll(
chargebee.item.list,
{
limit: 100,
item_type: { is: "plan" },
enabled_in_portal: { is: true },
item_family_id: { is: family }
},
function (planItemErr, planItems) {
if (planItemErr) return callback(planItemErr)
var planItemIds = planItems.map(function (r) {
return r.item.id
})
// ----------------------------------
// Fetch plan item prices (currency scoped)
// ----------------------------------
listAll(
chargebee.item_price.list,
{
limit: 100,
status: { is: "active" },
currency_code: { is: currency }
},
function (priceErr, priceRows) {
if (priceErr) return callback(priceErr)
var allPrices = priceRows.map(function (r) {
return r.item_price
})
var planPrices = allPrices.filter(function (p) {
return (
p.item_type === "plan" &&
planItemIds.indexOf(p.item_id) !== -1
)
})
// ----------------------------------
// Monthly baseline tier comparison
// ----------------------------------
var monthlyBaseline = {}
planPrices.forEach(function (p) {
if (p.period_unit === "month") {
monthlyBaseline[p.item_id] = parseFloat(p.price_in_decimal)
}
})
var currentBaseline = monthlyBaseline[currentPlanPrice.item_id]
var upgradePlans = planPrices.filter(function (p) {
var candidateBaseline = monthlyBaseline[p.item_id]
if (!candidateBaseline || !currentBaseline) return false
return candidateBaseline >= currentBaseline
})
if (upgradePlans.length === 0) {
upgradePlans = [currentPlanPrice]
}
// ----------------------------------
// 2. Fetch portal-enabled addon items
// ----------------------------------
listAll(
chargebee.item.list,
{
limit: 100,
item_type: { is: "addon" },
enabled_in_portal: { is: true }
},
function (addonItemErr, addonItems) {
if (addonItemErr) return callback(addonItemErr)
var addonItemIds = addonItems.map(function (r) {
return r.item.id
})
// ----------------------------------
// 3. Fetch portal-enabled charge items
// ----------------------------------
listAll(
chargebee.item.list,
{
limit: 100,
item_type: { is: "charge" },
enabled_in_portal: { is: true }
},
function (chargeItemErr, chargeItems) {
if (chargeItemErr) return callback(chargeItemErr)
var chargeItemIds = chargeItems.map(function (r) {
return r.item.id
})
// ----------------------------------
// Filter addon and charge item prices
// ----------------------------------
var addonPrices = allPrices.filter(function (p) {
return (
p.item_type === "addon" &&
addonItemIds.indexOf(p.item_id) !== -1
)
})
var chargePrices = allPrices.filter(function (p) {
return (
p.item_type === "charge" &&
chargeItemIds.indexOf(p.item_id) !== -1
)
})
function allowedAddonsForPlan(plan) {
var allowed = []
// Addons must match cadence
addonPrices.forEach(function (p) {
if (p.period_unit === plan.period_unit) {
allowed.push({ id: p.id })
}
})
// Charges ignore cadence
chargePrices.forEach(function (p) {
allowed.push({ id: p.id })
})
return allowed
}
var itemsOut = upgradePlans.map(function (p) {
return {
plan_id: p.id,
allowed_addons: allowedAddonsForPlan(p)
}
})
callback(null, {
items: itemsOut,
use_existing_addons: true
})
}
)
}
)
}
)
}
)
} catch (e) {
logger.error("fetchAllowedPlanConfig error", e)
callback(e)
}
}Sample output
{
"use_existing_addons": true,
"items": [
{
"plan_id": "professional-USD-monthly",
"allowed_addons": [
{ "id": "priority-support-USD-monthly" },
{ "id": "onboarding-fee-USD" }
]
},
{
"plan_id": "enterprise-USD-monthly",
"allowed_addons": [
{ "id": "priority-support-USD-monthly" },
{ "id": "onboarding-fee-USD" }
]
}
]
}Was this article helpful?