Skip to content

Commit 54da797

Browse files
committed
Fix broken unsubscribe from stripe.
1 parent 0619f3a commit 54da797

File tree

3 files changed

+146
-18
lines changed

3 files changed

+146
-18
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using AwesomeAssertions;
2+
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.EntityFrameworkCore.Diagnostics;
5+
using Microsoft.Extensions.Logging;
6+
7+
using NSubstitute;
8+
9+
using SkredvarselGarminWeb.Database;
10+
using SkredvarselGarminWeb.Entities;
11+
using SkredvarselGarminWeb.Entities.Mappers;
12+
using SkredvarselGarminWeb.Helpers;
13+
using SkredvarselGarminWeb.Services;
14+
15+
using Stripe;
16+
17+
namespace SkredvarselGarminWeb.Tests;
18+
19+
public class StripeServiceTests
20+
{
21+
private readonly SkredvarselDbContext _dbContext;
22+
private readonly StripeService _stripeService;
23+
24+
public StripeServiceTests()
25+
{
26+
var dbContextOptions = new DbContextOptionsBuilder<SkredvarselDbContext>()
27+
.UseInMemoryDatabase($"StripeServiceTests-{Guid.NewGuid()}")
28+
.ConfigureWarnings(builder => builder.Ignore(InMemoryEventId.TransactionIgnoredWarning))
29+
.Options;
30+
31+
_dbContext = new SkredvarselDbContext(dbContextOptions);
32+
_dbContext.Database.EnsureDeleted();
33+
_dbContext.Database.EnsureCreated();
34+
35+
_stripeService = new StripeService(
36+
_dbContext,
37+
Substitute.For<IStripeClient>(),
38+
Substitute.For<INotificationService>(),
39+
Substitute.For<IDateTimeNowProvider>(),
40+
Substitute.For<ILogger<StripeService>>());
41+
}
42+
43+
[Fact]
44+
public void ToStripeSubscriptionStatus_should_map_active_subscription_scheduled_for_cancellation_to_unsubscribed()
45+
{
46+
var subscription = CreateStripeSubscription(status: "active", cancelAtPeriodEnd: true);
47+
48+
var status = subscription.ToStripeSubscriptionStatus();
49+
50+
status.Should().Be(StripeSubscriptionStatus.UNSUBSCRIBED);
51+
}
52+
53+
[Fact]
54+
public void ToStripeSubscriptionStatus_should_map_canceled_subscription_even_when_cancel_at_period_end_is_true()
55+
{
56+
var subscription = CreateStripeSubscription(status: "canceled", cancelAtPeriodEnd: true);
57+
58+
var status = subscription.ToStripeSubscriptionStatus();
59+
60+
status.Should().Be(StripeSubscriptionStatus.CANCELED);
61+
}
62+
63+
[Fact]
64+
public void ToStripeSubscriptionStatus_should_map_active_subscription_with_cancel_at_to_unsubscribed()
65+
{
66+
var subscription = CreateStripeSubscription(status: "active", cancelAt: DateTime.UtcNow.AddDays(30));
67+
68+
var status = subscription.ToStripeSubscriptionStatus();
69+
70+
status.Should().Be(StripeSubscriptionStatus.UNSUBSCRIBED);
71+
}
72+
73+
private static Subscription CreateStripeSubscription(
74+
string id = "sub_123",
75+
string status = "active",
76+
bool cancelAtPeriodEnd = false,
77+
DateTime? cancelAt = null,
78+
DateTime? currentPeriodEnd = null)
79+
{
80+
return new Subscription
81+
{
82+
Id = id,
83+
Status = status,
84+
CancelAtPeriodEnd = cancelAtPeriodEnd,
85+
CancelAt = cancelAt,
86+
Items = new StripeList<SubscriptionItem>
87+
{
88+
Data =
89+
[
90+
new SubscriptionItem
91+
{
92+
CurrentPeriodEnd = currentPeriodEnd ?? new DateTime(2026, 4, 24, 0, 0, 0, DateTimeKind.Utc)
93+
}
94+
]
95+
}
96+
};
97+
}
98+
}

SkredvarselGarminWeb/SkredvarselGarminWeb/Entities/Mappers/StripeSubscriptionStatusMapper.cs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,28 @@ namespace SkredvarselGarminWeb.Entities.Mappers;
66

