added batch file upload feature

This commit is contained in:
Prince
2026-06-20 02:33:37 +02:00
parent dcb917e899
commit 4d0e18c1a6
26 changed files with 2263 additions and 104 deletions

View File

@@ -0,0 +1,167 @@
package zw.qantra.tm.domain.services;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import zw.qantra.tm.domain.enums.CurrencyType;
import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.models.AdditionalData;
import zw.qantra.tm.domain.models.Currency;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.repositories.AdditionalDataRepository;
import zw.qantra.tm.domain.repositories.TransactionRepository;
import zw.qantra.tm.exceptions.ApiException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class TransactionServiceTest {
@Mock
private TransactionRepository transactionRepository;
@Mock
private AdditionalDataService additionalDataService;
@Mock
private CurrencyService currencyService;
@Mock
private AdditionalDataRepository additionalDataRepository;
@InjectMocks
private TransactionService transactionService;
private Transaction transaction;
private final UUID txId = UUID.randomUUID();
@BeforeEach
void setUp() {
transaction = new Transaction();
transaction.setId(txId);
transaction.setUserId("user-1");
transaction.setAmount(new BigDecimal("100.00"));
transaction.setDebitCurrency(CurrencyType.USD);
transaction.setCreditCurrency(CurrencyType.ZWL);
lenient().when(additionalDataService.getAdditionalDataRepository()).thenReturn(additionalDataRepository);
}
@Test
@DisplayName("Should find transaction by ID")
void shouldFindById() {
when(transactionRepository.findById(txId)).thenReturn(Optional.of(transaction));
AdditionalData additionalData = new AdditionalData();
additionalData.setJsonString("[{\"key\":\"value\"}]");
when(additionalDataRepository.findByTransactionIdAndRequestType(
txId.toString(), RequestType.INTEGRATION))
.thenReturn(Optional.of(additionalData));
Transaction result = transactionService.findById(txId);
assertNotNull(result);
assertEquals(txId, result.getId());
assertNotNull(result.getAdditionalData());
}
@Test
@DisplayName("Should throw exception when transaction not found by ID")
void shouldThrowWhenNotFound() {
when(transactionRepository.findById(txId)).thenReturn(Optional.empty());
assertThrows(ApiException.class, () -> transactionService.findById(txId));
}
@Test
@DisplayName("Should find transaction without additional data gracefully")
void shouldFindByIdWithoutAdditionalData() {
when(transactionRepository.findById(txId)).thenReturn(Optional.of(transaction));
when(additionalDataRepository.findByTransactionIdAndRequestType(
txId.toString(), RequestType.INTEGRATION))
.thenThrow(new ApiException("Not found"));
Transaction result = transactionService.findById(txId);
assertNotNull(result);
assertNull(result.getAdditionalData());
}
@Test
@DisplayName("Should return paginated transactions")
void shouldFindAllPaginated() {
Specification<Transaction> spec = mock(Specification.class);
Pageable pageable = PageRequest.of(0, 10);
Page<Transaction> page = new PageImpl<>(List.of(transaction));
when(transactionRepository.findAll(spec, pageable)).thenReturn(page);
Page<Transaction> result = transactionService.findAll(spec, pageable);
assertEquals(1, result.getTotalElements());
assertEquals(txId, result.getContent().get(0).getId());
}
@Test
@DisplayName("Should calculate credit amount using bank rate")
void shouldCalculateCreditAmount() {
Currency currency = new Currency();
currency.setBankRate("15.0");
when(currencyService.findCurrencyByIsoCode("ZWL")).thenReturn(currency);
BigDecimal creditAmount = transactionService.calculateCreditAmount(transaction);
assertEquals(new BigDecimal("1500.0000"), creditAmount);
assertEquals(new BigDecimal("1500.0000"), transaction.getCreditAmount());
}
@Test
@DisplayName("Should return zero credit amount when credit currency is null")
void shouldReturnZeroWhenCreditCurrencyNull() {
transaction.setCreditCurrency(null);
BigDecimal result = transactionService.calculateCreditAmount(transaction);
assertEquals(BigDecimal.ZERO, result);
}
@Test
@DisplayName("Should reassign transactions from old user to new user")
void shouldReassignTransactions() {
List<Transaction> transactions = List.of(transaction);
when(transactionRepository.findAllByUserId("old-user")).thenReturn(transactions);
when(transactionRepository.save(any(Transaction.class))).thenReturn(transaction);
transactionService.reassignTransactions("old-user", "new-user");
assertEquals("new-user", transaction.getUserId());
verify(transactionRepository).save(transaction);
}
@Test
@DisplayName("Should save transaction")
void shouldSave() {
when(transactionRepository.save(transaction)).thenReturn(transaction);
Transaction result = transactionService.save(transaction);
assertNotNull(result);
verify(transactionRepository).save(transaction);
}
}

