Last active
November 8, 2022 12:54
-
-
Save haacked/0a34391bfc2fddda192a082cfe5867af to your computer and use it in GitHub Desktop.
Calculating MRR with C# and the Stripe API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} |
"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
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!