WIP
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
using Hutopy.Web.Features.Memberships.Data;
|
||||
using Hutopy.Web.Features.Memberships.Services;
|
||||
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||
|
||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||
|
||||
@@ -10,7 +10,7 @@ public class CancelSubscriptionRequest
|
||||
}
|
||||
|
||||
public class CancelSubscriptionHandler(
|
||||
MembershipDbContext dbDbContext,
|
||||
MembershipDbContext dbContext,
|
||||
StripeService stripeService)
|
||||
: Endpoint<CancelSubscriptionRequest>
|
||||
{
|
||||
@@ -24,7 +24,7 @@ public class CancelSubscriptionHandler(
|
||||
CancelSubscriptionRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var subscription = await dbDbContext
|
||||
var subscription = await dbContext
|
||||
.Subscriptions
|
||||
.FindAsync(
|
||||
[req.SubscriptionId],
|
||||
@@ -41,7 +41,7 @@ public class CancelSubscriptionHandler(
|
||||
|
||||
// Update subscription in the system
|
||||
subscription.EndDate = DateTime.UtcNow;
|
||||
await dbDbContext.SaveChangesAsync(ct);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(subscription.Id, ct);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Memberships.Data;
|
||||
using Hutopy.Web.Features.Memberships.Services;
|
||||
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||
|
||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
using Hutopy.Web.Features.Memberships.Data;
|
||||
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||
|
||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public class CreateMembershipTierRequest
|
||||
{
|
||||
public Guid CreatorId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
public record struct CreateMembershipTierRequest(
|
||||
Guid CreatorId,
|
||||
string Name,
|
||||
string Description,
|
||||
decimal Price,
|
||||
string Currency = "CAD");
|
||||
|
||||
[PublicAPI]
|
||||
public class CreateMembershipTierEndpoint(
|
||||
MembershipDbContext dbDbContext)
|
||||
MembershipDbContext dbContext,
|
||||
StripeService stripe)
|
||||
: Endpoint<CreateMembershipTierRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -25,11 +27,29 @@ public class CreateMembershipTierEndpoint(
|
||||
CreateMembershipTierRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tier = dbDbContext
|
||||
.Tiers
|
||||
.Add(new Tier { CreatorId = req.CreatorId, Price = req.Price, Name = req.Name });
|
||||
var tierId = Guid.NewGuid();
|
||||
|
||||
await dbDbContext.SaveChangesAsync(ct);
|
||||
var productId = await stripe.CreateProductAsync(
|
||||
req.CreatorId,
|
||||
tierId,
|
||||
req.Name,
|
||||
req.Currency,
|
||||
req.Price);
|
||||
|
||||
// Record the new Tier
|
||||
var tier = new Tier
|
||||
{
|
||||
Id = tierId,
|
||||
CreatorId = req.CreatorId,
|
||||
Price = req.Price,
|
||||
Name = req.Name,
|
||||
Description = req.Description,
|
||||
StripeProductId = productId,
|
||||
};
|
||||
|
||||
dbContext.Tiers.Add(tier);
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(tier, ct);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
using Hutopy.Web.Common;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Memberships.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public class GetActiveSubscriptionsRequest;
|
||||
public record struct GetActiveSubscriptionsResponse(
|
||||
Guid Id,
|
||||
Guid CreatorId,
|
||||
string CreatorName,
|
||||
string CreatorPortraitUrl,
|
||||
DateTimeOffset StartDate,
|
||||
DateTimeOffset? EndDate);
|
||||
|
||||
[PublicAPI]
|
||||
public class GetActiveSubscriptionsHandler(
|
||||
MembershipDbContext dbDbContext)
|
||||
: Endpoint<GetActiveSubscriptionsRequest>
|
||||
MembershipDbContext dbContext)
|
||||
: EndpointWithoutRequest<List<GetActiveSubscriptionsResponse>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
@@ -19,13 +24,19 @@ public class GetActiveSubscriptionsHandler(
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
GetActiveSubscriptionsRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var subscriptions = await dbDbContext
|
||||
var subscriptions = await dbContext
|
||||
.Subscriptions
|
||||
.Where(subscription => subscription.UserId == User.GetUserId())
|
||||
.Where(subscription => subscription.IsActive)
|
||||
.Where(subscription => subscription.EndDate == null || subscription.EndDate > DateTimeOffset.UtcNow)
|
||||
.Select(subscription => new GetActiveSubscriptionsResponse(
|
||||
subscription.Id,
|
||||
subscription.Creator.Id,
|
||||
subscription.Creator.Name,
|
||||
subscription.Creator.PortraitUrl,
|
||||
subscription.StartDate,
|
||||
subscription.EndDate))
|
||||
.ToListAsync(ct);
|
||||
|
||||
await SendOkAsync(subscriptions, ct);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public class GetMembershipTierRequest
|
||||
{
|
||||
public record GetMembershipTierRequest
|
||||
{
|
||||
public Guid CreatorId { get; set; }
|
||||
}
|
||||
|
||||
@@ -13,35 +13,40 @@ public record struct TierModel(
|
||||
Guid Id,
|
||||
DateTime CreatedAt,
|
||||
string Name,
|
||||
string Description,
|
||||
decimal Price,
|
||||
string CurrencyCode);
|
||||
string CurrencyCode,
|
||||
string StripeProductId);
|
||||
|
||||
[PublicAPI]
|
||||
public class GetMembershipTierEndpoint(
|
||||
MembershipDbContext dbDbContext)
|
||||
: Endpoint<CreateMembershipTierRequest, List<TierModel>>
|
||||
MembershipDbContext dbContext)
|
||||
: Endpoint<GetMembershipTierRequest, List<TierModel>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/membership/tiers");
|
||||
Get("/api/membership/tiers/{CreatorId:guid}");
|
||||
Options(o => o.WithTags("Memberships"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CreateMembershipTierRequest req,
|
||||
GetMembershipTierRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tiers = await dbDbContext
|
||||
var tiers = await dbContext
|
||||
.Tiers
|
||||
.Where(tier => tier.CreatorId == req.CreatorId)
|
||||
.Select(tier => new TierModel(
|
||||
tier.Id,
|
||||
tier.CreatedAt,
|
||||
tier.Name,
|
||||
tier.Description,
|
||||
tier.Price,
|
||||
tier.CurrencyCode))
|
||||
tier.CurrencyCode,
|
||||
tier.StripeProductId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
|
||||
await SendOkAsync(tiers, ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public record struct TipReceivedModel(
|
||||
|
||||
[PublicAPI]
|
||||
public class GetReceivedTipsHandler(
|
||||
MembershipDbContext dbDbContext)
|
||||
MembershipDbContext dbContext)
|
||||
: EndpointWithoutRequest<List<TipReceivedModel>>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -28,7 +28,7 @@ public class GetReceivedTipsHandler(
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tipsReceived = await dbDbContext
|
||||
var tipsReceived = await dbContext
|
||||
.Tips
|
||||
.Where(tip => tip.CreatorId == User.GetUserId())
|
||||
.Select(tip => new TipReceivedModel(
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
using Hutopy.Web.Features.Memberships.Data;
|
||||
using Hutopy.Web.Features.Memberships.Services;
|
||||
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Stripe;
|
||||
|
||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||
|
||||
public static class StripeEvents
|
||||
{
|
||||
public const string SubscriptionCreated = "subscription_created";
|
||||
public const string CustomerSubscriptionDeleted = "customer.subscription_deleted";
|
||||
public const string InvoicePaymentSucceeded = "invoice.payment_succeeded";
|
||||
public const string InvoicePaymentFailed = "invoice.payment_failed";
|
||||
public const string CheckoutSessionCompleted = "checkout.session.completed";
|
||||
}
|
||||
|
||||
public class StripeWebhookEndpoint(
|
||||
MembershipDbContext dbContext,
|
||||
StripeService stripeService,
|
||||
IOptions<StripeOptions> options)
|
||||
: EndpointWithoutRequest
|
||||
@@ -37,33 +26,24 @@ public class StripeWebhookEndpoint(
|
||||
|
||||
switch (stripeEvent.Type)
|
||||
{
|
||||
case StripeEvents.InvoicePaymentSucceeded:
|
||||
await stripeService.HandlePaymentSucceeded(stripeEvent, ct);
|
||||
break;
|
||||
case StripeEvents.InvoicePaymentFailed:
|
||||
await stripeService.HandlePaymentFailed(stripeEvent, ct);
|
||||
break;
|
||||
case StripeEvents.CheckoutSessionCompleted:
|
||||
case "checkout.session.completed":
|
||||
await stripeService.HandleCheckoutSessionCompleted(stripeEvent, ct);
|
||||
break;
|
||||
case StripeEvents.CustomerSubscriptionDeleted:
|
||||
{
|
||||
var subscription = stripeEvent.Data.Object as Stripe.Subscription;
|
||||
var existingSubscription = await dbContext
|
||||
.Subscriptions
|
||||
.FirstOrDefaultAsync(x => x.StripeSubscriptionId == subscription!.Id, ct);
|
||||
|
||||
if (existingSubscription != null)
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
int lastDay = DateTime.DaysInMonth(today.Year, today.Month);
|
||||
var lastDayOfMonth = new DateTime(today.Year, today.Month, lastDay);
|
||||
existingSubscription.EndDate = new DateTimeOffset(lastDayOfMonth);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "invoice.payment_succeeded":
|
||||
await stripeService.HandleInvoicePaymentSucceeded(stripeEvent, ct);
|
||||
break;
|
||||
case "invoice.payment_failed":
|
||||
await stripeService.HandleInvoicePaymentFailed(stripeEvent, ct);
|
||||
break;
|
||||
case "customer.subscription.created":
|
||||
await stripeService.HandleCustomerSubscriptionCreated(stripeEvent, ct);
|
||||
break;
|
||||
case "customer.subscription.updated":
|
||||
await stripeService.HandleCustomerSubscriptionUpdated(stripeEvent, ct);
|
||||
break;
|
||||
case "customer.subscription.deleted":
|
||||
await stripeService.HandleCustomerSubscriptionDeleted(stripeEvent, ct);
|
||||
break;
|
||||
}
|
||||
|
||||
await SendOkAsync(ct);
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
using Hutopy.Web.Common;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Memberships.Data;
|
||||
using Hutopy.Web.Features.Memberships.Services;
|
||||
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||
|
||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record SendTipRequest
|
||||
{
|
||||
public Guid CreatorId { get; set; }
|
||||
public required decimal Amount { get; init; }
|
||||
public required string Currency { get; init; }
|
||||
public required string Message { get; init; }
|
||||
public required string CheckoutSuccessUrl { get; init; }
|
||||
public required string CheckoutCancelledUrl { get; init; }
|
||||
}
|
||||
public record SendTipRequest(
|
||||
Guid CreatorId,
|
||||
decimal Amount,
|
||||
string Currency,
|
||||
string Message,
|
||||
string CheckoutSuccessUrl,
|
||||
string CheckoutCancelledUrl);
|
||||
|
||||
[PublicAPI]
|
||||
public class SendTipResponse
|
||||
{
|
||||
public required string Status { get; init; }
|
||||
public required string StripeCheckoutUrl { get; init; }
|
||||
}
|
||||
public record SendTipResponse(
|
||||
string Status,
|
||||
string StripeCheckoutUrl);
|
||||
|
||||
[PublicAPI]
|
||||
public class SendTipRequestValidator : Validator<SendTipRequest>
|
||||
@@ -35,11 +30,11 @@ public class SendTipRequestValidator : Validator<SendTipRequest>
|
||||
RuleFor(x => x.CreatorId)
|
||||
.NotEmpty()
|
||||
.WithMessage("Creator ID is required");
|
||||
|
||||
|
||||
RuleFor(x => x.CheckoutSuccessUrl)
|
||||
.NotEmpty()
|
||||
.WithMessage("CheckoutSuccessUrl is required");
|
||||
|
||||
|
||||
RuleFor(x => x.CheckoutCancelledUrl)
|
||||
.NotEmpty()
|
||||
.WithMessage("CheckoutCancelledUrl is required");
|
||||
@@ -48,13 +43,13 @@ public class SendTipRequestValidator : Validator<SendTipRequest>
|
||||
|
||||
[PublicAPI]
|
||||
public class SendTipHandler(
|
||||
MembershipDbContext dbDbContext,
|
||||
MembershipDbContext dbContext,
|
||||
StripeService stripeService)
|
||||
: Endpoint<SendTipRequest, SendTipResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/tips/{CreatorId}");
|
||||
Post("/api/tips");
|
||||
Options(o => o.WithTags("Memberships"));
|
||||
}
|
||||
|
||||
@@ -62,57 +57,30 @@ public class SendTipHandler(
|
||||
SendTipRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var userName = User.GetName();
|
||||
|
||||
var creator = await dbDbContext.Creators.FindAsync(
|
||||
var creator = await dbContext.Creators.FindAsync(
|
||||
[req.CreatorId],
|
||||
cancellationToken: ct);
|
||||
|
||||
if (creator == null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var checkoutSession = await stripeService.CreateTipCheckoutSession(
|
||||
userId,
|
||||
req.Amount,
|
||||
req.Currency,
|
||||
var checkoutSession = await stripeService.CreateTipCheckoutSessionAsync(
|
||||
User.GetUserId(),
|
||||
User.GetAlias()!,
|
||||
creator.Id,
|
||||
creator.Name,
|
||||
req.Amount,
|
||||
req.Currency,
|
||||
req.Message,
|
||||
creator.StripeAccountId,
|
||||
req.CheckoutSuccessUrl,
|
||||
req.CheckoutCancelledUrl);
|
||||
|
||||
dbDbContext.Tips.Add(
|
||||
new Tip
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
TipperId = userId,
|
||||
TipperName = userName,
|
||||
CreatorId = creator.Id,
|
||||
CreatorName = creator.Name,
|
||||
Amount = req.Amount,
|
||||
Currency = req.Currency,
|
||||
Message = req.Message,
|
||||
StripeSessionId = checkoutSession.Id
|
||||
});
|
||||
|
||||
dbDbContext.Transactions.Add(
|
||||
new Transaction
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
StripeCheckoutSessionId = checkoutSession.Id,
|
||||
Amount = req.Amount,
|
||||
Type = "Tip",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await dbDbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendAsync(
|
||||
new SendTipResponse { Status = "Pending", StripeCheckoutUrl = checkoutSession.Url },
|
||||
new SendTipResponse("Pending", checkoutSession.Url),
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Hutopy.Web.Common;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Memberships.Data;
|
||||
using Hutopy.Web.Features.Memberships.Services;
|
||||
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||
|
||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||
|
||||
@@ -10,17 +9,13 @@ public class SubscribeRequest
|
||||
{
|
||||
public Guid CreatorId { get; set; }
|
||||
public Guid TierId { get; set; }
|
||||
public required string CheckoutSuccessUrl { get; init; }
|
||||
public required string CheckoutCancelledUrl { get; init; }
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public record struct SubscriptionResponse(
|
||||
Guid SubscriptionId,
|
||||
Guid CreatorId,
|
||||
Guid UserId,
|
||||
bool IsActive,
|
||||
string Tier,
|
||||
DateTimeOffset StartDate,
|
||||
DateTimeOffset? EndDate);
|
||||
string StripeCheckoutUrl);
|
||||
|
||||
[PublicAPI]
|
||||
public class SubscribeValidator : Validator<SubscribeRequest>
|
||||
@@ -33,7 +28,7 @@ public class SubscribeValidator : Validator<SubscribeRequest>
|
||||
|
||||
[PublicAPI]
|
||||
public class SubscribeHandler(
|
||||
MembershipDbContext dbDbContext,
|
||||
MembershipDbContext dbContext,
|
||||
StripeService stripeService)
|
||||
: Endpoint<SubscribeRequest, SubscriptionResponse>
|
||||
{
|
||||
@@ -47,12 +42,12 @@ public class SubscribeHandler(
|
||||
SubscribeRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tier = await dbDbContext
|
||||
var tier = await dbContext
|
||||
.Tiers
|
||||
.Include(tier => tier.Creator) // Include the related table
|
||||
.Where(tier => tier.Id == req.TierId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
|
||||
if (tier == null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
@@ -60,52 +55,18 @@ public class SubscribeHandler(
|
||||
}
|
||||
|
||||
// Process Stripe subscription
|
||||
var stripeSubscription = await stripeService.CreateSubscriptionCheckoutSession(
|
||||
var checkoutSession = await stripeService.CreateSubscriptionCheckoutSession(
|
||||
User.GetUserId(),
|
||||
tier.Price,
|
||||
tier.CurrencyCode,
|
||||
$"{tier.Name} from {tier.Creator.Name}",
|
||||
tier.Creator.Id,
|
||||
tier.Creator.Name,
|
||||
tier.Creator.StripeAccountId,
|
||||
"",
|
||||
"");
|
||||
|
||||
// Record subscription and transaction
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
StripeSubscriptionId = stripeSubscription.Id,
|
||||
CreatorId = tier.CreatorId,
|
||||
UserId = User.GetUserId(),
|
||||
Tier = tier,
|
||||
StartDate = DateTimeOffset.Now,
|
||||
EndDate = DateTimeOffset.Now.AddMonths(1)
|
||||
};
|
||||
|
||||
dbDbContext.Subscriptions.Add(subscription);
|
||||
|
||||
dbDbContext.Transactions.Add(
|
||||
new Transaction
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
StripeCheckoutSessionId = stripeSubscription.Id,
|
||||
Amount = tier.Price,
|
||||
Type = "Subscription",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await dbDbContext.SaveChangesAsync(ct);
|
||||
tier.Id,
|
||||
tier.StripePriceId,
|
||||
req.CheckoutSuccessUrl,
|
||||
req.CheckoutCancelledUrl);
|
||||
|
||||
await SendOkAsync(
|
||||
new SubscriptionResponse
|
||||
{
|
||||
UserId = subscription.UserId,
|
||||
CreatorId = subscription.CreatorId,
|
||||
SubscriptionId = subscription.Id,
|
||||
IsActive = subscription.IsActive,
|
||||
StartDate = subscription.StartDate,
|
||||
EndDate = subscription.EndDate,
|
||||
Tier = tier.Name,
|
||||
},
|
||||
ct);
|
||||
new SubscriptionResponse { StripeCheckoutUrl = checkoutSession.Url },
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user