View File

@@ -0,0 +1,401 @@
package zw.qantra.tm.domain.services.processors.workflows.handlers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 zw.qantra.tm.domain.enums.ChargeConditionType;
import zw.qantra.tm.domain.enums.ChargeLabelEnum;
import zw.qantra.tm.domain.enums.CurrencyType;
import zw.qantra.tm.domain.models.Charge;
import zw.qantra.tm.domain.models.ChargeCondition;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.ChargeService;
import zw.qantra.tm.domain.repositories.ChargeRepository;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class CalculateChargesHandlerTest {
@Mock
private ChargeService chargeService;
@Mock
private ChargeRepository chargeRepository;
@InjectMocks
private CalculateChargesHandler handler;
private Transaction transaction;
@BeforeEach
void setUp() {
transaction = Transaction.builder()
.amount(new BigDecimal("100.00"))
.debitCurrency(CurrencyType.USD)
.paymentProcessorLabel("ecocash")
.integrationProcessorLabel("hotrecharge")
.build();
lenient().when(chargeService.getChargeRepository()).thenReturn(chargeRepository);
}
@Test
@DisplayName("Should apply flat fee charge when no percentage rate is set")
void shouldApplyFlatFeeCharge() {
Charge flatCharge = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.flat(new BigDecimal("2.50"))
.minimumAmount(null)
.maximumAmount(null)
.min(null)
.max(null)
.percentageRate(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of())
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(flatCharge));
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals(new BigDecimal("2.50"), result.getCharge());
assertEquals(BigDecimal.ZERO, result.getTax());
assertEquals(BigDecimal.ZERO, result.getGatewayCharge());
assertEquals(new BigDecimal("102.50"), result.getTotalAmount());
}
@Test
@DisplayName("Should apply percentage charge correctly")
void shouldApplyPercentageCharge() {
Charge percentageCharge = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.percentageRate(new BigDecimal("3.5"))
.flat(null)
.minimumAmount(null)
.maximumAmount(null)
.min(null)
.max(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of())
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(percentageCharge));
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals(new BigDecimal("3.50"), result.getCharge());
assertEquals(new BigDecimal("103.50"), result.getTotalAmount());
}
@Test
@DisplayName("Should apply composite charge (percentage + flat)")
void shouldApplyCompositeCharge() {
Charge compositeCharge = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.percentageRate(new BigDecimal("2.0"))
.flat(new BigDecimal("1.00"))
.minimumAmount(null)
.maximumAmount(null)
.min(null)
.max(null)
.composite(true)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of())
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(compositeCharge));
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals(new BigDecimal("3.00"), result.getCharge());
assertEquals(new BigDecimal("103.00"), result.getTotalAmount());
}
@Test
@DisplayName("Should clamp charge to minimum when calculated below min")
void shouldClampToMinimumCharge() {
Charge minCharge = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.percentageRate(new BigDecimal("0.5"))
.flat(null)
.minimumAmount(null)
.maximumAmount(null)
.min(new BigDecimal("5.00"))
.max(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of())
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(minCharge));
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals(new BigDecimal("5.00"), result.getCharge());
assertEquals(new BigDecimal("105.00"), result.getTotalAmount());
}
@Test
@DisplayName("Should clamp charge to maximum when calculated above max")
void shouldClampToMaximumCharge() {
Charge maxCharge = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.percentageRate(new BigDecimal("10.0"))
.flat(null)
.minimumAmount(null)
.maximumAmount(null)
.min(null)
.max(new BigDecimal("8.00"))
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of())
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(maxCharge));
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals(new BigDecimal("8.00"), result.getCharge());
assertEquals(new BigDecimal("108.00"), result.getTotalAmount());
}
@Test
@DisplayName("Should use minimum amount threshold to trigger min charge")
void shouldUseMinimumAmountThreshold() {
Charge chargeWithMinAmount = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.flat(new BigDecimal("10.00"))
.minimumAmount(new BigDecimal("50.00"))
.maximumAmount(null)
.min(new BigDecimal("2.00"))
.max(null)
.percentageRate(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of())
.build();
Transaction smallTx = Transaction.builder()
.amount(new BigDecimal("30.00"))
.debitCurrency(CurrencyType.USD)
.paymentProcessorLabel("ecocash")
.integrationProcessorLabel("hotrecharge")
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(chargeWithMinAmount));
Transaction result = handler.process(smallTx);
assertNotNull(result);
assertEquals(new BigDecimal("2.00"), result.getCharge());
}
@Test
@DisplayName("Should use maximum amount threshold to trigger max charge")
void shouldUseMaximumAmountThreshold() {
Charge chargeWithMaxAmount = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.flat(new BigDecimal("10.00"))
.minimumAmount(null)
.maximumAmount(new BigDecimal("200.00"))
.min(null)
.max(new BigDecimal("15.00"))
.percentageRate(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of())
.build();
Transaction largeTx = Transaction.builder()
.amount(new BigDecimal("500.00"))
.debitCurrency(CurrencyType.USD)
.paymentProcessorLabel("ecocash")
.integrationProcessorLabel("hotrecharge")
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(chargeWithMaxAmount));
Transaction result = handler.process(largeTx);
assertNotNull(result);
assertEquals(new BigDecimal("15.00"), result.getCharge());
}
@Test
@DisplayName("Should skip charge when gateway does not match includes condition")
void shouldSkipChargeWhenGatewayDoesNotMatch() {
Charge charge = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.flat(new BigDecimal("5.00"))
.minimumAmount(null)
.maximumAmount(null)
.min(null)
.max(null)
.percentageRate(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "mpgs")))
.excludes(Set.of())
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(charge));
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals(BigDecimal.ZERO, result.getCharge());
assertEquals(new BigDecimal("100.00"), result.getTotalAmount());
}
@Test
@DisplayName("Should skip charge when gateway is in excludes condition")
void shouldSkipChargeWhenGatewayIsExcluded() {
Charge charge = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.flat(new BigDecimal("5.00"))
.minimumAmount(null)
.maximumAmount(null)
.min(null)
.max(null)
.percentageRate(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(charge));
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals(BigDecimal.ZERO, result.getCharge());
assertEquals(new BigDecimal("100.00"), result.getTotalAmount());
}
@Test
@DisplayName("Should apply TAX and GATEWAY_FEE charges separately")
void shouldApplyMultipleChargeTypes() {
Charge fee = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.flat(new BigDecimal("2.00"))
.minimumAmount(null)
.maximumAmount(null)
.min(null)
.max(null)
.percentageRate(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of())
.build();
Charge tax = Charge.builder()
.chargeLabel(ChargeLabelEnum.TAX)
.flat(new BigDecimal("1.50"))
.minimumAmount(null)
.maximumAmount(null)
.min(null)
.max(null)
.percentageRate(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of())
.build();
Charge gatewayFee = Charge.builder()
.chargeLabel(ChargeLabelEnum.GATEWAY_FEE)
.flat(new BigDecimal("3.00"))
.minimumAmount(null)
.maximumAmount(null)
.min(null)
.max(null)
.percentageRate(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.GATEWAY, "ecocash")))
.excludes(Set.of())
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(fee, tax, gatewayFee));
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals(new BigDecimal("2.00"), result.getCharge());
assertEquals(new BigDecimal("1.50"), result.getTax());
assertEquals(new BigDecimal("3.00"), result.getGatewayCharge());
assertEquals(new BigDecimal("106.50"), result.getTotalAmount());
}
@Test
@DisplayName("Should handle empty charge list gracefully")
void shouldHandleEmptyChargeList() {
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of());
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals(BigDecimal.ZERO, result.getCharge());
assertEquals(BigDecimal.ZERO, result.getTax());
assertEquals(BigDecimal.ZERO, result.getGatewayCharge());
assertEquals(new BigDecimal("100.00"), result.getTotalAmount());
}
@Test
@DisplayName("Should match charge by provider condition")
void shouldMatchChargeByProviderCondition() {
Charge providerCharge = Charge.builder()
.chargeLabel(ChargeLabelEnum.FEE)
.flat(new BigDecimal("4.00"))
.minimumAmount(null)
.maximumAmount(null)
.min(null)
.max(null)
.percentageRate(null)
.composite(false)
.currency(CurrencyType.USD)
.includes(Set.of(createCondition(ChargeConditionType.PROVIDER, "hotrecharge")))
.excludes(Set.of())
.build();
when(chargeRepository.findAllByCurrency(CurrencyType.USD)).thenReturn(List.of(providerCharge));
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals(new BigDecimal("4.00"), result.getCharge());
}
private ChargeCondition createCondition(ChargeConditionType type, String code) {
ChargeCondition condition = new ChargeCondition();
condition.setType(type);
condition.setCodes(code);
return condition;
}
}

