From 7e4246b4730094c2a9e499cfbf4a0e2a3d782a34 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sat, 15 Nov 2025 13:52:28 +0530 Subject: [PATCH 1/4] chore: trigger GitOps pipeline (empty commit) From 9b6ee4883c1d24ba52945ad6ebe4c5b56437b14b Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sat, 15 Nov 2025 20:27:10 +0530 Subject: [PATCH 2/4] feat: enhance payment logging and debugging in BillingService and PayHereHashUtil --- .../payment_service/service/impl/BillingServiceImpl.java | 9 ++++++++- .../techtorque/payment_service/util/PayHereHashUtil.java | 9 ++++++++- .../src/main/resources/application.properties | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) 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 0765b2f..b1a9e35 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 @@ -315,6 +315,13 @@ public List 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(), @@ -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(); diff --git a/payment-service/src/main/java/com/techtorque/payment_service/util/PayHereHashUtil.java b/payment-service/src/main/java/com/techtorque/payment_service/util/PayHereHashUtil.java index 508673a..0044dee 100644 --- a/payment-service/src/main/java/com/techtorque/payment_service/util/PayHereHashUtil.java +++ b/payment-service/src/main/java/com/techtorque/payment_service/util/PayHereHashUtil.java @@ -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; } } diff --git a/payment-service/src/main/resources/application.properties b/payment-service/src/main/resources/application.properties index 228b71b..f23640b 100644 --- a/payment-service/src/main/resources/application.properties +++ b/payment-service/src/main/resources/application.properties @@ -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} From c7f3bebecb368ccfe29228c00bdb5387b02aaae3 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sat, 15 Nov 2025 23:11:18 +0530 Subject: [PATCH 3/4] 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); + } +} From 33b00ead457a5bf5a634f1b7fc1de27141b5111f Mon Sep 17 00:00:00 2001 From: AdithaBuwaneka Date: Fri, 21 Nov 2025 17:06:26 +0530 Subject: [PATCH 4/4] Add unit tests for Payment, ScheduledPayment, and Billing services - Implement comprehensive tests for PaymentRepository including save, find, update, and delete operations. - Create tests for ScheduledPaymentRepository to validate scheduled payment functionalities. - Develop unit tests for BillingServiceImpl covering invoice creation, payment processing, and scheduled payment management. - Ensure proper exception handling and authorization checks in service methods. - Validate payment and scheduled payment retrieval based on various criteria such as customer ID and status. --- .../InvoiceControllerIntegrationTest.java | 157 +++++++ .../PaymentControllerIntegrationTest.java | 150 ++++++ .../repository/InvoiceRepositoryTest.java | 373 +++++++++++++++ .../repository/PaymentRepositoryTest.java | 317 +++++++++++++ .../ScheduledPaymentRepositoryTest.java | 284 +++++++++++ .../service/BillingServiceImplTest.java | 440 ++++++++++++++++++ .../resources/application-test.properties | 16 +- 7 files changed, 1736 insertions(+), 1 deletion(-) create mode 100644 payment-service/src/test/java/com/techtorque/payment_service/controller/InvoiceControllerIntegrationTest.java create mode 100644 payment-service/src/test/java/com/techtorque/payment_service/controller/PaymentControllerIntegrationTest.java create mode 100644 payment-service/src/test/java/com/techtorque/payment_service/repository/InvoiceRepositoryTest.java create mode 100644 payment-service/src/test/java/com/techtorque/payment_service/repository/PaymentRepositoryTest.java create mode 100644 payment-service/src/test/java/com/techtorque/payment_service/repository/ScheduledPaymentRepositoryTest.java create mode 100644 payment-service/src/test/java/com/techtorque/payment_service/service/BillingServiceImplTest.java diff --git a/payment-service/src/test/java/com/techtorque/payment_service/controller/InvoiceControllerIntegrationTest.java b/payment-service/src/test/java/com/techtorque/payment_service/controller/InvoiceControllerIntegrationTest.java new file mode 100644 index 0000000..8ca27c3 --- /dev/null +++ b/payment-service/src/test/java/com/techtorque/payment_service/controller/InvoiceControllerIntegrationTest.java @@ -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")); + } +} diff --git a/payment-service/src/test/java/com/techtorque/payment_service/controller/PaymentControllerIntegrationTest.java b/payment-service/src/test/java/com/techtorque/payment_service/controller/PaymentControllerIntegrationTest.java new file mode 100644 index 0000000..cd21934 --- /dev/null +++ b/payment-service/src/test/java/com/techtorque/payment_service/controller/PaymentControllerIntegrationTest.java @@ -0,0 +1,150 @@ +package com.techtorque.payment_service.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.techtorque.payment_service.dto.request.PaymentRequestDto; +import com.techtorque.payment_service.dto.request.SchedulePaymentDto; +import com.techtorque.payment_service.dto.response.PaymentResponseDto; +import com.techtorque.payment_service.dto.response.ScheduledPaymentResponseDto; +import com.techtorque.payment_service.entity.PaymentMethod; +import com.techtorque.payment_service.entity.PaymentStatus; +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.Arrays; + +import static org.mockito.ArgumentMatchers.*; +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 PaymentControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private BillingService billingService; + + private PaymentResponseDto testPaymentResponse; + private ScheduledPaymentResponseDto testScheduledPaymentResponse; + + @BeforeEach + void setUp() { + testPaymentResponse = PaymentResponseDto.builder() + .paymentId("payment123") + .invoiceId("invoice123") + .amount(new BigDecimal("1000.00")) + .method(PaymentMethod.CARD) + .status(PaymentStatus.SUCCESS) + .createdAt(LocalDateTime.now()) + .build(); + + testScheduledPaymentResponse = ScheduledPaymentResponseDto.builder() + .scheduleId("scheduled123") + .invoiceId("invoice123") + .customerId("customer123") + .amount(new BigDecimal("500.00")) + .scheduledDate(LocalDate.now().plusDays(7)) + .status("SCHEDULED") + .createdAt(LocalDateTime.now()) + .build(); + } + + @Test + @WithMockUser(roles = "CUSTOMER") + void testProcessPayment_Success() throws Exception { + PaymentRequestDto request = new PaymentRequestDto(); + request.setInvoiceId("invoice123"); + request.setAmount(new BigDecimal("1000.00")); + request.setMethod(PaymentMethod.CASH); + + when(billingService.processPayment(any(PaymentRequestDto.class), anyString())) + .thenReturn(testPaymentResponse); + + mockMvc.perform(post("/payments") + .header("X-User-Subject", "customer123") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.paymentId").value("payment123")) + .andExpect(jsonPath("$.amount").value(1000.00)); + } + + @Test + @WithMockUser(roles = "CUSTOMER") + void testGetPaymentHistory_Success() throws Exception { + when(billingService.getPaymentHistoryForCustomer("customer123")) + .thenReturn(Arrays.asList(testPaymentResponse)); + + mockMvc.perform(get("/payments") + .header("X-User-Subject", "customer123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$[0].paymentId").value("payment123")); + } + + @Test + @WithMockUser(roles = "CUSTOMER") + void testGetPaymentDetails_Success() throws Exception { + when(billingService.getPaymentDetails("payment123", "customer123")) + .thenReturn(testPaymentResponse); + + mockMvc.perform(get("/payments/{paymentId}", "payment123") + .header("X-User-Subject", "customer123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.paymentId").value("payment123")) + .andExpect(jsonPath("$.amount").value(1000.00)); + } + + @Test + @WithMockUser(roles = "CUSTOMER") + void testSchedulePayment_Success() throws Exception { + SchedulePaymentDto request = new SchedulePaymentDto(); + request.setInvoiceId("invoice123"); + request.setAmount(new BigDecimal("500.00")); + request.setScheduledDate(LocalDate.now().plusDays(7)); + request.setNotes("Scheduled payment"); + + when(billingService.schedulePayment(any(SchedulePaymentDto.class), anyString())) + .thenReturn(testScheduledPaymentResponse); + + mockMvc.perform(post("/payments/schedule") + .header("X-User-Subject", "customer123") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.scheduleId").value("scheduled123")) + .andExpect(jsonPath("$.amount").value(500.00)); + } + + @Test + @WithMockUser(roles = "CUSTOMER") + void testGetScheduledPayments_Success() throws Exception { + when(billingService.getScheduledPaymentsForCustomer("customer123")) + .thenReturn(Arrays.asList(testScheduledPaymentResponse)); + + mockMvc.perform(get("/payments/schedule") + .header("X-User-Subject", "customer123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$[0].scheduleId").value("scheduled123")); + } +} diff --git a/payment-service/src/test/java/com/techtorque/payment_service/repository/InvoiceRepositoryTest.java b/payment-service/src/test/java/com/techtorque/payment_service/repository/InvoiceRepositoryTest.java new file mode 100644 index 0000000..f185cba --- /dev/null +++ b/payment-service/src/test/java/com/techtorque/payment_service/repository/InvoiceRepositoryTest.java @@ -0,0 +1,373 @@ +package com.techtorque.payment_service.repository; + +import com.techtorque.payment_service.entity.Invoice; +import com.techtorque.payment_service.entity.InvoiceItem; +import com.techtorque.payment_service.entity.InvoiceStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class InvoiceRepositoryTest { + + @Autowired + private InvoiceRepository invoiceRepository; + + private Invoice testInvoice; + + @BeforeEach + void setUp() { + invoiceRepository.deleteAll(); + + testInvoice = Invoice.builder() + .customerId("customer123") + .serviceOrProjectId("service456") + .amount(new BigDecimal("1000.00")) + .status(InvoiceStatus.SENT) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(30)) + .notes("Test invoice") + .requiresDeposit(false) + .build(); + } + + @Test + void testSaveInvoice() { + Invoice saved = invoiceRepository.save(testInvoice); + + assertThat(saved).isNotNull(); + assertThat(saved.getId()).isNotNull(); + assertThat(saved.getCustomerId()).isEqualTo("customer123"); + assertThat(saved.getAmount()).isEqualByComparingTo(new BigDecimal("1000.00")); + assertThat(saved.getStatus()).isEqualTo(InvoiceStatus.SENT); + assertThat(saved.getCreatedAt()).isNotNull(); + assertThat(saved.getUpdatedAt()).isNotNull(); + } + + @Test + void testFindById() { + invoiceRepository.save(testInvoice); + + Optional found = invoiceRepository.findById(testInvoice.getId()); + + assertThat(found).isPresent(); + assertThat(found.get().getServiceOrProjectId()).isEqualTo("service456"); + } + + @Test + void testFindByCustomerId() { + Invoice invoice2 = Invoice.builder() + .customerId("customer123") + .serviceOrProjectId("service789") + .amount(new BigDecimal("500.00")) + .status(InvoiceStatus.DRAFT) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(15)) + .requiresDeposit(false) + .build(); + + invoiceRepository.save(testInvoice); + invoiceRepository.save(invoice2); + + List invoices = invoiceRepository.findByCustomerId("customer123"); + + assertThat(invoices).hasSize(2); + assertThat(invoices).allMatch(i -> i.getCustomerId().equals("customer123")); + } + + @Test + void testFindByCustomerIdOrderByCreatedAtDesc() { + Invoice invoice2 = Invoice.builder() + .customerId("customer123") + .serviceOrProjectId("service789") + .amount(new BigDecimal("500.00")) + .status(InvoiceStatus.DRAFT) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(15)) + .requiresDeposit(false) + .build(); + + invoiceRepository.save(testInvoice); + invoiceRepository.save(invoice2); + + List invoices = invoiceRepository.findByCustomerIdOrderByCreatedAtDesc("customer123"); + + assertThat(invoices).hasSize(2); + assertThat(invoices.get(0).getCreatedAt()).isAfterOrEqualTo(invoices.get(1).getCreatedAt()); + } + + @Test + void testFindByStatus() { + Invoice invoice2 = Invoice.builder() + .customerId("customer456") + .serviceOrProjectId("service789") + .amount(new BigDecimal("500.00")) + .status(InvoiceStatus.PAID) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(15)) + .requiresDeposit(false) + .build(); + + Invoice invoice3 = Invoice.builder() + .customerId("customer789") + .serviceOrProjectId("service999") + .amount(new BigDecimal("750.00")) + .status(InvoiceStatus.SENT) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(20)) + .requiresDeposit(false) + .build(); + + invoiceRepository.save(testInvoice); + invoiceRepository.save(invoice2); + invoiceRepository.save(invoice3); + + List sentInvoices = invoiceRepository.findByStatus(InvoiceStatus.SENT); + + assertThat(sentInvoices).hasSize(2); + assertThat(sentInvoices).allMatch(i -> i.getStatus() == InvoiceStatus.SENT); + } + + @Test + void testFindByServiceOrProjectId() { + invoiceRepository.save(testInvoice); + + Optional found = invoiceRepository.findByServiceOrProjectId("service456"); + + assertThat(found).isPresent(); + assertThat(found.get().getServiceOrProjectId()).isEqualTo("service456"); + } + + @Test + void testFindByCustomerIdAndStatus() { + Invoice invoice2 = Invoice.builder() + .customerId("customer123") + .serviceOrProjectId("service789") + .amount(new BigDecimal("500.00")) + .status(InvoiceStatus.DRAFT) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(15)) + .requiresDeposit(false) + .build(); + + Invoice invoice3 = Invoice.builder() + .customerId("customer123") + .serviceOrProjectId("service999") + .amount(new BigDecimal("750.00")) + .status(InvoiceStatus.SENT) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(20)) + .requiresDeposit(false) + .build(); + + invoiceRepository.save(testInvoice); + invoiceRepository.save(invoice2); + invoiceRepository.save(invoice3); + + List sentInvoices = invoiceRepository.findByCustomerIdAndStatus( + "customer123", InvoiceStatus.SENT); + + assertThat(sentInvoices).hasSize(2); + assertThat(sentInvoices).allMatch(i -> + i.getCustomerId().equals("customer123") && i.getStatus() == InvoiceStatus.SENT); + } + + @Test + void testFindOverdueInvoices() { + Invoice overdueInvoice = Invoice.builder() + .customerId("customer456") + .serviceOrProjectId("service789") + .amount(new BigDecimal("500.00")) + .status(InvoiceStatus.SENT) + .issueDate(LocalDate.now().minusDays(60)) + .dueDate(LocalDate.now().minusDays(5)) + .requiresDeposit(false) + .build(); + + invoiceRepository.save(testInvoice); + invoiceRepository.save(overdueInvoice); + + List overdueInvoices = invoiceRepository.findOverdueInvoices(LocalDate.now()); + + assertThat(overdueInvoices).hasSize(1); + assertThat(overdueInvoices.get(0).getDueDate()).isBefore(LocalDate.now()); + } + + @Test + void testFindOverdueInvoicesByCustomer() { + Invoice overdueInvoice = Invoice.builder() + .customerId("customer123") + .serviceOrProjectId("service789") + .amount(new BigDecimal("500.00")) + .status(InvoiceStatus.SENT) + .issueDate(LocalDate.now().minusDays(60)) + .dueDate(LocalDate.now().minusDays(5)) + .requiresDeposit(false) + .build(); + + Invoice overdueInvoiceOtherCustomer = Invoice.builder() + .customerId("customer456") + .serviceOrProjectId("service999") + .amount(new BigDecimal("750.00")) + .status(InvoiceStatus.SENT) + .issueDate(LocalDate.now().minusDays(60)) + .dueDate(LocalDate.now().minusDays(10)) + .requiresDeposit(false) + .build(); + + invoiceRepository.save(testInvoice); + invoiceRepository.save(overdueInvoice); + invoiceRepository.save(overdueInvoiceOtherCustomer); + + List overdueInvoices = invoiceRepository.findOverdueInvoicesByCustomer( + "customer123", LocalDate.now()); + + assertThat(overdueInvoices).hasSize(1); + assertThat(overdueInvoices.get(0).getCustomerId()).isEqualTo("customer123"); + assertThat(overdueInvoices.get(0).getDueDate()).isBefore(LocalDate.now()); + } + + @Test + void testFindInvoicesBetweenDates() { + LocalDate startDate = LocalDate.now().minusDays(30); + LocalDate endDate = LocalDate.now().plusDays(30); + + Invoice invoice2 = Invoice.builder() + .customerId("customer456") + .serviceOrProjectId("service789") + .amount(new BigDecimal("500.00")) + .status(InvoiceStatus.PAID) + .issueDate(LocalDate.now().minusDays(100)) + .dueDate(LocalDate.now().minusDays(70)) + .requiresDeposit(false) + .build(); + + invoiceRepository.save(testInvoice); + invoiceRepository.save(invoice2); + + List invoices = invoiceRepository.findInvoicesBetweenDates(startDate, endDate); + + assertThat(invoices).hasSize(1); + assertThat(invoices.get(0).getIssueDate()).isBetween(startDate, endDate); + } + + @Test + void testUpdateInvoice() { + invoiceRepository.save(testInvoice); + + testInvoice.setStatus(InvoiceStatus.PAID); + testInvoice.setNotes("Invoice paid in full"); + Invoice updated = invoiceRepository.save(testInvoice); + + assertThat(updated.getStatus()).isEqualTo(InvoiceStatus.PAID); + assertThat(updated.getNotes()).contains("paid in full"); + assertThat(updated.getUpdatedAt()).isNotNull(); + } + + @Test + void testDeleteInvoice() { + invoiceRepository.save(testInvoice); + String invoiceId = testInvoice.getId(); + + invoiceRepository.deleteById(invoiceId); + + Optional deleted = invoiceRepository.findById(invoiceId); + assertThat(deleted).isEmpty(); + } + + @Test + void testInvoiceWithItems() { + InvoiceItem item1 = InvoiceItem.builder() + .description("Labor - 10 hours") + .quantity(10) + .unitPrice(new BigDecimal("50.00")) + .totalPrice(new BigDecimal("500.00")) + .itemType("LABOR") + .build(); + + InvoiceItem item2 = InvoiceItem.builder() + .description("Parts") + .quantity(5) + .unitPrice(new BigDecimal("100.00")) + .totalPrice(new BigDecimal("500.00")) + .itemType("PARTS") + .build(); + + testInvoice.addItem(item1); + testInvoice.addItem(item2); + + Invoice saved = invoiceRepository.save(testInvoice); + + assertThat(saved.getItems()).hasSize(2); + assertThat(saved.calculateTotal()).isEqualByComparingTo(new BigDecimal("1000.00")); + } + + @Test + void testInvoiceWithDepositRequirement() { + Invoice depositInvoice = Invoice.builder() + .customerId("customer123") + .serviceOrProjectId("service456") + .amount(new BigDecimal("2000.00")) + .status(InvoiceStatus.SENT) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(30)) + .requiresDeposit(true) + .depositAmount(new BigDecimal("1000.00")) + .depositPaid(new BigDecimal("1000.00")) + .finalAmount(new BigDecimal("1000.00")) + .finalPaid(BigDecimal.ZERO) + .build(); + + Invoice saved = invoiceRepository.save(depositInvoice); + + assertThat(saved.getRequiresDeposit()).isTrue(); + assertThat(saved.getDepositAmount()).isEqualByComparingTo(new BigDecimal("1000.00")); + assertThat(saved.getDepositPaid()).isEqualByComparingTo(new BigDecimal("1000.00")); + } + + @Test + void testDefaultValuesOnCreation() { + Invoice newInvoice = Invoice.builder() + .customerId("customer999") + .serviceOrProjectId("service999") + .amount(new BigDecimal("500.00")) + .requiresDeposit(false) + .build(); + + Invoice saved = invoiceRepository.save(newInvoice); + + assertThat(saved.getStatus()).isEqualTo(InvoiceStatus.DRAFT); + assertThat(saved.getIssueDate()).isNotNull(); + assertThat(saved.getDueDate()).isNotNull(); + assertThat(saved.getDueDate()).isAfter(saved.getIssueDate()); + } + + @Test + void testPartiallyPaidInvoice() { + Invoice partiallyPaid = Invoice.builder() + .customerId("customer123") + .serviceOrProjectId("service456") + .amount(new BigDecimal("1000.00")) + .status(InvoiceStatus.PARTIALLY_PAID) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(30)) + .requiresDeposit(false) + .build(); + + Invoice saved = invoiceRepository.save(partiallyPaid); + + assertThat(saved.getStatus()).isEqualTo(InvoiceStatus.PARTIALLY_PAID); + } +} diff --git a/payment-service/src/test/java/com/techtorque/payment_service/repository/PaymentRepositoryTest.java b/payment-service/src/test/java/com/techtorque/payment_service/repository/PaymentRepositoryTest.java new file mode 100644 index 0000000..d83c032 --- /dev/null +++ b/payment-service/src/test/java/com/techtorque/payment_service/repository/PaymentRepositoryTest.java @@ -0,0 +1,317 @@ +package com.techtorque.payment_service.repository; + +import com.techtorque.payment_service.entity.Payment; +import com.techtorque.payment_service.entity.PaymentMethod; +import com.techtorque.payment_service.entity.PaymentStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class PaymentRepositoryTest { + + @Autowired + private PaymentRepository paymentRepository; + + private Payment testPayment; + + @BeforeEach + void setUp() { + paymentRepository.deleteAll(); + + testPayment = Payment.builder() + .invoiceId("invoice123") + .customerId("customer123") + .amount(new BigDecimal("500.00")) + .method(PaymentMethod.CARD) + .status(PaymentStatus.SUCCESS) + .paymentGatewayTransactionId("txn123") + .notes("Test payment") + .build(); + } + + @Test + void testSavePayment() { + Payment saved = paymentRepository.save(testPayment); + + assertThat(saved).isNotNull(); + assertThat(saved.getId()).isNotNull(); + assertThat(saved.getInvoiceId()).isEqualTo("invoice123"); + assertThat(saved.getAmount()).isEqualByComparingTo(new BigDecimal("500.00")); + assertThat(saved.getStatus()).isEqualTo(PaymentStatus.SUCCESS); + assertThat(saved.getCreatedAt()).isNotNull(); + assertThat(saved.getUpdatedAt()).isNotNull(); + } + + @Test + void testFindById() { + paymentRepository.save(testPayment); + + Optional found = paymentRepository.findById(testPayment.getId()); + + assertThat(found).isPresent(); + assertThat(found.get().getInvoiceId()).isEqualTo("invoice123"); + } + + @Test + void testFindByInvoiceId() { + Payment payment2 = Payment.builder() + .invoiceId("invoice123") + .customerId("customer456") + .amount(new BigDecimal("300.00")) + .method(PaymentMethod.CASH) + .status(PaymentStatus.SUCCESS) + .build(); + + paymentRepository.save(testPayment); + paymentRepository.save(payment2); + + List payments = paymentRepository.findByInvoiceId("invoice123"); + + assertThat(payments).hasSize(2); + assertThat(payments).allMatch(p -> p.getInvoiceId().equals("invoice123")); + } + + @Test + void testFindByCustomerId() { + Payment payment2 = Payment.builder() + .invoiceId("invoice456") + .customerId("customer123") + .amount(new BigDecimal("200.00")) + .method(PaymentMethod.BANK_TRANSFER) + .status(PaymentStatus.SUCCESS) + .build(); + + paymentRepository.save(testPayment); + paymentRepository.save(payment2); + + List payments = paymentRepository.findByCustomerId("customer123"); + + assertThat(payments).hasSize(2); + assertThat(payments).allMatch(p -> p.getCustomerId().equals("customer123")); + } + + @Test + void testFindByCustomerIdOrderByCreatedAtDesc() { + Payment payment2 = Payment.builder() + .invoiceId("invoice456") + .customerId("customer123") + .amount(new BigDecimal("200.00")) + .method(PaymentMethod.ONLINE) + .status(PaymentStatus.SUCCESS) + .build(); + + paymentRepository.save(testPayment); + paymentRepository.save(payment2); + + List payments = paymentRepository.findByCustomerIdOrderByCreatedAtDesc("customer123"); + + assertThat(payments).hasSize(2); + assertThat(payments.get(0).getCreatedAt()).isAfterOrEqualTo(payments.get(1).getCreatedAt()); + } + + @Test + void testFindByStatus() { + Payment payment2 = Payment.builder() + .invoiceId("invoice456") + .customerId("customer456") + .amount(new BigDecimal("100.00")) + .method(PaymentMethod.CASH) + .status(PaymentStatus.PENDING) + .build(); + + Payment payment3 = Payment.builder() + .invoiceId("invoice789") + .customerId("customer789") + .amount(new BigDecimal("150.00")) + .method(PaymentMethod.CARD) + .status(PaymentStatus.SUCCESS) + .build(); + + paymentRepository.save(testPayment); + paymentRepository.save(payment2); + paymentRepository.save(payment3); + + List successfulPayments = paymentRepository.findByStatus(PaymentStatus.SUCCESS); + + assertThat(successfulPayments).hasSize(2); + assertThat(successfulPayments).allMatch(p -> p.getStatus() == PaymentStatus.SUCCESS); + } + + @Test + void testFindByPaymentGatewayTransactionId() { + paymentRepository.save(testPayment); + + Optional found = paymentRepository.findByPaymentGatewayTransactionId("txn123"); + + assertThat(found).isPresent(); + assertThat(found.get().getPaymentGatewayTransactionId()).isEqualTo("txn123"); + } + + @Test + void testFindByCustomerIdAndStatus() { + Payment payment2 = Payment.builder() + .invoiceId("invoice456") + .customerId("customer123") + .amount(new BigDecimal("200.00")) + .method(PaymentMethod.CASH) + .status(PaymentStatus.PENDING) + .build(); + + Payment payment3 = Payment.builder() + .invoiceId("invoice789") + .customerId("customer123") + .amount(new BigDecimal("300.00")) + .method(PaymentMethod.CARD) + .status(PaymentStatus.SUCCESS) + .build(); + + paymentRepository.save(testPayment); + paymentRepository.save(payment2); + paymentRepository.save(payment3); + + List successPayments = paymentRepository.findByCustomerIdAndStatus( + "customer123", PaymentStatus.SUCCESS); + + assertThat(successPayments).hasSize(2); + assertThat(successPayments).allMatch(p -> + p.getCustomerId().equals("customer123") && p.getStatus() == PaymentStatus.SUCCESS); + } + + @Test + void testFindPaymentsBetweenDates() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime startDate = now.minusDays(7); + LocalDateTime endDate = now.plusDays(1); + + paymentRepository.save(testPayment); + + List payments = paymentRepository.findPaymentsBetweenDates(startDate, endDate); + + assertThat(payments).hasSize(1); + assertThat(payments.get(0).getCreatedAt()).isBetween(startDate, endDate); + } + + @Test + void testFindCustomerPaymentsBetweenDates() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime startDate = now.minusDays(7); + LocalDateTime endDate = now.plusDays(1); + + Payment payment2 = Payment.builder() + .invoiceId("invoice456") + .customerId("customer456") + .amount(new BigDecimal("200.00")) + .method(PaymentMethod.CASH) + .status(PaymentStatus.SUCCESS) + .build(); + + paymentRepository.save(testPayment); + paymentRepository.save(payment2); + + List payments = paymentRepository.findCustomerPaymentsBetweenDates( + "customer123", startDate, endDate); + + assertThat(payments).hasSize(1); + assertThat(payments.get(0).getCustomerId()).isEqualTo("customer123"); + assertThat(payments.get(0).getCreatedAt()).isBetween(startDate, endDate); + } + + @Test + void testUpdatePayment() { + paymentRepository.save(testPayment); + + testPayment.setStatus(PaymentStatus.FAILED); + testPayment.setNotes("Payment failed due to insufficient funds"); + Payment updated = paymentRepository.save(testPayment); + + assertThat(updated.getStatus()).isEqualTo(PaymentStatus.FAILED); + assertThat(updated.getNotes()).contains("Payment failed"); + assertThat(updated.getUpdatedAt()).isNotNull(); + } + + @Test + void testDeletePayment() { + paymentRepository.save(testPayment); + String paymentId = testPayment.getId(); + + paymentRepository.deleteById(paymentId); + + Optional deleted = paymentRepository.findById(paymentId); + assertThat(deleted).isEmpty(); + } + + @Test + void testPaymentMethodTypes() { + Payment cardPayment = Payment.builder() + .invoiceId("inv1") + .customerId("cust1") + .amount(new BigDecimal("100.00")) + .method(PaymentMethod.CARD) + .status(PaymentStatus.SUCCESS) + .build(); + + Payment cashPayment = Payment.builder() + .invoiceId("inv2") + .customerId("cust2") + .amount(new BigDecimal("200.00")) + .method(PaymentMethod.CASH) + .status(PaymentStatus.SUCCESS) + .build(); + + Payment bankTransferPayment = Payment.builder() + .invoiceId("inv3") + .customerId("cust3") + .amount(new BigDecimal("300.00")) + .method(PaymentMethod.BANK_TRANSFER) + .status(PaymentStatus.SUCCESS) + .build(); + + Payment onlinePayment = Payment.builder() + .invoiceId("inv4") + .customerId("cust4") + .amount(new BigDecimal("400.00")) + .method(PaymentMethod.ONLINE) + .status(PaymentStatus.SUCCESS) + .build(); + + paymentRepository.save(cardPayment); + paymentRepository.save(cashPayment); + paymentRepository.save(bankTransferPayment); + paymentRepository.save(onlinePayment); + + List allPayments = paymentRepository.findAll(); + + assertThat(allPayments).hasSize(4); + assertThat(allPayments).anyMatch(p -> p.getMethod() == PaymentMethod.CARD); + assertThat(allPayments).anyMatch(p -> p.getMethod() == PaymentMethod.CASH); + assertThat(allPayments).anyMatch(p -> p.getMethod() == PaymentMethod.BANK_TRANSFER); + assertThat(allPayments).anyMatch(p -> p.getMethod() == PaymentMethod.ONLINE); + } + + @Test + void testDefaultStatusOnCreation() { + Payment newPayment = Payment.builder() + .invoiceId("invoice999") + .customerId("customer999") + .amount(new BigDecimal("100.00")) + .method(PaymentMethod.CARD) + .build(); + + Payment saved = paymentRepository.save(newPayment); + + assertThat(saved.getStatus()).isEqualTo(PaymentStatus.PENDING); + } +} diff --git a/payment-service/src/test/java/com/techtorque/payment_service/repository/ScheduledPaymentRepositoryTest.java b/payment-service/src/test/java/com/techtorque/payment_service/repository/ScheduledPaymentRepositoryTest.java new file mode 100644 index 0000000..becc435 --- /dev/null +++ b/payment-service/src/test/java/com/techtorque/payment_service/repository/ScheduledPaymentRepositoryTest.java @@ -0,0 +1,284 @@ +package com.techtorque.payment_service.repository; + +import com.techtorque.payment_service.entity.ScheduledPayment; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class ScheduledPaymentRepositoryTest { + + @Autowired + private ScheduledPaymentRepository scheduledPaymentRepository; + + private ScheduledPayment testScheduledPayment; + + @BeforeEach + void setUp() { + scheduledPaymentRepository.deleteAll(); + + testScheduledPayment = ScheduledPayment.builder() + .invoiceId("invoice123") + .customerId("customer123") + .amount(new BigDecimal("500.00")) + .scheduledDate(LocalDate.now().plusDays(7)) + .status("SCHEDULED") + .notes("Test scheduled payment") + .build(); + } + + @Test + void testSaveScheduledPayment() { + ScheduledPayment saved = scheduledPaymentRepository.save(testScheduledPayment); + + assertThat(saved).isNotNull(); + assertThat(saved.getId()).isNotNull(); + assertThat(saved.getInvoiceId()).isEqualTo("invoice123"); + assertThat(saved.getAmount()).isEqualByComparingTo(new BigDecimal("500.00")); + assertThat(saved.getStatus()).isEqualTo("SCHEDULED"); + assertThat(saved.getCreatedAt()).isNotNull(); + assertThat(saved.getUpdatedAt()).isNotNull(); + } + + @Test + void testFindById() { + scheduledPaymentRepository.save(testScheduledPayment); + + Optional found = scheduledPaymentRepository.findById(testScheduledPayment.getId()); + + assertThat(found).isPresent(); + assertThat(found.get().getInvoiceId()).isEqualTo("invoice123"); + } + + @Test + void testFindByCustomerId() { + ScheduledPayment payment2 = ScheduledPayment.builder() + .invoiceId("invoice456") + .customerId("customer123") + .amount(new BigDecimal("300.00")) + .scheduledDate(LocalDate.now().plusDays(14)) + .status("SCHEDULED") + .build(); + + scheduledPaymentRepository.save(testScheduledPayment); + scheduledPaymentRepository.save(payment2); + + List payments = scheduledPaymentRepository.findByCustomerId("customer123"); + + assertThat(payments).hasSize(2); + assertThat(payments).allMatch(p -> p.getCustomerId().equals("customer123")); + } + + @Test + void testFindByInvoiceId() { + ScheduledPayment payment2 = ScheduledPayment.builder() + .invoiceId("invoice123") + .customerId("customer456") + .amount(new BigDecimal("200.00")) + .scheduledDate(LocalDate.now().plusDays(7)) + .status("SCHEDULED") + .build(); + + scheduledPaymentRepository.save(testScheduledPayment); + scheduledPaymentRepository.save(payment2); + + List payments = scheduledPaymentRepository.findByInvoiceId("invoice123"); + + assertThat(payments).hasSize(2); + assertThat(payments).allMatch(p -> p.getInvoiceId().equals("invoice123")); + } + + @Test + void testFindByStatus() { + ScheduledPayment payment2 = ScheduledPayment.builder() + .invoiceId("invoice456") + .customerId("customer456") + .amount(new BigDecimal("300.00")) + .scheduledDate(LocalDate.now().plusDays(14)) + .status("PROCESSED") + .paymentId("payment123") + .build(); + + ScheduledPayment payment3 = ScheduledPayment.builder() + .invoiceId("invoice789") + .customerId("customer789") + .amount(new BigDecimal("400.00")) + .scheduledDate(LocalDate.now().plusDays(21)) + .status("SCHEDULED") + .build(); + + scheduledPaymentRepository.save(testScheduledPayment); + scheduledPaymentRepository.save(payment2); + scheduledPaymentRepository.save(payment3); + + List scheduledPayments = scheduledPaymentRepository.findByStatus("SCHEDULED"); + + assertThat(scheduledPayments).hasSize(2); + assertThat(scheduledPayments).allMatch(p -> p.getStatus().equals("SCHEDULED")); + } + + @Test + void testFindScheduledPaymentsForDate() { + LocalDate targetDate = LocalDate.now().plusDays(7); + + ScheduledPayment payment2 = ScheduledPayment.builder() + .invoiceId("invoice456") + .customerId("customer456") + .amount(new BigDecimal("300.00")) + .scheduledDate(targetDate) + .status("SCHEDULED") + .build(); + + ScheduledPayment payment3 = ScheduledPayment.builder() + .invoiceId("invoice789") + .customerId("customer789") + .amount(new BigDecimal("400.00")) + .scheduledDate(LocalDate.now().plusDays(14)) + .status("SCHEDULED") + .build(); + + scheduledPaymentRepository.save(testScheduledPayment); + scheduledPaymentRepository.save(payment2); + scheduledPaymentRepository.save(payment3); + + List payments = scheduledPaymentRepository.findScheduledPaymentsForDate(targetDate); + + assertThat(payments).hasSize(2); + assertThat(payments).allMatch(p -> p.getScheduledDate().equals(targetDate)); + assertThat(payments).allMatch(p -> p.getStatus().equals("SCHEDULED")); + } + + @Test + void testFindOverdueScheduledPayments() { + ScheduledPayment overduePayment = ScheduledPayment.builder() + .invoiceId("invoice456") + .customerId("customer456") + .amount(new BigDecimal("300.00")) + .scheduledDate(LocalDate.now().minusDays(5)) + .status("SCHEDULED") + .build(); + + ScheduledPayment futurePayment = ScheduledPayment.builder() + .invoiceId("invoice789") + .customerId("customer789") + .amount(new BigDecimal("400.00")) + .scheduledDate(LocalDate.now().plusDays(14)) + .status("SCHEDULED") + .build(); + + scheduledPaymentRepository.save(overduePayment); + scheduledPaymentRepository.save(futurePayment); + + List overduePayments = scheduledPaymentRepository + .findOverdueScheduledPayments(LocalDate.now()); + + assertThat(overduePayments).hasSize(1); + assertThat(overduePayments.get(0).getScheduledDate()).isBeforeOrEqualTo(LocalDate.now()); + } + + @Test + void testUpdateScheduledPayment() { + scheduledPaymentRepository.save(testScheduledPayment); + + testScheduledPayment.setStatus("PROCESSED"); + testScheduledPayment.setPaymentId("payment123"); + testScheduledPayment.setNotes("Payment processed successfully"); + ScheduledPayment updated = scheduledPaymentRepository.save(testScheduledPayment); + + assertThat(updated.getStatus()).isEqualTo("PROCESSED"); + assertThat(updated.getPaymentId()).isEqualTo("payment123"); + assertThat(updated.getUpdatedAt()).isNotNull(); + } + + @Test + void testDeleteScheduledPayment() { + scheduledPaymentRepository.save(testScheduledPayment); + String paymentId = testScheduledPayment.getId(); + + scheduledPaymentRepository.deleteById(paymentId); + + Optional deleted = scheduledPaymentRepository.findById(paymentId); + assertThat(deleted).isEmpty(); + } + + @Test + void testCancelScheduledPayment() { + scheduledPaymentRepository.save(testScheduledPayment); + + testScheduledPayment.setStatus("CANCELLED"); + testScheduledPayment.setNotes("Payment cancelled by customer"); + ScheduledPayment updated = scheduledPaymentRepository.save(testScheduledPayment); + + assertThat(updated.getStatus()).isEqualTo("CANCELLED"); + } + + @Test + void testFailedScheduledPayment() { + scheduledPaymentRepository.save(testScheduledPayment); + + testScheduledPayment.setStatus("FAILED"); + testScheduledPayment.setNotes("Payment failed - insufficient funds"); + ScheduledPayment updated = scheduledPaymentRepository.save(testScheduledPayment); + + assertThat(updated.getStatus()).isEqualTo("FAILED"); + assertThat(updated.getNotes()).contains("insufficient funds"); + } + + @Test + void testDefaultStatusOnCreation() { + ScheduledPayment newPayment = ScheduledPayment.builder() + .invoiceId("invoice999") + .customerId("customer999") + .amount(new BigDecimal("100.00")) + .scheduledDate(LocalDate.now().plusDays(30)) + .build(); + + ScheduledPayment saved = scheduledPaymentRepository.save(newPayment); + + assertThat(saved.getStatus()).isEqualTo("SCHEDULED"); + } + + @Test + void testMultiplePaymentsForSameInvoice() { + ScheduledPayment payment1 = ScheduledPayment.builder() + .invoiceId("invoice123") + .customerId("customer123") + .amount(new BigDecimal("250.00")) + .scheduledDate(LocalDate.now().plusDays(7)) + .status("SCHEDULED") + .notes("First installment") + .build(); + + ScheduledPayment payment2 = ScheduledPayment.builder() + .invoiceId("invoice123") + .customerId("customer123") + .amount(new BigDecimal("250.00")) + .scheduledDate(LocalDate.now().plusDays(14)) + .status("SCHEDULED") + .notes("Second installment") + .build(); + + scheduledPaymentRepository.save(payment1); + scheduledPaymentRepository.save(payment2); + + List invoicePayments = scheduledPaymentRepository.findByInvoiceId("invoice123"); + + assertThat(invoicePayments).hasSize(2); + assertThat(invoicePayments.stream() + .map(ScheduledPayment::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add)) + .isEqualByComparingTo(new BigDecimal("500.00")); + } +} diff --git a/payment-service/src/test/java/com/techtorque/payment_service/service/BillingServiceImplTest.java b/payment-service/src/test/java/com/techtorque/payment_service/service/BillingServiceImplTest.java new file mode 100644 index 0000000..be43620 --- /dev/null +++ b/payment-service/src/test/java/com/techtorque/payment_service/service/BillingServiceImplTest.java @@ -0,0 +1,440 @@ +package com.techtorque.payment_service.service; + +import com.techtorque.payment_service.config.PayHereConfig; +import com.techtorque.payment_service.dto.request.CreateInvoiceDto; +import com.techtorque.payment_service.dto.request.PaymentRequestDto; +import com.techtorque.payment_service.dto.request.SchedulePaymentDto; +import com.techtorque.payment_service.dto.response.InvoiceResponseDto; +import com.techtorque.payment_service.dto.response.PaymentResponseDto; +import com.techtorque.payment_service.dto.response.ScheduledPaymentResponseDto; +import com.techtorque.payment_service.entity.*; +import com.techtorque.payment_service.exception.InvoiceNotFoundException; +import com.techtorque.payment_service.exception.InvalidPaymentException; +import com.techtorque.payment_service.exception.PaymentNotFoundException; +import com.techtorque.payment_service.exception.UnauthorizedAccessException; +import com.techtorque.payment_service.repository.InvoiceItemRepository; +import com.techtorque.payment_service.repository.InvoiceRepository; +import com.techtorque.payment_service.repository.PaymentRepository; +import com.techtorque.payment_service.repository.ScheduledPaymentRepository; +import com.techtorque.payment_service.service.impl.BillingServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.reactive.function.client.WebClient; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class BillingServiceImplTest { + + @Mock + private InvoiceRepository invoiceRepository; + + @Mock + private InvoiceItemRepository invoiceItemRepository; + + @Mock + private PaymentRepository paymentRepository; + + @Mock + private ScheduledPaymentRepository scheduledPaymentRepository; + + @Mock + private WebClient.Builder webClientBuilder; + + @Mock + private PayHereConfig payHereConfig; + + @InjectMocks + private BillingServiceImpl billingService; + + private Invoice testInvoice; + private Payment testPayment; + private ScheduledPayment testScheduledPayment; + + @BeforeEach + void setUp() { + testInvoice = Invoice.builder() + .id("invoice123") + .customerId("customer123") + .serviceOrProjectId("service456") + .amount(new BigDecimal("1000.00")) + .status(InvoiceStatus.SENT) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(30)) + .requiresDeposit(false) + .items(new ArrayList<>()) + .build(); + + testPayment = Payment.builder() + .id("payment123") + .invoiceId("invoice123") + .customerId("customer123") + .amount(new BigDecimal("1000.00")) + .method(PaymentMethod.CARD) + .status(PaymentStatus.SUCCESS) + .build(); + + testScheduledPayment = ScheduledPayment.builder() + .id("scheduled123") + .invoiceId("invoice123") + .customerId("customer123") + .amount(new BigDecimal("500.00")) + .scheduledDate(LocalDate.now().plusDays(7)) + .status("SCHEDULED") + .build(); + } + + @Test + void testCreateInvoice_Success() { + CreateInvoiceDto dto = new CreateInvoiceDto(); + dto.setCustomerId("customer123"); + dto.setServiceOrProjectId("service456"); + dto.setDueDate(LocalDate.now().plusDays(30)); + dto.setNotes("Test invoice"); + dto.setRequiresDeposit(false); + + CreateInvoiceDto.InvoiceItemRequest itemRequest = new CreateInvoiceDto.InvoiceItemRequest(); + itemRequest.setDescription("Labor"); + itemRequest.setQuantity(10); + itemRequest.setUnitPrice(new BigDecimal("100.00")); + itemRequest.setItemType("LABOR"); + + dto.setItems(Arrays.asList(itemRequest)); + + when(invoiceRepository.save(any(Invoice.class))).thenReturn(testInvoice); + when(invoiceItemRepository.save(any(InvoiceItem.class))).thenReturn(new InvoiceItem()); + when(invoiceItemRepository.findByInvoiceId(anyString())).thenReturn(new ArrayList<>()); + when(paymentRepository.findByInvoiceId(anyString())).thenReturn(new ArrayList<>()); + + InvoiceResponseDto response = billingService.createInvoice(dto); + + assertThat(response).isNotNull(); + assertThat(response.getInvoiceId()).isEqualTo("invoice123"); + verify(invoiceRepository, times(1)).save(any(Invoice.class)); + verify(invoiceItemRepository, times(1)).save(any(InvoiceItem.class)); + } + + @Test + void testCreateInvoice_WithDepositRequirement() { + CreateInvoiceDto dto = new CreateInvoiceDto(); + dto.setCustomerId("customer123"); + dto.setServiceOrProjectId("service456"); + dto.setDueDate(LocalDate.now().plusDays(30)); + dto.setRequiresDeposit(true); + + CreateInvoiceDto.InvoiceItemRequest itemRequest = new CreateInvoiceDto.InvoiceItemRequest(); + itemRequest.setDescription("Labor"); + itemRequest.setQuantity(10); + itemRequest.setUnitPrice(new BigDecimal("100.00")); + itemRequest.setItemType("LABOR"); + + dto.setItems(Arrays.asList(itemRequest)); + + Invoice depositInvoice = Invoice.builder() + .id("invoice123") + .customerId("customer123") + .serviceOrProjectId("service456") + .amount(new BigDecimal("1000.00")) + .status(InvoiceStatus.DRAFT) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(30)) + .requiresDeposit(true) + .depositAmount(new BigDecimal("500.00")) + .finalAmount(new BigDecimal("500.00")) + .items(new ArrayList<>()) + .build(); + + when(invoiceRepository.save(any(Invoice.class))).thenReturn(depositInvoice); + when(invoiceItemRepository.save(any(InvoiceItem.class))).thenReturn(new InvoiceItem()); + when(invoiceItemRepository.findByInvoiceId(anyString())).thenReturn(new ArrayList<>()); + when(paymentRepository.findByInvoiceId(anyString())).thenReturn(new ArrayList<>()); + + InvoiceResponseDto response = billingService.createInvoice(dto); + + assertThat(response).isNotNull(); + assertThat(response.getRequiresDeposit()).isTrue(); + verify(invoiceRepository, times(1)).save(any(Invoice.class)); + } + + @Test + void testGetInvoiceById_Success() { + when(invoiceRepository.findById("invoice123")).thenReturn(Optional.of(testInvoice)); + when(invoiceItemRepository.findByInvoiceId(anyString())).thenReturn(new ArrayList<>()); + when(paymentRepository.findByInvoiceId(anyString())).thenReturn(new ArrayList<>()); + + InvoiceResponseDto response = billingService.getInvoiceById("invoice123", "customer123"); + + assertThat(response).isNotNull(); + assertThat(response.getInvoiceId()).isEqualTo("invoice123"); + verify(invoiceRepository, times(1)).findById("invoice123"); + } + + @Test + void testGetInvoiceById_NotFound() { + when(invoiceRepository.findById("nonexistent")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> billingService.getInvoiceById("nonexistent", "customer123")) + .isInstanceOf(InvoiceNotFoundException.class) + .hasMessageContaining("Invoice not found"); + } + + @Test + void testGetInvoiceById_UnauthorizedAccess() { + when(invoiceRepository.findById("invoice123")).thenReturn(Optional.of(testInvoice)); + + assertThatThrownBy(() -> billingService.getInvoiceById("invoice123", "wrongCustomer")) + .isInstanceOf(UnauthorizedAccessException.class) + .hasMessageContaining("does not have access"); + } + + @Test + void testListInvoicesForCustomer() { + when(invoiceRepository.findByCustomerIdOrderByCreatedAtDesc("customer123")) + .thenReturn(Arrays.asList(testInvoice)); + when(invoiceItemRepository.findByInvoiceId(anyString())).thenReturn(new ArrayList<>()); + when(paymentRepository.findByInvoiceId(anyString())).thenReturn(new ArrayList<>()); + + List responses = billingService.listInvoicesForCustomer("customer123"); + + assertThat(responses).hasSize(1); + assertThat(responses.get(0).getCustomerId()).isEqualTo("customer123"); + verify(invoiceRepository, times(1)).findByCustomerIdOrderByCreatedAtDesc("customer123"); + } + + @Test + void testListAllInvoices() { + when(invoiceRepository.findAll()).thenReturn(Arrays.asList(testInvoice)); + when(invoiceItemRepository.findByInvoiceId(anyString())).thenReturn(new ArrayList<>()); + when(paymentRepository.findByInvoiceId(anyString())).thenReturn(new ArrayList<>()); + + List responses = billingService.listAllInvoices(); + + assertThat(responses).hasSize(1); + verify(invoiceRepository, times(1)).findAll(); + } + + @Test + void testSendInvoice_Success() { + Invoice draftInvoice = Invoice.builder() + .id("invoice123") + .customerId("customer123") + .serviceOrProjectId("service456") + .amount(new BigDecimal("1000.00")) + .status(InvoiceStatus.DRAFT) + .issueDate(LocalDate.now()) + .dueDate(LocalDate.now().plusDays(30)) + .requiresDeposit(false) + .items(new ArrayList<>()) + .build(); + + when(invoiceRepository.findById("invoice123")).thenReturn(Optional.of(draftInvoice)); + when(invoiceRepository.save(any(Invoice.class))).thenReturn(draftInvoice); + + billingService.sendInvoice("invoice123", "customer@example.com"); + + verify(invoiceRepository, times(1)).save(any(Invoice.class)); + } + + @Test + void testProcessPayment_Success() { + PaymentRequestDto dto = new PaymentRequestDto(); + dto.setInvoiceId("invoice123"); + dto.setAmount(new BigDecimal("1000.00")); + dto.setMethod(PaymentMethod.CASH); + + when(invoiceRepository.findById("invoice123")).thenReturn(Optional.of(testInvoice)); + when(paymentRepository.save(any(Payment.class))).thenReturn(testPayment); + when(paymentRepository.findByInvoiceId(anyString())).thenReturn(Arrays.asList(testPayment)); + when(invoiceRepository.save(any(Invoice.class))).thenReturn(testInvoice); + + PaymentResponseDto response = billingService.processPayment(dto, "customer123"); + + assertThat(response).isNotNull(); + verify(paymentRepository, times(1)).save(any(Payment.class)); + verify(invoiceRepository, times(1)).save(any(Invoice.class)); + } + + @Test + void testProcessPayment_InvoiceNotFound() { + PaymentRequestDto dto = new PaymentRequestDto(); + dto.setInvoiceId("nonexistent"); + dto.setAmount(new BigDecimal("1000.00")); + dto.setMethod(PaymentMethod.CASH); + + when(invoiceRepository.findById("nonexistent")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> billingService.processPayment(dto, "customer123")) + .isInstanceOf(InvoiceNotFoundException.class) + .hasMessageContaining("Invoice not found"); + } + + @Test + void testProcessPayment_UnauthorizedAccess() { + PaymentRequestDto dto = new PaymentRequestDto(); + dto.setInvoiceId("invoice123"); + dto.setAmount(new BigDecimal("1000.00")); + dto.setMethod(PaymentMethod.CASH); + + when(invoiceRepository.findById("invoice123")).thenReturn(Optional.of(testInvoice)); + + assertThatThrownBy(() -> billingService.processPayment(dto, "wrongCustomer")) + .isInstanceOf(UnauthorizedAccessException.class) + .hasMessageContaining("does not have access"); + } + + @Test + void testProcessPayment_InvalidAmount_TooHigh() { + PaymentRequestDto dto = new PaymentRequestDto(); + dto.setInvoiceId("invoice123"); + dto.setAmount(new BigDecimal("2000.00")); + dto.setMethod(PaymentMethod.CASH); + + when(invoiceRepository.findById("invoice123")).thenReturn(Optional.of(testInvoice)); + + assertThatThrownBy(() -> billingService.processPayment(dto, "customer123")) + .isInstanceOf(InvalidPaymentException.class) + .hasMessageContaining("exceeds invoice amount"); + } + + @Test + void testProcessPayment_InvalidAmount_Negative() { + PaymentRequestDto dto = new PaymentRequestDto(); + dto.setInvoiceId("invoice123"); + dto.setAmount(new BigDecimal("-100.00")); + dto.setMethod(PaymentMethod.CASH); + + when(invoiceRepository.findById("invoice123")).thenReturn(Optional.of(testInvoice)); + + assertThatThrownBy(() -> billingService.processPayment(dto, "customer123")) + .isInstanceOf(InvalidPaymentException.class) + .hasMessageContaining("must be positive"); + } + + @Test + void testGetPaymentHistoryForCustomer() { + when(paymentRepository.findByCustomerIdOrderByCreatedAtDesc("customer123")) + .thenReturn(Arrays.asList(testPayment)); + + List responses = billingService.getPaymentHistoryForCustomer("customer123"); + + assertThat(responses).hasSize(1); + assertThat(responses.get(0).getPaymentId()).isEqualTo("payment123"); + verify(paymentRepository, times(1)).findByCustomerIdOrderByCreatedAtDesc("customer123"); + } + + @Test + void testGetPaymentDetails_Success() { + when(paymentRepository.findById("payment123")).thenReturn(Optional.of(testPayment)); + + PaymentResponseDto response = billingService.getPaymentDetails("payment123", "customer123"); + + assertThat(response).isNotNull(); + assertThat(response.getPaymentId()).isEqualTo("payment123"); + verify(paymentRepository, times(1)).findById("payment123"); + } + + @Test + void testGetPaymentDetails_NotFound() { + when(paymentRepository.findById("nonexistent")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> billingService.getPaymentDetails("nonexistent", "customer123")) + .isInstanceOf(PaymentNotFoundException.class) + .hasMessageContaining("Payment not found"); + } + + @Test + void testGetPaymentDetails_UnauthorizedAccess() { + when(paymentRepository.findById("payment123")).thenReturn(Optional.of(testPayment)); + + assertThatThrownBy(() -> billingService.getPaymentDetails("payment123", "wrongCustomer")) + .isInstanceOf(UnauthorizedAccessException.class) + .hasMessageContaining("does not have access"); + } + + @Test + void testSchedulePayment_Success() { + SchedulePaymentDto dto = new SchedulePaymentDto(); + dto.setInvoiceId("invoice123"); + dto.setAmount(new BigDecimal("500.00")); + dto.setScheduledDate(LocalDate.now().plusDays(7)); + dto.setNotes("Scheduled payment"); + + when(invoiceRepository.findById("invoice123")).thenReturn(Optional.of(testInvoice)); + when(scheduledPaymentRepository.save(any(ScheduledPayment.class))).thenReturn(testScheduledPayment); + + ScheduledPaymentResponseDto response = billingService.schedulePayment(dto, "customer123"); + + assertThat(response).isNotNull(); + assertThat(response.getScheduleId()).isEqualTo("scheduled123"); + verify(scheduledPaymentRepository, times(1)).save(any(ScheduledPayment.class)); + } + + @Test + void testSchedulePayment_InvoiceNotFound() { + SchedulePaymentDto dto = new SchedulePaymentDto(); + dto.setInvoiceId("nonexistent"); + dto.setAmount(new BigDecimal("500.00")); + dto.setScheduledDate(LocalDate.now().plusDays(7)); + + when(invoiceRepository.findById("nonexistent")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> billingService.schedulePayment(dto, "customer123")) + .isInstanceOf(InvoiceNotFoundException.class) + .hasMessageContaining("Invoice not found"); + } + + @Test + void testSchedulePayment_UnauthorizedAccess() { + SchedulePaymentDto dto = new SchedulePaymentDto(); + dto.setInvoiceId("invoice123"); + dto.setAmount(new BigDecimal("500.00")); + dto.setScheduledDate(LocalDate.now().plusDays(7)); + + when(invoiceRepository.findById("invoice123")).thenReturn(Optional.of(testInvoice)); + + assertThatThrownBy(() -> billingService.schedulePayment(dto, "wrongCustomer")) + .isInstanceOf(UnauthorizedAccessException.class) + .hasMessageContaining("does not have access"); + } + + @Test + void testSchedulePayment_InvalidAmount() { + SchedulePaymentDto dto = new SchedulePaymentDto(); + dto.setInvoiceId("invoice123"); + dto.setAmount(new BigDecimal("2000.00")); + dto.setScheduledDate(LocalDate.now().plusDays(7)); + + when(invoiceRepository.findById("invoice123")).thenReturn(Optional.of(testInvoice)); + + assertThatThrownBy(() -> billingService.schedulePayment(dto, "customer123")) + .isInstanceOf(InvalidPaymentException.class) + .hasMessageContaining("exceeds invoice amount"); + } + + @Test + void testGetScheduledPaymentsForCustomer() { + when(scheduledPaymentRepository.findByCustomerId("customer123")) + .thenReturn(Arrays.asList(testScheduledPayment)); + + List responses = billingService.getScheduledPaymentsForCustomer("customer123"); + + assertThat(responses).hasSize(1); + assertThat(responses.get(0).getScheduleId()).isEqualTo("scheduled123"); + verify(scheduledPaymentRepository, times(1)).findByCustomerId("customer123"); + } +} diff --git a/payment-service/src/test/resources/application-test.properties b/payment-service/src/test/resources/application-test.properties index 07b0e47..8d484ac 100644 --- a/payment-service/src/test/resources/application-test.properties +++ b/payment-service/src/test/resources/application-test.properties @@ -1,3 +1,6 @@ +# Test Configuration +spring.application.name=payment-service-test + # H2 Test Database Configuration spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH spring.datasource.driverClassName=org.h2.Driver @@ -10,7 +13,18 @@ spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=false spring.jpa.properties.hibernate.format_sql=true +# Disable Actuator for Tests +management.endpoints.enabled-by-default=false + # Logging logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE -logging.level.com.techtorque.payment_service=DEBUG +logging.level.com.techtorque.payment_service=INFO + +# PayHere Test Configuration +payhere.merchant-id=test-merchant-id +payhere.merchant-secret=test-merchant-secret +payhere.return-url=http://localhost:8080/payment/success +payhere.cancel-url=http://localhost:8080/payment/cancel +payhere.notify-url=http://localhost:8080/payment/notify +payhere.sandbox=true