44import com .zenfulcode .commercify .commercify .PaymentStatus ;
55import com .zenfulcode .commercify .commercify .api .requests .PaymentRequest ;
66import com .zenfulcode .commercify .commercify .api .requests .WebhookPayload ;
7+ import com .zenfulcode .commercify .commercify .api .requests .products .PriceRequest ;
78import 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 ;
911import com .zenfulcode .commercify .commercify .entity .PaymentEntity ;
1012import com .zenfulcode .commercify .commercify .entity .WebhookConfigEntity ;
11- import com .zenfulcode .commercify .commercify .exception .OrderNotFoundException ;
1213import com .zenfulcode .commercify .commercify .exception .PaymentProcessingException ;
1314import com .zenfulcode .commercify .commercify .integration .WebhookRegistrationResponse ;
14- import com .zenfulcode .commercify .commercify .repository .OrderRepository ;
1515import com .zenfulcode .commercify .commercify .repository .PaymentRepository ;
1616import com .zenfulcode .commercify .commercify .repository .WebhookConfigRepository ;
1717import com .zenfulcode .commercify .commercify .service .PaymentService ;
18+ import com .zenfulcode .commercify .commercify .service .email .EmailService ;
19+ import com .zenfulcode .commercify .commercify .service .order .OrderService ;
1820import jakarta .servlet .http .HttpServletRequest ;
19- import lombok .RequiredArgsConstructor ;
2021import lombok .extern .slf4j .Slf4j ;
2122import org .springframework .beans .factory .annotation .Value ;
2223import org .springframework .http .*;
2324import org .springframework .retry .annotation .Backoff ;
2425import org .springframework .retry .annotation .Retryable ;
26+ import org .springframework .scheduling .annotation .Async ;
2527import org .springframework .stereotype .Service ;
2628import org .springframework .transaction .annotation .Transactional ;
2729import org .springframework .web .client .RestTemplate ;
3537import java .security .MessageDigest ;
3638import java .security .NoSuchAlgorithmException ;
3739import 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+
353442record MobilePayCheckoutResponse (
354443 String redirectUrl ,
355444 String reference
0 commit comments