Skip to content

Commit 28f05f1

Browse files
committed
WIP stripe
1 parent 308ef63 commit 28f05f1

34 files changed

Lines changed: 1177 additions & 319 deletions

SkredvarselGarminWeb/SkredvarselGarminWeb.Tests/SubscriptionServiceTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class SubscriptionServiceTests
3434
private readonly SkredvarselDbContext _dbContext;
3535
private readonly IDateTimeNowProvider _dateTimeNowProvider;
3636
private readonly IVippsApiClient _vippsApiClient;
37-
private readonly ISubscriptionService _subscriptionService;
37+
private readonly IVippsAgreementService _subscriptionService;
3838

3939
public SubscriptionServiceTests()
4040
{
@@ -54,11 +54,11 @@ public SubscriptionServiceTests()
5454
_vippsApiClient = Substitute.For<IVippsApiClient>();
5555
_dateTimeNowProvider = Substitute.For<IDateTimeNowProvider>();
5656

57-
_subscriptionService = new SubscriptionService(
57+
_subscriptionService = new VippsAgreementService(
5858
_dbContext,
5959
_vippsApiClient,
6060
_dateTimeNowProvider,
61-
Substitute.For<ILogger<SubscriptionService>>());
61+
Substitute.For<ILogger<VippsAgreementService>>());
6262
}
6363

6464
[Fact]

SkredvarselGarminWeb/SkredvarselGarminWeb/Database/DbContextAgreementExtensions.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using Microsoft.EntityFrameworkCore;
21
using SkredvarselGarminWeb.Entities;
32
using SkredvarselGarminWeb.Helpers;
43

@@ -22,7 +21,14 @@ public static List<Agreement> GetPendingAgreements(this SkredvarselDbContext dbC
2221

2322
public static bool DoesUserHaveActiveAgreement(this SkredvarselDbContext dbContext, string userId)
2423
{
25-
return dbContext.Agreements.Where(a => a.UserId == userId)
24+
var activeOrUnsubbedVippsAgreements = dbContext.Agreements
25+
.Where(a => a.UserId == userId)
2626
.Any(a => a.Status == AgreementStatus.ACTIVE || a.Status == AgreementStatus.UNSUBSCRIBED);
27+
28+
var activeOrUnsubbedStripeSubscriptions = dbContext.StripeSubscriptions
29+
.Where(ss => ss.UserId == userId)
30+
.Any(ss => ss.Status == StripeSubscriptionStatus.ACTIVE || ss.Status == StripeSubscriptionStatus.UNSUBSCRIBED);
31+
32+
return activeOrUnsubbedVippsAgreements || activeOrUnsubbedStripeSubscriptions;
2733
}
2834
}

SkredvarselGarminWeb/SkredvarselGarminWeb/Database/SkredvarselDbContext.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@
33

44
namespace SkredvarselGarminWeb.Database;
55

6-
public class SkredvarselDbContext : DbContext
6+
public class SkredvarselDbContext(DbContextOptions options) : DbContext(options)
77
{
8-
public SkredvarselDbContext(DbContextOptions options) : base(options)
9-
{
10-
}
11-
128
public virtual DbSet<User> Users => Set<User>();
139
public virtual DbSet<Agreement> Agreements => Set<Agreement>();
1410
public virtual DbSet<Watch> Watches => Set<Watch>();
1511
public virtual DbSet<WatchAddRequest> WatchAddRequests => Set<WatchAddRequest>();
12+
public virtual DbSet<StripeSubscription> StripeSubscriptions => Set<StripeSubscription>();
1613

1714
protected override void OnModelCreating(ModelBuilder modelBuilder)
1815
{
16+
modelBuilder.Entity<User>()
17+
.HasIndex(x => x.StripeCustomerId)
18+
.IsUnique()
19+
.AreNullsDistinct(true);
20+
1921
modelBuilder.Entity<Agreement>()
2022
.HasOne(a => a.User)
2123
.WithMany(u => u.Agreements)
@@ -25,5 +27,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
2527
.HasOne(a => a.User)
2628
.WithMany(u => u.Watches)
2729
.HasForeignKey(a => a.UserId);
30+
31+
modelBuilder.Entity<StripeSubscription>()
32+
.HasOne(ss => ss.User)
33+
.WithMany(u => u.StripeSubscriptions)
34+
.HasForeignKey(ss => ss.UserId);
2835
}
2936
}

