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

Dev #18

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,13 @@ public List<ScheduledPaymentResponseDto> getScheduledPaymentsForCustomer(String
public PaymentInitiationResponseDto initiatePayHerePayment(PaymentInitiationDto dto) {
log.info("Initiating PayHere payment for invoice: {}", dto.getInvoiceId());

// Log payment details for debugging
log.info("Payment Details - Merchant ID: {}, Order ID: {}, Amount: {}, Currency: {}",
payHereConfig.getMerchantId(),
dto.getInvoiceId(),
dto.getAmount(),
dto.getCurrency());

// Generate payment hash using utility method
String hash = PayHereHashUtil.generatePaymentHash(
payHereConfig.getMerchantId(),
Expand All @@ -324,7 +331,7 @@ public PaymentInitiationResponseDto initiatePayHerePayment(PaymentInitiationDto
payHereConfig.getMerchantSecret()
);

log.info("PayHere payment hash generated for order: {}", dto.getInvoiceId());
log.info("PayHere payment hash generated: {} for order: {}", hash, dto.getInvoiceId());

// Build response with all PayHere required parameters
PaymentInitiationResponseDto response = new PaymentInitiationResponseDto();
Expand Down Expand Up @@ -373,10 +380,28 @@ public void verifyAndProcessNotification(MultiValueMap<String, String> 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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,25 @@ public static String generatePaymentHash(
// Try to decode as Base64
byte[] decodedBytes = Base64.getDecoder().decode(merchantSecret);
decodedSecret = new String(decodedBytes);
System.out.println("DEBUG: Merchant secret was Base64 encoded, decoded successfully");
} catch (IllegalArgumentException e) {
// If decoding fails, use as-is (it's already plain text)
decodedSecret = merchantSecret;
System.out.println("DEBUG: Merchant secret is plain text (not Base64)");
}

// Step 1: Hash the merchant secret
String hashedSecret = getMd5(decodedSecret);
System.out.println("DEBUG: Hashed secret: " + hashedSecret);

// Step 2: Concatenate: merchant_id + order_id + amount + currency + hashed_secret
String concatenated = merchantId + orderId + formattedAmount + currency + hashedSecret;
System.out.println("DEBUG: Hash input string: " + concatenated);

// Step 3: Hash the concatenated string
return getMd5(concatenated);
String finalHash = getMd5(concatenated);
System.out.println("DEBUG: Final hash: " + finalHash);

return finalHash;
}
}
2 changes: 1 addition & 1 deletion payment-service/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ spring.profiles.active=${SPRING_PROFILE:dev}
# http://localhost:8086/swagger-ui/index.html

# PayHere Payment Gateway Configuration (Sandbox)
# Domain: techtorque
# Domain: techtorque.randitha.net
payhere.merchant.id=${PAYHERE_MERCHANT_ID:1231968}
payhere.merchant.secret=${PAYHERE_MERCHANT_SECRET:MjE0Mzc0NzgxMjEzMDE1NzYxNzE1NDIzNzA1MTIzMDI2NTc1ODQw}
payhere.sandbox=${PAYHERE_SANDBOX:true}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.techtorque.payment_service.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.techtorque.payment_service.dto.request.CreateInvoiceDto;
import com.techtorque.payment_service.dto.request.SendInvoiceDto;
import com.techtorque.payment_service.dto.response.InvoiceResponseDto;
import com.techtorque.payment_service.entity.InvoiceStatus;
import com.techtorque.payment_service.service.BillingService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc(addFilters = false)
@ActiveProfiles("test")
class InvoiceControllerIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@MockBean
private BillingService billingService;

private InvoiceResponseDto testInvoiceResponse;

@BeforeEach
void setUp() {
testInvoiceResponse = InvoiceResponseDto.builder()
.invoiceId("invoice123")
.invoiceNumber("INV-12345678")
.customerId("customer123")
.customerName("John Doe")
.customerEmail("john@example.com")
.serviceId("service456")
.items(new ArrayList<>())
.subtotal(new BigDecimal("1000.00"))
.taxAmount(BigDecimal.ZERO)
.discountAmount(BigDecimal.ZERO)
.totalAmount(new BigDecimal("1000.00"))
.paidAmount(BigDecimal.ZERO)
.balanceAmount(new BigDecimal("1000.00"))
.status(InvoiceStatus.SENT)
.dueDate(LocalDate.now().plusDays(30))
.issuedAt(LocalDateTime.now())
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.requiresDeposit(false)
.build();
}

@Test
@WithMockUser(roles = "EMPLOYEE")
void testCreateInvoice_Success() throws Exception {
CreateInvoiceDto request = new CreateInvoiceDto();
request.setCustomerId("customer123");
request.setServiceOrProjectId("service456");
request.setDueDate(LocalDate.now().plusDays(30));
request.setNotes("Test invoice");
request.setRequiresDeposit(false);

CreateInvoiceDto.InvoiceItemRequest itemRequest = new CreateInvoiceDto.InvoiceItemRequest();
itemRequest.setDescription("Labor");
itemRequest.setQuantity(10);
itemRequest.setUnitPrice(new BigDecimal("100.00"));
itemRequest.setItemType("LABOR");

request.setItems(Arrays.asList(itemRequest));

when(billingService.createInvoice(any(CreateInvoiceDto.class)))
.thenReturn(testInvoiceResponse);

mockMvc.perform(post("/invoices")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.invoiceId").value("invoice123"))
.andExpect(jsonPath("$.totalAmount").value(1000.00));
}

@Test
@WithMockUser(roles = "CUSTOMER")
void testGetInvoice_Success() throws Exception {
when(billingService.getInvoiceById("invoice123", "customer123"))
.thenReturn(testInvoiceResponse);

mockMvc.perform(get("/invoices/{invoiceId}", "invoice123")
.header("X-User-Subject", "customer123"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.invoiceId").value("invoice123"))
.andExpect(jsonPath("$.customerId").value("customer123"));
}

@Test
@WithMockUser(roles = "CUSTOMER")
void testListInvoices_AsCustomer() throws Exception {
when(billingService.listInvoicesForCustomer("customer123"))
.thenReturn(Arrays.asList(testInvoiceResponse));

mockMvc.perform(get("/invoices")
.header("X-User-Subject", "customer123")
.header("X-User-Roles", "CUSTOMER"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$[0].invoiceId").value("invoice123"));
}

@Test
@WithMockUser(roles = "ADMIN")
void testListInvoices_AsAdmin() throws Exception {
when(billingService.listAllInvoices())
.thenReturn(Arrays.asList(testInvoiceResponse));

mockMvc.perform(get("/invoices")
.header("X-User-Subject", "admin123")
.header("X-User-Roles", "ADMIN"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$[0].invoiceId").value("invoice123"));
}

@Test
@WithMockUser(roles = "EMPLOYEE")
void testSendInvoice_Success() throws Exception {
SendInvoiceDto request = new SendInvoiceDto();
request.setEmail("customer@example.com");

doNothing().when(billingService).sendInvoice(anyString(), anyString());

mockMvc.perform(post("/invoices/{invoiceId}/send", "invoice123")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Invoice sent successfully"));
}
}
Loading