Thomas Fransolet 2dc0cfceeb Lot F : portail de facturation Stripe et coût réel d'une question
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>
2026-08-13 10:10:50 +02:00

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);
}
}
}