View File

@@ -0,0 +1,185 @@
package zw.qantra.tm.domain.services.processors.workflows.handlers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 zw.qantra.tm.domain.enums.PartyType;
import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Provider;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.ProviderService;
import zw.qantra.tm.domain.repositories.ProviderRepository;
import zw.qantra.tm.exceptions.ApiException;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class CleanupHandlerTest {
@Mock
private ProviderService providerService;
@Mock
private ProviderRepository providerRepository;
@InjectMocks
private CleanupHandler handler;
private Transaction transaction;
@BeforeEach
void setUp() {
transaction = new Transaction();
transaction.setType(RequestType.CONFIRM);
transaction.setBillClientId("client-123");
transaction.setDebitPhone("+263771234567");
transaction.setCreditPhone("+263778901234");
transaction.setCreditAccount("ACC-123");
transaction.setRegion("ZW");
lenient().when(providerService.getProviderRepository()).thenReturn(providerRepository);
}
@Test
@DisplayName("Should set trace, reference, and initial statuses")
void shouldSetInitialFields() throws Exception {
Provider provider = new Provider();
provider.setLabel("econet");
provider.setIntegrationProcessorLabel("hotrecharge");
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result.getTrace());
assertNotNull(result.getReference());
assertEquals(Status.PENDING, result.getConfirmationStatus());
assertEquals(Status.PENDING, result.getPaymentStatus());
assertEquals(Status.PENDING, result.getIntegrationStatus());
}
@Test
@DisplayName("Should set default party type to INDIVIDUAL when null")
void shouldSetDefaultPartyType() throws Exception {
transaction.setPartyType(null);
Provider provider = new Provider();
provider.setLabel("econet");
provider.setIntegrationProcessorLabel("hotrecharge");
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
Transaction result = (Transaction) handler.process(transaction);
assertEquals(PartyType.INDIVIDUAL, result.getPartyType());
}
@Test
@DisplayName("Should preserve existing party type when set")
void shouldPreserveExistingPartyType() throws Exception {
transaction.setPartyType(PartyType.GROUP);
Provider provider = new Provider();
provider.setLabel("econet");
provider.setIntegrationProcessorLabel("hotrecharge");
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
Transaction result = (Transaction) handler.process(transaction);
assertEquals(PartyType.GROUP, result.getPartyType());
}
@Test
@DisplayName("Should set confirmation processor label for CONFIRM type")
void shouldSetConfirmationProcessorLabel() throws Exception {
Provider provider = new Provider();
provider.setLabel("econet");
provider.setIntegrationProcessorLabel("hotrecharge");
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
Transaction result = (Transaction) handler.process(transaction);
assertEquals("CONFIRM_econet_hotrecharge", result.getConfirmationProcessorLabel());
assertEquals("econet", result.getProviderLabel());
}
@Test
@DisplayName("Should throw exception when provider not found for billClientId")
void shouldThrowWhenProviderNotFound() {
when(providerRepository.findByClientId("client-123")).thenReturn(null);
assertThrows(ApiException.class, () -> handler.process(transaction));
}
@Test
@DisplayName("Should strip non-hex characters from credit account before phone formatting")
void shouldStripCreditAccount() throws Exception {
transaction.setCreditAccount("ACC-123!@#");
// Set creditPhone to null so it doesn't overwrite creditAccount
transaction.setCreditPhone(null);
Provider provider = new Provider();
provider.setLabel("econet");
provider.setIntegrationProcessorLabel("hotrecharge");
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
Transaction result = (Transaction) handler.process(transaction);
assertEquals("ACC123", result.getCreditAccount());
}
@Test
@DisplayName("Should set credit account to null when stripped result is empty and no credit phone")
void shouldSetCreditAccountNullWhenEmpty() throws Exception {
transaction.setCreditAccount("!@#$%");
transaction.setCreditPhone(null);
Provider provider = new Provider();
provider.setLabel("econet");
provider.setIntegrationProcessorLabel("hotrecharge");
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
Transaction result = (Transaction) handler.process(transaction);
assertNull(result.getCreditAccount());
}
@Test
@DisplayName("Should format debit phone number")
void shouldFormatDebitPhone() throws Exception {
transaction.setDebitPhone("0771234567");
Provider provider = new Provider();
provider.setLabel("econet");
provider.setIntegrationProcessorLabel("hotrecharge");
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result.getDebitPhone());
}
@Test
@DisplayName("Should handle null debit phone gracefully")
void shouldHandleNullDebitPhone() throws Exception {
transaction.setDebitPhone(null);
Provider provider = new Provider();
provider.setLabel("econet");
provider.setIntegrationProcessorLabel("hotrecharge");
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
Transaction result = (Transaction) handler.process(transaction);
assertNull(result.getDebitPhone());
}
@Test
@DisplayName("Should not set confirmation label for non-CONFIRM types")
void shouldSkipConfirmationLabelForNonConfirm() throws Exception {
transaction.setType(RequestType.REQUEST);
Transaction result = (Transaction) handler.process(transaction);
assertNull(result.getConfirmationProcessorLabel());
assertNull(result.getProviderLabel());
}
}