SkredvarselGarminWeb/SkredvarselGarminWeb/Endpoints/Models/Subscription.cs

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Text.Json.Serialization;
2+
using SkredvarselGarminWeb.Entities;
3+
4+
namespace SkredvarselGarminWeb.Endpoints.Models;
5+
6+
public class SubscriptionResponse
7+
{
8+
[JsonConverter(typeof(JsonStringEnumConverter))]
9+
public required SubscriptionType SubscriptionType { get; init; }
10+
11+
[JsonConverter(typeof(JsonStringEnumConverter))]
12+
public AgreementStatus? VippsAgreementStatus { get; init; }
13+
14+
[JsonConverter(typeof(JsonStringEnumConverter))]
15+
public StripeSubscriptionStatus? StripeSubscriptionStatus { get; init; }
16+
17+
public required DateOnly? NextChargeDate { get; init; }
18+
public string? VippsConfirmationUrl { get; init; }
19+
}
20+
21+
public enum SubscriptionType
22+
{
23+
Vipps,
24+
Stripe
25+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.AspNetCore.Mvc.ModelBinding;
3+
using Microsoft.Extensions.Options;
4+
using Microsoft.IdentityModel.Tokens;
5+
using SkredvarselGarminWeb.Database;
6+
using SkredvarselGarminWeb.Extensions;
7+
using SkredvarselGarminWeb.Options;
8+
using SkredvarselGarminWeb.Services;
9+
using Stripe;
10+
using Stripe.Checkout;
11+
12+
namespace SkredvarselGarminWeb.Endpoints;
13+
14+
public static class StripeSubscriptionEndpointsRouteBuilderExtensions
15+
{
16+
public static void MapStripeSubscriptionEndpoints(this IEndpointRouteBuilder app)
17+
{
18+
app.MapGet("/createStripeSubscription", async (
19+
HttpContext ctx,
20+
SkredvarselDbContext dbContext,
21+
IStripeClient stripeClient,
22+
IOptions<StripeOptions> stripeOptions) =>
23+
{
24+
var user = dbContext.GetUserOrThrow(ctx.User.Identity);
25+
var baseUrl = ctx.GetBaseUrl();
26+
27+
var userHasActiveStripeSubscriptions = dbContext.StripeSubscriptions
28+
.Where(ss => ss.UserId == user.Id)
29+
.Where(ss => ss.Status == Entities.StripeSubscriptionStatus.ACTIVE)
30+
.Any();
31+
32+
if (userHasActiveStripeSubscriptions)
33+
{
34+
return Results.Redirect("/stripe-customer-portal");
35+
}
36+
37+
var options = new SessionCreateOptions
38+
{
39+
SuccessUrl = $"{baseUrl}/stripe-subscribe-callback?session_id={{CHECKOUT_SESSION_ID}}",
40+
CancelUrl = $"{baseUrl}/minSide",
41+
Mode = "subscription",
42+
CustomerEmail = user.StripeCustomerId.IsNullOrEmpty() ? user.Email : null,
43+
Customer = user.StripeCustomerId,
44+
ClientReferenceId = user.Id,
45+
LineItems = [
46+
new SessionLineItemOptions {
47+
Price = stripeOptions.Value.PriceId,
48+
Quantity = 1,
49+
}
50+
],
51+
};
52+
53+
var service = new SessionService(stripeClient);
54+
var session = await service.CreateAsync(options);
55+
56+
return Results.Redirect(session.Url);
57+
}).RequireAuthorization();
58+
59+
app.MapGet("/stripe-subscribe-callback", async (
60+
HttpContext ctx,
61+
IStripeClient stripeClient,
62+
IStripeService stripeService,
63+
[FromQuery(Name = "session_id")] string sessionId,
64+
ILoggerFactory loggerFactory) =>
65+
{
66+
var logger = loggerFactory.CreateLogger("stripe-subscribe-callback");
67+
logger.LogInformation("Received stripe subscribe callback.");
68+
69+
var baseUrl = ctx.GetBaseUrl();
70+
var service = new SessionService(stripeClient);
71+
var session = await service.GetAsync(sessionId);
72+
73+
stripeService.StoreNewSubscriptionIfNotExists(session);
74+
75+
return Results.Redirect("/minSide");
76+
}).RequireAuthorization();
77+
78+
app.MapGet("/stripe-customer-portal", async (
79+
HttpContext ctx,
80+
IStripeClient stripeClient,
81+
SkredvarselDbContext dbContext) =>
82+
{
83+
var user = dbContext.GetUserOrThrow(ctx.User.Identity);
84+
var baseUrl = ctx.GetBaseUrl();
85+
86+
var options = new Stripe.BillingPortal.SessionCreateOptions
87+
{
88+
Customer = user.StripeCustomerId,
89+
ReturnUrl = $"{baseUrl}/minSide",
90+
};
91+
92+
var service = new Stripe.BillingPortal.SessionService(stripeClient);
93+
var session = await service.CreateAsync(options);
94+
95+
return Results.Redirect(session.Url);
96+
}).RequireAuthorization();
97+
98+
app.MapPost("/stripe-webhook", async (
99+
HttpContext ctx,
100+
[FromHeader(Name = "Stripe-Signature")] string stripeSignature,
101+
IStripeService stripeService,
102+
IOptions<StripeOptions> stripeOptions,
103+
ILoggerFactory loggerFactory) =>
104+
{
105+
var logger = loggerFactory.CreateLogger("stripe-webhook");
106+
107+
var json = await new StreamReader(ctx.Request.Body).ReadToEndAsync();
108+
109+
Event stripeEvent;
110+
try
111+
{
112+
stripeEvent = EventUtility.ConstructEvent(
113+
json,
114+
stripeSignature,
115+
stripeOptions.Value.WebhookSecret
116+
);
117+
logger.LogInformation("Webhook notification with type: {eventType} found for {eventId}", stripeEvent.Type, stripeEvent.Id);
118+
119+
stripeService.HandleWebhook(stripeEvent);
120+
}
121+
catch (Exception e)
122+
{
123+
logger.LogError(e, "Failed to parse stripe webhook event.");
124+
return Results.BadRequest();
125+
}
126+
127+
return Results.Ok();
128+
});
129+
}
130+
}

0 commit comments

Comments
 (0)