Skip to content

Instantly share code, notes, and snippets.

@jdnichollsc
Last active August 9, 2025 00:35
Show Gist options
  • Select an option

  • Save jdnichollsc/72c0769dab6fe246895a444735d67c79 to your computer and use it in GitHub Desktop.

Select an option

Save jdnichollsc/72c0769dab6fe246895a444735d67c79 to your computer and use it in GitHub Desktop.
Prorating Subscriptions - Challenge

Prorating Subscriptions

Background

Our company has started selling to larger customers, so we are creating subscription tiers with different feature sets to cater to our customers’ unique needs. We previously charged every customer a flat fee per month, but now we plan on charging for the number of users active on the customer's subscription plan. As a result, we're changing our billing system.

Instructions

You’ve picked up the work item to implement the logic to compute the monthly charge:

Prorating Subscriptions (#8675309)

We'd like you to implement a monthlyCharge function to calculate the total monthly bill for a customer.

Customers are billed based on their subscription tier. We charge them a prorated amount for the portion of the month each user’s subscription was active. For example, if a user was activated or deactivated part way through the month, then we charge them only for the days their subscription was active.

We want to bill customers for all days users were active in the month (including any activation and deactivation dates, since the user had some access on those days).

  • We do need to support historical calculations (previous dates)
  • We only charge whole cents

Notes

Here’s an idea of how we might go about this:

  • Calculate a daily rate for the subscription tier.
  • For each day of the month, identify which users had an active subscription on that day.
  • Multiply the number of active users for the day by the daily rate to calculate the total for the day.
  • Return the running total for the month at the end.

Notes

  • Your goal is production-quality code.
  • The provided unit tests only cover a few cases that one of your colleagues shared.
  • Performance is not a top concern since this code is mostly used in a batch process
  • Function names and signatures are a constraint and should not change.
  • Reminder: Please use the Online IDE so that your problem-solving approach is visible.

Goal

Implement monthlyCharge that returns:

  • total bill in whole cents (rounded to whole cents)
  • if there is no subscription or no billable user-days, return 0.

Things to consider:

  • customer for a given month
  • prorated by the exact days each user was active (activation/deactivation days inclusive)
  • work for historical months

Inputs

  • month: string 'YYYY-MM'
  • subscription: object or null (per-user per month price in cents)
  • users: array

Strategy

  • compute days in the month
  • Each user, compute the # of active days within that month by intersecting:
    • User window.
    • Month window
  • Sum all users's active days => totalUserDays
  • Compute once - O(n) for the batch process constraint:
totalInCents = Math.round(
  subscription.monthlyPriceInCents * totalUserDays / daysInMonth
)

Constraints

  • Output must be whole cents
  • Round once at month level, no per-day rounding to avoid cumulative errors
  • No subscription => 0
  • No users -> 0
  • Users active entire month -> users.length * monthlyPriceInCents
  • Activated before month and deactivated after month -> full month
  • Activated/deactivated inside month (mid-month)
  • Activated on last day -> 1 day
  • Activated and deactivated same day -> 1 day
  • Leap year Feb -> 29 days
  • Rounding sanity -> 1 user active 1 day in 31-day month
/**
* Computes the monthly charge for a given subscription.
*
* @returns {number} The total monthly bill for the customer in cents, rounded
* to the nearest cent. For example, a bill of $20.00 should return 2000.
* If there are no active users or the subscription is null, returns 0.
*
* @param {string} month - Always present
* Has the following structure:
* "2022-04" // April 2022 in YYYY-MM format
*
* @param {object} subscription - May be null
* If present, has the following structure:
* {
* 'id': 763,
* 'customerId': 328,
* 'monthlyPriceInCents': 359 // price per active user per month
* }
*
* @param {array} users - May be empty, but not null
* Has the following structure:
* [
* {
* 'id': 1,
* 'name': "Employee #1",
* 'customerId': 1,
*
* // when this user started
* 'activatedOn': new Date("2021-11-04"),
*
* // last day to bill for user
* // should bill up to and including this date
* // since user had some access on this date
* 'deactivatedOn': new Date("2022-04-10")
* },
* {
* 'id': 2,
* 'name': "Employee #2",
* 'customerId': 1,
*
* // when this user started
* 'activatedOn': new Date("2021-12-04"),
*
* // hasn't been deactivated yet
* 'deactivatedOn': null
* },
* ]
*/
function monthlyCharge(month, subscription, users) {
// your code here!
if (!subscription || !users?.length) return 0;
const [y, m] = month.split("-").map(Number)
const monthStartDate = new Date(y, m - 1, 1);
const monthEndDate = lastDayOfMonth(monthStartDate);
const daysInMonth = Math.round((monthEndDate - monthStartDate) / DAY_IN_MILLISECONDS) + 1;
const totalUserDays = users.reduce((acc, u) => {
const startDate = u.activatedOn ? maxDate(u.activatedOn, monthStartDate) : null;
const endDate = u.deactivatedOn ? minDate(u.deactivatedOn, monthEndDate): monthEndDate;
if (startDate && endDate >= startDate) {
const days = Math.round((stripTime(endDate) - stripTime(startDate)) / DAY_IN_MILLISECONDS) + 1;
acc += days;
}
return acc;
}, 0)
return Math.round(
subscription.monthlyPriceInCents * totalUserDays / daysInMonth
);
}
/*******************
* Helper functions *
*******************/
/**
* Takes a Date instance and returns a Date which is the first day
* of that month. For example:
*
* firstDayOfMonth(new Date(2022, 3, 17)) // => new Date(2022, 3, 1)
*
* Input type: Date
* Output type: Date
**/
function firstDayOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth(), 1)
}
/**
* Takes a Date object and returns a Date which is the last day of that month.
*
* lastDayOfMonth(new Date(2022, 3, 17)) // => new Date(2022, 3, 31)
*
* Input type: Date
* Output type: Date
**/
function lastDayOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth() + 1, 0)
}
/**
* Takes a Date object and returns a Date which is the next day.
* For example:
*
* nextDay(new Date(2022, 3, 17)) // => new Date(2022, 3, 18)
* nextDay(new Date(2022, 3, 31)) // => new Date(2022, 4, 1)
*
* Input type: Date
* Output type: Date
**/
function nextDay(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)
}
// Additional utility functions
function stripTime(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
function minDate(a, b) {
return a < b ? a : b;
}
function maxDate(a, b) {
return a > b ? a : b;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment