test(billing): prove downgrade lifecycle
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user