Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit d293f0c

Browse files
authored
Merge pull request #93 from Zenfulcode/91-capture-payments
adding mobilepay capture (not tested)
2 parents f21b947 + 0212794 commit d293f0c

17 files changed

Lines changed: 298 additions & 307 deletions

File tree

src/main/java/com/zenfulcode/commercify/commercify/PaymentStatus.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ public enum PaymentStatus {
88
REFUNDED,
99
NOT_FOUND,
1010
TERMINATED,
11-
EXPIRED
11+
CAPTURED, EXPIRED
1212
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.zenfulcode.commercify.commercify.api.requests;
2+
3+
public record CapturePaymentRequest(double captureAmount, boolean isPartialCapture) {
4+
}

src/main/java/com/zenfulcode/commercify/commercify/controller/PaymentController.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package com.zenfulcode.commercify.commercify.controller;
22

33
import com.zenfulcode.commercify.commercify.PaymentStatus;
4-
import com.zenfulcode.commercify.commercify.service.PaymentService;
4+
import com.zenfulcode.commercify.commercify.api.requests.CapturePaymentRequest;
5+
import com.zenfulcode.commercify.commercify.integration.mobilepay.MobilePayService;
56
import lombok.AllArgsConstructor;
67
import lombok.extern.slf4j.Slf4j;
78
import org.springframework.http.ResponseEntity;
@@ -14,15 +15,15 @@
1415
@AllArgsConstructor
1516
@Slf4j
1617
public class PaymentController {
17-
private final PaymentService paymentService;
18+
private final MobilePayService mobilePayService;
1819

1920
@PostMapping("/{orderId}/status")
2021
@PreAuthorize("hasRole('ADMIN')")
2122
public ResponseEntity<String> updatePaymentStatus(
2223
@PathVariable Long orderId,
2324
@RequestParam PaymentStatus status) {
2425
try {
25-
paymentService.handlePaymentStatusUpdate(orderId, status);
26+
mobilePayService.handlePaymentStatusUpdate(orderId, status);
2627
return ResponseEntity.ok("Payment status updated successfully");
2728
} catch (Exception e) {
2829
log.error("Error updating payment status", e);
@@ -32,7 +33,19 @@ public ResponseEntity<String> updatePaymentStatus(
3233

3334
@GetMapping("/{orderId}/status")
3435
public ResponseEntity<PaymentStatus> getPaymentStatus(@PathVariable Long orderId) {
35-
PaymentStatus status = paymentService.getPaymentStatus(orderId);
36+
PaymentStatus status = mobilePayService.getPaymentStatus(orderId);
3637
return ResponseEntity.ok(status);
3738
}
39+
40+
@PreAuthorize("hasRole('ADMIN')")
41+
@PostMapping("/{paymentId}/capture")
42+
public ResponseEntity<String> capturePayment(@PathVariable Long paymentId, @RequestBody CapturePaymentRequest request) {
43+
try {
44+
mobilePayService.capturePayment(paymentId, request.captureAmount(), request.isPartialCapture());
45+
return ResponseEntity.ok("Payment captured successfully");
46+
} catch (Exception e) {
47+
log.error("Error capturing payment", e);
48+
return ResponseEntity.badRequest().body("Error capturing payment");
49+
}
50+
}
3851
}

src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDTO.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public class OrderDTO {
2121
private Instant createdAt;
2222
private Instant updatedAt;
2323

24+
public OrderDTO() {
25+
26+
}
27+
2428
public double getTotal() {
2529
return subTotal + shippingCost;
2630
}

src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDetailsDTO.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ public class OrderDetailsDTO {
1515
private CustomerDetailsDTO customerDetails;
1616
private AddressDTO shippingAddress;
1717
private AddressDTO billingAddress;
18+
19+
public OrderDetailsDTO() {
20+
21+
}
1822
}

src/main/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlow.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ public OrderStateFlow() {
2222
// Payment received -> Processing or Cancelled
2323
validTransitions.put(OrderStatus.PAID, Set.of(
2424
OrderStatus.SHIPPED,
25-
OrderStatus.CANCELLED
25+
OrderStatus.CANCELLED,
26+
OrderStatus.COMPLETED
2627
));
2728

2829
// Shipped -> Completed or Returned

src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public ResponseEntity<String> handleCallback(
4141
try {
4242
// First authenticate the request with the raw string payload
4343
mobilePayService.authenticateRequest(date, contentSha256, authorization, body, request);
44-
log.info("MP Webhook authenticated");
44+
log.info("Mobilepay Webhook authenticated");
4545

4646
// Convert the string payload to WebhookPayload object
4747
ObjectMapper objectMapper = new ObjectMapper();

src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayService.java

Lines changed: 117 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,26 @@
44
import com.zenfulcode.commercify.commercify.PaymentStatus;
55
import com.zenfulcode.commercify.commercify.api.requests.PaymentRequest;
66
import com.zenfulcode.commercify.commercify.api.requests.WebhookPayload;
7+
import com.zenfulcode.commercify.commercify.api.requests.products.PriceRequest;
78
import com.zenfulcode.commercify.commercify.api.responses.PaymentResponse;
8-
import com.zenfulcode.commercify.commercify.entity.OrderEntity;
9+
import com.zenfulcode.commercify.commercify.dto.OrderDTO;
10+
import com.zenfulcode.commercify.commercify.dto.OrderDetailsDTO;
911
import com.zenfulcode.commercify.commercify.entity.PaymentEntity;
1012
import com.zenfulcode.commercify.commercify.entity.WebhookConfigEntity;
11-
import com.zenfulcode.commercify.commercify.exception.OrderNotFoundException;
1213
import com.zenfulcode.commercify.commercify.exception.PaymentProcessingException;
1314
import com.zenfulcode.commercify.commercify.integration.WebhookRegistrationResponse;
14-
import com.zenfulcode.commercify.commercify.repository.OrderRepository;
1515
import com.zenfulcode.commercify.commercify.repository.PaymentRepository;
1616
import com.zenfulcode.commercify.commercify.repository.WebhookConfigRepository;
1717
import com.zenfulcode.commercify.commercify.service.PaymentService;
18+
import com.zenfulcode.commercify.commercify.service.email.EmailService;
19+
import com.zenfulcode.commercify.commercify.service.order.OrderService;
1820
import jakarta.servlet.http.HttpServletRequest;
19-
import lombok.RequiredArgsConstructor;
2021
import lombok.extern.slf4j.Slf4j;
2122
import org.springframework.beans.factory.annotation.Value;
2223
import org.springframework.http.*;
2324
import org.springframework.retry.annotation.Backoff;
2425
import org.springframework.retry.annotation.Retryable;
26+
import org.springframework.scheduling.annotation.Async;
2527
import org.springframework.stereotype.Service;
2628
import org.springframework.transaction.annotation.Transactional;
2729
import org.springframework.web.client.RestTemplate;
@@ -35,15 +37,15 @@
3537
import java.security.MessageDigest;
3638
import java.security.NoSuchAlgorithmException;
3739
import java.util.*;
40+
import java.util.concurrent.CompletableFuture;
41+
import java.util.concurrent.ExecutionException;
3842

3943
@Service
40-
@RequiredArgsConstructor
4144
@Slf4j
42-
public class MobilePayService {
43-
private final PaymentService paymentService;
45+
public class MobilePayService extends PaymentService {
4446
private final MobilePayTokenService tokenService;
4547

46-
private final OrderRepository orderRepository;
48+
private final OrderService orderService;
4749
private final PaymentRepository paymentRepository;
4850

4951
private final RestTemplate restTemplate;
@@ -61,24 +63,59 @@ public class MobilePayService {
6163
@Value("${mobilepay.api-url}")
6264
private String apiUrl;
6365

66+
@Value("${commercify.host}")
67+
private String host;
68+
6469
private static final String PROVIDER_NAME = "MOBILEPAY";
6570

71+
public MobilePayService(PaymentRepository paymentRepository, EmailService emailService, OrderService orderService, MobilePayTokenService tokenService, OrderService orderService1, PaymentRepository paymentRepository1, RestTemplate restTemplate, WebhookConfigRepository webhookConfigRepository) {
72+
super(paymentRepository, emailService, orderService);
73+
this.tokenService = tokenService;
74+
this.orderService = orderService1;
75+
this.paymentRepository = paymentRepository1;
76+
this.restTemplate = restTemplate;
77+
this.webhookConfigRepository = webhookConfigRepository;
78+
}
79+
80+
@Override
81+
public void capturePayment(Long paymentId, double captureAmount, boolean isPartialCapture) {
82+
PaymentEntity payment = paymentRepository.findById(paymentId)
83+
.orElseThrow(() -> new RuntimeException("Payment not found: " + paymentId));
84+
85+
if (payment.getStatus() != PaymentStatus.PAID) {
86+
throw new RuntimeException("Payment cannot captured");
87+
}
88+
89+
OrderDetailsDTO order = orderService.getOrderById(payment.getOrderId());
90+
91+
double capturingAmount = isPartialCapture ? captureAmount : payment.getTotalAmount();
92+
93+
PriceRequest priceRequest = new PriceRequest(order.getOrder().getCurrency(), capturingAmount);
94+
95+
// Capture payment
96+
if (payment.getMobilePayReference() != null) {
97+
capturePayment(payment.getMobilePayReference(), priceRequest);
98+
}
99+
100+
// Update payment status
101+
payment.setStatus(PaymentStatus.PAID);
102+
paymentRepository.save(payment);
103+
}
104+
66105
@Transactional
67106
public PaymentResponse initiatePayment(PaymentRequest request) {
68107
try {
69-
OrderEntity order = orderRepository.findById(request.orderId())
70-
.orElseThrow(() -> new OrderNotFoundException(request.orderId()));
71-
108+
OrderDetailsDTO orderDetails = orderService.getOrderById(request.orderId());
72109
// Create MobilePay payment request
73-
Map<String, Object> paymentRequest = createMobilePayRequest(order, request);
110+
Map<String, Object> paymentRequest = createMobilePayRequest(orderDetails.getOrder(), request);
74111

75112
// Call MobilePay API
76113
MobilePayCheckoutResponse mobilePayCheckoutResponse = createMobilePayPayment(paymentRequest);
77114

78115
// Create and save payment entity
79116
PaymentEntity payment = PaymentEntity.builder()
80-
.orderId(order.getId())
81-
.totalAmount(order.getTotal())
117+
.orderId(orderDetails.getOrder().getId())
118+
.totalAmount(orderDetails.getOrder().getTotal())
82119
.paymentProvider(PaymentProvider.MOBILEPAY)
83120
.status(PaymentStatus.PENDING)
84121
.paymentMethod(request.paymentMethod()) // 'WALLET' or 'CARD'
@@ -106,7 +143,7 @@ public void handlePaymentCallback(WebhookPayload payload) {
106143
PaymentStatus newStatus = mapMobilePayStatus(payload.name());
107144

108145
// Update payment status and trigger confirmation email if needed
109-
paymentService.handlePaymentStatusUpdate(payment.getOrderId(), newStatus);
146+
handlePaymentStatusUpdate(payment.getOrderId(), newStatus);
110147
}
111148

112149
private HttpHeaders mobilePayRequestHeaders() {
@@ -151,7 +188,7 @@ private MobilePayCheckoutResponse createMobilePayPayment(Map<String, Object> req
151188
}
152189
}
153190

154-
public Map<String, Object> createMobilePayRequest(OrderEntity order, PaymentRequest request) {
191+
public Map<String, Object> createMobilePayRequest(OrderDTO order, PaymentRequest request) {
155192
validationPaymentRequest(request);
156193

157194
Map<String, Object> paymentRequest = new HashMap<>();
@@ -174,7 +211,7 @@ public Map<String, Object> createMobilePayRequest(OrderEntity order, PaymentRequ
174211
paymentRequest.put("customer", customer);
175212

176213
// Other fields
177-
String reference = String.join("-", merchantId, systemName, order.getId().toString(), value);
214+
String reference = String.join("-", merchantId, systemName, String.valueOf(order.getId()), value);
178215
paymentRequest.put("reference", reference);
179216
paymentRequest.put("returnUrl", request.returnUrl() + "?orderId=" + order.getId());
180217
paymentRequest.put("userFlow", "WEB_REDIRECT");
@@ -211,9 +248,11 @@ private PaymentStatus mapMobilePayStatus(String status) {
211248
return switch (status.toUpperCase()) {
212249
case "CREATED" -> PaymentStatus.PENDING;
213250
case "AUTHORIZED" -> PaymentStatus.PAID;
214-
case "ABORTED" -> PaymentStatus.CANCELLED;
251+
case "ABORTED", "CANCELLED" -> PaymentStatus.CANCELLED;
215252
case "EXPIRED" -> PaymentStatus.EXPIRED;
216253
case "TERMINATED" -> PaymentStatus.TERMINATED;
254+
case "CAPTURED" -> PaymentStatus.CAPTURED;
255+
case "REFUNDED" -> PaymentStatus.REFUNDED;
217256
default -> throw new PaymentProcessingException("Unknown MobilePay status: " + status, null);
218257
};
219258
}
@@ -254,6 +293,8 @@ public void registerWebhooks(String callbackUrl) {
254293
config.setWebhookUrl(callbackUrl);
255294
config.setWebhookSecret(response.getBody().secret());
256295
webhookConfigRepository.save(config);
296+
297+
log.info("Webhook updated successfully");
257298
},
258299
() -> {
259300
WebhookConfigEntity newConfig = WebhookConfigEntity.builder()
@@ -262,6 +303,8 @@ public void registerWebhooks(String callbackUrl) {
262303
.webhookSecret(response.getBody().secret())
263304
.build();
264305
webhookConfigRepository.save(newConfig);
306+
307+
log.info("Webhook registered successfully");
265308
}
266309
);
267310

@@ -271,10 +314,11 @@ public void registerWebhooks(String callbackUrl) {
271314
}
272315
}
273316

274-
317+
@Transactional(readOnly = true)
275318
public void authenticateRequest(String date, String contentSha256, String authorization, String payload, HttpServletRequest request) {
276319
try {
277320
// Verify content
321+
log.info("Verifying content");
278322
MessageDigest digest = MessageDigest.getInstance("SHA-256");
279323
byte[] hash = digest.digest(payload.getBytes(StandardCharsets.UTF_8));
280324
String encodedHash = Base64.getEncoder().encodeToString(hash);
@@ -283,26 +327,34 @@ public void authenticateRequest(String date, String contentSha256, String author
283327
throw new SecurityException("Hash mismatch");
284328
}
285329

286-
URI uri = new URI(request.getRequestURL().toString());
287-
String path = uri.getPath() + (uri.getQuery() != null ? "?" + uri.getQuery() : "");
330+
log.info("Content verified");
288331

289332
// Verify signature
333+
log.info("Verifying signature");
334+
String path = request.getRequestURI();
335+
URI uri = new URI(host + path);
336+
290337
String expectedSignedString = String.format("POST\n%s\n%s;%s;%s", path, date, uri.getHost(), encodedHash);
291338

292339
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
293-
SecretKeySpec secretKey = new SecretKeySpec(getWebhookSecret().getBytes(StandardCharsets.UTF_8), "HmacSHA256");
340+
341+
CompletableFuture<byte[]> secretByteArray = getWebhookSecret().thenApply(s -> s.getBytes(StandardCharsets.UTF_8));
342+
343+
SecretKeySpec secretKey = new SecretKeySpec(secretByteArray.get(), "HmacSHA256");
294344
hmacSha256.init(secretKey);
295345

296346
byte[] hmacSha256Bytes = hmacSha256.doFinal(expectedSignedString.getBytes(StandardCharsets.UTF_8));
297347
String expectedSignature = Base64.getEncoder().encodeToString(hmacSha256Bytes);
298-
String expectedAuthorization = "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=" + expectedSignature;
348+
String expectedAuthorization = String.format("HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=%s", expectedSignature);
299349

300350
if (!authorization.equals(expectedAuthorization)) {
301351
throw new SecurityException("Signature mismatch");
302352
}
353+
354+
log.info("Signature verified");
303355
} catch (NoSuchAlgorithmException e) {
304356
throw new RuntimeException("SHA-256 algorithm not found", e);
305-
} catch (InvalidKeyException | URISyntaxException e) {
357+
} catch (InvalidKeyException | URISyntaxException | ExecutionException | InterruptedException e) {
306358
throw new RuntimeException(e);
307359
}
308360
}
@@ -343,13 +395,50 @@ public Object getWebhooks() {
343395
}
344396
}
345397

346-
private String getWebhookSecret() {
347-
return webhookConfigRepository.findByProvider(PROVIDER_NAME)
348-
.map(WebhookConfigEntity::getWebhookSecret)
349-
.orElseThrow(() -> new PaymentProcessingException("Webhook secret not found", null));
398+
@Async
399+
protected CompletableFuture<String> getWebhookSecret() {
400+
try {
401+
final String secret = webhookConfigRepository.findByProvider(PROVIDER_NAME)
402+
.map(WebhookConfigEntity::getWebhookSecret)
403+
.orElseThrow(() -> new PaymentProcessingException("Webhook secret not found", null));
404+
405+
return CompletableFuture.completedFuture(secret);
406+
} catch (Exception e) {
407+
log.error("Error getting webhook secret: {}", e.getMessage());
408+
return CompletableFuture.failedFuture(e);
409+
}
410+
}
411+
412+
public void capturePayment(String mobilePayReference, PriceRequest captureAmount) {
413+
paymentRepository.findByMobilePayReference(mobilePayReference)
414+
.orElseThrow(() -> new PaymentProcessingException("Payment not found", null));
415+
416+
HttpHeaders headers = mobilePayRequestHeaders();
417+
418+
Map<String, Object> request = new HashMap<>();
419+
request.put("modificationAmount", new MobilePayPrice(Math.round(captureAmount.amount() * 100), captureAmount.currency()));
420+
421+
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
422+
423+
try {
424+
restTemplate.exchange(
425+
apiUrl + "/epayment/v1/payments/" + mobilePayReference + "/capture",
426+
HttpMethod.POST,
427+
entity,
428+
Object.class);
429+
} catch (Exception e) {
430+
log.error("Error capturing MobilePay payment: {}", e.getMessage());
431+
throw new PaymentProcessingException("Failed to capture MobilePay payment", e);
432+
}
350433
}
351434
}
352435

436+
record MobilePayPrice(
437+
long value,
438+
String currency
439+
) {
440+
}
441+
353442
record MobilePayCheckoutResponse(
354443
String redirectUrl,
355444
String reference

0 commit comments

Comments
 (0)