View File

@@ -0,0 +1,135 @@
package zw.qantra.tm.domain.services.processors.workflows.handlers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.models.TransactionEvent;
import zw.qantra.tm.domain.services.*;
import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
import java.math.BigDecimal;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ConfirmHandlerTest {
@Mock
private TransactionEventService transactionEventService;
@Mock
private PaymentProcessorFactory paymentProcessorFactory;
@Mock
private TransactionService transactionService;
@Mock
private AdditionalDataService additionalDataService;
@Mock
private TransactionProcessorInterface transactionProcessor;
@InjectMocks
private ConfirmHandler handler;
private Transaction transaction;
@BeforeEach
void setUp() {
transaction = new Transaction();
transaction.setId(UUID.randomUUID());
transaction.setUserId("user-1");
transaction.setType(RequestType.CONFIRM);
transaction.setConfirmationProcessorLabel("CONFIRM_econet_hotrecharge");
transaction.setAmount(new BigDecimal("100.00"));
}
@Test
@DisplayName("Should process confirmation successfully")
void shouldProcessConfirmationSuccessfully() throws Exception {
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("CONFIRM_econet_hotrecharge"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result);
assertEquals(Status.SUCCESS, result.getConfirmationStatus());
verify(transactionService, times(2)).save(any(Transaction.class));
verify(transactionEventService, times(2)).save(any(TransactionEvent.class));
}
@Test
@DisplayName("Should set failed status when processor throws exception")
void shouldSetFailedStatusOnProcessorError() throws Exception {
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("CONFIRM_econet_hotrecharge"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class)))
.thenThrow(new RuntimeException("Provider unavailable"));
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result);
assertEquals(Status.FAILED, result.getStatus());
assertEquals(Status.FAILED, result.getConfirmationStatus());
assertEquals(Status.FAILED, result.getPaymentStatus());
assertEquals(Status.FAILED, result.getIntegrationStatus());
assertEquals("01", result.getResponseCode());
assertEquals("Provider unavailable", result.getErrorMessage());
}
@Test
@DisplayName("Should create transaction event with PROCESSING status initially")
void shouldCreateProcessingEvent() throws Exception {
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("CONFIRM_econet_hotrecharge"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
handler.process(transaction);
verify(transactionEventService, times(2)).save(any(TransactionEvent.class));
}
@Test
@DisplayName("Should save transaction before and after processing")
void shouldSaveTransactionBeforeAndAfter() throws Exception {
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("CONFIRM_econet_hotrecharge"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
handler.process(transaction);
verify(transactionService, times(2)).save(any(Transaction.class));
}
}

View File

@@ -0,0 +1,181 @@
package zw.qantra.tm.domain.services.processors.workflows.handlers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 zw.qantra.tm.domain.enums.AuthType;
import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.models.TransactionEvent;
import zw.qantra.tm.domain.repositories.TransactionRepository;
import zw.qantra.tm.domain.services.*;
import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
import zw.qantra.tm.exceptions.ApiException;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GatewayPaymentHandlerTest {
@Mock
private PaymentProcessorFactory paymentProcessorFactory;
@Mock
private ChargeService chargeService;
@Mock
private RecipientService recipientService;
@Mock
private TransactionEventService transactionEventService;
@Mock
private TransactionService transactionService;
@Mock
private TransactionRepository transactionRepository;
@Mock
private TransactionProcessorInterface transactionProcessor;
@InjectMocks
private GatewayPaymentHandler handler;
private Transaction transaction;
private final UUID txId = UUID.randomUUID();
@BeforeEach
void setUp() {
transaction = new Transaction();
transaction.setId(txId);
transaction.setUserId("user-1");
transaction.setType(RequestType.REQUEST);
transaction.setAuthType(AuthType.MOBILE);
transaction.setPaymentProcessorLabel("ecocash");
transaction.setPaymentStatus(null);
lenient().when(transactionService.getTransactionRepository()).thenReturn(transactionRepository);
}
@Test
@DisplayName("Should initiate gateway payment successfully")
void shouldInitiateGatewayPaymentSuccessfully() throws Exception {
when(transactionRepository.findById(txId)).thenReturn(Optional.of(transaction));
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("REQUEST_ecocash_MOBILE"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result);
assertEquals(Status.SUCCESS, result.getPaymentStatus());
assertEquals(Status.PENDING, result.getPollingStatus());
}
@Test
@DisplayName("Should throw exception when transaction is null")
void shouldThrowWhenTransactionIsNull() {
assertThrows(ApiException.class, () -> handler.process(null));
}
@Test
@DisplayName("Should fetch transaction from DB when ID is present")
void shouldFetchTransactionFromDb() throws Exception {
when(transactionRepository.findById(txId)).thenReturn(Optional.of(transaction));
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("REQUEST_ecocash_MOBILE"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
handler.process(transaction);
verify(transactionRepository).findById(txId);
}
@Test
@DisplayName("Should return existing transaction when payment already successful")
void shouldReturnExistingWhenAlreadySuccess() throws Exception {
transaction.setPaymentStatus(Status.SUCCESS);
when(transactionRepository.findById(txId)).thenReturn(Optional.of(transaction));
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result);
assertEquals(Status.SUCCESS, result.getPaymentStatus());
verify(transactionRepository).findById(txId);
verify(transactionProcessor, never()).process(any());
}
@Test
@DisplayName("Should set failed status when processor throws exception")
void shouldSetFailedStatusOnProcessorError() throws Exception {
when(transactionRepository.findById(txId)).thenReturn(Optional.of(transaction));
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("REQUEST_ecocash_MOBILE"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class)))
.thenThrow(new RuntimeException("Gateway timeout"));
Transaction result = (Transaction) handler.process(transaction);
assertEquals(Status.FAILED, result.getStatus());
assertEquals(Status.FAILED, result.getPaymentStatus());
assertEquals(Status.FAILED, result.getIntegrationStatus());
assertEquals("01", result.getResponseCode());
assertEquals("Gateway timeout", result.getErrorMessage());
}
@Test
@DisplayName("Should use incoming transaction type and authType")
void shouldUseIncomingTypeAndAuthType() throws Exception {
Transaction incoming = new Transaction();
incoming.setId(txId);
incoming.setType(RequestType.REQUEST);
incoming.setAuthType(AuthType.WEB);
incoming.setPaymentProcessorLabel("ecocash");
Transaction existing = new Transaction();
existing.setId(txId);
existing.setUserId("user-1");
existing.setType(RequestType.CONFIRM);
existing.setPaymentProcessorLabel("ecocash");
when(transactionRepository.findById(txId)).thenReturn(Optional.of(existing));
when(transactionService.save(any(Transaction.class))).thenReturn(existing);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("REQUEST_ecocash_WEB"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
handler.process(incoming);
verify(paymentProcessorFactory).getPaymentProcessor("REQUEST_ecocash_WEB");
}
}

View File

@@ -0,0 +1,176 @@
package zw.qantra.tm.domain.services.processors.workflows.handlers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Provider;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.models.TransactionEvent;
import zw.qantra.tm.domain.repositories.ProviderRepository;
import zw.qantra.tm.domain.services.*;
import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
import zw.qantra.tm.exceptions.ApiException;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class IntegrationHandlerTest {
@Mock
private PaymentProcessorFactory paymentProcessorFactory;
@Mock
private ProviderService providerService;
@Mock
private RecipientService recipientService;
@Mock
private TransactionEventService transactionEventService;
@Mock
private TransactionService transactionService;
@Mock
private ProviderRepository providerRepository;
@Mock
private TransactionProcessorInterface transactionProcessor;
@InjectMocks
private IntegrationHandler handler;
private Transaction transaction;
private Provider provider;
@BeforeEach
void setUp() {
transaction = new Transaction();
transaction.setId(UUID.randomUUID());
transaction.setUserId("user-1");
transaction.setBillClientId("client-123");
transaction.setPollingStatus(Status.SUCCESS);
provider = new Provider();
provider.setIntegrationProcessorLabel("hotrecharge");
lenient().when(providerService.getProviderRepository()).thenReturn(providerRepository);
}
@Test
@DisplayName("Should process integration successfully")
void shouldProcessIntegrationSuccessfully() throws Exception {
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("INTEGRATION_hotrecharge"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result);
assertEquals(Status.SUCCESS, result.getIntegrationStatus());
assertEquals(Status.SUCCESS, result.getPaymentStatus());
assertEquals("hotrecharge", result.getIntegrationProcessorLabel());
}
@Test
@DisplayName("Should return early when integration already successful")
void shouldReturnEarlyWhenAlreadySuccess() throws Exception {
transaction.setIntegrationStatus(Status.SUCCESS);
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result);
verify(transactionProcessor, never()).process(any());
}
@Test
@DisplayName("Should throw exception when polling is not complete")
void shouldThrowWhenPollingNotComplete() {
transaction.setPollingStatus(Status.PENDING);
assertThrows(ApiException.class, () -> handler.process(transaction));
}
@Test
@DisplayName("Should throw exception when provider not found")
void shouldThrowWhenProviderNotFound() {
when(providerRepository.findByClientId("client-123")).thenReturn(null);
assertThrows(ApiException.class, () -> handler.process(transaction));
}
@Test
@DisplayName("Should set failed status when integration processor throws exception")
void shouldSetFailedStatusOnProcessorError() throws Exception {
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("INTEGRATION_hotrecharge"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class)))
.thenThrow(new RuntimeException("Integration failed"));
assertThrows(ApiException.class, () -> handler.process(transaction));
assertEquals(Status.FAILED, transaction.getStatus());
assertEquals(Status.FAILED, transaction.getIntegrationStatus());
assertEquals("01", transaction.getResponseCode());
}
@Test
@DisplayName("Should set type to INTEGRATION")
void shouldSetTypeToIntegration() throws Exception {
transaction.setType(RequestType.REQUEST);
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("INTEGRATION_hotrecharge"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
handler.process(transaction);
assertEquals(RequestType.INTEGRATION, transaction.getType());
}
@Test
@DisplayName("Should handle race condition with lock mechanism")
void shouldHandleRaceCondition() throws Exception {
when(providerRepository.findByClientId("client-123")).thenReturn(provider);
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("INTEGRATION_hotrecharge"))
.thenReturn(transactionProcessor);
when(transactionProcessor.process(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
// First call should succeed
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result);
assertEquals(Status.SUCCESS, result.getIntegrationStatus());
}
}

