many fixes and improvements - rework for modules/ and common/
feat(emailer): add Postmark and Resend providers
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Modules.Memberships.Contracts;
|
||||
|
||||
public interface IMembershipCancellationProcessor
|
||||
{
|
||||
Task<DateTimeOffset?> CancelAsync(string subscriptionId, CancellationToken ct = default);
|
||||
}
|
||||
26
backend/Modules/Memberships/Contracts/IMembershipNotifier.cs
Normal file
26
backend/Modules/Memberships/Contracts/IMembershipNotifier.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace Hutopy.Modules.Memberships.Contracts;
|
||||
|
||||
public interface IMembershipNotifier
|
||||
{
|
||||
Task NotifyCheckoutSessionCompleted(string stripeSessionId, string stripeSubscriptionId,
|
||||
string userId,
|
||||
string creatorId,
|
||||
string tierId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task NotifyPaymentSucceedAsync(
|
||||
string stripeSubscriptionId,
|
||||
string hostedInvoiceUrl,
|
||||
decimal amount,
|
||||
string currency,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task NotifySubscriptionUpdatedAsync(
|
||||
string subscriptionId,
|
||||
DateTimeOffset? endDate,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task NotifySubscriptionDeletedAsync(
|
||||
string subscriptionId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Hutopy.Modules.Creators.Contracts;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Contracts;
|
||||
|
||||
public interface IMembershipPaymentProcessor
|
||||
{
|
||||
Task<MembershipCheckoutSession> CreateCheckoutSessionAsync(
|
||||
Guid userId,
|
||||
CreatorReference creatorReference,
|
||||
Guid tierId,
|
||||
string priceId,
|
||||
string successUrl,
|
||||
string cancelUrl);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Modules.Memberships.Contracts;
|
||||
|
||||
public interface IMembershipTierProcessor
|
||||
{
|
||||
Task<string> CreateAsync(
|
||||
Guid creatorId,
|
||||
Guid tierId,
|
||||
string productName,
|
||||
string currencyCode,
|
||||
decimal amount);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Modules.Memberships.Contracts;
|
||||
|
||||
[PublicAPI]
|
||||
public record MembershipCheckoutSession(
|
||||
string Id,
|
||||
string Url);
|
||||
20
backend/Modules/Memberships/Data/Membership.cs
Normal file
20
backend/Modules/Memberships/Data/Membership.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Data;
|
||||
|
||||
public class Membership
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public Guid CreatorId { get; set; }
|
||||
public Guid TierId { get; set; }
|
||||
public MembershipTier? MembershipTier { get; set; }
|
||||
public MembershipState State { get; set; }
|
||||
public DateTimeOffset? StartDate { get; set; }
|
||||
public DateTimeOffset? EndDate { get; set; }
|
||||
public bool IsActive => EndDate == null || EndDate > DateTimeOffset.UtcNow;
|
||||
[MaxLength(256)]public string? StripeSubscriptionId { get; set; }
|
||||
|
||||
public ICollection<Payment> Payments { get; set; } = [];
|
||||
}
|
||||
8
backend/Modules/Memberships/Data/MembershipState.cs
Normal file
8
backend/Modules/Memberships/Data/MembershipState.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Hutopy.Modules.Memberships.Data;
|
||||
|
||||
public enum MembershipState
|
||||
{
|
||||
Pending,
|
||||
Active,
|
||||
Inactive
|
||||
}
|
||||
17
backend/Modules/Memberships/Data/MembershipTier.cs
Normal file
17
backend/Modules/Memberships/Data/MembershipTier.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Hutopy.Common.Domain;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Data;
|
||||
|
||||
public class MembershipTier : Entity
|
||||
{
|
||||
public Guid CreatorId { get; set; }
|
||||
[MaxLength(128)] public string Name { get; set; } = null!;
|
||||
[MaxLength(4096)] public string Description { get; set; } = null!;
|
||||
public decimal Price { get; set; }
|
||||
[MaxLength(128)] public string CurrencyCode { get; set; } = null!;
|
||||
[MaxLength(128)] public string StripeProductId { get; set; } = null!;
|
||||
[MaxLength(128)] public string StripePriceId { get; set; } = null!;
|
||||
|
||||
public ICollection<Membership> Subscriptions { get; set; } = [];
|
||||
}
|
||||
36
backend/Modules/Memberships/Data/MembershipsDbContext.cs
Normal file
36
backend/Modules/Memberships/Data/MembershipsDbContext.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace Hutopy.Modules.Memberships.Data;
|
||||
|
||||
public sealed class MembershipsDbContext(
|
||||
DbContextOptions<MembershipsDbContext> options)
|
||||
: DbContext(options)
|
||||
{
|
||||
public const string SchemaName = "Memberships";
|
||||
|
||||
public DbSet<MembershipTier> MembershipTiers => Set<MembershipTier>();
|
||||
public DbSet<Membership> Memberships => Set<Membership>();
|
||||
public DbSet<Payment> Payments => Set<Payment>();
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema(SchemaName);
|
||||
|
||||
modelBuilder
|
||||
.Entity<MembershipTier>()
|
||||
.Property(c => c.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
modelBuilder
|
||||
.Entity<Membership>()
|
||||
.Property(c => c.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
modelBuilder
|
||||
.Entity<Payment>()
|
||||
.Property(c => c.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
}
|
||||
}
|
||||
13
backend/Modules/Memberships/Data/Payment.cs
Normal file
13
backend/Modules/Memberships/Data/Payment.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Data;
|
||||
|
||||
public class Payment
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
[MaxLength(8)] public required string Currency { get; set; }
|
||||
[MaxLength(2048)] public required string InvoiceUrl { get; set; }
|
||||
|
||||
}
|
||||
32
backend/Modules/Memberships/DependencyInjection.cs
Normal file
32
backend/Modules/Memberships/DependencyInjection.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Hutopy.Modules.Memberships.Contracts;
|
||||
using Hutopy.Modules.Memberships.Data;
|
||||
using Hutopy.Modules.Memberships.Services;
|
||||
|
||||
namespace Hutopy.Modules.Memberships;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static WebApplicationBuilder AddMembershipModule(
|
||||
this WebApplicationBuilder builder,
|
||||
Action<DbContextOptionsBuilder>? configureAction = null)
|
||||
{
|
||||
builder.Services.AddDbContext<MembershipsDbContext>(configureAction);
|
||||
|
||||
builder.Services.AddTransient<IMembershipNotifier, MembershipNotifier>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
public static async Task<IApplicationBuilder> UseMembershipModuleAsync(
|
||||
this IApplicationBuilder app,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
await using var context = scope.ServiceProvider.GetRequiredService<MembershipsDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken: cancellationToken);
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
49
backend/Modules/Memberships/Handlers/CancelMembership.cs
Normal file
49
backend/Modules/Memberships/Handlers/CancelMembership.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Hutopy.Modules.Memberships.Contracts;
|
||||
using Hutopy.Modules.Memberships.Data;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public class CancelMembershipRequest
|
||||
{
|
||||
public Guid SubscriptionId { get; set; }
|
||||
}
|
||||
|
||||
public class CancelMembershipHandler(
|
||||
MembershipsDbContext dbContext,
|
||||
IMembershipCancellationProcessor cancellationProcessor)
|
||||
: Endpoint<CancelMembershipRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/api/memberships");
|
||||
Options(o => o.WithTags("Memberships"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CancelMembershipRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var subscription = await dbContext
|
||||
.Memberships
|
||||
.FindAsync(
|
||||
[req.SubscriptionId],
|
||||
cancellationToken: ct);
|
||||
|
||||
if (subscription is not { EndDate: null }
|
||||
|| subscription.StripeSubscriptionId is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel Stripe subscription
|
||||
await cancellationProcessor.CancelAsync(subscription.StripeSubscriptionId, ct);
|
||||
|
||||
// Update subscription in the system
|
||||
subscription.EndDate = DateTime.UtcNow;
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(subscription.Id, ct);
|
||||
}
|
||||
}
|
||||
56
backend/Modules/Memberships/Handlers/CreateMembershipTier.cs
Normal file
56
backend/Modules/Memberships/Handlers/CreateMembershipTier.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Hutopy.Modules.Memberships.Contracts;
|
||||
using Hutopy.Modules.Memberships.Data;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record struct CreateMembershipTierRequest(
|
||||
Guid CreatorId,
|
||||
string Name,
|
||||
string Description,
|
||||
decimal Price,
|
||||
string Currency = "CAD");
|
||||
|
||||
[PublicAPI]
|
||||
public class CreateMembershipTierEndpoint(
|
||||
MembershipsDbContext dbContext,
|
||||
IMembershipTierProcessor membershipTierProcessor)
|
||||
: Endpoint<CreateMembershipTierRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/memberships/tiers");
|
||||
Options(o => o.WithTags("Memberships"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CreateMembershipTierRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tierId = Guid.CreateVersion7();
|
||||
|
||||
var productId = await membershipTierProcessor.CreateAsync(
|
||||
req.CreatorId,
|
||||
tierId,
|
||||
req.Name,
|
||||
req.Currency,
|
||||
req.Price);
|
||||
|
||||
// Record the new Tier
|
||||
var tier = new MembershipTier
|
||||
{
|
||||
Id = tierId,
|
||||
CreatorId = req.CreatorId,
|
||||
Price = req.Price,
|
||||
Name = req.Name,
|
||||
Description = req.Description,
|
||||
StripeProductId = productId,
|
||||
};
|
||||
|
||||
dbContext.MembershipTiers.Add(tier);
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(tier, ct);
|
||||
}
|
||||
}
|
||||
54
backend/Modules/Memberships/Handlers/GetActiveMemberships.cs
Normal file
54
backend/Modules/Memberships/Handlers/GetActiveMemberships.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Contracts;
|
||||
using Hutopy.Modules.Memberships.Data;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record struct GetActiveMembershipsResponse(
|
||||
Guid Id,
|
||||
Guid CreatorId,
|
||||
string CreatorName,
|
||||
string CreatorPortraitUrl,
|
||||
DateTimeOffset? StartDate,
|
||||
DateTimeOffset? EndDate);
|
||||
|
||||
[PublicAPI]
|
||||
public class GetActiveMembershipsHandler(
|
||||
ICreatorLookup creatorLookup,
|
||||
MembershipsDbContext dbContext)
|
||||
: EndpointWithoutRequest<IEnumerable<GetActiveMembershipsResponse>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/memberships/active");
|
||||
Options(o => o.WithTags("Memberships"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken ct)
|
||||
{
|
||||
var subscriptions = await dbContext
|
||||
.Memberships
|
||||
.Where(subscription => subscription.UserId == User.GetUserId())
|
||||
.Where(subscription => subscription.State == MembershipState.Active)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var result = await Task.WhenAll(
|
||||
subscriptions.Select(async subscription =>
|
||||
{
|
||||
var creator = await creatorLookup.GetCreatorAsync(subscription.CreatorId, ct);
|
||||
|
||||
return new GetActiveMembershipsResponse(
|
||||
subscription.Id,
|
||||
subscription.CreatorId,
|
||||
creator?.Name ?? "Unknown Creator",
|
||||
creator?.PortraitUrl ?? string.Empty,
|
||||
subscription.StartDate,
|
||||
subscription.EndDate);
|
||||
}));
|
||||
|
||||
|
||||
await SendOkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
52
backend/Modules/Memberships/Handlers/GetMembershipTiers.cs
Normal file
52
backend/Modules/Memberships/Handlers/GetMembershipTiers.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Hutopy.Modules.Memberships.Data;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record GetMembershipTiersRequest
|
||||
{
|
||||
public Guid CreatorId { get; set; }
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public record struct TierModel(
|
||||
Guid Id,
|
||||
DateTimeOffset CreatedAt,
|
||||
string Name,
|
||||
string Description,
|
||||
decimal Price,
|
||||
string CurrencyCode,
|
||||
string StripeProductId);
|
||||
|
||||
[PublicAPI]
|
||||
public class GetMembershipTiersEndpoint(
|
||||
MembershipsDbContext dbContext)
|
||||
: Endpoint<GetMembershipTiersRequest, List<TierModel>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/memberships/tiers/{CreatorId:guid}");
|
||||
Options(o => o.WithTags("Memberships"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
GetMembershipTiersRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tiers = await dbContext
|
||||
.MembershipTiers
|
||||
.Where(tier => tier.CreatorId == req.CreatorId)
|
||||
.Select(tier => new TierModel(
|
||||
tier.Id,
|
||||
tier.CreatedAt,
|
||||
tier.Name,
|
||||
tier.Description,
|
||||
tier.Price,
|
||||
tier.CurrencyCode,
|
||||
tier.StripeProductId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
await SendOkAsync(tiers, ct);
|
||||
}
|
||||
}
|
||||
90
backend/Modules/Memberships/Handlers/HandleStripe.cs
Normal file
90
backend/Modules/Memberships/Handlers/HandleStripe.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System.Diagnostics;
|
||||
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||
using Hutopy.Modules.Memberships.Contracts;
|
||||
using Hutopy.Modules.Tipping.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Stripe;
|
||||
using Stripe.Checkout;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Handlers;
|
||||
|
||||
public class StripeWebhookEndpoint(
|
||||
ITipPaymentNotifier tipPaymentNotifier,
|
||||
IMembershipNotifier membershipNotifier,
|
||||
IOptions<StripeOptions> options)
|
||||
: EndpointWithoutRequest
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/stripe");
|
||||
AllowAnonymous();
|
||||
Options(o => o.WithTags( "Webhooks"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
using var streamReader = new StreamReader(HttpContext.Request.Body);
|
||||
var json = await streamReader.ReadToEndAsync(ct);
|
||||
|
||||
var signatureHeader = HttpContext.Request.Headers["Stripe-Signature"];
|
||||
var stripeEvent = EventUtility.ConstructEvent(json, signatureHeader, options.Value.WebhookSecret);
|
||||
|
||||
var stripeSession = stripeEvent.Data.Object as Session;
|
||||
var stripeSubscription = stripeEvent.Data.Object as Subscription;
|
||||
|
||||
switch (stripeEvent.Type)
|
||||
{
|
||||
case "checkout.session.completed":
|
||||
Debug.Assert(stripeSession != null);
|
||||
switch (stripeSession.Mode)
|
||||
{
|
||||
// Check if this is a one-time tip
|
||||
case "payment" when stripeSession.PaymentIntentId != null
|
||||
&& stripeSession.PaymentIntent.Status == "paid":
|
||||
await tipPaymentNotifier.NotifyPaymentSucceedAsync(
|
||||
stripeSession.Id,
|
||||
stripeSession.Invoice.HostedInvoiceUrl,
|
||||
ct);
|
||||
break;
|
||||
// Check if this is a subscription
|
||||
case "subscription" when stripeSession.SubscriptionId != null:
|
||||
await membershipNotifier.NotifyPaymentSucceedAsync(
|
||||
stripeSession.SubscriptionId,
|
||||
stripeSession.Invoice.HostedInvoiceUrl,
|
||||
stripeSession.Invoice.Total,
|
||||
stripeSession.Invoice.Currency,
|
||||
cancellationToken: ct);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case "invoice.payment_succeeded":
|
||||
var invoice = (stripeEvent.Data.Object as Invoice);
|
||||
Debug.Assert(invoice != null);
|
||||
Debug.Assert(invoice.Subscription != null);
|
||||
await membershipNotifier.NotifyPaymentSucceedAsync(
|
||||
invoice.SubscriptionId,
|
||||
invoice.HostedInvoiceUrl,
|
||||
invoice.Total,
|
||||
invoice.Currency,
|
||||
cancellationToken: ct);
|
||||
break;
|
||||
|
||||
case "customer.subscription.updated":
|
||||
Debug.Assert(stripeSubscription != null);
|
||||
await membershipNotifier.NotifySubscriptionUpdatedAsync(
|
||||
stripeSubscription.Id,
|
||||
stripeSubscription.CancelAt ?? stripeSubscription.CanceledAt,
|
||||
ct);
|
||||
break;
|
||||
case "customer.subscription.deleted":
|
||||
Debug.Assert(stripeSubscription != null);
|
||||
await membershipNotifier.NotifySubscriptionDeletedAsync(
|
||||
stripeSubscription.Id,
|
||||
ct);
|
||||
break;
|
||||
}
|
||||
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
}
|
||||
83
backend/Modules/Memberships/Handlers/SubscribeToCreator.cs
Normal file
83
backend/Modules/Memberships/Handlers/SubscribeToCreator.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Contracts;
|
||||
using Hutopy.Modules.Memberships.Contracts;
|
||||
using Hutopy.Modules.Memberships.Data;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public class SubscribeRequest
|
||||
{
|
||||
public Guid CreatorId { get; set; }
|
||||
public Guid MembershipTierId { get; set; }
|
||||
public required string CheckoutSuccessUrl { get; init; }
|
||||
public required string CheckoutCancelledUrl { get; init; }
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public record struct SubscriptionResponse(
|
||||
string StripeCheckoutUrl);
|
||||
|
||||
[PublicAPI]
|
||||
public class SubscribeValidator : Validator<SubscribeRequest>
|
||||
{
|
||||
public SubscribeValidator()
|
||||
{
|
||||
RuleFor(x => x.MembershipTierId).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public class SubscribeHandler(
|
||||
MembershipsDbContext dbContext,
|
||||
ICreatorLookup creatorLookup,
|
||||
IMembershipPaymentProcessor membershipPaymentProcessor)
|
||||
: Endpoint<SubscribeRequest, SubscriptionResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/memberships/subscribe");
|
||||
Options(o => o.WithTags("Memberships"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
SubscribeRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tier = await dbContext
|
||||
.MembershipTiers
|
||||
.Where(tier => tier.Id == req.MembershipTierId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (tier == null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var creator = await creatorLookup.GetCreatorAsync(tier.CreatorId, ct);
|
||||
if (creator == null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!creator.AcceptCharges)
|
||||
{
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process Stripe subscription
|
||||
var checkoutSession = await membershipPaymentProcessor.CreateCheckoutSessionAsync(
|
||||
User.GetUserId(),
|
||||
creator,
|
||||
tier.Id,
|
||||
tier.StripePriceId,
|
||||
req.CheckoutSuccessUrl,
|
||||
req.CheckoutCancelledUrl);
|
||||
|
||||
await SendOkAsync(
|
||||
new SubscriptionResponse { StripeCheckoutUrl = checkoutSession.Url },
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
190
backend/Modules/Memberships/Migrations/20250609212641_Initial.Designer.cs
generated
Normal file
190
backend/Modules/Memberships/Migrations/20250609212641_Initial.Designer.cs
generated
Normal file
@@ -0,0 +1,190 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Hutopy.Modules.Memberships.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Migrations
|
||||
{
|
||||
[DbContext(typeof(MembershipsDbContext))]
|
||||
[Migration("20250609212641_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Memberships")
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.Membership", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("CreatorId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("MembershipTierId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("State")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("StripeSubscriptionId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid>("TierId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MembershipTierId");
|
||||
|
||||
b.ToTable("Memberships", "Memberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.MembershipTier", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("CreatorId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("DeletedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("StripePriceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("StripeProductId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MembershipTiers", "Memberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.Payment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("InvoiceUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<Guid?>("MembershipId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MembershipId");
|
||||
|
||||
b.ToTable("Payments", "Memberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.Membership", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Modules.Memberships.Data.MembershipTier", "MembershipTier")
|
||||
.WithMany("Subscriptions")
|
||||
.HasForeignKey("MembershipTierId");
|
||||
|
||||
b.Navigation("MembershipTier");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.Payment", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Modules.Memberships.Data.Membership", null)
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("MembershipId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.Membership", b =>
|
||||
{
|
||||
b.Navigation("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.MembershipTier", b =>
|
||||
{
|
||||
b.Navigation("Subscriptions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
119
backend/Modules/Memberships/Migrations/20250609212641_Initial.cs
Normal file
119
backend/Modules/Memberships/Migrations/20250609212641_Initial.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "Memberships");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MembershipTiers",
|
||||
schema: "Memberships",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||
Price = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
CurrencyCode = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
StripeProductId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
StripePriceId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
DeletedBy = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
DeletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MembershipTiers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Memberships",
|
||||
schema: "Memberships",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TierId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
MembershipTierId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
State = table.Column<int>(type: "integer", nullable: false),
|
||||
StartDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
EndDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
StripeSubscriptionId = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Memberships", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Memberships_MembershipTiers_MembershipTierId",
|
||||
column: x => x.MembershipTierId,
|
||||
principalSchema: "Memberships",
|
||||
principalTable: "MembershipTiers",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Payments",
|
||||
schema: "Memberships",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
Amount = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
Currency = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
|
||||
InvoiceUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
MembershipId = table.Column<Guid>(type: "uuid", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Payments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Payments_Memberships_MembershipId",
|
||||
column: x => x.MembershipId,
|
||||
principalSchema: "Memberships",
|
||||
principalTable: "Memberships",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Memberships_MembershipTierId",
|
||||
schema: "Memberships",
|
||||
table: "Memberships",
|
||||
column: "MembershipTierId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Payments_MembershipId",
|
||||
schema: "Memberships",
|
||||
table: "Payments",
|
||||
column: "MembershipId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Payments",
|
||||
schema: "Memberships");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Memberships",
|
||||
schema: "Memberships");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MembershipTiers",
|
||||
schema: "Memberships");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Hutopy.Modules.Memberships.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Migrations
|
||||
{
|
||||
[DbContext(typeof(MembershipsDbContext))]
|
||||
partial class MembershipsDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Memberships")
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.Membership", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("CreatorId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("MembershipTierId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("State")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("StripeSubscriptionId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid>("TierId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MembershipTierId");
|
||||
|
||||
b.ToTable("Memberships", "Memberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.MembershipTier", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("CreatorId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("CurrencyCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("DeletedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("StripePriceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("StripeProductId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MembershipTiers", "Memberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.Payment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("InvoiceUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<Guid?>("MembershipId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MembershipId");
|
||||
|
||||
b.ToTable("Payments", "Memberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.Membership", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Modules.Memberships.Data.MembershipTier", "MembershipTier")
|
||||
.WithMany("Subscriptions")
|
||||
.HasForeignKey("MembershipTierId");
|
||||
|
||||
b.Navigation("MembershipTier");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.Payment", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Modules.Memberships.Data.Membership", null)
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("MembershipId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.Membership", b =>
|
||||
{
|
||||
b.Navigation("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Modules.Memberships.Data.MembershipTier", b =>
|
||||
{
|
||||
b.Navigation("Subscriptions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
109
backend/Modules/Memberships/Services/MembershipNotifier.cs
Normal file
109
backend/Modules/Memberships/Services/MembershipNotifier.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Hutopy.Modules.Memberships.Contracts;
|
||||
using Hutopy.Modules.Memberships.Data;
|
||||
|
||||
namespace Hutopy.Modules.Memberships.Services;
|
||||
|
||||
public class MembershipNotifier(
|
||||
MembershipsDbContext dbContext)
|
||||
: IMembershipNotifier
|
||||
{
|
||||
public async Task NotifyCheckoutSessionCompleted(
|
||||
string stripeSessionId,
|
||||
string stripeSubscriptionId,
|
||||
string userId,
|
||||
string creatorId,
|
||||
string tierId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var membership = new Membership
|
||||
{
|
||||
Id = Guid.CreateVersion7(),
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UserId = Guid.Parse(userId),
|
||||
CreatorId = Guid.Parse(creatorId),
|
||||
TierId = Guid.Parse(tierId),
|
||||
StripeSubscriptionId = stripeSubscriptionId,
|
||||
State = MembershipState.Pending,
|
||||
StartDate = null,
|
||||
EndDate = null
|
||||
};
|
||||
|
||||
dbContext.Memberships.Add(membership);
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task NotifyPaymentSucceedAsync(
|
||||
string stripeSubscriptionId,
|
||||
string hostedInvoiceUrl,
|
||||
decimal amount,
|
||||
string currency,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var membership = await dbContext
|
||||
.Memberships
|
||||
.SingleOrDefaultAsync(
|
||||
m => m.StripeSubscriptionId == stripeSubscriptionId,
|
||||
cancellationToken: cancellationToken);
|
||||
if (membership is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var payment = new Payment
|
||||
{
|
||||
Id = Guid.CreateVersion7(),
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Amount = amount,
|
||||
Currency = currency,
|
||||
InvoiceUrl = hostedInvoiceUrl
|
||||
};
|
||||
|
||||
membership.State = MembershipState.Active;
|
||||
membership.StartDate = DateTimeOffset.UtcNow;
|
||||
membership.Payments.Add(payment);
|
||||
|
||||
dbContext.Payments.Add(payment);
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task NotifySubscriptionUpdatedAsync(
|
||||
string subscriptionId,
|
||||
DateTimeOffset? endDate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var membership = await dbContext
|
||||
.Memberships
|
||||
.SingleOrDefaultAsync(
|
||||
s => s.StripeSubscriptionId == subscriptionId,
|
||||
cancellationToken: cancellationToken);
|
||||
if (membership == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
membership.EndDate = endDate;
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task NotifySubscriptionDeletedAsync(
|
||||
string subscriptionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var membership = await dbContext
|
||||
.Memberships
|
||||
.SingleOrDefaultAsync(
|
||||
s => s.StripeSubscriptionId == subscriptionId,
|
||||
cancellationToken: cancellationToken);
|
||||
if (membership == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
membership.State = MembershipState.Inactive;
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user