using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MotoresArgentinosV2.Core.DTOs; using MotoresArgentinosV2.Core.Interfaces; using Microsoft.AspNetCore.RateLimiting; using System.Security.Cryptography; using System.Text; namespace MotoresArgentinosV2.API.Controllers; [ApiController] [Route("api/[controller]")] [Authorize] public class PaymentsController : ControllerBase { private readonly IPaymentService _paymentService; private readonly IConfiguration _config; public PaymentsController(IPaymentService paymentService, IConfiguration config) { _paymentService = paymentService; _config = config; } [HttpPost("process")] [EnableRateLimiting("AuthPolicy")] public async Task ProcessPayment([FromBody] CreatePaymentRequestDto request) { try { var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); var result = await _paymentService.ProcessPaymentAsync(request, userId); if (result.Status == "approved") { return Ok(new { status = "approved", id = result.PaymentId }); } else if (result.Status == "in_process") { return Ok(new { status = "in_process", message = "El pago está siendo revisado." }); } else { return BadRequest(new { status = "rejected", detail = result.StatusDetail }); } } catch (Exception ex) { return StatusCode(500, new { message = ex.Message }); } } // --- MÉTODO WEBHOOK ACTUALIZADO CON VALIDACIÓN DE FIRMA --- [HttpPost("webhook")] [AllowAnonymous] public async Task MercadoPagoWebhook([FromQuery] string? topic, [FromQuery] string? id) { // 1. EXTRACCIÓN DE DATOS DE LA PETICIÓN Request.Headers.TryGetValue("x-request-id", out var xRequestId); Request.Headers.TryGetValue("x-signature", out var xSignature); var resourceId = id; var resourceTopic = topic; if (string.IsNullOrEmpty(resourceId)) { try { Request.EnableBuffering(); using var reader = new System.IO.StreamReader(Request.Body, leaveOpen: true); var body = await reader.ReadToEndAsync(); Request.Body.Position = 0; var json = System.Text.Json.JsonDocument.Parse(body); if (json.RootElement.TryGetProperty("type", out var typeProp)) { resourceTopic = typeProp.GetString(); } if (json.RootElement.TryGetProperty("data", out var dataProp) && dataProp.TryGetProperty("id", out var idProp)) { resourceId = idProp.GetString(); } } catch { /* Ignorar errores de lectura */ } } if (string.IsNullOrEmpty(resourceId) || resourceTopic != "payment") { return Ok(); } // 2. VALIDACIÓN DE LA FIRMA var secret = _config["MercadoPago:WebhookSecret"]; if (!string.IsNullOrEmpty(secret)) { var signatureParts = xSignature.ToString().Split(',') .Select(part => part.Trim().Split('=')) .ToDictionary(split => split[0], split => split.Length > 1 ? split[1] : ""); if (!signatureParts.TryGetValue("ts", out var ts) || !signatureParts.TryGetValue("v1", out var hash)) { return Unauthorized("Invalid signature header."); } // Según la documentación de MP, el data.id debe estar en minúsculas para la firma. var manifest = $"id:{resourceId.ToLower()};request-id:{xRequestId};ts:{ts};"; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var computedHashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(manifest)); var computedHash = Convert.ToHexString(computedHashBytes).ToLower(); if (computedHash != hash) { return Unauthorized("Signature mismatch."); } } // 3. PROCESAMIENTO (SOLO SI LA FIRMA ES VÁLIDA) try { await _paymentService.ProcessWebhookAsync(resourceTopic, resourceId); return Ok(); } catch (Exception ex) { Console.WriteLine($"Error procesando webhook validado: {ex.Message}"); return StatusCode(500, "Internal server error processing webhook."); } } [HttpPost("check-status/{adId}")] public async Task CheckStatus(int adId) { try { var result = await _paymentService.CheckPaymentStatusAsync(adId); return Ok(result); } catch (Exception ex) { return BadRequest(new { message = ex.Message }); } } }