Customer Portal : StripeService.CreateBillingPortalSessionAsync + POST /api/onboarding/billing-portal, calqué sur checkout-session. Une garde absente de l'énoncé : StripeCustomerId peut être nul, car CreateCustomerAsync n'est appelée que par l'inscription self-service. Les 4 instances venues de Mongo n'ont jamais vu Stripe — sans la garde, elles recevaient une erreur d'API Stripe illisible côté front. 409, distinct du 404 d'instance inconnue. Ratio jetons -> questions : InstanceQuotaDTO.aiTokensPerQuestion, mesuré sur les VisitorQuestion.TokensUsed de l'instance, seuil de 20 questions avant de faire foi — une seule réponse citant un long article doublerait la moyenne. En dessous, repli sur l'hypothèse de la grille tarifaire (10 000, pas 1 000 : le /1000 de manager-app était faux d'un facteur 10). Trois autres points du lot F étaient déjà faits et n'attendaient qu'une vérification : rate limiting, endpoint ApplicationInstance, quotas seed. 6 tests. dotnet test : 197 passés, 14 sautés, 0 échec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
96 lines
3.8 KiB
C#
96 lines
3.8 KiB
C#
using System.Threading.Tasks;
|
|
using ManagerService.Data;
|
|
using ManagerService.Helpers;
|
|
using Microsoft.Extensions.Options;
|
|
using Stripe;
|
|
using Stripe.Checkout;
|
|
|
|
namespace ManagerService.Services
|
|
{
|
|
/// <summary>
|
|
/// Stripe integration for the Essentiel plan self-service subscription (customer creation,
|
|
/// Checkout Session, Billing Portal Session, webhook signature verification). Both Checkout
|
|
/// and the Billing Portal are hosted by Stripe — no card form is built in any of our own apps.
|
|
/// </summary>
|
|
public class StripeService
|
|
{
|
|
private readonly StripeSettings _settings;
|
|
private readonly CustomerService _customerService;
|
|
private readonly SessionService _checkoutSessionService;
|
|
private readonly Stripe.BillingPortal.SessionService _billingPortalSessionService;
|
|
|
|
public StripeService(IOptions<StripeSettings> settings)
|
|
{
|
|
_settings = settings.Value;
|
|
StripeConfiguration.ApiKey = _settings.SecretKey;
|
|
_customerService = new CustomerService();
|
|
_checkoutSessionService = new SessionService();
|
|
_billingPortalSessionService = new Stripe.BillingPortal.SessionService();
|
|
}
|
|
|
|
public async Task<string> CreateCustomerAsync(Instance instance)
|
|
{
|
|
var customer = await _customerService.CreateAsync(new CustomerCreateOptions
|
|
{
|
|
Name = instance.Name,
|
|
Metadata = new System.Collections.Generic.Dictionary<string, string>
|
|
{
|
|
{ "instanceId", instance.Id },
|
|
},
|
|
});
|
|
return customer.Id;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a Stripe Checkout Session for the Essentiel plan subscription.
|
|
/// Stripe Tax is applied automatically based on the customer's billing details.
|
|
/// </summary>
|
|
public async Task<string> CreateCheckoutSessionAsync(Instance instance, string successUrl, string cancelUrl)
|
|
{
|
|
var options = new SessionCreateOptions
|
|
{
|
|
Mode = "subscription",
|
|
Customer = instance.StripeCustomerId,
|
|
LineItems = new System.Collections.Generic.List<SessionLineItemOptions>
|
|
{
|
|
new SessionLineItemOptions
|
|
{
|
|
Price = _settings.EssentielPriceId,
|
|
Quantity = 1,
|
|
},
|
|
},
|
|
AutomaticTax = new SessionAutomaticTaxOptions { Enabled = true },
|
|
SuccessUrl = successUrl,
|
|
CancelUrl = cancelUrl,
|
|
Metadata = new System.Collections.Generic.Dictionary<string, string>
|
|
{
|
|
{ "instanceId", instance.Id },
|
|
},
|
|
};
|
|
|
|
var session = await _checkoutSessionService.CreateAsync(options);
|
|
return session.Url;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a Stripe Billing Portal Session: the customer manages card, invoices and
|
|
/// cancellation on Stripe's own pages. Nothing to build on our side beyond the redirect.
|
|
/// </summary>
|
|
public async Task<string> CreateBillingPortalSessionAsync(Instance instance, string returnUrl)
|
|
{
|
|
var session = await _billingPortalSessionService.CreateAsync(
|
|
new Stripe.BillingPortal.SessionCreateOptions
|
|
{
|
|
Customer = instance.StripeCustomerId,
|
|
ReturnUrl = returnUrl,
|
|
});
|
|
return session.Url;
|
|
}
|
|
|
|
public Event ConstructWebhookEvent(string json, string stripeSignatureHeader)
|
|
{
|
|
return EventUtility.ConstructEvent(json, stripeSignatureHeader, _settings.WebhookSecret);
|
|
}
|
|
}
|
|
}
|