View File

@@ -0,0 +1,158 @@
package zw.qantra.tm.domain.services.processors.workflows.handlers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 zw.qantra.tm.domain.enums.AuthType;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.models.TransactionEvent;
import zw.qantra.tm.domain.services.*;
import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
import zw.qantra.tm.exceptions.ApiException;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class PollStatusHandlerTest {
@Mock
private PaymentProcessorFactory paymentProcessorFactory;
@Mock
private TransactionEventService transactionEventService;
@Mock
private TransactionService transactionService;
@Mock
private TransactionProcessorInterface transactionProcessor;
@InjectMocks
private PollStatusHandler handler;
private Transaction transaction;
@BeforeEach
void setUp() {
transaction = new Transaction();
transaction.setId(UUID.randomUUID());
transaction.setUserId("user-1");
transaction.setPaymentProcessorLabel("ecocash");
transaction.setAuthType(AuthType.MOBILE);
}
@Test
@DisplayName("Should poll and return success when gateway returns SUCCESS")
void shouldPollSuccessfully() throws Exception {
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("REQUEST_ecocash_MOBILE"))
.thenReturn(transactionProcessor);
when(transactionProcessor.poll(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result);
assertEquals(Status.SUCCESS, result.getPollingStatus());
assertEquals(Status.SUCCESS, result.getPaymentStatus());
assertNull(result.getErrorMessage());
}
@Test
@DisplayName("Should return early when polling already successful")
void shouldReturnEarlyWhenAlreadySuccess() throws Exception {
transaction.setPollingStatus(Status.SUCCESS);
Transaction result = (Transaction) handler.process(transaction);
assertNotNull(result);
verify(transactionProcessor, never()).poll(any());
verify(transactionService, never()).save(any());
}
@Test
@DisplayName("Should throw exception when poll returns non-SUCCESS status")
void shouldThrowWhenPollReturnsNonSuccess() throws Exception {
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("REQUEST_ecocash_MOBILE"))
.thenReturn(transactionProcessor);
when(transactionProcessor.poll(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.PENDING);
tx.setErrorMessage("Still processing");
return tx;
});
ApiException exception = assertThrows(ApiException.class, () -> handler.process(transaction));
assertTrue(exception.getMessage().contains("Still processing"));
}
@Test
@DisplayName("Should save transaction when poll status is not SUCCESS")
void shouldSaveTransactionWhenNotSuccess() throws Exception {
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("REQUEST_ecocash_MOBILE"))
.thenReturn(transactionProcessor);
when(transactionProcessor.poll(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.PENDING);
tx.setErrorMessage("Verification pending");
return tx;
});
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
assertThrows(ApiException.class, () -> handler.process(transaction));
verify(transactionService).save(any(Transaction.class));
}
@Test
@DisplayName("Should set pollingStatus and paymentStatus from poll result")
void shouldSetStatusesFromPollResult() throws Exception {
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("REQUEST_ecocash_MOBILE"))
.thenReturn(transactionProcessor);
when(transactionProcessor.poll(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
handler.process(transaction);
assertEquals(Status.SUCCESS, transaction.getPollingStatus());
assertEquals(Status.SUCCESS, transaction.getPaymentStatus());
}
@Test
@DisplayName("Should create POLL event on each poll attempt")
void shouldCreatePollEvent() throws Exception {
doNothing().when(transactionEventService).save(any(TransactionEvent.class));
when(paymentProcessorFactory.getPaymentProcessor("REQUEST_ecocash_MOBILE"))
.thenReturn(transactionProcessor);
when(transactionProcessor.poll(any(Transaction.class))).thenAnswer(invocation -> {
Transaction tx = invocation.getArgument(0);
tx.setStatus(Status.SUCCESS);
return tx;
});
when(transactionService.save(any(Transaction.class))).thenReturn(transaction);
handler.process(transaction);
verify(transactionEventService, times(2)).save(any(TransactionEvent.class));
}
}

