test(billing): prove downgrade lifecycle
CI and Deploy / test (pull_request) Successful in 5m27s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 20:28:23 +02:00
parent 25a6da951e
commit 191de69c48
9 changed files with 217 additions and 17 deletions
+10 -10
View File
@@ -18,19 +18,22 @@ public sealed class BillingController : ControllerBase
private readonly RoleManager<IdentityRole> _roles;
private readonly ILogger<BillingController> _logger;
private readonly ExternalOrigin _externalOrigin;
private readonly IStripeBillingGateway _stripe;
public BillingController(
IConfiguration configuration,
UserManager<ApplicationUser> users,
RoleManager<IdentityRole> roles,
ILogger<BillingController> logger,
ExternalOrigin? externalOrigin = null)
ExternalOrigin? externalOrigin = null,
IStripeBillingGateway? stripe = null)
{
_configuration = configuration;
_users = users;
_roles = roles;
_logger = logger;
_externalOrigin = externalOrigin ?? ExternalOrigin.FromConfiguration(configuration);
_stripe = stripe ?? new StripeBillingGateway();
}
public sealed record BillingRedirectDto(string Url);
@@ -86,8 +89,7 @@ public sealed class BillingController : ControllerBase
try
{
var session = await new Stripe.Checkout.SessionService(new StripeClient(secretKey))
.CreateAsync(options, cancellationToken: cancellationToken);
var session = await _stripe.CreateCheckoutAsync(secretKey, options, cancellationToken);
if (string.IsNullOrWhiteSpace(session.Url))
return Problem(statusCode: StatusCodes.Status502BadGateway, title: "Stripe did not return a checkout URL.");
return Ok(new BillingRedirectDto(session.Url));
@@ -113,12 +115,11 @@ public sealed class BillingController : ControllerBase
try
{
var session = await new Stripe.BillingPortal.SessionService(new StripeClient(secretKey))
.CreateAsync(new Stripe.BillingPortal.SessionCreateOptions
var session = await _stripe.CreatePortalAsync(secretKey, new Stripe.BillingPortal.SessionCreateOptions
{
Customer = user.StripeCustomerId,
ReturnUrl = $"{publicBaseUrl}/settings",
}, cancellationToken: cancellationToken);
}, cancellationToken);
return Ok(new BillingRedirectDto(session.Url));
}
catch (StripeException ex)
@@ -143,7 +144,7 @@ public sealed class BillingController : ControllerBase
Event stripeEvent;
try
{
stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"].ToString(), webhookSecret);
stripeEvent = _stripe.ConstructEvent(json, Request.Headers["Stripe-Signature"].ToString(), webhookSecret);
}
catch (StripeException ex)
{
@@ -164,8 +165,7 @@ public sealed class BillingController : ControllerBase
{
// Stripe does not guarantee webhook delivery order. Re-read the subscription so a late
// event cannot restore access after a newer cancellation or payment failure.
subscription = await new SubscriptionService(new StripeClient(secretKey))
.GetAsync(eventSubscription.Id, cancellationToken: cancellationToken);
subscription = await _stripe.GetSubscriptionAsync(secretKey, eventSubscription.Id, cancellationToken);
}
catch (StripeException ex)
{
@@ -229,6 +229,6 @@ public sealed class BillingController : ControllerBase
premiumPrice = (_configuration["Stripe:PricePremium"] ?? string.Empty).Trim();
webhookSecret = (_configuration["Stripe:WebhookSecret"] ?? string.Empty).Trim();
publicBaseUrl = _externalOrigin.BaseUrl;
return secretKey.Length > 0 && premiumPrice.Length > 0 && webhookSecret.Length > 0;
return secretKey.Length > 0 && premiumPrice.StartsWith("price_", StringComparison.Ordinal) && webhookSecret.Length > 0;
}
}
+1
View File
@@ -54,6 +54,7 @@ builder.Services.AddScoped<IAiSidecarCachePurger, AiSidecarCachePurger>();
builder.Services.AddScoped<AccountDeletionService>();
builder.Services.AddScoped<AiOperationAdmission>();
builder.Services.AddScoped<AiUsageMeter>();
builder.Services.AddSingleton<IStripeBillingGateway, StripeBillingGateway>();
builder.Services.AddScoped<StrategySnapshotService>();
builder.Services.AddSingleton<IAiOperationHandler, StrategySnapshotOperationHandler>();
builder.Services.AddSingleton<IAiOperationHandler, CvProcessingOperationHandler>();
@@ -0,0 +1,35 @@
using Stripe;
namespace JobTrackerApi.Services;
public interface IStripeBillingGateway
{
Task<Stripe.Checkout.Session> CreateCheckoutAsync(string secretKey, Stripe.Checkout.SessionCreateOptions options, CancellationToken cancellationToken);
Task<Stripe.BillingPortal.Session> CreatePortalAsync(string secretKey, Stripe.BillingPortal.SessionCreateOptions options, CancellationToken cancellationToken);
Event ConstructEvent(string json, string signature, string webhookSecret);
Task<Subscription> GetSubscriptionAsync(string secretKey, string subscriptionId, CancellationToken cancellationToken);
}
public sealed class StripeBillingGateway : IStripeBillingGateway
{
public Task<Stripe.Checkout.Session> CreateCheckoutAsync(
string secretKey,
Stripe.Checkout.SessionCreateOptions options,
CancellationToken cancellationToken)
=> new Stripe.Checkout.SessionService(new StripeClient(secretKey))
.CreateAsync(options, cancellationToken: cancellationToken);
public Task<Stripe.BillingPortal.Session> CreatePortalAsync(
string secretKey,
Stripe.BillingPortal.SessionCreateOptions options,
CancellationToken cancellationToken)
=> new Stripe.BillingPortal.SessionService(new StripeClient(secretKey))
.CreateAsync(options, cancellationToken: cancellationToken);
public Event ConstructEvent(string json, string signature, string webhookSecret)
=> EventUtility.ConstructEvent(json, signature, webhookSecret);
public Task<Subscription> GetSubscriptionAsync(string secretKey, string subscriptionId, CancellationToken cancellationToken)
=> new SubscriptionService(new StripeClient(secretKey))
.GetAsync(subscriptionId, cancellationToken: cancellationToken);
}