From ff8bf96a49fc3bd6922392695b8c90fe72efcdd9 Mon Sep 17 00:00:00 2001 From: Kristian Ivanov Date: Mon, 8 Jul 2024 14:00:18 +0300 Subject: [PATCH] Added Stripe serivices for retrieveing all subscriptions, last 10 subscriptions, subscribed/canceled ratio and last 30 day profits. Added 3 view models for CustomerData, IncomeStatistics and SubscriptionStatistics. Added StripeController endpoints to connect with those services and display the necessary data. --- .../Controllers/StripeController.cs | 57 ++++++++++- .../Services/Stripe/IStripeService.cs | 7 +- .../Services/Stripe/StripeService.cs | 99 ++++++++++++++++++- .../Stripe/Customer/CustomerData.cs | 26 +++++ .../Stripe/Statistics/IncomeStatistics.cs | 12 +++ .../Statistics/SubscriptionStatistics.cs | 8 ++ 6 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs create mode 100644 src/server/CookingApp/ViewModels/Stripe/Statistics/IncomeStatistics.cs create mode 100644 src/server/CookingApp/ViewModels/Stripe/Statistics/SubscriptionStatistics.cs diff --git a/src/server/CookingApp/Controllers/StripeController.cs b/src/server/CookingApp/Controllers/StripeController.cs index d4404ad9..eddd1ef3 100644 --- a/src/server/CookingApp/Controllers/StripeController.cs +++ b/src/server/CookingApp/Controllers/StripeController.cs @@ -1,13 +1,14 @@  namespace CookingApp.Controllers { - using CookingApp.Infrastructure.Configurations.Stripe; using CookingApp.Services.Stripe; using CookingApp.ViewModels.Api; using CookingApp.ViewModels.Stripe.Customer; + using CookingApp.ViewModels.Stripe.Statistics; using CookingApp.ViewModels.Stripe.Subscription; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; - using Microsoft.Extensions.Options; + using Stripe; using Product = ViewModels.Stripe.Product; [Route("api/stripe")] @@ -51,5 +52,57 @@ public async Task> CancelSubscript }; } + + [HttpGet("subs-count")] + + public async Task>> GetSubsCount() + { + var users = await stripeService.GetAllSubs(); + + return new ApiResponse>() + { + Status = 200, + Data = users.ToList() + }; + } + + [HttpGet("last-10-subs")] + public async Task>> GetLast10Subs() + { + var users = await stripeService.GetAllSubs(); + + var lastTen = users.OrderByDescending(x => x.Created).Take(10); + + return new ApiResponse>() + { + Status = 200, + Data = lastTen.ToList() + }; + } + + [HttpGet("subs-stats")] + [AllowAnonymous] + public async Task> GetSubscriptionStat() + { + var subscribedToCanceledRatio = await stripeService.GetSubsStats(); + + return new ApiResponse() + { + Status = 200, + Data = subscribedToCanceledRatio + }; + } + + [HttpGet("last-month-income")] + public async Task> GetIncome30DaysBack() + { + var incomeStats = await stripeService.GetIncome30DaysBack(); + + return new ApiResponse() + { + Status = 200, + Data = incomeStats + }; + } } } diff --git a/src/server/CookingApp/Services/Stripe/IStripeService.cs b/src/server/CookingApp/Services/Stripe/IStripeService.cs index 96956e08..e89db4c1 100644 --- a/src/server/CookingApp/Services/Stripe/IStripeService.cs +++ b/src/server/CookingApp/Services/Stripe/IStripeService.cs @@ -1,13 +1,18 @@ using CookingApp.ViewModels.Stripe; using CookingApp.ViewModels.Stripe.Customer; +using CookingApp.ViewModels.Stripe.Statistics; using CookingApp.ViewModels.Stripe.Subscription; +using Stripe; namespace CookingApp.Services.Stripe { public interface IStripeService { - Task> GetProductsAsync(); + Task> GetProductsAsync(); Task CreateSubscriptionAsync(SubscriptionCreation model); Task CancelSubscriptionAsync(SubscriptionCancellation model); + Task> GetAllSubs(); + Task GetSubsStats(); + Task GetIncome30DaysBack(); } } diff --git a/src/server/CookingApp/Services/Stripe/StripeService.cs b/src/server/CookingApp/Services/Stripe/StripeService.cs index 428857d5..171b047f 100644 --- a/src/server/CookingApp/Services/Stripe/StripeService.cs +++ b/src/server/CookingApp/Services/Stripe/StripeService.cs @@ -1,6 +1,8 @@ namespace CookingApp.Services.Stripe { + using AutoMapper; using CookingApp.ViewModels.Stripe.Customer; + using CookingApp.ViewModels.Stripe.Statistics; using CookingApp.ViewModels.Stripe.Subscription; using global::Stripe; using static CookingApp.Common.ExceptionMessages; @@ -9,7 +11,8 @@ public class StripeService(CustomerService customerService, PriceService priceService, ProductService productService, - SubscriptionService subscriptionService) : IStripeService + SubscriptionService subscriptionService, + IMapper mapper) : IStripeService { /// /// Gets all products that are in the Stripe account. @@ -23,7 +26,7 @@ public async Task> GetProductsAsync() foreach (var product in products) { - + var price = await priceService.GetAsync(product.DefaultPriceId); result.Add( new Product(product.Id, @@ -90,5 +93,97 @@ public async Task CancelSubscriptionAsync(Subs return new SubscriptionCancellationResponse(subscription.CanceledAt); } + + public async Task> GetAllSubs() + { + var options = new SubscriptionSearchOptions + { + Query = $"status:'active'" + }; + + var subscriptions = await subscriptionService.SearchAsync(options); + var allActiveUsers = new List(); + + foreach (var subscription in subscriptions) + { + try + { + var customer = await customerService.GetAsync(subscription.CustomerId); + var customerModel = mapper.Map(customer); + allActiveUsers.Add(customerModel); + } + catch (Exception ex) + { + Console.WriteLine($"Error mapping customer for subscription {subscription.CustomerId}: {ex.Message}"); + } + } + + return allActiveUsers; + } + + public async Task GetSubsStats() + { + var subStats = new SubscriptionStatistics(); + + // List active subscriptions + var activeOptions = new SubscriptionListOptions() + { + Status = "active" + }; + + // List ended (canceled) subscriptions + var canceledOptions = new SubscriptionListOptions() + { + Status = "canceled" + }; + + var activeSubscriptions = await subscriptionService.ListAsync(activeOptions); + var canceledSubscriptions = await subscriptionService.ListAsync(canceledOptions); + + subStats.ActiveSubscriptions = activeSubscriptions.Data.Count; + subStats.CanceledSubscriptions = canceledSubscriptions.Data.Count; + + if (subStats.CanceledSubscriptions == 0) + { + return null; + } + + return subStats; + } + + public async Task GetIncome30DaysBack() + { + var incomeStat = new IncomeStatistics(); + var balanceTransactionService = new BalanceTransactionService(); + + var options = new BalanceTransactionListOptions() + { + Created = new DateRangeOptions + { + GreaterThanOrEqual = DateTime.UtcNow.AddDays(-30), + LessThanOrEqual = DateTime.UtcNow + } + }; + + try + { + var last30DayTransactions = await balanceTransactionService.ListAsync(options); + + incomeStat.TotalTransactions = last30DayTransactions.Count(); + + foreach (var transaction in last30DayTransactions) + { + incomeStat.Total += transaction.Amount; + incomeStat.AmountAfterTax += transaction.Amount - transaction.Fee; + } + + } + catch (Exception ex) + { + Console.WriteLine($"{ex.Message}"); + } + + return incomeStat; + } } } diff --git a/src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs b/src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs new file mode 100644 index 00000000..2c50c73c --- /dev/null +++ b/src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs @@ -0,0 +1,26 @@ +namespace CookingApp.ViewModels.Stripe.Customer +{ + using CookingApp.Infrastructure.Mapping; + using System.Collections.Generic; + using global::Stripe; + + public class CustomerData : IMapFrom + { + public string Id { get; set; } + public DateTime Created { get; set; } + public string? Description { get; set; } + public string? Email { get; set; } + public Dictionary Metadata { get; set; } + public string? Name { get; set; } + public string? Phone { get; set; } + public List Subscriptions { get; set; } + } + + public class SubscriptionState + { + public string Id { get; set; } + public string PriceId { get; set; } + public string State { get; set; } + public DateTime CancelAt { get; set; } + } +} \ No newline at end of file diff --git a/src/server/CookingApp/ViewModels/Stripe/Statistics/IncomeStatistics.cs b/src/server/CookingApp/ViewModels/Stripe/Statistics/IncomeStatistics.cs new file mode 100644 index 00000000..2d8604a9 --- /dev/null +++ b/src/server/CookingApp/ViewModels/Stripe/Statistics/IncomeStatistics.cs @@ -0,0 +1,12 @@ +namespace CookingApp.ViewModels.Stripe.Statistics +{ + using CookingApp.Infrastructure.Mapping; + using global::Stripe; + + public class IncomeStatistics : IMapFrom + { + public long Total { get; set; } + public double AmountAfterTax { get; set; } + public int TotalTransactions { get; set; } + } +} diff --git a/src/server/CookingApp/ViewModels/Stripe/Statistics/SubscriptionStatistics.cs b/src/server/CookingApp/ViewModels/Stripe/Statistics/SubscriptionStatistics.cs new file mode 100644 index 00000000..f733d437 --- /dev/null +++ b/src/server/CookingApp/ViewModels/Stripe/Statistics/SubscriptionStatistics.cs @@ -0,0 +1,8 @@ +namespace CookingApp.ViewModels.Stripe.Statistics +{ + public class SubscriptionStatistics + { + public double ActiveSubscriptions { get; set; } + public double CanceledSubscriptions { get; set; } + } +}