Skip to content

Commit

Permalink
Added Stripe serivices for retrieveing all subscriptions, last 10 sub…
Browse files Browse the repository at this point in the history
…scriptions, 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.
  • Loading branch information
ChrisIvanov committed Jul 8, 2024
1 parent d3c72bc commit ff8bf96
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 5 deletions.
57 changes: 55 additions & 2 deletions src/server/CookingApp/Controllers/StripeController.cs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down Expand Up @@ -51,5 +52,57 @@ public async Task<ApiResponse<SubscriptionCancellationResponse>> CancelSubscript
};
}


[HttpGet("subs-count")]

public async Task<ApiResponse<List<CustomerData>>> GetSubsCount()
{
var users = await stripeService.GetAllSubs();

return new ApiResponse<List<CustomerData>>()
{
Status = 200,
Data = users.ToList()
};
}

[HttpGet("last-10-subs")]
public async Task<ApiResponse<List<CustomerData>>> GetLast10Subs()
{
var users = await stripeService.GetAllSubs();

var lastTen = users.OrderByDescending(x => x.Created).Take(10);

return new ApiResponse<List<CustomerData>>()
{
Status = 200,
Data = lastTen.ToList()
};
}

[HttpGet("subs-stats")]
[AllowAnonymous]
public async Task<ApiResponse<SubscriptionStatistics>> GetSubscriptionStat()
{
var subscribedToCanceledRatio = await stripeService.GetSubsStats();

return new ApiResponse<SubscriptionStatistics>()
{
Status = 200,
Data = subscribedToCanceledRatio
};
}

[HttpGet("last-month-income")]
public async Task<ApiResponse<IncomeStatistics>> GetIncome30DaysBack()
{
var incomeStats = await stripeService.GetIncome30DaysBack();

return new ApiResponse<IncomeStatistics>()
{
Status = 200,
Data = incomeStats
};
}
}
}
7 changes: 6 additions & 1 deletion src/server/CookingApp/Services/Stripe/IStripeService.cs
Original file line number Diff line number Diff line change
@@ -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<IEnumerable<Product>> GetProductsAsync();
Task<IEnumerable<ViewModels.Stripe.Product>> GetProductsAsync();
Task<SubscriptionCreationResponse> CreateSubscriptionAsync(SubscriptionCreation model);
Task<SubscriptionCancellationResponse> CancelSubscriptionAsync(SubscriptionCancellation model);
Task<List<CustomerData>> GetAllSubs();
Task<SubscriptionStatistics> GetSubsStats();
Task<IncomeStatistics> GetIncome30DaysBack();
}
}
99 changes: 97 additions & 2 deletions src/server/CookingApp/Services/Stripe/StripeService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,7 +11,8 @@
public class StripeService(CustomerService customerService,
PriceService priceService,
ProductService productService,
SubscriptionService subscriptionService) : IStripeService
SubscriptionService subscriptionService,
IMapper mapper) : IStripeService
{
/// <summary>
/// Gets all products that are in the Stripe account.
Expand All @@ -23,7 +26,7 @@ public async Task<IEnumerable<Product>> GetProductsAsync()

foreach (var product in products)
{

var price = await priceService.GetAsync(product.DefaultPriceId);
result.Add(
new Product(product.Id,
Expand Down Expand Up @@ -90,5 +93,97 @@ public async Task<SubscriptionCancellationResponse> CancelSubscriptionAsync(Subs

return new SubscriptionCancellationResponse(subscription.CanceledAt);
}

public async Task<List<CustomerData>> GetAllSubs()
{
var options = new SubscriptionSearchOptions
{
Query = $"status:'active'"
};

var subscriptions = await subscriptionService.SearchAsync(options);
var allActiveUsers = new List<CustomerData>();

foreach (var subscription in subscriptions)
{
try
{
var customer = await customerService.GetAsync(subscription.CustomerId);
var customerModel = mapper.Map<CustomerData>(customer);
allActiveUsers.Add(customerModel);
}
catch (Exception ex)
{
Console.WriteLine($"Error mapping customer for subscription {subscription.CustomerId}: {ex.Message}");
}
}

return allActiveUsers;
}

public async Task<SubscriptionStatistics> 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<IncomeStatistics> 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;
}
}
}
26 changes: 26 additions & 0 deletions src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace CookingApp.ViewModels.Stripe.Customer
{
using CookingApp.Infrastructure.Mapping;
using System.Collections.Generic;
using global::Stripe;

public class CustomerData : IMapFrom<Customer>
{
public string Id { get; set; }

Check warning on line 9 in src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public DateTime Created { get; set; }
public string? Description { get; set; }
public string? Email { get; set; }
public Dictionary<string, string> Metadata { get; set; }

Check warning on line 13 in src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Metadata' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public string? Name { get; set; }
public string? Phone { get; set; }
public List<SubscriptionState> Subscriptions { get; set; }

Check warning on line 16 in src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Subscriptions' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
}

public class SubscriptionState
{
public string Id { get; set; }

Check warning on line 21 in src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public string PriceId { get; set; }

Check warning on line 22 in src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'PriceId' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public string State { get; set; }

Check warning on line 23 in src/server/CookingApp/ViewModels/Stripe/Customer/CustomerData.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'State' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public DateTime CancelAt { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace CookingApp.ViewModels.Stripe.Statistics
{
using CookingApp.Infrastructure.Mapping;
using global::Stripe;

public class IncomeStatistics : IMapFrom<Balance>
{
public long Total { get; set; }
public double AmountAfterTax { get; set; }
public int TotalTransactions { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace CookingApp.ViewModels.Stripe.Statistics
{
public class SubscriptionStatistics
{
public double ActiveSubscriptions { get; set; }
public double CanceledSubscriptions { get; set; }
}
}

0 comments on commit ff8bf96

Please sign in to comment.