added batch file upload feature
This commit is contained in:
@@ -2,6 +2,13 @@ FROM 144.91.121.112:8082/java-17
|
|||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install fontconfig and fonts for Apache POI XSSFWorkbook font rendering
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
fontconfig \
|
||||||
|
fonts-dejavu \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN mvn clean compile -P production package
|
RUN mvn clean compile -P production package
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
CMD ["java", "-Dspring.profiles.active=nflow.db.postgresql", "-jar", "/app/target/tm-0.0.1-RELEASE.jar"]
|
CMD ["java", "-Djava.awt.headless=true", "-Dspring.profiles.active=nflow.db.postgresql", "-jar", "/app/target/tm-0.0.1-RELEASE.jar"]
|
||||||
|
|||||||
@@ -8,8 +8,12 @@ import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
|||||||
import org.hibernate.query.Page;
|
import org.hibernate.query.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import zw.qantra.tm.domain.dtos.GroupCreateFromFileRequest;
|
||||||
|
import zw.qantra.tm.domain.dtos.GroupCreateFromFileResult;
|
||||||
import zw.qantra.tm.domain.dtos.GroupMemberRequest;
|
import zw.qantra.tm.domain.dtos.GroupMemberRequest;
|
||||||
import zw.qantra.tm.domain.dtos.RecipientGroupCreateRequest;
|
import zw.qantra.tm.domain.dtos.RecipientGroupCreateRequest;
|
||||||
import zw.qantra.tm.domain.dtos.RecipientGroupUpdateRequest;
|
import zw.qantra.tm.domain.dtos.RecipientGroupUpdateRequest;
|
||||||
@@ -45,6 +49,24 @@ public class RecipientGroupController {
|
|||||||
return ResponseEntity.ok(recipientGroupService.create(group, request.getRecipients()));
|
return ResponseEntity.ok(recipientGroupService.create(group, request.getRecipients()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/create-from-file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
public ResponseEntity<GroupCreateFromFileResult> createFromFile(
|
||||||
|
@RequestParam("file") MultipartFile file,
|
||||||
|
@RequestParam("groupName") String groupName,
|
||||||
|
@RequestParam(value = "description", required = false) String description,
|
||||||
|
@RequestParam(value = "requestedBy", required = false) String requestedBy,
|
||||||
|
@RequestParam(value = "currency", required = false) String currency,
|
||||||
|
@RequestParam("userId") String userId) {
|
||||||
|
|
||||||
|
GroupCreateFromFileRequest request = new GroupCreateFromFileRequest();
|
||||||
|
request.setGroupName(groupName.trim());
|
||||||
|
request.setDescription(description.trim());
|
||||||
|
request.setRequestedBy(requestedBy);
|
||||||
|
request.setCurrency(currency);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(recipientGroupService.createFromFile(file, request, userId));
|
||||||
|
}
|
||||||
|
|
||||||
@PutMapping("/update/{groupId}")
|
@PutMapping("/update/{groupId}")
|
||||||
public ResponseEntity<RecipientGroup> update(@PathVariable UUID groupId,
|
public ResponseEntity<RecipientGroup> update(@PathVariable UUID groupId,
|
||||||
@RequestBody RecipientGroupUpdateRequest request) {
|
@RequestBody RecipientGroupUpdateRequest request) {
|
||||||
|
|||||||
@@ -21,4 +21,3 @@ public class GroupBatchCreateRequest {
|
|||||||
private String paymentProcessorImage;
|
private String paymentProcessorImage;
|
||||||
private Transaction transactionTemplate;
|
private Transaction transactionTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Data;
|
||||||
|
import zw.qantra.tm.domain.models.Transaction;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class GroupCreateFromFileRequest {
|
||||||
|
private String groupName;
|
||||||
|
private String groupDescription;
|
||||||
|
private String description;
|
||||||
|
private String requestedBy;
|
||||||
|
private String currency;
|
||||||
|
private BigDecimal amount;
|
||||||
|
private String paymentProcessorLabel;
|
||||||
|
private String paymentProcessorName;
|
||||||
|
private String paymentProcessorImage;
|
||||||
|
private Transaction transactionTemplate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import zw.qantra.tm.domain.models.GroupBatch;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class GroupCreateFromFileResult {
|
||||||
|
private GroupBatch batch;
|
||||||
|
private int totalRows;
|
||||||
|
private int successCount;
|
||||||
|
private int errorCount;
|
||||||
|
private List<RowError> errors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public static class RowError {
|
||||||
|
private int lineNumber;
|
||||||
|
private String account;
|
||||||
|
private String name;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import java.util.UUID;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public class SBZProductDto {
|
public class ProductDto {
|
||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
private String displayName;
|
private String displayName;
|
||||||
@@ -72,6 +72,8 @@ public class Transaction extends BaseEntity {
|
|||||||
private String creditCard;
|
private String creditCard;
|
||||||
private String creditRef;
|
private String creditRef;
|
||||||
private String creditEmail;
|
private String creditEmail;
|
||||||
|
private String creditAddress;
|
||||||
|
private String creditVoucher;
|
||||||
private String billClientId;
|
private String billClientId;
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
private String aggregatorId;
|
private String aggregatorId;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package zw.qantra.tm.domain.repositories;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||||
import zw.qantra.tm.domain.models.Provider;
|
import zw.qantra.tm.domain.models.Provider;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -12,5 +13,7 @@ import java.util.UUID;
|
|||||||
public interface ProviderRepository extends JpaRepository<Provider, UUID>, JpaSpecificationExecutor<Provider> {
|
public interface ProviderRepository extends JpaRepository<Provider, UUID>, JpaSpecificationExecutor<Provider> {
|
||||||
List<Provider> findByCategory(String category);
|
List<Provider> findByCategory(String category);
|
||||||
Provider findByClientId(String clientId);
|
Provider findByClientId(String clientId);
|
||||||
|
Provider findByLabel(String label);
|
||||||
|
Provider findByLabelAndCurrency(String label, CurrencyType currency);
|
||||||
boolean existsByClientId(String clientId);
|
boolean existsByClientId(String clientId);
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ public class BatchReportService {
|
|||||||
Cell titleCell = titleRow.createCell(0);
|
Cell titleCell = titleRow.createCell(0);
|
||||||
titleCell.setCellValue("Batch Report - " + (batch.getDescription() != null ? batch.getDescription() : batch.getId().toString()));
|
titleCell.setCellValue("Batch Report - " + (batch.getDescription() != null ? batch.getDescription() : batch.getId().toString()));
|
||||||
titleCell.setCellStyle(titleStyle);
|
titleCell.setCellStyle(titleStyle);
|
||||||
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 9));
|
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 11));
|
||||||
titleRow.setHeightInPoints(30);
|
titleRow.setHeightInPoints(30);
|
||||||
rowNum++;
|
rowNum++;
|
||||||
|
|
||||||
@@ -149,7 +149,8 @@ public class BatchReportService {
|
|||||||
String[] headers = {
|
String[] headers = {
|
||||||
"#", "Recipient Name", "Phone", "Amount",
|
"#", "Recipient Name", "Phone", "Amount",
|
||||||
"Status", "Confirm Status", "Integration Status",
|
"Status", "Confirm Status", "Integration Status",
|
||||||
"Transaction Ref", "Confirm Error", "Integration Error"
|
"Transaction Ref", "Confirm Error", "Integration Error",
|
||||||
|
"Credit Address", "Credit Voucher"
|
||||||
};
|
};
|
||||||
|
|
||||||
Row headerRow = sheet.createRow(rowNum++);
|
Row headerRow = sheet.createRow(rowNum++);
|
||||||
@@ -214,6 +215,22 @@ public class BatchReportService {
|
|||||||
Cell integrationErrCell = row.createCell(9);
|
Cell integrationErrCell = row.createCell(9);
|
||||||
integrationErrCell.setCellValue(item.getIntegrationError() != null ? item.getIntegrationError() : "");
|
integrationErrCell.setCellValue(item.getIntegrationError() != null ? item.getIntegrationError() : "");
|
||||||
integrationErrCell.setCellStyle(dataStyle);
|
integrationErrCell.setCellStyle(dataStyle);
|
||||||
|
|
||||||
|
Cell creditAddressCell = row.createCell(10);
|
||||||
|
if (item.getTransaction() != null && item.getTransaction().getCreditAddress() != null) {
|
||||||
|
creditAddressCell.setCellValue(item.getTransaction().getCreditAddress());
|
||||||
|
} else {
|
||||||
|
creditAddressCell.setCellValue("");
|
||||||
|
}
|
||||||
|
creditAddressCell.setCellStyle(dataStyle);
|
||||||
|
|
||||||
|
Cell creditVoucherCell = row.createCell(11);
|
||||||
|
if (item.getTransaction() != null && item.getTransaction().getCreditVoucher() != null) {
|
||||||
|
creditVoucherCell.setCellValue(item.getTransaction().getCreditVoucher());
|
||||||
|
} else {
|
||||||
|
creditVoucherCell.setCellValue("");
|
||||||
|
}
|
||||||
|
creditVoucherCell.setCellStyle(dataStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-size columns
|
// Auto-size columns
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import io.nflow.engine.service.WorkflowInstanceService;
|
|||||||
import io.nflow.engine.workflow.instance.WorkflowInstance;
|
import io.nflow.engine.workflow.instance.WorkflowInstance;
|
||||||
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jobrunr.jobs.context.JobRunrDashboardLogger;
|
import org.jobrunr.jobs.context.JobRunrDashboardLogger;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -22,10 +23,10 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
|
|
||||||
import static org.joda.time.DateTime.now;
|
import static org.joda.time.DateTime.now;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class JobService {
|
public class JobService {
|
||||||
Logger log = new JobRunrDashboardLogger(LoggerFactory.getLogger(JobService.class));
|
|
||||||
|
|
||||||
private final WorkflowInstanceService workflowInstanceService;
|
private final WorkflowInstanceService workflowInstanceService;
|
||||||
private final WorkflowUtils workflowUtils;
|
private final WorkflowUtils workflowUtils;
|
||||||
|
|||||||
@@ -6,16 +6,13 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.cache.annotation.CacheEvict;
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
import org.springframework.cache.annotation.Cacheable;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import zw.qantra.tm.domain.dtos.ProductDto;
|
||||||
|
|
||||||
import zw.qantra.tm.domain.dtos.SBZProductDto;
|
|
||||||
import zw.qantra.tm.domain.models.Provider;
|
import zw.qantra.tm.domain.models.Provider;
|
||||||
import zw.qantra.tm.domain.repositories.ProviderRepository;
|
import zw.qantra.tm.domain.repositories.ProviderRepository;
|
||||||
import zw.qantra.tm.exceptions.ApiException;
|
import zw.qantra.tm.exceptions.ApiException;
|
||||||
@@ -35,59 +32,33 @@ public class ProviderService {
|
|||||||
private final RestService restService;
|
private final RestService restService;
|
||||||
private final HotRechargeTokenService hotRechargeTokenService;
|
private final HotRechargeTokenService hotRechargeTokenService;
|
||||||
|
|
||||||
|
private Map<String, List<ProductDto>> providerProductsCache = new HashMap<>();
|
||||||
|
|
||||||
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
||||||
private String hotApiUrl;
|
private String hotApiUrl;
|
||||||
|
|
||||||
@Value("${sbz.aggregator.url}")
|
@Value("${sbz.aggregator.url}")
|
||||||
private String aggregatorUrl;
|
private String aggregatorUrl;
|
||||||
|
|
||||||
public SBZProductDto getProviderProduct(UUID providerId, String productName) {
|
public ProductDto getProviderProduct(UUID providerId, String productName) {
|
||||||
List<SBZProductDto> products = getProviderProducts(providerId.toString());
|
List<ProductDto> products = getProviderProducts(providerId.toString());
|
||||||
SBZProductDto productResult = products.stream()
|
ProductDto productResult = products.stream()
|
||||||
.filter(product -> product.getName().equals(productName))
|
.filter(product -> product.getName().equals(productName))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
log.info("Product: {}", productResult);
|
||||||
return productResult;
|
return productResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
public List<ProductDto> getProviderProducts(String providerUid) {
|
||||||
public List<SBZProductDto> getProviderProducts(UUID providerId) {
|
|
||||||
String token = restService.getMerchantToken();
|
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
||||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
|
||||||
headers.setBearerAuth(token);
|
|
||||||
|
|
||||||
LinkedHashMap response;
|
|
||||||
try {
|
try {
|
||||||
response = this.restService.getAs(
|
List<ProductDto> cachedProducts = providerProductsCache.get(providerUid);
|
||||||
aggregatorUrl + "/merchant/aggregation/products?size=20&clientUid=" + providerId.toString(),
|
if (cachedProducts != null) {
|
||||||
headers,
|
log.info("Returning cached products for provider: {}, number of products: {}", providerUid,
|
||||||
LinkedHashMap.class
|
cachedProducts.size());
|
||||||
);
|
return cachedProducts;
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error fetching products for provider " + providerId, e);
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LinkedHashMap body = (LinkedHashMap) response.get("body");
|
|
||||||
List<LinkedHashMap> content = (List<LinkedHashMap>) body.get("content");
|
|
||||||
|
|
||||||
List<SBZProductDto> products = new ArrayList<>();
|
|
||||||
for (LinkedHashMap product : content) {
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
SBZProductDto productDto = mapper.convertValue(product, SBZProductDto.class);
|
|
||||||
products.add(productDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
return products;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: figure out caching for different providers
|
|
||||||
// @Cacheable(value = "providerProducts", key = "#providerUid")
|
|
||||||
public List<SBZProductDto> getProviderProducts(String providerUid) {
|
|
||||||
try {
|
|
||||||
Provider provider = providerRepository.findById(UUID.fromString(providerUid)).orElse(null);
|
Provider provider = providerRepository.findById(UUID.fromString(providerUid)).orElse(null);
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
@@ -113,25 +84,28 @@ public class ProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List stock = (List) response.get("stock");
|
List stock = (List) response.get("stock");
|
||||||
List<SBZProductDto> products = new ArrayList<>();
|
List<ProductDto> products = new ArrayList<>();
|
||||||
for (Object item : stock) {
|
for (Object item : stock) {
|
||||||
LinkedHashMap<String, Object> stockItem = (LinkedHashMap<String, Object>) item;
|
LinkedHashMap<String, Object> stockItem = (LinkedHashMap<String, Object>) item;
|
||||||
int stockProviderId = (int) stockItem.get("productId"); // providerId is referred to as productId in the stock response
|
int stockProviderId = (int) stockItem.get("productId"); // providerId is referred to as productId in the stock response
|
||||||
if (String.valueOf(stockProviderId).equals(providerId)) {
|
if (String.valueOf(stockProviderId).equals(providerId)) {
|
||||||
log.info("Found matching provider in stock: " + stockItem);
|
|
||||||
|
|
||||||
SBZProductDto sbzProductDto = new SBZProductDto();
|
ProductDto productDto = new ProductDto();
|
||||||
sbzProductDto.setName((String) stockItem.get("name"));
|
productDto.setName((String) stockItem.get("name"));
|
||||||
sbzProductDto.setDescription((String) stockItem.get("description"));
|
productDto.setDescription((String) stockItem.get("description"));
|
||||||
sbzProductDto.setDisplayName((String) stockItem.get("name"));
|
productDto.setDisplayName((String) stockItem.get("name"));
|
||||||
sbzProductDto.setDefaultAmount(new BigDecimal(stockItem.get("amount").toString()));
|
productDto.setDefaultAmount(new BigDecimal(stockItem.get("amount").toString()));
|
||||||
sbzProductDto.setRequiresAmount(true); // always true for now
|
productDto.setRequiresAmount(true); // always true for now
|
||||||
sbzProductDto.setUid(UUID.fromString(Utils.getUid()));
|
productDto.setUid(UUID.fromString(Utils.getUid()));
|
||||||
sbzProductDto.setExternalId((String) stockItem.get("productCode"));
|
productDto.setExternalId((String) stockItem.get("productCode"));
|
||||||
products.add(sbzProductDto);
|
products.add(productDto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
providerProductsCache.put(providerUid, products);
|
||||||
|
log.info("Cached products for provider: {}, number of products: {}", providerUid, products.size());
|
||||||
|
log.info("Provider products cache size: {}", providerProductsCache.size());
|
||||||
|
|
||||||
return products;
|
return products;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.error("Error fetching products: {}", providerUid);
|
log.error("Error fetching products: {}", providerUid);
|
||||||
@@ -152,11 +126,11 @@ public class ProviderService {
|
|||||||
|
|
||||||
public String getExternalId(String providerId, String productName) {
|
public String getExternalId(String providerId, String productName) {
|
||||||
try {
|
try {
|
||||||
List<SBZProductDto> products = getProviderProducts(providerId);
|
List<ProductDto> products = getProviderProducts(providerId);
|
||||||
|
|
||||||
return products.stream()
|
return products.stream()
|
||||||
.filter(product -> product.getName().equals(productName))
|
.filter(product -> product.getName().equals(productName))
|
||||||
.map(SBZProductDto::getExternalId)
|
.map(ProductDto::getExternalId)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -6,14 +6,25 @@ import org.springframework.data.domain.Pageable;
|
|||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import zw.qantra.tm.domain.models.Recipient;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import zw.qantra.tm.domain.models.RecipientGroup;
|
import zw.qantra.tm.domain.dtos.GroupCreateFromFileRequest;
|
||||||
import zw.qantra.tm.domain.models.RecipientGroupMember;
|
import zw.qantra.tm.domain.dtos.GroupCreateFromFileResult;
|
||||||
|
import zw.qantra.tm.domain.dtos.ProductDto;
|
||||||
|
import zw.qantra.tm.domain.enums.*;
|
||||||
|
import zw.qantra.tm.domain.models.*;
|
||||||
|
import zw.qantra.tm.domain.repositories.GroupBatchItemRepository;
|
||||||
|
import zw.qantra.tm.domain.repositories.GroupBatchRepository;
|
||||||
import zw.qantra.tm.domain.repositories.RecipientGroupMemberRepository;
|
import zw.qantra.tm.domain.repositories.RecipientGroupMemberRepository;
|
||||||
import zw.qantra.tm.domain.repositories.RecipientGroupRepository;
|
import zw.qantra.tm.domain.repositories.RecipientGroupRepository;
|
||||||
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
||||||
import zw.qantra.tm.exceptions.ApiException;
|
import zw.qantra.tm.exceptions.ApiException;
|
||||||
|
import zw.qantra.tm.utils.Utils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -24,6 +35,347 @@ public class RecipientGroupService {
|
|||||||
private final RecipientGroupRepository recipientGroupRepository;
|
private final RecipientGroupRepository recipientGroupRepository;
|
||||||
private final RecipientGroupMemberRepository recipientGroupMemberRepository;
|
private final RecipientGroupMemberRepository recipientGroupMemberRepository;
|
||||||
private final RecipientRepository recipientRepository;
|
private final RecipientRepository recipientRepository;
|
||||||
|
private final GroupBatchRepository groupBatchRepository;
|
||||||
|
private final GroupBatchItemRepository groupBatchItemRepository;
|
||||||
|
private final ProviderService providerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a recipient group, creates a batch from it, then updates batch items
|
||||||
|
* with bill/provider fields from the CSV file, and returns the new batch
|
||||||
|
* along with any per-row errors.
|
||||||
|
*
|
||||||
|
* CSV file structure: name, account, phone, billName, billProductName
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* 1. Parse CSV rows, collecting per-row validation errors
|
||||||
|
* 2. Create recipient group from all valid rows
|
||||||
|
* 3. Create group batch from the group
|
||||||
|
* 4. Update batch items with billName & billProductName from CSV, skipping rows
|
||||||
|
* where the provider cannot be resolved and recording those errors
|
||||||
|
* 5. Return the batch + any row-level errors
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public GroupCreateFromFileResult createFromFile(MultipartFile file, GroupCreateFromFileRequest request, String userId) {
|
||||||
|
ParseResult parseResult = parseCsv(file);
|
||||||
|
|
||||||
|
if(!parseResult.parseErrors.isEmpty()) {
|
||||||
|
return GroupCreateFromFileResult.builder()
|
||||||
|
.totalRows(parseResult.rows.size() + parseResult.parseErrors.size())
|
||||||
|
.errorCount(parseResult.parseErrors.size())
|
||||||
|
.errors(parseResult.parseErrors)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseResult.rows.isEmpty()) {
|
||||||
|
throw new ApiException("CSV file has no valid data rows after validation");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: build recipient list from valid CSV rows only
|
||||||
|
List<Recipient> recipients = new ArrayList<>();
|
||||||
|
for (CsvRow row : parseResult.rows) {
|
||||||
|
Recipient recipient = Recipient.builder()
|
||||||
|
.name(row.name)
|
||||||
|
.account(row.account)
|
||||||
|
.phoneNumber(row.phone)
|
||||||
|
.userId(userId)
|
||||||
|
.build();
|
||||||
|
recipients.add(recipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: create recipient group with recipients
|
||||||
|
RecipientGroup group = RecipientGroup.builder()
|
||||||
|
.name(request.getGroupName())
|
||||||
|
.description(request.getDescription())
|
||||||
|
.userId(userId)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RecipientGroup savedGroup = recipientGroupRepository.save(group);
|
||||||
|
|
||||||
|
List<RecipientGroupMember> members = addMembers(savedGroup.getId(), userId, recipients);
|
||||||
|
|
||||||
|
// Step 3: create group batch (inline to avoid circular dependency with GroupBatchService)
|
||||||
|
GroupBatch batch = createBatchInline(savedGroup.getId(), userId, request, members);
|
||||||
|
|
||||||
|
// Step 4: update batch items with bill/provider fields from CSV
|
||||||
|
List<GroupBatchItem> batchItems = groupBatchItemRepository.findAllByGroupBatchId(batch.getId());
|
||||||
|
|
||||||
|
List<GroupBatchItemUpdate> itemUpdates = new ArrayList<>();
|
||||||
|
List<GroupCreateFromFileResult.RowError> rowErrors = new ArrayList<>(parseResult.parseErrors);
|
||||||
|
|
||||||
|
for (int i = 0; i < batchItems.size() && i < parseResult.rows.size(); i++) {
|
||||||
|
GroupBatchItem batchItem = batchItems.get(i);
|
||||||
|
CsvRow row = parseResult.rows.get(i);
|
||||||
|
|
||||||
|
// Attempt to resolve provider by label
|
||||||
|
Provider provider = providerService.getProviderRepository().findByLabelAndCurrency(
|
||||||
|
row.billLabel, CurrencyType.valueOf(request.getCurrency()));
|
||||||
|
if (provider == null) {
|
||||||
|
rowErrors.add(GroupCreateFromFileResult.RowError.builder()
|
||||||
|
.lineNumber(row.lineNumber)
|
||||||
|
.account(row.account)
|
||||||
|
.name(row.name)
|
||||||
|
.message("Provider not found for bill label: " + row.billLabel)
|
||||||
|
.build());
|
||||||
|
// Skip this row - item remains with default values from batch creation
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemUpdates.add(new GroupBatchItemUpdate(batchItem.getId(), row, provider));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply successful item updates directly
|
||||||
|
if (!itemUpdates.isEmpty()) {
|
||||||
|
List<GroupCreateFromFileResult.RowError> providerErrors = updateBatchItemsInline(batch.getId(), itemUpdates);
|
||||||
|
if (!providerErrors.isEmpty()) {
|
||||||
|
return GroupCreateFromFileResult.builder()
|
||||||
|
.totalRows(parseResult.rows.size() + parseResult.parseErrors.size())
|
||||||
|
.errorCount(providerErrors.size())
|
||||||
|
.errors(providerErrors)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the updated batch + errors
|
||||||
|
GroupBatch resultBatch = groupBatchRepository.findByIdAndUserId(batch.getId(), userId)
|
||||||
|
.orElseThrow(() -> new ApiException("Batch not found after creation"));
|
||||||
|
|
||||||
|
return GroupCreateFromFileResult.builder()
|
||||||
|
.batch(resultBatch)
|
||||||
|
.totalRows(parseResult.rows.size() + parseResult.parseErrors.size())
|
||||||
|
.successCount(itemUpdates.size())
|
||||||
|
.errorCount(rowErrors.size())
|
||||||
|
.errors(rowErrors)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a GroupBatch and its items directly (mirrors GroupBatchService.createBatch logic
|
||||||
|
* but without circular dependency).
|
||||||
|
*/
|
||||||
|
private GroupBatch createBatchInline(UUID recipientGroupId, String userId,
|
||||||
|
GroupCreateFromFileRequest request,
|
||||||
|
List<RecipientGroupMember> members) {
|
||||||
|
GroupBatch batch = GroupBatch.builder()
|
||||||
|
.recipientGroupId(recipientGroupId)
|
||||||
|
.userId(userId)
|
||||||
|
.description(request.getDescription())
|
||||||
|
.requestedBy(request.getRequestedBy())
|
||||||
|
.currency(request.getCurrency())
|
||||||
|
.status(GroupBatchStatus.CREATED)
|
||||||
|
.transactionTemplate(Utils.toJson(request.getTransactionTemplate()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
GroupBatch savedBatch = groupBatchRepository.save(batch);
|
||||||
|
|
||||||
|
int itemCount = 0;
|
||||||
|
|
||||||
|
for (RecipientGroupMember member : members) {
|
||||||
|
Recipient recipient = recipientRepository.findById(member.getRecipientUid())
|
||||||
|
.orElseThrow(() -> new ApiException("Recipient not found: " + member.getRecipientUid()));
|
||||||
|
|
||||||
|
GroupBatchItem item = GroupBatchItem.builder()
|
||||||
|
.groupBatchId(savedBatch.getId())
|
||||||
|
.recipientId(recipient.getId())
|
||||||
|
.recipientName(recipient.getName())
|
||||||
|
.recipientPhone(recipient.getPhoneNumber())
|
||||||
|
.status(GroupBatchItemStatus.NEW)
|
||||||
|
.confirmStatus(GroupBatchItemConfirmStatus.PENDING)
|
||||||
|
.integrationStatus(GroupBatchItemIntegrationStatus.PENDING)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
itemCount++;
|
||||||
|
groupBatchItemRepository.save(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
savedBatch.setCount(itemCount);
|
||||||
|
savedBatch.setValue(BigDecimal.ZERO);
|
||||||
|
savedBatch.setSuccessfulCount(0);
|
||||||
|
savedBatch.setFailedCount(0);
|
||||||
|
return groupBatchRepository.save(savedBatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update batch items with CSV data directly (mirrors GroupBatchService.updateBatchItems logic
|
||||||
|
* but without circular dependency).
|
||||||
|
*/
|
||||||
|
private List<GroupCreateFromFileResult.RowError> updateBatchItemsInline(UUID batchId, List<GroupBatchItemUpdate> updates) {
|
||||||
|
List<GroupBatchItem> existingItems = groupBatchItemRepository.findAllByGroupBatchId(batchId);
|
||||||
|
java.util.Map<UUID, GroupBatchItem> itemMap = new java.util.HashMap<>();
|
||||||
|
for (GroupBatchItem item : existingItems) {
|
||||||
|
itemMap.put(item.getId(), item);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GroupBatchItem> updatedItems = new ArrayList<>();
|
||||||
|
|
||||||
|
List<GroupCreateFromFileResult.RowError> providerErrors = new ArrayList<>();
|
||||||
|
for (GroupBatchItemUpdate update : updates) {
|
||||||
|
GroupBatchItem item = itemMap.get(update.batchItemId);
|
||||||
|
if (item == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
item.setRecipientName(update.row.name);
|
||||||
|
item.setRecipientPhone(update.row.phone);
|
||||||
|
item.setBillName(update.provider.getName());
|
||||||
|
item.setBillProductName(update.row.billProductName);
|
||||||
|
item.setProviderLabel(update.row.billLabel);
|
||||||
|
item.setProviderImage(update.provider.getImage());
|
||||||
|
item.setBillClientId(update.provider.getClientId());
|
||||||
|
|
||||||
|
if(update.row.billProductName.isBlank() && update.row.amount.compareTo(BigDecimal.ZERO) != 0) {
|
||||||
|
item.setAmount(update.row.amount);
|
||||||
|
} else if (!update.row.billProductName.isBlank()) {
|
||||||
|
ProductDto productDto = providerService.getProviderProduct(
|
||||||
|
update.provider.getId(), update.row.billProductName);
|
||||||
|
if( productDto == null) {
|
||||||
|
throw new ApiException("Product not found for provider: " + update.provider.getName() +
|
||||||
|
", product name: " + update.row.billProductName);
|
||||||
|
}
|
||||||
|
item.setAmount(productDto.getDefaultAmount());
|
||||||
|
}
|
||||||
|
updatedItems.add(groupBatchItemRepository.save(item));
|
||||||
|
} catch (Exception e) {
|
||||||
|
providerErrors.add(GroupCreateFromFileResult.RowError.builder()
|
||||||
|
.lineNumber(update.row.lineNumber)
|
||||||
|
.account(update.row.account)
|
||||||
|
.name(update.row.name)
|
||||||
|
.message("Failed to update batch item: " + e.getMessage())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate batch totals
|
||||||
|
if (!updatedItems.isEmpty()) {
|
||||||
|
List<GroupBatchItem> allItems = groupBatchItemRepository.findAllByGroupBatchId(batchId);
|
||||||
|
BigDecimal newTotalValue = allItems.stream()
|
||||||
|
.map(GroupBatchItem::getAmount)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
GroupBatch batch = groupBatchRepository.findById(batchId)
|
||||||
|
.orElseThrow(() -> new ApiException("Batch not found"));
|
||||||
|
batch.setValue(newTotalValue);
|
||||||
|
batch.setCount(allItems.size());
|
||||||
|
groupBatchRepository.save(batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return providerErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record GroupBatchItemUpdate(UUID batchItemId, CsvRow row, Provider provider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse CSV rows, collecting validation errors per row rather than failing fast.
|
||||||
|
*/
|
||||||
|
private ParseResult parseCsv(MultipartFile file) {
|
||||||
|
List<CsvRow> rows = new ArrayList<>();
|
||||||
|
List<GroupCreateFromFileResult.RowError> parseErrors = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Reader reader = new InputStreamReader(file.getInputStream());
|
||||||
|
BufferedReader br = new BufferedReader(reader)) {
|
||||||
|
|
||||||
|
String headerLine = br.readLine(); // skip header
|
||||||
|
if (headerLine == null) {
|
||||||
|
throw new ApiException("CSV file is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
String line;
|
||||||
|
int lineNum = 1;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
lineNum++;
|
||||||
|
if (line.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String[] parts = parseCsvLine(line);
|
||||||
|
if (parts.length < 5) {
|
||||||
|
parseErrors.add(GroupCreateFromFileResult.RowError.builder()
|
||||||
|
.lineNumber(lineNum)
|
||||||
|
.message("Invalid CSV format: expected at least 5 columns (name, account, phone, billName, billProductName) but got " + parts.length)
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = parts[0].trim();
|
||||||
|
String account = parts[1].trim();
|
||||||
|
String phone = parts[2].trim();
|
||||||
|
String billLabel = parts[3].trim();
|
||||||
|
String billProductName = parts[4].trim();
|
||||||
|
String amount = parts[5].trim();
|
||||||
|
|
||||||
|
if (name.isEmpty() || account.isEmpty()) {
|
||||||
|
parseErrors.add(GroupCreateFromFileResult.RowError.builder()
|
||||||
|
.lineNumber(lineNum)
|
||||||
|
.account(account)
|
||||||
|
.name(name)
|
||||||
|
.message("name and account are required")
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(amount.isEmpty() && billProductName.isEmpty()) {
|
||||||
|
parseErrors.add(GroupCreateFromFileResult.RowError.builder()
|
||||||
|
.lineNumber(lineNum)
|
||||||
|
.account(account)
|
||||||
|
.name(name)
|
||||||
|
.message("amount or billProductName is required")
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CsvRow row = new CsvRow();
|
||||||
|
row.lineNumber = lineNum;
|
||||||
|
row.name = name;
|
||||||
|
row.account = account;
|
||||||
|
row.phone = phone;
|
||||||
|
row.billLabel = billLabel;
|
||||||
|
row.billProductName = billProductName;
|
||||||
|
row.amount = !amount.isEmpty() ? new BigDecimal(amount) : BigDecimal.ZERO;
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.isEmpty() && parseErrors.isEmpty()) {
|
||||||
|
throw new ApiException("CSV file has no data rows");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ApiException("Failed to read CSV file: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return new ParseResult(rows, parseErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a CSV line respecting simple double-quoted fields.
|
||||||
|
*/
|
||||||
|
private String[] parseCsvLine(String line) {
|
||||||
|
List<String> fields = new ArrayList<>();
|
||||||
|
StringBuilder current = new StringBuilder();
|
||||||
|
boolean inQuotes = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < line.length(); i++) {
|
||||||
|
char c = line.charAt(i);
|
||||||
|
if (c == '"') {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
} else if (c == ',' && !inQuotes) {
|
||||||
|
fields.add(current.toString());
|
||||||
|
current = new StringBuilder();
|
||||||
|
} else {
|
||||||
|
current.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields.add(current.toString());
|
||||||
|
return fields.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CsvRow {
|
||||||
|
int lineNumber;
|
||||||
|
String name;
|
||||||
|
String account;
|
||||||
|
String phone;
|
||||||
|
String billLabel;
|
||||||
|
String billProductName;
|
||||||
|
BigDecimal amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ParseResult(List<CsvRow> rows, List<GroupCreateFromFileResult.RowError> parseErrors) {}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public RecipientGroup create(RecipientGroup group, List<Recipient> recipients) {
|
public RecipientGroup create(RecipientGroup group, List<Recipient> recipients) {
|
||||||
@@ -43,10 +395,6 @@ public class RecipientGroupService {
|
|||||||
for (Recipient incomingRecipient : recipients) {
|
for (Recipient incomingRecipient : recipients) {
|
||||||
Recipient recipient = resolveOrCreateRecipient(incomingRecipient, userId);
|
Recipient recipient = resolveOrCreateRecipient(incomingRecipient, userId);
|
||||||
|
|
||||||
if (recipientGroupMemberRepository.existsByRecipientGroupIdAndRecipientId(groupId, recipient.getId())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
RecipientGroupMember member = RecipientGroupMember.builder()
|
RecipientGroupMember member = RecipientGroupMember.builder()
|
||||||
.recipientGroupId(groupId)
|
.recipientGroupId(groupId)
|
||||||
.recipientUid(recipient.getId())
|
.recipientUid(recipient.getId())
|
||||||
@@ -155,4 +503,3 @@ public class RecipientGroupService {
|
|||||||
return recipientGroupMemberRepository.findAll(spec, pageable);
|
return recipientGroupMemberRepository.findAll(spec, pageable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,17 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
|||||||
.build();
|
.build();
|
||||||
additionalDataService.getAdditionalDataRepository().save(additionalData);
|
additionalDataService.getAdditionalDataRepository().save(additionalData);
|
||||||
|
|
||||||
|
// update customer address in creditAddress field
|
||||||
|
Map<String, Object> details = (Map<String, Object>) customerData.get("details");
|
||||||
|
if (details != null) {
|
||||||
|
for (Map.Entry<String, Object> entry : details.entrySet()) {
|
||||||
|
if (entry.getKey().equalsIgnoreCase("AccountName")) {
|
||||||
|
transaction.setCreditAddress(entry.getValue().toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error processing ZESA confirmation: " + e.getMessage(), e);
|
logger.error("Error processing ZESA confirmation: " + e.getMessage(), e);
|
||||||
transaction.setStatus(Status.FAILED);
|
transaction.setStatus(Status.FAILED);
|
||||||
@@ -122,7 +133,7 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
|||||||
Map<String, Object> details = (Map<String, Object>) customerData.get("details");
|
Map<String, Object> details = (Map<String, Object>) customerData.get("details");
|
||||||
if (details != null) {
|
if (details != null) {
|
||||||
for (Map.Entry<String, Object> entry : details.entrySet()) {
|
for (Map.Entry<String, Object> entry : details.entrySet()) {
|
||||||
// don't show customer ZWG value. It leads to confusion
|
// don't show customer ZWG value. It causes confusion
|
||||||
if(entry.getValue().toString().equals("ZWG"))
|
if(entry.getValue().toString().equals("ZWG"))
|
||||||
continue;
|
continue;
|
||||||
additionalDataList.add(Map.of("name", entry.getKey(), "value", entry.getValue().toString()));
|
additionalDataList.add(Map.of("name", entry.getKey(), "value", entry.getValue().toString()));
|
||||||
|
|||||||
@@ -105,6 +105,18 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
|||||||
.jsonString(Utils.toJson(additionalDataList))
|
.jsonString(Utils.toJson(additionalDataList))
|
||||||
.build();
|
.build();
|
||||||
additionalDataService.getAdditionalDataRepository().save(additionalData);
|
additionalDataService.getAdditionalDataRepository().save(additionalData);
|
||||||
|
|
||||||
|
// if provider is zesa, update customer token in creditVoucher field
|
||||||
|
String providerId = providerService.getProviderId(transaction.getBillClientId());
|
||||||
|
if(providerId.equals("41")) {
|
||||||
|
for (Map.Entry<String, Object> entry : rechargeData.entrySet()) {
|
||||||
|
if (entry.getKey().equalsIgnoreCase("Token")) {
|
||||||
|
transaction.setCreditVoucher(entry.getValue().toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
transaction.setStatus(Status.FAILED);
|
transaction.setStatus(Status.FAILED);
|
||||||
@@ -138,19 +150,15 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
|||||||
request.put("ProductId", providerId);
|
request.put("ProductId", providerId);
|
||||||
request.put("Target", transaction.getCreditAccount());
|
request.put("Target", transaction.getCreditAccount());
|
||||||
request.put("Amount", transaction.getCreditAmount());
|
request.put("Amount", transaction.getCreditAmount());
|
||||||
|
|
||||||
// 41 = zesa
|
|
||||||
if(providerId.equals("41")) {
|
|
||||||
request.put("CustomerSMS", "A ZETDC token was purchased for " +
|
|
||||||
"%ACOUNTNAME% (%METERNUMBER%) that resulted in %KWH% units.");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
request.put("CustomerSMS", "%COMPANYNAME% topped up your account with $%AMOUNT%.");
|
request.put("CustomerSMS", "%COMPANYNAME% topped up your account with $%AMOUNT%.");
|
||||||
}
|
|
||||||
|
|
||||||
// do this for zesa only
|
// do this for zesa only
|
||||||
if(providerId.equals("41")) {
|
if(providerId.equals("41")) {
|
||||||
// update product to zwg product id
|
// override sms for zesa
|
||||||
|
request.put("CustomerSMS", "A ZETDC token was purchased for " +
|
||||||
|
"%ACCOUNTNAME% (%METERNUMBER%) that resulted in %KWH% units.");
|
||||||
|
|
||||||
|
// if necessary, update product to zwg product id
|
||||||
if(transaction.getCreditCurrency().equals(CurrencyType.ZWG)) {
|
if(transaction.getCreditCurrency().equals(CurrencyType.ZWG)) {
|
||||||
String zwgProviderId = providerService.getProviderId(ZESA_ZWG_CLIENT);
|
String zwgProviderId = providerService.getProviderId(ZESA_ZWG_CLIENT);
|
||||||
request.put("ProductId", zwgProviderId);
|
request.put("ProductId", zwgProviderId);
|
||||||
@@ -169,7 +177,7 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// do this for Econet bundles
|
// do this for providers with bundles
|
||||||
Provider provider = providerService.getProviderRepository()
|
Provider provider = providerService.getProviderRepository()
|
||||||
.findByClientId(transaction.getBillClientId());
|
.findByClientId(transaction.getBillClientId());
|
||||||
if(provider.getRequiresProducts().equals(true)) {
|
if(provider.getRequiresProducts().equals(true)) {
|
||||||
|
|||||||
@@ -1,28 +1,13 @@
|
|||||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||||
<changeSet author="khoza (generated)" id="1781700776654-3">
|
<changeSet author="khoza (generated)" id="1781914969011-3">
|
||||||
<addColumn tableName="group_batch_item">
|
<addColumn tableName="transaction">
|
||||||
<column name="bill_client_id" type="varchar(255)"/>
|
<column name="credit_address" type="varchar(255)"/>
|
||||||
</addColumn>
|
</addColumn>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
<changeSet author="khoza (generated)" id="1781700776654-4">
|
<changeSet author="khoza (generated)" id="1781914969011-4">
|
||||||
<addColumn tableName="group_batch_item">
|
<addColumn tableName="transaction">
|
||||||
<column name="bill_name" type="varchar(255)"/>
|
<column name="credit_voucher" type="varchar(255)"/>
|
||||||
</addColumn>
|
|
||||||
</changeSet>
|
|
||||||
<changeSet author="khoza (generated)" id="1781700776654-5">
|
|
||||||
<addColumn tableName="group_batch_item">
|
|
||||||
<column name="bill_product_name" type="varchar(255)"/>
|
|
||||||
</addColumn>
|
|
||||||
</changeSet>
|
|
||||||
<changeSet author="khoza (generated)" id="1781700776654-6">
|
|
||||||
<addColumn tableName="group_batch_item">
|
|
||||||
<column name="provider_image" type="varchar(255)"/>
|
|
||||||
</addColumn>
|
|
||||||
</changeSet>
|
|
||||||
<changeSet author="khoza (generated)" id="1781700776654-7">
|
|
||||||
<addColumn tableName="group_batch_item">
|
|
||||||
<column name="provider_label" type="varchar(255)"/>
|
|
||||||
</addColumn>
|
</addColumn>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
14
src/test/java/zw/qantra/tm/TmApplicationTests.java
Normal file
14
src/test/java/zw/qantra/tm/TmApplicationTests.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package zw.qantra.tm;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class TmApplicationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contextLoads() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package zw.qantra.tm.domain.repositories;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||||
|
import zw.qantra.tm.domain.enums.RequestType;
|
||||||
|
import zw.qantra.tm.domain.enums.Status;
|
||||||
|
import zw.qantra.tm.domain.models.Transaction;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
@DataJpaTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class TransactionRepositoryTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TransactionRepository transactionRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should save and find transaction by ID")
|
||||||
|
void shouldSaveAndFindById() {
|
||||||
|
Transaction transaction = createTestTransaction();
|
||||||
|
Transaction saved = transactionRepository.save(transaction);
|
||||||
|
|
||||||
|
Optional<Transaction> found = transactionRepository.findById(saved.getId());
|
||||||
|
|
||||||
|
assertTrue(found.isPresent());
|
||||||
|
assertEquals("user-1", found.get().getUserId());
|
||||||
|
assertEquals(new BigDecimal("100.00"), found.get().getAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return empty when transaction not found")
|
||||||
|
void shouldReturnEmptyWhenNotFound() {
|
||||||
|
Optional<Transaction> found = transactionRepository.findById(UUID.randomUUID());
|
||||||
|
|
||||||
|
assertTrue(found.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should find transactions by userId")
|
||||||
|
void shouldFindByUserId() {
|
||||||
|
Transaction tx1 = createTestTransaction();
|
||||||
|
tx1.setUserId("user-1");
|
||||||
|
Transaction tx2 = createTestTransaction();
|
||||||
|
tx2.setUserId("user-2");
|
||||||
|
transactionRepository.save(tx1);
|
||||||
|
transactionRepository.save(tx2);
|
||||||
|
|
||||||
|
var results = transactionRepository.findAllByUserId("user-1");
|
||||||
|
|
||||||
|
assertEquals(1, results.size());
|
||||||
|
assertEquals("user-1", results.get(0).getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return paginated results")
|
||||||
|
void shouldReturnPaginatedResults() {
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
Transaction tx = createTestTransaction();
|
||||||
|
tx.setUserId("user-" + i);
|
||||||
|
transactionRepository.save(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(0, 2);
|
||||||
|
Page<Transaction> page = transactionRepository.findAll(pageable);
|
||||||
|
|
||||||
|
assertEquals(5, page.getTotalElements());
|
||||||
|
assertEquals(2, page.getContent().size());
|
||||||
|
assertEquals(0, page.getNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should exclude soft-deleted transactions")
|
||||||
|
void shouldExcludeSoftDeleted() {
|
||||||
|
Transaction transaction = createTestTransaction();
|
||||||
|
Transaction saved = transactionRepository.save(transaction);
|
||||||
|
|
||||||
|
// Hard delete from the test perspective
|
||||||
|
transactionRepository.deleteById(saved.getId());
|
||||||
|
|
||||||
|
Optional<Transaction> found = transactionRepository.findById(saved.getId());
|
||||||
|
assertTrue(found.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should persist all transaction fields correctly")
|
||||||
|
void shouldPersistAllFields() {
|
||||||
|
Transaction transaction = Transaction.builder()
|
||||||
|
.userId("user-1")
|
||||||
|
.trace("TRACE-123")
|
||||||
|
.region("ZW")
|
||||||
|
.amount(new BigDecimal("150.00"))
|
||||||
|
.charge(new BigDecimal("5.00"))
|
||||||
|
.gatewayCharge(new BigDecimal("2.00"))
|
||||||
|
.tax(new BigDecimal("1.50"))
|
||||||
|
.totalAmount(new BigDecimal("158.50"))
|
||||||
|
.reference("REF-001")
|
||||||
|
.providerUid("PROV-UID-1")
|
||||||
|
.productUid("PROD-UID-1")
|
||||||
|
.paymentProcessorLabel("ecocash")
|
||||||
|
.integrationProcessorLabel("hotrecharge")
|
||||||
|
.confirmationProcessorLabel("CONFIRM_econet_hotrecharge")
|
||||||
|
.providerLabel("econet")
|
||||||
|
.rrn("RRN-12345")
|
||||||
|
.debitPhone("+263771234567")
|
||||||
|
.debitAccount("ACC-001")
|
||||||
|
.debitCurrency(CurrencyType.USD)
|
||||||
|
.debitName("John Doe")
|
||||||
|
.creditPhone("+263778901234")
|
||||||
|
.creditAccount("ACC-002")
|
||||||
|
.creditCurrency(CurrencyType.ZWL)
|
||||||
|
.creditAmount(new BigDecimal("2250.0000"))
|
||||||
|
.creditName("Jane Doe")
|
||||||
|
.billClientId("BILL-001")
|
||||||
|
.confirmationStatus(Status.SUCCESS)
|
||||||
|
.paymentStatus(Status.SUCCESS)
|
||||||
|
.integrationStatus(Status.PENDING)
|
||||||
|
.pollingStatus(Status.PENDING)
|
||||||
|
.status(Status.SUCCESS)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Transaction saved = transactionRepository.save(transaction);
|
||||||
|
|
||||||
|
Optional<Transaction> found = transactionRepository.findById(saved.getId());
|
||||||
|
assertTrue(found.isPresent());
|
||||||
|
assertEquals("user-1", found.get().getUserId());
|
||||||
|
assertEquals(new BigDecimal("150.00"), found.get().getAmount());
|
||||||
|
assertEquals(new BigDecimal("5.00"), found.get().getCharge());
|
||||||
|
assertEquals(new BigDecimal("158.50"), found.get().getTotalAmount());
|
||||||
|
assertEquals(Status.SUCCESS, found.get().getConfirmationStatus());
|
||||||
|
assertEquals(Status.SUCCESS, found.get().getPaymentStatus());
|
||||||
|
assertEquals(Status.PENDING, found.get().getIntegrationStatus());
|
||||||
|
assertEquals("+263771234567", found.get().getDebitPhone());
|
||||||
|
assertEquals("+263778901234", found.get().getCreditPhone());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Transaction createTestTransaction() {
|
||||||
|
return Transaction.builder()
|
||||||
|
.userId("user-1")
|
||||||
|
.amount(new BigDecimal("100.00"))
|
||||||
|
.debitCurrency(CurrencyType.USD)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/test/resources/application-test.properties
Normal file
24
src/test/resources/application-test.properties
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Test configuration
|
||||||
|
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||||
|
spring.datasource.driver-class-name=org.h2.Driver
|
||||||
|
spring.datasource.username=sa
|
||||||
|
spring.datasource.password=
|
||||||
|
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||||
|
spring.jpa.hibernate.ddl-auto=create-drop
|
||||||
|
spring.jpa.show-sql=false
|
||||||
|
spring.jpa.properties.hibernate.format_sql=true
|
||||||
|
|
||||||
|
# Disable nFlow for unit tests
|
||||||
|
nflow.db.h2.url=jdbc:h2:mem:nflow-test;DB_CLOSE_DELAY=-1
|
||||||
|
nflow.db.h2.user=sa
|
||||||
|
nflow.db.h2.password=
|
||||||
|
nflow.autostart=false
|
||||||
|
|
||||||
|
# Disable security for tests
|
||||||
|
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,io.nflow.spring.boot.autoconfigure.NflowAutoConfiguration
|
||||||
|
|
||||||
|
# Disable jobrunr for tests
|
||||||
|
jobrunr.enabled=false
|
||||||
|
|
||||||
|
# Disable mail for tests
|
||||||
|
spring.mail.enabled=false
|
||||||
Reference in New Issue
Block a user