From c7f3bebecb368ccfe29228c00bdb5387b02aaae3 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sat, 15 Nov 2025 23:11:18 +0530 Subject: [PATCH] feat: improve payment hash generation and validation in BillingService, add unit tests for PayHereHashUtil --- .../service/impl/BillingServiceImpl.java | 26 ++++++++-- .../util/PayHereHashUtilTest.java | 48 +++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 payment-service/src/test/java/com/techtorque/payment_service/util/PayHereHashUtilTest.java diff --git a/payment-service/src/main/java/com/techtorque/payment_service/service/impl/BillingServiceImpl.java b/payment-service/src/main/java/com/techtorque/payment_service/service/impl/BillingServiceImpl.java index b1a9e35..5739075 100644 --- a/payment-service/src/main/java/com/techtorque/payment_service/service/impl/BillingServiceImpl.java +++ b/payment-service/src/main/java/com/techtorque/payment_service/service/impl/BillingServiceImpl.java @@ -380,10 +380,28 @@ public void verifyAndProcessNotification(MultiValueMap formData) String md5sig = formData.getFirst("md5sig"); String paymentId = formData.getFirst("payment_id"); - // Generate hash for verification - String hashString = merchantId + orderId + payhereAmount + payhereCurrency + - statusCode + payHereConfig.getMerchantSecret(); - String generatedHash = PayHereHashUtil.getMd5(hashString); + // Format the received amount to 2 decimal places to match how hash was generated + String formattedAmount = payhereAmount; + try { + java.text.DecimalFormat df = new java.text.DecimalFormat("0.00"); + java.math.BigDecimal bd = new java.math.BigDecimal(payhereAmount); + formattedAmount = df.format(bd); + } catch (Exception e) { + log.warn("Failed to format payhere_amount '{}', using raw value", payhereAmount); + } + + // Validate presence of orderId before proceeding + if (orderId == null || orderId.isBlank()) { + log.warn("Missing order_id in PayHere notification, skipping processing"); + return; + } + + // Generate verification hash according to PayHere docs + // md5sig = MD5(merchant_id + order_id + payhere_amount + payhere_currency + status_code + MD5(merchant_secret)).toUpperCase() + String hashedSecret = PayHereHashUtil.getMd5(payHereConfig.getMerchantSecret()); + String hashString = merchantId + orderId + formattedAmount + payhereCurrency + + statusCode + hashedSecret; + String generatedHash = PayHereHashUtil.getMd5(hashString); // Verify signature if (!generatedHash.equalsIgnoreCase(md5sig)) { diff --git a/payment-service/src/test/java/com/techtorque/payment_service/util/PayHereHashUtilTest.java b/payment-service/src/test/java/com/techtorque/payment_service/util/PayHereHashUtilTest.java new file mode 100644 index 0000000..6c7ebec --- /dev/null +++ b/payment-service/src/test/java/com/techtorque/payment_service/util/PayHereHashUtilTest.java @@ -0,0 +1,48 @@ +package com.techtorque.payment_service.util; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PayHereHashUtilTest { + + @Test + public void testGeneratePaymentHash_WithPlainSecret() { + String merchantId = "1231971"; + String orderId = "ORDER-ABC-123"; + BigDecimal amount = new BigDecimal("1000.00"); + String currency = "LKR"; + String merchantSecret = "plain-secret-123"; + + // Generate expected: hashedSecret = MD5(merchantSecret) + String hashedSecret = PayHereHashUtil.getMd5(merchantSecret); + String input = merchantId + orderId + "1000.00" + currency + hashedSecret; + String expected = PayHereHashUtil.getMd5(input); + + String actual = PayHereHashUtil.generatePaymentHash(merchantId, orderId, amount, currency, merchantSecret); + + assertEquals(expected, actual); + } + + @Test + public void testGeneratePaymentHash_WithBase64Secret() { + String merchantId = "1231971"; + String orderId = "ORD-BASE64"; + BigDecimal amount = new BigDecimal("250.00"); + String currency = "LKR"; + + // merchant secret base64 - for test use simple known string + String plainSecret = "super-secret"; + String base64Secret = java.util.Base64.getEncoder().encodeToString(plainSecret.getBytes()); + + // The util should decode base64 then MD5 + String hashedSecret = PayHereHashUtil.getMd5(plainSecret); // decode happens internally in generatePaymentHash + String input = merchantId + orderId + "250.00" + currency + hashedSecret; + String expected = PayHereHashUtil.getMd5(input); + + String actual = PayHereHashUtil.generatePaymentHash(merchantId, orderId, amount, currency, base64Secret); + assertEquals(expected, actual); + } +}