diff --git a/src/main/java/com/zenfulcode/commercify/commercify/PaymentStatus.java b/src/main/java/com/zenfulcode/commercify/commercify/PaymentStatus.java
index 49d2cdb..dc368ae 100644
--- a/src/main/java/com/zenfulcode/commercify/commercify/PaymentStatus.java
+++ b/src/main/java/com/zenfulcode/commercify/commercify/PaymentStatus.java
@@ -8,5 +8,5 @@ public enum PaymentStatus {
REFUNDED,
NOT_FOUND,
TERMINATED,
- EXPIRED
+ CAPTURED, EXPIRED
}
diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/CapturePaymentRequest.java b/src/main/java/com/zenfulcode/commercify/commercify/api/requests/CapturePaymentRequest.java
new file mode 100644
index 0000000..8ea9d3f
--- /dev/null
+++ b/src/main/java/com/zenfulcode/commercify/commercify/api/requests/CapturePaymentRequest.java
@@ -0,0 +1,4 @@
+package com.zenfulcode.commercify.commercify.api.requests;
+
+public record CapturePaymentRequest(double captureAmount, boolean isPartialCapture) {
+}
diff --git a/src/main/java/com/zenfulcode/commercify/commercify/controller/PaymentController.java b/src/main/java/com/zenfulcode/commercify/commercify/controller/PaymentController.java
index 3fb7f51..5484f64 100644
--- a/src/main/java/com/zenfulcode/commercify/commercify/controller/PaymentController.java
+++ b/src/main/java/com/zenfulcode/commercify/commercify/controller/PaymentController.java
@@ -1,7 +1,8 @@
package com.zenfulcode.commercify.commercify.controller;
import com.zenfulcode.commercify.commercify.PaymentStatus;
-import com.zenfulcode.commercify.commercify.service.PaymentService;
+import com.zenfulcode.commercify.commercify.api.requests.CapturePaymentRequest;
+import com.zenfulcode.commercify.commercify.integration.mobilepay.MobilePayService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
@@ -14,7 +15,7 @@
@AllArgsConstructor
@Slf4j
public class PaymentController {
- private final PaymentService paymentService;
+ private final MobilePayService mobilePayService;
@PostMapping("/{orderId}/status")
@PreAuthorize("hasRole('ADMIN')")
@@ -22,7 +23,7 @@ public ResponseEntity updatePaymentStatus(
@PathVariable Long orderId,
@RequestParam PaymentStatus status) {
try {
- paymentService.handlePaymentStatusUpdate(orderId, status);
+ mobilePayService.handlePaymentStatusUpdate(orderId, status);
return ResponseEntity.ok("Payment status updated successfully");
} catch (Exception e) {
log.error("Error updating payment status", e);
@@ -32,7 +33,19 @@ public ResponseEntity updatePaymentStatus(
@GetMapping("/{orderId}/status")
public ResponseEntity getPaymentStatus(@PathVariable Long orderId) {
- PaymentStatus status = paymentService.getPaymentStatus(orderId);
+ PaymentStatus status = mobilePayService.getPaymentStatus(orderId);
return ResponseEntity.ok(status);
}
+
+ @PreAuthorize("hasRole('ADMIN')")
+ @PostMapping("/{paymentId}/capture")
+ public ResponseEntity capturePayment(@PathVariable Long paymentId, @RequestBody CapturePaymentRequest request) {
+ try {
+ mobilePayService.capturePayment(paymentId, request.captureAmount(), request.isPartialCapture());
+ return ResponseEntity.ok("Payment captured successfully");
+ } catch (Exception e) {
+ log.error("Error capturing payment", e);
+ return ResponseEntity.badRequest().body("Error capturing payment");
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDTO.java b/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDTO.java
index 0b257cc..34508d9 100644
--- a/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDTO.java
+++ b/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDTO.java
@@ -21,6 +21,10 @@ public class OrderDTO {
private Instant createdAt;
private Instant updatedAt;
+ public OrderDTO() {
+
+ }
+
public double getTotal() {
return subTotal + shippingCost;
}
diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDetailsDTO.java b/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDetailsDTO.java
index 36330b7..c3f1d61 100644
--- a/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDetailsDTO.java
+++ b/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDetailsDTO.java
@@ -15,4 +15,8 @@ public class OrderDetailsDTO {
private CustomerDetailsDTO customerDetails;
private AddressDTO shippingAddress;
private AddressDTO billingAddress;
+
+ public OrderDetailsDTO() {
+
+ }
}
diff --git a/src/main/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlow.java b/src/main/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlow.java
index 8e55b10..2d8bbaf 100644
--- a/src/main/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlow.java
+++ b/src/main/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlow.java
@@ -22,7 +22,8 @@ public OrderStateFlow() {
// Payment received -> Processing or Cancelled
validTransitions.put(OrderStatus.PAID, Set.of(
OrderStatus.SHIPPED,
- OrderStatus.CANCELLED
+ OrderStatus.CANCELLED,
+ OrderStatus.COMPLETED
));
// Shipped -> Completed or Returned
diff --git a/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayController.java b/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayController.java
index ab766b6..166dcaa 100644
--- a/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayController.java
+++ b/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayController.java
@@ -41,7 +41,7 @@ public ResponseEntity handleCallback(
try {
// First authenticate the request with the raw string payload
mobilePayService.authenticateRequest(date, contentSha256, authorization, body, request);
- log.info("MP Webhook authenticated");
+ log.info("Mobilepay Webhook authenticated");
// Convert the string payload to WebhookPayload object
ObjectMapper objectMapper = new ObjectMapper();
diff --git a/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayService.java b/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayService.java
index aafa1d2..4e46fa1 100644
--- a/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayService.java
+++ b/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayService.java
@@ -4,24 +4,26 @@
import com.zenfulcode.commercify.commercify.PaymentStatus;
import com.zenfulcode.commercify.commercify.api.requests.PaymentRequest;
import com.zenfulcode.commercify.commercify.api.requests.WebhookPayload;
+import com.zenfulcode.commercify.commercify.api.requests.products.PriceRequest;
import com.zenfulcode.commercify.commercify.api.responses.PaymentResponse;
-import com.zenfulcode.commercify.commercify.entity.OrderEntity;
+import com.zenfulcode.commercify.commercify.dto.OrderDTO;
+import com.zenfulcode.commercify.commercify.dto.OrderDetailsDTO;
import com.zenfulcode.commercify.commercify.entity.PaymentEntity;
import com.zenfulcode.commercify.commercify.entity.WebhookConfigEntity;
-import com.zenfulcode.commercify.commercify.exception.OrderNotFoundException;
import com.zenfulcode.commercify.commercify.exception.PaymentProcessingException;
import com.zenfulcode.commercify.commercify.integration.WebhookRegistrationResponse;
-import com.zenfulcode.commercify.commercify.repository.OrderRepository;
import com.zenfulcode.commercify.commercify.repository.PaymentRepository;
import com.zenfulcode.commercify.commercify.repository.WebhookConfigRepository;
import com.zenfulcode.commercify.commercify.service.PaymentService;
+import com.zenfulcode.commercify.commercify.service.email.EmailService;
+import com.zenfulcode.commercify.commercify.service.order.OrderService;
import jakarta.servlet.http.HttpServletRequest;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
+import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
@@ -35,15 +37,15 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
@Service
-@RequiredArgsConstructor
@Slf4j
-public class MobilePayService {
- private final PaymentService paymentService;
+public class MobilePayService extends PaymentService {
private final MobilePayTokenService tokenService;
- private final OrderRepository orderRepository;
+ private final OrderService orderService;
private final PaymentRepository paymentRepository;
private final RestTemplate restTemplate;
@@ -61,24 +63,59 @@ public class MobilePayService {
@Value("${mobilepay.api-url}")
private String apiUrl;
+ @Value("${commercify.host}")
+ private String host;
+
private static final String PROVIDER_NAME = "MOBILEPAY";
+ public MobilePayService(PaymentRepository paymentRepository, EmailService emailService, OrderService orderService, MobilePayTokenService tokenService, OrderService orderService1, PaymentRepository paymentRepository1, RestTemplate restTemplate, WebhookConfigRepository webhookConfigRepository) {
+ super(paymentRepository, emailService, orderService);
+ this.tokenService = tokenService;
+ this.orderService = orderService1;
+ this.paymentRepository = paymentRepository1;
+ this.restTemplate = restTemplate;
+ this.webhookConfigRepository = webhookConfigRepository;
+ }
+
+ @Override
+ public void capturePayment(Long paymentId, double captureAmount, boolean isPartialCapture) {
+ PaymentEntity payment = paymentRepository.findById(paymentId)
+ .orElseThrow(() -> new RuntimeException("Payment not found: " + paymentId));
+
+ if (payment.getStatus() != PaymentStatus.PAID) {
+ throw new RuntimeException("Payment cannot captured");
+ }
+
+ OrderDetailsDTO order = orderService.getOrderById(payment.getOrderId());
+
+ double capturingAmount = isPartialCapture ? captureAmount : payment.getTotalAmount();
+
+ PriceRequest priceRequest = new PriceRequest(order.getOrder().getCurrency(), capturingAmount);
+
+ // Capture payment
+ if (payment.getMobilePayReference() != null) {
+ capturePayment(payment.getMobilePayReference(), priceRequest);
+ }
+
+ // Update payment status
+ payment.setStatus(PaymentStatus.PAID);
+ paymentRepository.save(payment);
+ }
+
@Transactional
public PaymentResponse initiatePayment(PaymentRequest request) {
try {
- OrderEntity order = orderRepository.findById(request.orderId())
- .orElseThrow(() -> new OrderNotFoundException(request.orderId()));
-
+ OrderDetailsDTO orderDetails = orderService.getOrderById(request.orderId());
// Create MobilePay payment request
- Map paymentRequest = createMobilePayRequest(order, request);
+ Map paymentRequest = createMobilePayRequest(orderDetails.getOrder(), request);
// Call MobilePay API
MobilePayCheckoutResponse mobilePayCheckoutResponse = createMobilePayPayment(paymentRequest);
// Create and save payment entity
PaymentEntity payment = PaymentEntity.builder()
- .orderId(order.getId())
- .totalAmount(order.getTotal())
+ .orderId(orderDetails.getOrder().getId())
+ .totalAmount(orderDetails.getOrder().getTotal())
.paymentProvider(PaymentProvider.MOBILEPAY)
.status(PaymentStatus.PENDING)
.paymentMethod(request.paymentMethod()) // 'WALLET' or 'CARD'
@@ -106,7 +143,7 @@ public void handlePaymentCallback(WebhookPayload payload) {
PaymentStatus newStatus = mapMobilePayStatus(payload.name());
// Update payment status and trigger confirmation email if needed
- paymentService.handlePaymentStatusUpdate(payment.getOrderId(), newStatus);
+ handlePaymentStatusUpdate(payment.getOrderId(), newStatus);
}
private HttpHeaders mobilePayRequestHeaders() {
@@ -151,7 +188,7 @@ private MobilePayCheckoutResponse createMobilePayPayment(Map req
}
}
- public Map createMobilePayRequest(OrderEntity order, PaymentRequest request) {
+ public Map createMobilePayRequest(OrderDTO order, PaymentRequest request) {
validationPaymentRequest(request);
Map paymentRequest = new HashMap<>();
@@ -174,7 +211,7 @@ public Map createMobilePayRequest(OrderEntity order, PaymentRequ
paymentRequest.put("customer", customer);
// Other fields
- String reference = String.join("-", merchantId, systemName, order.getId().toString(), value);
+ String reference = String.join("-", merchantId, systemName, String.valueOf(order.getId()), value);
paymentRequest.put("reference", reference);
paymentRequest.put("returnUrl", request.returnUrl() + "?orderId=" + order.getId());
paymentRequest.put("userFlow", "WEB_REDIRECT");
@@ -211,9 +248,11 @@ private PaymentStatus mapMobilePayStatus(String status) {
return switch (status.toUpperCase()) {
case "CREATED" -> PaymentStatus.PENDING;
case "AUTHORIZED" -> PaymentStatus.PAID;
- case "ABORTED" -> PaymentStatus.CANCELLED;
+ case "ABORTED", "CANCELLED" -> PaymentStatus.CANCELLED;
case "EXPIRED" -> PaymentStatus.EXPIRED;
case "TERMINATED" -> PaymentStatus.TERMINATED;
+ case "CAPTURED" -> PaymentStatus.CAPTURED;
+ case "REFUNDED" -> PaymentStatus.REFUNDED;
default -> throw new PaymentProcessingException("Unknown MobilePay status: " + status, null);
};
}
@@ -254,6 +293,8 @@ public void registerWebhooks(String callbackUrl) {
config.setWebhookUrl(callbackUrl);
config.setWebhookSecret(response.getBody().secret());
webhookConfigRepository.save(config);
+
+ log.info("Webhook updated successfully");
},
() -> {
WebhookConfigEntity newConfig = WebhookConfigEntity.builder()
@@ -262,6 +303,8 @@ public void registerWebhooks(String callbackUrl) {
.webhookSecret(response.getBody().secret())
.build();
webhookConfigRepository.save(newConfig);
+
+ log.info("Webhook registered successfully");
}
);
@@ -271,10 +314,11 @@ public void registerWebhooks(String callbackUrl) {
}
}
-
+ @Transactional(readOnly = true)
public void authenticateRequest(String date, String contentSha256, String authorization, String payload, HttpServletRequest request) {
try {
// Verify content
+ log.info("Verifying content");
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(payload.getBytes(StandardCharsets.UTF_8));
String encodedHash = Base64.getEncoder().encodeToString(hash);
@@ -283,26 +327,34 @@ public void authenticateRequest(String date, String contentSha256, String author
throw new SecurityException("Hash mismatch");
}
- URI uri = new URI(request.getRequestURL().toString());
- String path = uri.getPath() + (uri.getQuery() != null ? "?" + uri.getQuery() : "");
+ log.info("Content verified");
// Verify signature
+ log.info("Verifying signature");
+ String path = request.getRequestURI();
+ URI uri = new URI(host + path);
+
String expectedSignedString = String.format("POST\n%s\n%s;%s;%s", path, date, uri.getHost(), encodedHash);
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
- SecretKeySpec secretKey = new SecretKeySpec(getWebhookSecret().getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+
+ CompletableFuture secretByteArray = getWebhookSecret().thenApply(s -> s.getBytes(StandardCharsets.UTF_8));
+
+ SecretKeySpec secretKey = new SecretKeySpec(secretByteArray.get(), "HmacSHA256");
hmacSha256.init(secretKey);
byte[] hmacSha256Bytes = hmacSha256.doFinal(expectedSignedString.getBytes(StandardCharsets.UTF_8));
String expectedSignature = Base64.getEncoder().encodeToString(hmacSha256Bytes);
- String expectedAuthorization = "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=" + expectedSignature;
+ String expectedAuthorization = String.format("HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=%s", expectedSignature);
if (!authorization.equals(expectedAuthorization)) {
throw new SecurityException("Signature mismatch");
}
+
+ log.info("Signature verified");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 algorithm not found", e);
- } catch (InvalidKeyException | URISyntaxException e) {
+ } catch (InvalidKeyException | URISyntaxException | ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}
@@ -343,13 +395,50 @@ public Object getWebhooks() {
}
}
- private String getWebhookSecret() {
- return webhookConfigRepository.findByProvider(PROVIDER_NAME)
- .map(WebhookConfigEntity::getWebhookSecret)
- .orElseThrow(() -> new PaymentProcessingException("Webhook secret not found", null));
+ @Async
+ protected CompletableFuture getWebhookSecret() {
+ try {
+ final String secret = webhookConfigRepository.findByProvider(PROVIDER_NAME)
+ .map(WebhookConfigEntity::getWebhookSecret)
+ .orElseThrow(() -> new PaymentProcessingException("Webhook secret not found", null));
+
+ return CompletableFuture.completedFuture(secret);
+ } catch (Exception e) {
+ log.error("Error getting webhook secret: {}", e.getMessage());
+ return CompletableFuture.failedFuture(e);
+ }
+ }
+
+ public void capturePayment(String mobilePayReference, PriceRequest captureAmount) {
+ paymentRepository.findByMobilePayReference(mobilePayReference)
+ .orElseThrow(() -> new PaymentProcessingException("Payment not found", null));
+
+ HttpHeaders headers = mobilePayRequestHeaders();
+
+ Map request = new HashMap<>();
+ request.put("modificationAmount", new MobilePayPrice(Math.round(captureAmount.amount() * 100), captureAmount.currency()));
+
+ HttpEntity
View Order in Dashboard
Please process this order as soon as possible.
-
-Best regards,
Commercify System