77
public static class StripeSubscriptionStatusMapper
88
{
9-
public static StripeSubscriptionStatus ToStripeSubscriptionStatus(this Subscription subscription) => (subscription.Status, subscription.CancelAtPeriodEnd) switch
9+
public static StripeSubscriptionStatus ToStripeSubscriptionStatus(this Subscription subscription)
1010
{
11-
("active", false) => StripeSubscriptionStatus.ACTIVE,
12-
("active", true) => StripeSubscriptionStatus.UNSUBSCRIBED,
13-
("canceled", false) => StripeSubscriptionStatus.CANCELED,
14-
("incomplete", false) => StripeSubscriptionStatus.INCOMPLETE,
15-
("incomplete_expired", false) => StripeSubscriptionStatus.INCOMPLETE_EXPIRED,
16-
("past_due", false) => StripeSubscriptionStatus.PAST_DUE,
17-
("paused", false) => StripeSubscriptionStatus.PAUSED,
18-
("trialing", false) => StripeSubscriptionStatus.TRIALING,
19-
("unpaid", false) => StripeSubscriptionStatus.UNPAID,
20-
_ => throw new InvalidEnumArgumentException(nameof(subscription.Status))
21-
};
11+
if (subscription.Status == "canceled")
12+
{
13+
return StripeSubscriptionStatus.CANCELED;
14+
}
15+
16+
if ((subscription.CancelAtPeriodEnd || subscription.CancelAt != null) && subscription.Status is "active" or "trialing")
17+
{
18+
return StripeSubscriptionStatus.UNSUBSCRIBED;
19+
}
20+
21+
return subscription.Status switch
22+
{
23+
"active" => StripeSubscriptionStatus.ACTIVE,
24+
"incomplete" => StripeSubscriptionStatus.INCOMPLETE,
25+
"incomplete_expired" => StripeSubscriptionStatus.INCOMPLETE_EXPIRED,
26+
"past_due" => StripeSubscriptionStatus.PAST_DUE,
27+
"paused" => StripeSubscriptionStatus.PAUSED,
28+
"trialing" => StripeSubscriptionStatus.TRIALING,
29+
"unpaid" => StripeSubscriptionStatus.UNPAID,
30+
_ => throw new InvalidEnumArgumentException(nameof(subscription.Status))
31+
};
32+
}
2233
}

SkredvarselGarminWeb/SkredvarselGarminWeb/Services/StripeService.cs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ namespace SkredvarselGarminWeb.Services;
1212

1313
public class StripeService(
1414
SkredvarselDbContext dbContext,
15+
IStripeClient stripeClient,
1516
INotificationService notificationService,
1617
IDateTimeNowProvider dateTimeNowProvider,
1718
ILogger<StripeService> logger) : IStripeService
1819
{
19-
private static Subscription GetStripeSubscription(string subscriptionId)
20+
private Subscription GetStripeSubscription(string subscriptionId)
2021
{
21-
var service = new StripeSubscriptionService();
22+
var service = new StripeSubscriptionService(stripeClient);
2223
return service.Get(subscriptionId);
2324
}
2425

@@ -92,17 +93,35 @@ public void HandleSubscriptionUpdated(Subscription subscription)
9293
{
9394
using var transaction = dbContext.Database.BeginTransaction();
9495

95-
var subscriptionInDb = dbContext.StripeSubscriptions.SingleOrDefault(s => s.SubscriptionId == subscription.Id);
96+
var latestSubscription = GetStripeSubscription(subscription.Id);
97+
98+
logger.LogInformation(
99+
"Processing stripe subscription update for {subscriptionId} with status {status} and cancelAtPeriodEnd {cancelAtPeriodEnd}.",
100+
latestSubscription.Id,
101+
latestSubscription.Status,
102+
latestSubscription.CancelAtPeriodEnd);
103+
104+
var subscriptionInDb = dbContext.StripeSubscriptions.SingleOrDefault(s => s.SubscriptionId == latestSubscription.Id);
96105

97106
if (subscriptionInDb == null)
98107
{
99-
logger.LogInformation("Received subscription updated event for subscription not in local database.");
108+
logger.LogInformation(
109+
"Received subscription updated event for subscription {subscriptionId} not in local database.",
110+
latestSubscription.Id);
100111
transaction.Commit();
101112
return;
102113
}
103114

104-
subscriptionInDb.Status = subscription.ToStripeSubscriptionStatus();
105-
subscriptionInDb.NextChargeDate = DateOnly.FromDateTime(subscription.Items.Data[0].CurrentPeriodEnd);
115+
var updatedStatus = latestSubscription.ToStripeSubscriptionStatus();
116+
117+
logger.LogInformation(
118+
"Updating local stripe subscription {subscriptionId} from {previousStatus} to {updatedStatus}.",
119+
latestSubscription.Id,
120+
subscriptionInDb.Status,
121+
updatedStatus);
122+
123+
subscriptionInDb.Status = updatedStatus;
124+
subscriptionInDb.NextChargeDate = DateOnly.FromDateTime(latestSubscription.Items.Data[0].CurrentPeriodEnd);
106125

107126
dbContext.SaveChanges();
108127

0 commit comments

Comments
 (0)