Skip to content

Commit

Permalink
feat: webhook validation
Browse files Browse the repository at this point in the history
  • Loading branch information
oproprioleonardo committed Dec 14, 2024
1 parent b849c92 commit d03638a
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ public HandlePaymentUseCase(IPaymentGateway paymentGateway, IOrderGateway orderG
public HandlePaymentOutput execute(HandlePaymentInput anIn) {
Payment payment;
final Optional<Payment> paymentOptional = this.paymentGateway.findByExternalId(anIn.externalId());
if (paymentOptional.isPresent()) payment = paymentOptional.get();
if (paymentOptional.isPresent()) {
payment = paymentOptional.get();
payment.changeStatus(anIn.status());
}
else {
final OrderID orderID = OrderID.with(anIn.orderId());
final LocalDateTime createdAt = anIn.createdAt();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,8 @@ public void handle(Order order) {

final boolean alreadyExists = this.enrollmentGateway.existsByEmailAndEventID(emailString, event.getId());

if (alreadyExists) {
if (alreadyExists)
Notification.create("Validation Error").append("User already enrolled in this event").throwAnyErrors();
}

final Enrollment enrollment = Enrollment
.newEnrollment(name, emailString, document, birthDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
public class Payment extends AggregateRoot<PaymentID> {

private final String externalId;
private final PaymentStatus status;
private PaymentStatus status;
private final OrderID orderId;
private final String currency;
private final BigDecimal amount;
private final String paymentType;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
private final LocalDateTime approvalDate;
private LocalDateTime updatedAt;
private LocalDateTime approvalDate;

public Payment(PaymentID id, String externalId, PaymentStatus status, OrderID orderId, String currency, BigDecimal amount, String paymentType, LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime approvalDate) {
super(id);
Expand All @@ -34,6 +34,18 @@ public Payment(PaymentID id, String externalId, PaymentStatus status, OrderID or
this.approvalDate = approvalDate;
}

public void changeStatus(PaymentStatus status) {
if (this.status == status) {
return;
}
this.status = status;
this.updatedAt = LocalDateTime.now();
if (status == PaymentStatus.APPROVED) {
this.approvalDate = LocalDateTime.now();
}
}


public static Payment with(PaymentID id, String externalId, PaymentStatus status, OrderID orderId, String currency, BigDecimal amount, String paymentType, LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime approvalDate) {
return new Payment(id, externalId, status, orderId, currency, amount, paymentType, createdAt, updatedAt, approvalDate);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping("/v1/billing")
Expand All @@ -24,6 +26,6 @@ public interface BillingAPI {
@ApiResponse(responseCode = "400", description = "Invalid request", content = @Content(schema = @Schema(implementation = APIErrorResponse.class), mediaType = "application/json"))
}
)
ResponseEntity<Void> listener(PaymentListenerRequest request);
ResponseEntity<Void> listener(@RequestBody PaymentListenerRequest request, HttpHeaders headers);

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,34 @@
import com.mercadopago.resources.payment.Payment;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

@RestController
@RequiredArgsConstructor(onConstructor_ = @__(@Autowired))
public class BillingController implements BillingAPI {

private final PaymentService paymentService;
@Value("${mercadopago.access.token}")
private String mercadoPagoAccessToken;

@Override
public ResponseEntity<Void> listener(PaymentListenerRequest request) {
public ResponseEntity<Void> listener(PaymentListenerRequest request, HttpHeaders headers) {
try {
if (!this.verifyWebhook(headers, request.data().id().toString(), mercadoPagoAccessToken))
return ResponseEntity.status(401).build();
} catch (Exception e) {
return ResponseEntity.status(401).build();
}

final PaymentClient paymentClient = new PaymentClient();

final Payment payment;
try {
payment = paymentClient.get(request.data().id());
Expand All @@ -45,4 +61,32 @@ public ResponseEntity<Void> listener(PaymentListenerRequest request) {
this.paymentService.handle(input);
return ResponseEntity.ok().build();
}

private boolean verifyWebhook(HttpHeaders headers, String id, String secret) throws Exception {
final String xSignature = headers.getFirst("x-signature");
final String xRequestId = headers.getFirst("x-request-id");

assert xSignature != null;
final String[] parts = xSignature.split(",");
String ts = null;
String hash = null;
for (String part : parts) {
final String[] keyValue = part.split("=");
if ("ts".equals(keyValue[0])) {
ts = keyValue[1];
} else if ("v1".equals(keyValue[0])) {
hash = keyValue[1];
}
}

final String manifest = String.format("id:%s;request-id:%s;ts:%s;", id, xRequestId, ts);

final Mac hmac = Mac.getInstance("HmacSHA256");
final SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
hmac.init(secretKeySpec);
byte[] signature = hmac.doFinal(manifest.getBytes());

final String computedHash = Base64.getEncoder().encodeToString(signature);
return computedHash.equals(hash);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class PaymentJpaEntity implements Serializable {
private Long id;
@Column(name = "external_id", nullable = false, updatable = false)
private String externalId;
@Column(name = "status", nullable = false, updatable = false)
@Column(name = "status", nullable = false)
@Enumerated(EnumType.STRING)
private PaymentStatus status;
@Column(name = "order_id", nullable = false, updatable = false)
Expand All @@ -35,9 +35,9 @@ public class PaymentJpaEntity implements Serializable {
private String paymentType;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false, updatable = false)
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@Column(name = "approval_date", nullable = false, updatable = false)
@Column(name = "approval_date", nullable = false)
private LocalDateTime approvalDate;

public PaymentJpaEntity(
Expand Down

0 comments on commit d03638a

Please sign in to comment.