recurly/recurly-js

View on GitHub
lib/recurly/pricing/subscription/tiered-pricing-calculator.js

Summary

Maintainability
A
1 hr
Test Coverage
/* eslint-disable no-param-reassign */
import find from 'component-find';

/**
 * 
 * Takes in an addon and returns boolean to decide if addOn should used tiered math
 * 
 * @param {object} addOn 
 * @returns {boolean}
 */
export function isTieredAddOn (addOn) {
  return addOn.add_on_type === 'fixed' && addOn.tier_type !== 'flat';
}

/**
 *
 * generates an array of objects with each containing
 * the values relevant to an individual tier row.
 * will return a null in place of irrelevant (starting_quantity > selectedNumUnits) tier rows.
 *
 * @param {object} addOn
 * @param {int} selectedNumUnits
 * @param {string} currencyCode
 * @returns {array<object>}
 *
 */
export function getTieredPricingValues (
  addOn,
  selectedNumUnits,
  currencyCode,
) {
  const unformattedTiers = addOn.tiers; 
  return unformattedTiers?.map((tier, index) => {
    const isFirstTier = index === 0;
    const prevTier = isFirstTier ? null : unformattedTiers[index - 1];
    const tierEndingQuantity = tier.ending_quantity;
    const tierCurrency = tier.currencies.filter((tc) => { return tc.currency_code === currencyCode; })[0];

    if (tierCurrency === undefined) {
      return null;
    }

    const tierValue = tierCurrency.unit_amount;
    let startingValue;

    if (isFirstTier) {
      startingValue = 1;
    } else {
      startingValue = prevTier.ending_quantity + 1;
    }
    const floatSelectedNumUnits = parseFloat(selectedNumUnits);

    // if floatSelectedNumUnits is less than the startingValue of a tier DONT render it
    if (floatSelectedNumUnits < startingValue) {
      return null;
    }

    let unitAmount = 0;
    let unitsPerTier = null;

    switch (addOn.tier_type) {
    case 'tiered':
      // tier ranges are inclusive. e.g. when tier is 1-10 and floatSelectedNumUnits
      // is greater than or equal to 10, we want unitsPerTier to be 10,
      // so max - min + 1 (10 - 1 + 1 === 10)
      unitsPerTier = floatSelectedNumUnits >= tierEndingQuantity
        ? tierEndingQuantity - startingValue + 1
        : floatSelectedNumUnits - startingValue + 1;

      unitAmount = unitsPerTier * tierValue;
      break;
    case 'volume':
      if (floatSelectedNumUnits <= tierEndingQuantity) {
        unitAmount = floatSelectedNumUnits * tierValue;
      }
      break;
    case 'stairstep':
      if (selectedNumUnits && selectedNumUnits <= tierEndingQuantity) {
        unitAmount = tierValue;
      }
      break;
    default:
      break;
    }

    return unitAmount;
  });
}

/**
 *
 * returns the total estimated cost (unitAmount)
 * of a `tiered` | `volume` | `stairstep` pricing model
 *
 * @param {object} addOn
 * @param {int} selectedNumUnits
 * @param {string} currencyCode
 * @returns {object}
 *
 */
export function getTieredPricingTotal (
  addOn,
  selectedNumUnits,
  currencyCode,
) {
  const initialUnitAmount = 0;

  return getTieredPricingValues(
    addOn,
    selectedNumUnits,
    currencyCode
  )?.reduce(
    (unitAmount, tierAmount) => {
      if (tierAmount) {
        unitAmount += tierAmount;
      }
      return unitAmount;
    },
    initialUnitAmount,
  );
}

/**
 * returns the unit amount for a tiered pricing model in the current tier based on selectedNumUnits
 * @param {object} addOn
 * @param {int} selectedNumUnits
 * @param {string} currencyCode
 * @returns number
 */
export function getTieredPricingUnitAmount (addOn, selectedNumUnits, currencyCode) {
  const tier = find(addOn.tiers, (tier) => (selectedNumUnits || 1) <= tier.ending_quantity);
  return find(tier.currencies, (currency) => currency.currency_code === currencyCode).unit_amount;
}