View File

@@ -0,0 +1,136 @@
package zw.qantra.tm.domain.services.processors.workflows.handlers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Recipient;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.RecipientService;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class RecipientsUpdateHandlerTest {
@Mock
private RecipientService recipientService;
@InjectMocks
private RecipientsUpdateHandler handler;
private Transaction transaction;
@BeforeEach
void setUp() {
transaction = new Transaction();
transaction.setStatus(Status.SUCCESS);
transaction.setCreditAccount("ACC-123");
transaction.setUserId("user-1");
transaction.setProviderLabel("econet");
transaction.setCreditName("John Doe");
transaction.setCreditEmail("john@example.com");
transaction.setCreditPhone("+263771234567");
transaction.setBillName("Econet Bundle");
}
@Test
@DisplayName("Should update existing recipient when found")
void shouldUpdateExistingRecipient() {
Recipient existingRecipient = new Recipient();
existingRecipient.setAccount("ACC-123");
existingRecipient.setUserId("user-1");
existingRecipient.setName("Old Name");
when(recipientService.getRecipient("ACC-123", "user-1"))
.thenReturn(List.of(existingRecipient));
when(recipientService.save(any(Recipient.class))).thenReturn(existingRecipient);
Transaction result = handler.process(transaction);
assertNotNull(result);
assertEquals("John Doe", existingRecipient.getName());
assertEquals("john@example.com", existingRecipient.getEmail());
assertEquals("+263771234567", existingRecipient.getPhoneNumber());
assertEquals("econet", existingRecipient.getLatestProviderLabel());
assertEquals("JO", existingRecipient.getInitials());
verify(recipientService).save(existingRecipient);
}
@Test
@DisplayName("Should create new recipient when not found")
void shouldCreateNewRecipient() {
when(recipientService.getRecipient("ACC-123", "user-1"))
.thenReturn(List.of());
when(recipientService.save(any(Recipient.class))).thenAnswer(i -> i.getArgument(0));
Transaction result = handler.process(transaction);
assertNotNull(result);
verify(recipientService).save(any(Recipient.class));
}
@Test
@DisplayName("Should skip recipient update when transaction status is not SUCCESS")
void shouldSkipWhenNotSuccess() {
transaction.setStatus(Status.PENDING);
handler.process(transaction);
verify(recipientService, never()).getRecipient(any(), any());
verify(recipientService, never()).save(any());
}
@Test
@DisplayName("Should use billName for initials when creditName is null")
void shouldUseBillNameForInitials() {
transaction.setCreditName(null);
when(recipientService.getRecipient("ACC-123", "user-1"))
.thenReturn(List.of());
when(recipientService.save(any(Recipient.class))).thenAnswer(i -> i.getArgument(0));
handler.process(transaction);
verify(recipientService).save(any(Recipient.class));
}
@Test
@DisplayName("Should handle exception gracefully without throwing")
void shouldHandleExceptionGracefully() {
when(recipientService.getRecipient("ACC-123", "user-1"))
.thenThrow(new RuntimeException("DB error"));
assertDoesNotThrow(() -> handler.process(transaction));
}
@Test
@DisplayName("Should not set null fields on recipient")
void shouldNotSetNullFields() {
transaction.setCreditName(null);
transaction.setCreditEmail(null);
transaction.setCreditPhone(null);
Recipient existingRecipient = new Recipient();
existingRecipient.setAccount("ACC-123");
existingRecipient.setUserId("user-1");
when(recipientService.getRecipient("ACC-123", "user-1"))
.thenReturn(List.of(existingRecipient));
when(recipientService.save(any(Recipient.class))).thenReturn(existingRecipient);
handler.process(transaction);
assertNull(existingRecipient.getName());
assertNull(existingRecipient.getEmail());
assertNull(existingRecipient.getPhoneNumber());
}
}