Skip to content

Instantly share code, notes, and snippets.

@haacked
Last active Nov 8, 2022
Embed
What would you like to do?
Calculating MRR with C# and the Stripe API
// This is the source for the blog post here: https://haacked.com/archive/2022/10/12/calculating-mrr-with-stripe-and-csharp/
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace Stripe;
public static class StripeExtensions
{
public static decimal CalculateSubscriptionMonthlyRevenue(Subscription subscription)
{
decimal revenue = 0;
foreach (var item in subscription.Items)
{
var multiplier = item.Plan.Interval switch
{
"day" => 30M,
"week" => 4M,
"month" => 1M,
"year" => 1M / 12M,
_ => throw new UnreachableException($"Unexpected plan interval: {item.Plan.Interval}.")
};
revenue += multiplier * item.Quantity * item.Price.UnitAmountDecimal.GetValueOrDefault();
}
return revenue / 100M; // The UnitAmount is in cents.
}
public static decimal CalculateCustomerMonthlyRevenue(Customer customer)
{
var subscriptions = customer.Subscriptions;
var revenue = 0M;
foreach (var subscription in subscriptions)
{
revenue += CalculateSubscriptionMonthlyRevenue(subscription);
}
// Apply the coupon, if any. We only look at % off coupons.
// We can ignore the amount off discount. That's a one time discount and doesn't affect ongoing MRR.
if (customer.Discount is { Coupon.PercentOff: { } percentOff })
{
revenue *= 1 - percentOff / 100M;
}
return revenue;
}
public static async Task<decimal> CalculateMonthlyRecurringRevenue()
{
string? lastId = null;
var customerClient = new CustomerService();
decimal revenue = 0M;
bool hasMore = true;
while (hasMore)
{
var customers = await customerClient.ListAsync(
new CustomerListOptions
{
Limit = 100, /* Max Limit is 100 */
Expand = new List<string> { "data.subscriptions" },
StartingAfter = lastId
});
revenue += customers.Sum(CalculateCustomerMonthlyRevenue);
hasMore = customers.HasMore;
if (hasMore)
{
lastId = customers.LastOrDefault()?.Id;
if (lastId is null)
{
throw new InvalidOperationException("API reports more customers but no last id was returned.");
}
}
}
return revenue;
}
}
@TAzmir
Copy link

TAzmir commented Oct 14, 2022

I haven't gone through the complete code, but I would like to give you some suggestions here. :)

(1) I would rename this CalculateSubscriptionMonthlyRevenue to CalculateSubscriptionMonthlyRevenueInCents. This would remove your comment inside the "UnitAmount is in cents" method.

(2) I would not throw ex _ => throw new UnreachableException($"Unexpected plan interval: {item.Plan.Interval}.") instead I would return 0M. This way you can say that your CalculateSubscriptionMonthlyRevenueInCents function only supports day/week/month and year calculations. Other calculations you would simply ignore with 0M instead of "breaking" your application. I hope it makes sense to you. :)

(3) I would pass only the subscription items instead of passing the entire subscription. I'm still refering to CalculateSubscriptionMonthlyRevenueInCents.

(4) If there are no subscription items, I would immediately return 0. Again, I'm still refering CalculateSubscriptionMonthlyRevenueInCents.

I hope these suggestions make sense for you. :)

Enjoy!

@SecretDeveloper
Copy link

SecretDeveloper commented Nov 8, 2022

"week" => 4M,

You should probably make this 52M / 12M which gives you 4.333 weeks per month.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment