using System; using System.IO; using System.Linq; using System.Threading.Tasks; using ManagerService.Data; using ManagerService.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NSwag.Annotations; using Stripe; using Stripe.Checkout; namespace ManagerService.Controllers { [AllowAnonymous] [ApiController, Route("api/webhooks/stripe")] [OpenApiTag("Webhooks", Description = "Stripe webhook events")] public class StripeWebhookController : ControllerBase { private readonly MyInfoMateDbContext _myInfoMateDbContext; private readonly StripeService _stripeService; private readonly IEmailService _emailService; private readonly IConfiguration _configuration; private readonly ILogger _logger; public StripeWebhookController( MyInfoMateDbContext myInfoMateDbContext, StripeService stripeService, IEmailService emailService, IConfiguration configuration, ILogger logger) { _myInfoMateDbContext = myInfoMateDbContext; _stripeService = stripeService; _emailService = emailService; _configuration = configuration; _logger = logger; } [HttpPost] public async Task HandleWebhook() { var json = await new StreamReader(Request.Body).ReadToEndAsync(); Event stripeEvent; try { stripeEvent = _stripeService.ConstructWebhookEvent(json, Request.Headers["Stripe-Signature"]); } catch (Exception ex) { _logger.LogWarning(ex, "Stripe webhook signature verification failed"); return new BadRequestObjectResult("Invalid signature"); } try { switch (stripeEvent.Type) { case "checkout.session.completed": await HandleCheckoutSessionCompleted((Session)stripeEvent.Data.Object); break; case "invoice.payment_failed": await HandleInvoicePaymentFailed((Invoice)stripeEvent.Data.Object); break; default: _logger.LogInformation("Unhandled Stripe event type {Type}", stripeEvent.Type); break; } return new OkObjectResult("ok"); } catch (Exception ex) { _logger.LogError(ex, "Error handling Stripe event {Type}", stripeEvent.Type); return new ObjectResult(ex.Message) { StatusCode = 500 }; } } private async Task HandleCheckoutSessionCompleted(Session session) { var instanceId = session.Metadata != null && session.Metadata.TryGetValue("instanceId", out var id) ? id : null; var instance = instanceId != null ? _myInfoMateDbContext.Instances.FirstOrDefault(i => i.Id == instanceId) : _myInfoMateDbContext.Instances.FirstOrDefault(i => i.StripeCustomerId == session.CustomerId); if (instance == null) { _logger.LogWarning("checkout.session.completed: no matching instance for customer {CustomerId}", session.CustomerId); return; } instance.IsTrialActive = false; instance.StripeSubscriptionId = session.SubscriptionId; _myInfoMateDbContext.SaveChanges(); var user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.InstanceId == instance.Id); if (user != null) await _emailService.SendPaymentConfirmedEmailAsync(user.Email, user.FirstName); await _emailService.NotifyAdminAsync( "Conversion essai -> payant", $"Instance: {instance.Name} ({instance.Id})\nSubscription: {instance.StripeSubscriptionId}"); } private async Task HandleInvoicePaymentFailed(Invoice invoice) { var instance = _myInfoMateDbContext.Instances.FirstOrDefault(i => i.StripeCustomerId == invoice.CustomerId); if (instance == null) { _logger.LogWarning("invoice.payment_failed: no matching instance for customer {CustomerId}", invoice.CustomerId); return; } var user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.InstanceId == instance.Id); if (user != null) { var managerAppUrl = _configuration["AppUrls:ManagerApp"]; await _emailService.SendPaymentFailedEmailAsync(user.Email, user.FirstName, $"{managerAppUrl}/billing"); } await _emailService.NotifyAdminAsync( "Paiement échoué", $"Instance: {instance.Name} ({instance.Id})"); } } }