added batch file upload feature
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user