New in Chargebee: Explore Reveal and understand your payment performance end-to-end.Try Now
Docschargebee docs
HomeBillingCPQPaymentsRevRecGrowthReveal
Support

Product Updates


  • Release Notes

Getting Started


  • Overview
  • Chargebee Billing Data Centers
  • Object Relationship Model
  • Understanding Sites
  • Chargebee Tech Glossary
  • Articles and FAQ

Implementing Chargebee


  • Implementation Guide
  • Go-live Checklist
  • Articles and FAQ

Agentic AI


  • Chargebee Copilot
  • Catalog Setup Assistant
  • MCP Servers

Developer Resources


  • Developer Resources Overview
  • API Explorer
  • Articles and FAQ

Chargebee Apps


  • Chargebee Apps CLI Developer Guide

Product Catalog


  • Product Catalog Overview
  • Coupons
  • Articles and FAQ

Subscriptions


  • Working with Subscriptions
  • Billing
  • Orders
  • Articles and FAQ

Customers


  • Managing Customers
  • Account Hierarchy
  • Email Notifications
  • Branding
  • Configure Multiple Languages
  • Articles and FAQ

Entitlements


  • Entitlements Overview
  • Features Overview
  • Feature Management
  • Managing Product Entitlements
  • Subscription Entitlements
  • Customer Entitlements
  • Grandfathering Entitlements
  • Articles and FAQ

Usage Based Billing


  • Understanding Usages
  • Setting up Usage Based Billing
  • Usage Alerts
  • Prepaid credits

Invoices and Credit Notes


  • Invoices
  • Credit Notes
  • Quotes [Legacy]
  • Transactions
  • Articles and FAQ

Taxes


  • Overview
  • Configuring Taxes
  • Country-specific Taxes
  • Articles and FAQ

Hosted Capabilities


  • Overview
  • Hosted Checkout
  • Hosted Self-Serve Portal
    • Integration Options
    • Chargebee Self-Serve Portal
    • Portal Custom Code
      • Fetch Allowed Plan Configuration
        • Use Cases
      • Retrieve Subscription Change Timeline
    • Chargebee Widgets
  • Hosted Pages Features
  • Additional Hosted Pages
  • Payment Components
  • Pricing Table
  • Mobile SDKs and Wrappers
  • Articles and FAQ

Site Configuration


  • Users & Roles
  • Custom Fields & Metadata
  • Approvals
  • Mandatory Fields
  • File Attachments & Comments
  • Advanced Filter Options
  • Multicurrency Pricing
  • Multi-decimal Support
  • Configuring Reason Codes
  • Events and Webhooks
  • API Keys
  • Time Zone
  • Time Machine
  • Transfer Configurations
  • Articles and FAQ

Multi Business Entity


  • Multi Business Entity Overview
  • Customer Transfer Overview
  • Articles and FAQ

Mobile Subscriptions


  • Overview
  • Omnichannel Subscriptions
  • Omnichannel Subscriptions (Legacy)

Reports and Analytics


  • RevenueStory
  • Home Dashboard
  • Frequently Asked Questions
  • FAQs for Classic Reports Sunset
  • Articles and FAQ

Integrations


  • Sales
  • Customer Support and Success
  • Finance
  • Tax
  • eInvoicing
  • Marketing
  • Stitch
  • Collaboration
  • Contract Management
  • Ecommerce Management
  • Articles and FAQ

Data Privacy & Security


  • Two Factor Authentication
  • SAML Single Sign-On
  • System for Cross-Domain Identity Management (SCIM)
  • EU-GDPR
  • Consent Management
  • Personal Data Management
  • Compliance Certificates
  • HIPAA Guidelines
  • PCI Recommendations and Integration Types
  • Articles and FAQ

Data Operations


  • Bulk Operations
  • Migration
  • Articles and FAQ
  1. Billing
  2. Hosted Capabilities
  3. Hosted Self-Serve Portal
  4. Portal Custom Code
  5. Fetch Allowed Plan Configuration
  6. Use Cases
  1. Billing
  2. Hosted Capabilities
  3. Hosted Self-Serve Portal
  4. Portal Custom Code
  5. Fetch Allowed Plan Configuration
  6. Use Cases

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?