updates on batch logic

This commit is contained in:
Prince
2026-06-17 01:11:48 +02:00
parent 5d3c9a46a3
commit 0eda74414e
16 changed files with 288 additions and 254 deletions

View File

@@ -9,6 +9,7 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import zw.qantra.tm.domain.dtos.GroupBatchCreateRequest;
import zw.qantra.tm.domain.dtos.GroupBatchItemUpdateRequest;
import zw.qantra.tm.domain.dtos.GroupBatchUpdateRequest;
import zw.qantra.tm.domain.dtos.GroupBatchTriggerRequest;
import zw.qantra.tm.domain.models.GroupBatch;
@@ -16,6 +17,7 @@ import zw.qantra.tm.domain.models.GroupBatchItem;
import zw.qantra.tm.domain.services.BatchReportService;
import zw.qantra.tm.domain.services.GroupBatchService;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -93,6 +95,16 @@ public class GroupBatchController {
return ResponseEntity.ok(groupBatchService.getBatchItems(spec, pageable));
}
@PutMapping("/{batchId}/items")
public ResponseEntity<List<GroupBatchItem>> updateBatchItems(
@PathVariable UUID batchId,
@RequestBody GroupBatchItemUpdateRequest.ListWrapper wrapper) {
return ResponseEntity.ok(groupBatchService.updateBatchItems(
batchId,
wrapper.getUserId(),
wrapper.getItems()));
}
@DeleteMapping("/{batchId}")
public ResponseEntity<Void> deleteBatch(@PathVariable UUID batchId,
@RequestParam String userId) {

View File

@@ -0,0 +1,29 @@
package zw.qantra.tm.domain.dtos;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
import java.util.UUID;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class GroupBatchItemUpdateRequest {
private UUID id;
private BigDecimal amount;
private String recipientName;
private String recipientPhone;
private String billClientId;
private String billName;
private String billProductName;
private String providerImage;
private String providerLabel;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ListWrapper {
private List<GroupBatchItemUpdateRequest> items;
private String userId;
}
}

View File

@@ -58,5 +58,11 @@ public class GroupBatchItem extends BaseEntity {
@ManyToOne
@JoinColumn(name = "transaction_id")
private Transaction transaction;
private String billClientId;
private String billName;
private String billProductName;
private String providerImage;
private String providerLabel;
}

View File

@@ -34,6 +34,7 @@ public class Provider extends BaseEntity {
private Boolean requiresAmount;
private Boolean requiresPhone;
private Boolean requiresAccount;
private Boolean requiresProducts;
private double percentageCommission;
@Column(columnDefinition = "TEXT")
private String meta;

View File

@@ -81,6 +81,7 @@ public class Transaction extends BaseEntity {
private String paymentProcessorName;
private String providerImage;
private String paymentProcessorImage;
private UUID batchId;
@Enumerated(EnumType.STRING)
private Status authorizationStatus;

View File

@@ -13,6 +13,7 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import zw.qantra.tm.domain.dtos.GroupBatchCreateRequest;
import zw.qantra.tm.domain.dtos.GroupBatchItemUpdateRequest;
import zw.qantra.tm.domain.dtos.GroupBatchUpdateRequest;
import zw.qantra.tm.domain.dtos.StateResponse;
import zw.qantra.tm.domain.enums.AuthType;
@@ -30,10 +31,13 @@ import zw.qantra.tm.exceptions.ApiException;
import zw.qantra.tm.utils.Utils;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -43,7 +47,8 @@ public class GroupBatchService {
private static final String BATCH_WORKFLOW_TYPE = "batchWorkflow";
private static final Set<GroupBatchStatus> UPDATABLE_STATUSES = Set.of(
GroupBatchStatus.CREATED
GroupBatchStatus.CREATED,
GroupBatchStatus.CONFIRM_FAILED
);
private static final Set<GroupBatchStatus> DELETABLE_STATUSES = Set.of(
@@ -74,9 +79,6 @@ public class GroupBatchService {
if (members.isEmpty()) {
throw new ApiException("Recipient group has no members");
}
if (request.getTransactionTemplate() == null) {
throw new ApiException("transactionTemplate is required");
}
GroupBatch batch = GroupBatch.builder()
.recipientGroupId(group.getId())
@@ -327,6 +329,77 @@ public class GroupBatchService {
return groupBatchRepository.save(batch);
}
@Transactional
public List<GroupBatchItem> updateBatchItems(UUID batchId, String userId,
List<GroupBatchItemUpdateRequest> items) {
GroupBatch batch = getBatch(batchId, userId);
if (!UPDATABLE_STATUSES.contains(batch.getStatus())) {
throw new ApiException(
"Batch items cannot be updated at current status: " + batch.getStatus()
+ ". Only batches in CREATED status can have items updated."
);
}
List<GroupBatchItem> existingItems = groupBatchItemRepository.findAllByGroupBatchId(batchId);
Map<UUID, GroupBatchItem> itemMap = existingItems.stream()
.collect(Collectors.toMap(GroupBatchItem::getId, item -> item));
List<GroupBatchItem> updatedItems = new ArrayList<>();
for (GroupBatchItemUpdateRequest update : items) {
if (update.getId() == null) {
throw new ApiException("Item id is required for each item update");
}
GroupBatchItem item = itemMap.get(update.getId());
if (item == null) {
throw new ApiException("Group batch item not found: " + update.getId());
}
if (update.getAmount() != null) {
item.setAmount(update.getAmount());
}
if (update.getRecipientName() != null) {
item.setRecipientName(update.getRecipientName());
}
if (update.getRecipientPhone() != null) {
item.setRecipientPhone(update.getRecipientPhone());
}
if (update.getBillClientId() != null) {
item.setBillClientId(update.getBillClientId());
}
if (update.getBillName() != null) {
item.setBillName(update.getBillName());
}
if (update.getBillProductName() != null) {
item.setBillProductName(update.getBillProductName());
}
if (update.getProviderImage() != null) {
item.setProviderImage(update.getProviderImage());
}
if (update.getProviderLabel() != null) {
item.setProviderLabel(update.getProviderLabel());
}
updatedItems.add(groupBatchItemRepository.save(item));
}
// Recalculate batch totals if any amount changed
boolean amountChanged = items.stream().anyMatch(u -> u.getAmount() != null);
if (amountChanged) {
List<GroupBatchItem> allItems = groupBatchItemRepository.findAllByGroupBatchId(batchId);
BigDecimal newTotalValue = allItems.stream()
.map(GroupBatchItem::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
batch.setValue(newTotalValue);
batch.setCount(allItems.size());
groupBatchRepository.save(batch);
}
return updatedItems;
}
@Transactional
public void deleteBatch(UUID batchId, String userId) {
GroupBatch batch = getBatch(batchId, userId);

View File

@@ -10,6 +10,7 @@ import zw.qantra.tm.domain.repositories.GroupBatchRepository;
import zw.qantra.tm.domain.repositories.RecipientRepository;
import zw.qantra.tm.domain.services.processors.workflows.handlers.*;
import zw.qantra.tm.exceptions.ApiException;
import zw.qantra.tm.utils.SecurityUtils;
import zw.qantra.tm.utils.Utils;
import java.math.BigDecimal;
@@ -32,6 +33,7 @@ public class GroupBatchWorkflowService {
private final IntegrationHandler integrationHandler;
private final CalculateChargesHandler calculateChargesHandler;
private final CleanupHandler cleanUpHandler;
private final UserService userService;
@Transactional
public GroupBatch processConfirm(UUID batchId) {
@@ -49,15 +51,10 @@ public class GroupBatchWorkflowService {
batch.setConfirmStartedAt(LocalDateTime.now());
groupBatchRepository.save(batch);
Transaction template = Utils.fromJson(batch.getTransactionTemplate(), Transaction.class);
if (template == null) {
throw new ApiException("Batch transaction template is missing");
}
List<GroupBatchItem> items = groupBatchItemRepository.findAllByGroupBatchId(batchId);
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (GroupBatchItem item : items) {
futures.add(CompletableFuture.runAsync(() -> confirmItem(batch, item, template)));
futures.add(CompletableFuture.runAsync(() -> confirmItem(batch, item)));
}
futures.forEach(CompletableFuture::join);
@@ -98,17 +95,18 @@ public class GroupBatchWorkflowService {
throw new ApiException("No confirmed batch items available for payment request");
}
Transaction template = Utils.fromJson(batch.getTransactionTemplate(), Transaction.class);
if (template == null) {
throw new ApiException("Batch transaction template is missing");
}
BigDecimal total = confirmedItems.stream()
.map(item -> item.getAmount() == null ? BigDecimal.ZERO : item.getAmount())
.reduce(BigDecimal.ZERO, BigDecimal::add);
User user = userService.getUserRepository().findById(UUID.fromString(batch.getUserId())).orElseThrow();
Transaction transaction = Utils.deepCopy(template, Transaction.class);
Transaction transaction = new Transaction();
transaction.setId(null);
transaction.setDebitPhone(user.getPhone());
transaction.setDebitName(user.getFirstName());
transaction.setDebitCurrency(CurrencyType.valueOf(batch.getCurrency()));
transaction.setRegion("ZW");
transaction.setPartyType(PartyType.GROUP);
transaction.setType(RequestType.REQUEST);
transaction.setAuthType(authType == null ? AuthType.REMOTE : authType);
@@ -119,6 +117,7 @@ public class GroupBatchWorkflowService {
transaction.setPaymentProcessorLabel(batch.getPaymentProcessorLabel());
transaction.setPaymentProcessorName(batch.getPaymentProcessorName());
transaction.setPaymentProcessorImage(batch.getPaymentProcessorImage());
transaction.setBatchId(batch.getId());
transaction = calculateChargesHandler.process(transaction);
transaction = (Transaction) cleanUpHandler.process(transaction);
@@ -203,11 +202,23 @@ public class GroupBatchWorkflowService {
return groupBatchRepository.save(batch);
}
private void confirmItem(GroupBatch batch, GroupBatchItem item, Transaction template) {
private void confirmItem(GroupBatch batch, GroupBatchItem item) {
GroupBatchItem mutableItem = groupBatchItemRepository.findById(item.getId()).orElse(item);
try {
Transaction transaction = Utils.deepCopy(template, Transaction.class);
User user = userService.getUserRepository().findById(UUID.fromString(batch.getUserId())).orElseThrow();
Transaction transaction = new Transaction();
transaction.setId(null);
transaction.setDebitPhone(user.getPhone());
transaction.setDebitName(user.getFirstName());
transaction.setDebitCurrency(CurrencyType.valueOf(batch.getCurrency()));
transaction.setRegion("ZW");
transaction.setBillClientId(item.getBillClientId());
transaction.setBillName(item.getBillName());
transaction.setBillProductName(item.getBillProductName());
transaction.setProviderImage(item.getProviderImage());
transaction.setProviderLabel(item.getProviderLabel());
transaction.setUserId(batch.getUserId());
transaction.setType(RequestType.CONFIRM);
transaction.setPartyType(PartyType.BATCH);
@@ -241,6 +252,7 @@ public class GroupBatchWorkflowService {
mutableItem.setConfirmError(result.getErrorMessage());
}
} catch (Exception e) {
e.printStackTrace();
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.FAILED);
mutableItem.setStatus(GroupBatchItemStatus.FAILED);
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.SKIPPED);
@@ -278,6 +290,7 @@ public class GroupBatchWorkflowService {
mutableItem.setStatus(GroupBatchItemStatus.INTEGRATED);
mutableItem.setIntegrationError(null);
} catch (Exception e) {
e.printStackTrace();
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.FAILED);
mutableItem.setStatus(GroupBatchItemStatus.FAILED);
mutableItem.setIntegrationError(e.getMessage());

View File

@@ -90,6 +90,10 @@ public class ProviderService {
if (provider == null) {
return Collections.emptyList();
}
if(provider.getRequiresProducts().equals(false)){
return Collections.emptyList();
}
Map<String, String> meta = Utils.fromJson(provider.getMeta(), Map.class);
String providerId = meta.get("hotProductId");

View File

@@ -16,7 +16,9 @@ import zw.qantra.tm.domain.dtos.RegisterRequest;
import zw.qantra.tm.domain.dtos.erp.*;
import zw.qantra.tm.domain.enums.CurrencyType;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.GroupBatchItem;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.repositories.GroupBatchItemRepository;
import zw.qantra.tm.domain.repositories.TransactionRepository;
import zw.qantra.tm.configs.VelocityApiProperties;
import zw.qantra.tm.exceptions.AlreadyExistsException;
@@ -27,12 +29,14 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class VelocityPaymentService {
private final TransactionRepository transactionRepository;
private final GroupBatchItemRepository groupBatchItemRepository;
private final ObjectMapper objectMapper;
private final RestService restService;
private final VelocityApiProperties velocityApiProperties;
@@ -101,19 +105,47 @@ public class VelocityPaymentService {
public Transaction createSalesOrder(Transaction transaction) {
BigDecimal amount = transaction.getTotalAmount();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
List<VelocitySalesOrderDto.ItemDto> items;
if (transaction.getBatchId() != null) {
// Fetch GroupBatchItems for this batch and summarize by providerLabel
List<GroupBatchItem> batchItems = groupBatchItemRepository.findAllByGroupBatchId(transaction.getBatchId());
Map<String, BigDecimal> totalsByProvider = batchItems.stream()
.filter(item -> item.getProviderLabel() != null && item.getAmount() != null)
.collect(Collectors.groupingBy(
GroupBatchItem::getProviderLabel,
Collectors.mapping(
GroupBatchItem::getAmount,
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)
)
));
items = totalsByProvider.entrySet().stream()
.map(entry -> VelocitySalesOrderDto.ItemDto.builder()
.name(transaction.getBillName())
.itemCode("VLT-" + entry.getKey())
.qty(BigDecimal.ONE)
.unitPrice(entry.getValue())
.amount(entry.getValue())
.build())
.collect(Collectors.toList());
} else {
items = Collections.singletonList(VelocitySalesOrderDto.ItemDto.builder()
.name(transaction.getBillName())
.itemCode("VLT-" + transaction.getProviderLabel())
.qty(BigDecimal.ONE)
.unitPrice(amount)
.amount(amount)
.build());
}
VelocitySalesOrderDto payload = VelocitySalesOrderDto.builder()
.currencyCodeString(transaction.getDebitCurrency().name())
.orderDate(LocalDate.now().format(fmt))
.dueDate(LocalDate.now().plusDays(7).format(fmt))
.notes(transaction.getReference())
.notes("Transaction trace - " + transaction.getTrace())
.authorized(Boolean.TRUE)
.items(Collections.singletonList(VelocitySalesOrderDto.ItemDto.builder()
.name(transaction.getBillName())
.itemCode("VLT-" + transaction.getProviderLabel())
.qty(BigDecimal.ONE)
.unitPrice(amount)
.amount(amount)
.build()))
.items(items)
.charges(Collections.emptyList())
.build();

View File

@@ -170,10 +170,11 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
}
// do this for Econet bundles
if(providerId.equals("111")) {
Provider provider = providerService.getProviderRepository()
.findByClientId(transaction.getBillClientId());
String productCode = providerService.getExternalId(provider.getId().toString(), transaction.getBillProductName());
Provider provider = providerService.getProviderRepository()
.findByClientId(transaction.getBillClientId());
if(provider.getRequiresProducts().equals(true)) {
String productCode = providerService.getExternalId(
provider.getId().toString(), transaction.getBillProductName());
List<Map<String, String>> rechargeOptions = new ArrayList<>();
Map<String, String> option = new LinkedHashMap<>();
option.put("Name", "ProductCode");

View File

@@ -41,19 +41,21 @@ public class CleanupHandler implements HandlerInterface {
// UPDATE TRAN WITH CONFIRMATION HANDLER
// =======================
Provider provider = providerService.getProviderRepository()
.findByClientId(transaction.getBillClientId());
if(provider == null){
throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId());
}
String label =
transaction.getType() + "_" +
provider.getLabel() + "_" +
provider.getIntegrationProcessorLabel();
if(transaction.getType().equals(RequestType.CONFIRM)) {
Provider provider = providerService.getProviderRepository()
.findByClientId(transaction.getBillClientId());
if(provider == null){
throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId());
}
String label =
transaction.getType() + "_" +
provider.getLabel() + "_" +
provider.getIntegrationProcessorLabel();
transaction.setConfirmationProcessorLabel(label);
transaction.setProviderLabel(provider.getLabel());
logger.info("label: {}", label);
transaction.setConfirmationProcessorLabel(label);
transaction.setProviderLabel(provider.getLabel());
logger.info("label: {}", label);
}
// FORMAT FIELDS CORRECTLY
// =======================

View File

@@ -45,6 +45,25 @@ public class ProviderSeeder implements Seeder {
.meta("{\"hotProductId\": \"101\"}")
.uid(Utils.getUid())
.build(),
Provider.builder()
.name("Econet Airtime")
.description("Econet Airtime")
.clientId("econet_airtime_zwg")
.currency(CurrencyType.ZWG)
.label("ECONET")
.category("AIRTIME")
.image("econet.png")
.externalId("78f1f497-c9eb-401c-b22c-884756e68e41")
.integrationProcessorLabel("HOT")
.accountFieldName("Phone Number")
.percentageCommission(5)
.requiresAccount(false)
.requiresAmount(true)
.requiresPhone(true)
.priority(2)
.meta("{\"hotProductId\": \"7\"}")
.uid(Utils.getUid())
.build(),
Provider.builder()
.name("NetOne Airtime")
.description("NetOne Airtime")
@@ -120,6 +139,26 @@ public class ProviderSeeder implements Seeder {
.priority(4)
.meta("{\"hotProductId\": \"111\"}")
.uid(Utils.getUid())
.build(),
Provider.builder()
.name("Econet Bundles")
.description("Econet Bundles")
.clientId("econet_bundles_zwg")
.currency(CurrencyType.ZWG)
.label("ECONET_BUNDLES")
.category("AIRTIME")
.image("econet.png")
.externalId("287863e2-a791-4106-b629-79dadc9a6780")
.integrationProcessorLabel("HOT")
.accountFieldName("Phone Number")
.percentageCommission(5)
.requiresAccount(false)
.requiresAmount(false)
.requiresPhone(true)
.requiresProducts(true)
.priority(4)
.meta("{\"hotProductId\": \"16\"}")
.uid(Utils.getUid())
.build()
);

View File

@@ -0,0 +1,21 @@
package zw.qantra.tm.utils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
public class SecurityUtils {
public static String getCurrentUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
} else {
return principal.toString(); // For cases where principal might be a String
}
}
return null; // Or throw an exception if the user is expected to be logged in
}
}