Skip to content

Commit e2e8275

Browse files
committed
Integra pasarela de pagos con mercadopago
1 parent 53684c7 commit e2e8275

File tree

45 files changed

+299
-1155
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+299
-1155
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@
6666
<scope>runtime</scope>
6767
</dependency>
6868

69+
<dependency>
70+
<groupId>com.mercadopago</groupId>
71+
<artifactId>sdk-java</artifactId>
72+
<version>2.1.7</version>
73+
</dependency>
74+
6975
<dependency>
7076
<groupId>org.projectlombok</groupId>
7177
<artifactId>lombok</artifactId>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.outfitlab.project.domain.service;
2+
3+
import com.mercadopago.MercadoPagoConfig;
4+
import com.mercadopago.client.payment.PaymentClient;
5+
import com.mercadopago.client.preference.PreferenceClient;
6+
import com.mercadopago.client.preference.PreferenceItemRequest;
7+
import com.mercadopago.client.preference.PreferencePayerRequest;
8+
import com.mercadopago.client.preference.PreferenceRequest;
9+
import com.mercadopago.exceptions.MPApiException;
10+
import com.mercadopago.resources.payment.Payment;
11+
import com.mercadopago.resources.preference.Preference;
12+
import com.mercadopago.exceptions.MPException;
13+
import org.springframework.beans.factory.annotation.Value;
14+
import org.springframework.stereotype.Service;
15+
import java.math.BigDecimal;
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
19+
@Service
20+
public class SubscriptionService {
21+
22+
/**
23+
* Constructor para inicializar el SDK con el Access Token.
24+
* Se ejecuta al iniciar Spring Boot.
25+
*/
26+
public SubscriptionService(@Value("${mercadopago.access.token}") String accessToken) {
27+
if (accessToken == null || accessToken.trim().isEmpty() || accessToken.equals("${mercadopago.access.token}")) {
28+
throw new IllegalArgumentException("ERROR: El Access Token de Mercado Pago no está configurado. Revisa la variable de entorno MP_ACCESS_TOKEN.");
29+
}
30+
MercadoPagoConfig.setAccessToken(accessToken);
31+
System.out.println("Mercado Pago SDK de Java inicializado (desde SubscriptionService).");
32+
}
33+
34+
/**
35+
* Crea una Preferencia de Pago Único (para la demo).
36+
*/
37+
public String createMercadoPagoPreference(String planId, String userEmail, BigDecimal price, String currency) throws MPException, MPApiException {
38+
39+
// 1. Definir el ítem
40+
PreferenceItemRequest itemRequest = PreferenceItemRequest.builder()
41+
.id(planId)
42+
.title("Demo Premium Outfit Lab")
43+
.description("Acceso único a funciones premium")
44+
.quantity(1)
45+
.unitPrice(price)
46+
.currencyId(currency)
47+
.build();
48+
49+
List<PreferenceItemRequest> items = new ArrayList<>();
50+
items.add(itemRequest);
51+
52+
// 2. Definir el pagador (con el email de prueba)
53+
PreferencePayerRequest payer = PreferencePayerRequest.builder()
54+
.email(userEmail)
55+
.build();
56+
57+
// 3. Crear la solicitud de Preferencia
58+
PreferenceRequest request = PreferenceRequest.builder()
59+
.items(items)
60+
.payer(payer)
61+
// --- SE ELIMINAN backUrls y autoReturn ---
62+
// Esto evita el error 500 (MPApiException) al validar localhost.
63+
// Mercado Pago usará las URLs configuradas en el dashboard
64+
// o mostrará un botón simple de "Volver al sitio".
65+
.externalReference(planId)
66+
.build();
67+
68+
// 4. Ejecutar la API
69+
PreferenceClient client = new PreferenceClient();
70+
Preference preference = client.create(request);
71+
72+
// 5. Devolver la URL de pago
73+
return preference.getInitPoint();
74+
}
75+
76+
/**
77+
* Procesa el Webhook para un Pago Único.
78+
*/
79+
public void processPaymentNotification(Long paymentId) throws MPException, MPApiException {
80+
PaymentClient client = new PaymentClient();
81+
Payment payment = client.get(paymentId);
82+
83+
String status = payment.getStatus();
84+
String externalReference = payment.getExternalReference();
85+
86+
System.out.printf("Procesando Webhook de PAGO. ID Pago MP: %s, ID Plan Interno: %s, Status: %s%n",
87+
paymentId, externalReference, status);
88+
89+
if ("approved".equals(status)) {
90+
System.out.println("Pago AUTHORIZED. Activando Premium (Demo) para: " + externalReference);
91+
// TODO: Aquí iría tu lógica de base de datos para activar el servicio
92+
} else if ("rejected".equals(status) || "cancelled".equals(status)) {
93+
System.out.println("Pago REJECTED/CANCELLED para: " + externalReference);
94+
}
95+
}
96+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.outfitlab.project.presentation;
2+
3+
import com.mercadopago.exceptions.MPApiException;
4+
import com.mercadopago.exceptions.MPException;
5+
import com.outfitlab.project.domain.service.SubscriptionService;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.*;
9+
import java.util.Map;
10+
import java.util.HashMap;
11+
import java.math.BigDecimal;
12+
13+
class SubscriptionRequest {
14+
private String planId;
15+
private String userEmail;
16+
private BigDecimal price;
17+
private String currency;
18+
19+
public String getPlanId() { return planId; }
20+
public void setPlanId(String planId) { this.planId = planId; }
21+
public String getUserEmail() { return userEmail; }
22+
public void setUserEmail(String userEmail) { this.userEmail = userEmail; }
23+
public BigDecimal getPrice() { return price; }
24+
public void setPrice(BigDecimal price) { this.price = price; }
25+
public String getCurrency() { return currency; }
26+
public void setCurrency(String currency) { this.currency = currency; }
27+
}
28+
29+
@RestController
30+
@RequestMapping("/api/mp")
31+
public class SubscriptionController {
32+
33+
@Autowired
34+
private SubscriptionService subscriptionService;
35+
36+
@PostMapping("/crear-suscripcion")
37+
@CrossOrigin(origins = "http://localhost:5173")
38+
public ResponseEntity<Map<String, String>> createPreference(@RequestBody SubscriptionRequest request) {
39+
40+
if (request.getPlanId() == null || request.getUserEmail() == null || request.getPrice() == null) {
41+
return ResponseEntity.badRequest().body(Map.of("error", "Faltan planId, userEmail o price."));
42+
}
43+
44+
try {
45+
String initPointUrl = subscriptionService.createMercadoPagoPreference(
46+
request.getPlanId(),
47+
request.getUserEmail(),
48+
request.getPrice(),
49+
request.getCurrency() != null ? request.getCurrency() : "ARS"
50+
);
51+
52+
Map<String, String> response = new HashMap<>();
53+
response.put("initPoint", initPointUrl);
54+
return ResponseEntity.ok(response);
55+
56+
} catch (MPException | MPApiException e) {
57+
e.printStackTrace();
58+
System.err.println("Error de API de Mercado Pago: " + e.getMessage());
59+
return ResponseEntity.status(500).body(Map.of("error", "Error al crear la preferencia en Mercado Pago."));
60+
} catch (Exception e) {
61+
e.printStackTrace();
62+
return ResponseEntity.status(500).body(Map.of("error", "Error interno del servidor."));
63+
}
64+
}
65+
66+
@PostMapping("/webhooks")
67+
public ResponseEntity<String> handleMercadoPagoWebhook(
68+
@RequestParam(name = "id", required = false) String id,
69+
@RequestParam(name = "topic", required = false) String topic)
70+
{
71+
if ("payment".equals(topic) && id != null) {
72+
try {
73+
Long paymentId = Long.parseLong(id);
74+
75+
subscriptionService.processPaymentNotification(paymentId);
76+
77+
return ResponseEntity.ok("Notification processed successfully.");
78+
} catch (NumberFormatException e) {
79+
System.err.println("Error: El ID del Webhook no es un número (Long). ID: " + id);
80+
return ResponseEntity.badRequest().body("ID inválido.");
81+
} catch (MPException | MPApiException e) {
82+
System.err.println("Error procesando Webhook de Pago: " + e.getMessage());
83+
return ResponseEntity.status(500).body("Error processing notification.");
84+
}
85+
}
86+
87+
return ResponseEntity.ok("Notification received, not a relevant topic.");
88+
}
89+
}
90+

src/main/resources/application.properties

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ spring.datasource.username=${SPRING_DATASOURCE_RENDER_USERNAME}
5757
spring.datasource.password=${SPRING_DATASOURCE_RENDER_PASSWORD}
5858
spring.datasource.driver-class-name=org.postgresql.Driver
5959

60-
61-
60+
# ===== mercado pago =====
61+
mercadopago.access.token=${MP_ACCESS_TOKEN}
62+
mercadopago.urls.back=http://localhost:5173/suscripcion-finalizada

target/classes/application-docker.properties

Lines changed: 0 additions & 57 deletions
This file was deleted.

target/classes/application.properties

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ spring.datasource.username=${SPRING_DATASOURCE_RENDER_USERNAME}
5757
spring.datasource.password=${SPRING_DATASOURCE_RENDER_PASSWORD}
5858
spring.datasource.driver-class-name=org.postgresql.Driver
5959

60-
61-
60+
# ===== mercado pago =====
61+
mercadopago.access.token=${MP_ACCESS_TOKEN}
62+
mercadopago.urls.back=http://localhost:5173/suscripcion-finalizada

target/classes/application.yml

Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)