Files
velocity-pay-springboot/src/main/java/zw/qantra/tm/domain/services/GroupBatchService.java
2026-06-11 23:52:20 +02:00

346 lines
14 KiB
Java

package zw.qantra.tm.domain.services;
import io.nflow.engine.service.WorkflowInstanceService;
import io.nflow.engine.workflow.instance.WorkflowInstance;
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
import lombok.RequiredArgsConstructor;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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.GroupBatchUpdateRequest;
import zw.qantra.tm.domain.dtos.StateResponse;
import zw.qantra.tm.domain.enums.AuthType;
import zw.qantra.tm.domain.enums.GroupBatchItemConfirmStatus;
import zw.qantra.tm.domain.enums.GroupBatchItemIntegrationStatus;
import zw.qantra.tm.domain.enums.GroupBatchItemStatus;
import zw.qantra.tm.domain.enums.GroupBatchStatus;
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.RecipientRepository;
import zw.qantra.tm.domain.services.processors.workflows.BatchWorkflow;
import zw.qantra.tm.domain.services.processors.workflows.WorkflowUtils;
import zw.qantra.tm.exceptions.ApiException;
import zw.qantra.tm.utils.Utils;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@Service
@RequiredArgsConstructor
public class GroupBatchService {
private static final String BATCH_WORKFLOW_TYPE = "batchWorkflow";
private static final Set<GroupBatchStatus> UPDATABLE_STATUSES = Set.of(
GroupBatchStatus.CREATED
);
private static final Set<GroupBatchStatus> DELETABLE_STATUSES = Set.of(
GroupBatchStatus.CREATED,
GroupBatchStatus.CONFIRMING,
GroupBatchStatus.CONFIRMED_ALL,
GroupBatchStatus.CONFIRMED_PARTIAL,
GroupBatchStatus.CONFIRM_FAILED
);
private final RecipientGroupService recipientGroupService;
private final RecipientRepository recipientRepository;
private final GroupBatchRepository groupBatchRepository;
private final GroupBatchItemRepository groupBatchItemRepository;
private final WorkflowInstanceFactory workflowInstanceFactory;
private final WorkflowInstanceService workflowInstanceService;
private final WorkflowUtils workflowUtils;
private final TransactionService transactionService;
@Transactional
public GroupBatch createBatch(GroupBatchCreateRequest request) {
RecipientGroup group = recipientGroupService.get(
request.getRecipientGroupId(), request.getUserId());
List<RecipientGroupMember> members = recipientGroupService.members(
group.getId(), request.getUserId());
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())
.userId(request.getUserId())
.description(request.getDescription())
.requestedBy(request.getRequestedBy())
.currency(request.getCurrency())
.status(GroupBatchStatus.CREATED)
.paymentProcessorLabel(request.getPaymentProcessorLabel())
.paymentProcessorName(request.getPaymentProcessorName())
.paymentProcessorImage(request.getPaymentProcessorImage())
.transactionTemplate(Utils.toJson(request.getTransactionTemplate()))
.build();
GroupBatch savedBatch = groupBatchRepository.save(batch);
BigDecimal defaultAmount = request.getAmount() == null
? request.getTransactionTemplate().getAmount()
: request.getAmount();
BigDecimal totalValue = BigDecimal.ZERO;
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())
.amount(defaultAmount)
.status(GroupBatchItemStatus.NEW)
.confirmStatus(GroupBatchItemConfirmStatus.PENDING)
.integrationStatus(GroupBatchItemIntegrationStatus.PENDING)
.build();
totalValue = totalValue.add(defaultAmount);
itemCount++;
groupBatchItemRepository.save(item);
}
savedBatch.setCount(itemCount);
savedBatch.setValue(totalValue);
savedBatch.setSuccessfulCount(0);
savedBatch.setFailedCount(0);
return groupBatchRepository.save(savedBatch);
}
public GroupBatch triggerConfirm(UUID batchId, String userId, String idempotencyKey) {
GroupBatch batch = getBatch(batchId, userId);
if (batch.getConfirmIdempotencyKey() != null && batch.getConfirmIdempotencyKey().equals(idempotencyKey)) {
return batch;
}
batch.setConfirmIdempotencyKey(idempotencyKey);
batch.setStatus(GroupBatchStatus.CONFIRMING);
groupBatchRepository.save(batch);
if (batch.getWorkflowId() == null) {
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
.setType(BatchWorkflow.TYPE)
.setExternalId(batch.getUserId() + "-batch-" + new Instant().getMillis())
.setBusinessKey(batch.getId().toString())
.putStateVariable("batchId", Utils.toJson(batch.getId().toString()))
.setNextActivation(DateTime.now())
.build();
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
batch.setWorkflowId(workflowId);
return groupBatchRepository.save(batch);
}
WorkflowInstance update = new WorkflowInstance.Builder()
.setId(batch.getWorkflowId())
.setState("confirm")
.setNextActivation(DateTime.now())
.setStateVariables(new HashMap<>() {{
put("batchId", Utils.toJson(batch.getId().toString()));
}})
.setStateText("Resumed from network trigger")
.build();
WorkflowInstanceAction action = new WorkflowInstanceAction.Builder(update)
.setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
.setExecutionEnd(DateTime.now())
.build();
workflowInstanceService.updateWorkflowInstance(update, action);
return batch;
}
public Object triggerRequest(UUID batchId, String userId,
AuthType authType, String idempotencyKey) {
GroupBatch batch = getBatch(batchId, userId);
if (batch.getRequestIdempotencyKey() != null && batch.getRequestIdempotencyKey().equals(idempotencyKey)) {
return batch;
}
batch.setRequestIdempotencyKey(idempotencyKey);
groupBatchRepository.save(batch);
if (batch.getWorkflowId() == null) {
throw new ApiException("Batch workflow has not been started. Trigger confirm first.");
}
WorkflowInstance update = new WorkflowInstance.Builder()
.setId(batch.getWorkflowId())
.setState("request")
.setNextActivation(DateTime.now())
.setStateVariables(new HashMap<>() {{
put("batchId", Utils.toJson(batch.getId().toString()));
put("authType", Utils.toJson((authType == null ? AuthType.REMOTE : authType).name()));
}})
.setStateText("Resumed from network trigger")
.build();
WorkflowInstanceAction action = new WorkflowInstanceAction.Builder(update)
.setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
.setExecutionEnd(DateTime.now())
.build();
workflowInstanceService.updateWorkflowInstance(update, action);
try {
CompletableFuture<Object> future = workflowUtils
.waitForState(batch.getWorkflowId(), new String[]{"requestSuccess", "done", "failed"},
15000, 50);
StateResponse response = (StateResponse) future.get();
String id = response.getBody().toString(); // this returns Batch Transaction ID
String cleaned = id.replaceAll("^\"|\"$", "");
return transactionService.findById(UUID.fromString(cleaned));
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
public Object triggerPoll(UUID batchId, String userId, String idempotencyKey) {
GroupBatch batch = getBatch(batchId, userId);
if (batch.getPollIdempotencyKey() != null && batch.getPollIdempotencyKey().equals(idempotencyKey)) {
return batch;
}
batch.setPollIdempotencyKey(idempotencyKey);
groupBatchRepository.save(batch);
if (batch.getWorkflowId() == null) {
throw new ApiException("Batch workflow has not been started. Trigger confirm first.");
}
WorkflowInstance update = new WorkflowInstance.Builder()
.setId(batch.getWorkflowId())
.setState("poll")
.setNextActivation(DateTime.now())
.setStateVariables(new HashMap<>() {{
put("batchId", Utils.toJson(batch.getId().toString()));
}})
.setStateText("Resumed from network trigger")
.build();
WorkflowInstanceAction action = new WorkflowInstanceAction.Builder(update)
.setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
.setExecutionEnd(DateTime.now())
.build();
workflowInstanceService.updateWorkflowInstance(update, action);
try {
CompletableFuture<Object> future = workflowUtils
.waitForState(batch.getWorkflowId(), new String[]{"done", "failed"},
15000, 50);
StateResponse response = (StateResponse) future.get();
String id = response.getBody().toString(); // this returns Batch Transaction ID
String cleaned = id.replaceAll("^\"|\"$", "");
return transactionService.findById(UUID.fromString(cleaned));
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
public GroupBatch getBatch(UUID batchId, String userId) {
return groupBatchRepository.findByIdAndUserId(batchId, userId)
.orElseThrow(() -> new ApiException("Group batch not found"));
}
public Page<GroupBatchItem> getBatchItems(Specification<GroupBatchItem> spec, Pageable pageable) {
return groupBatchItemRepository.findAll(spec, pageable);
}
public Page<GroupBatch> findAll(Specification<GroupBatch> spec, Pageable pageable) {
return groupBatchRepository.findAll(spec, pageable);
}
@Transactional
public GroupBatch updateBatch(GroupBatchUpdateRequest request) {
GroupBatch batch = getBatch(request.getId(), request.getUserId());
if (!UPDATABLE_STATUSES.contains(batch.getStatus())) {
throw new ApiException(
"Batch cannot be updated at current status: " + batch.getStatus()
+ ". Only batches in CREATED status can be updated."
);
}
if (request.getDescription() != null) {
batch.setDescription(request.getDescription());
}
if (request.getRequestedBy() != null) {
batch.setRequestedBy(request.getRequestedBy());
}
if (request.getCurrency() != null) {
batch.setCurrency(request.getCurrency());
}
if (request.getPaymentProcessorLabel() != null) {
batch.setPaymentProcessorLabel(request.getPaymentProcessorLabel());
}
if (request.getPaymentProcessorName() != null) {
batch.setPaymentProcessorName(request.getPaymentProcessorName());
}
if (request.getPaymentProcessorImage() != null) {
batch.setPaymentProcessorImage(request.getPaymentProcessorImage());
}
if (request.getTransactionTemplate() != null) {
batch.setTransactionTemplate(Utils.toJson(request.getTransactionTemplate()));
}
if (request.getAmount() != null) {
BigDecimal newAmount = request.getAmount();
BigDecimal newTotalValue = BigDecimal.ZERO;
int newCount = 0;
List<GroupBatchItem> items = groupBatchItemRepository.findAllByGroupBatchId(batch.getId());
for (GroupBatchItem item : items) {
item.setAmount(newAmount);
groupBatchItemRepository.save(item);
newTotalValue = newTotalValue.add(newAmount);
newCount++;
}
batch.setValue(newTotalValue);
batch.setCount(newCount);
}
return groupBatchRepository.save(batch);
}
@Transactional
public void deleteBatch(UUID batchId, String userId) {
GroupBatch batch = getBatch(batchId, userId);
if (!DELETABLE_STATUSES.contains(batch.getStatus())) {
throw new ApiException(
"Batch cannot be deleted at current status: " + batch.getStatus()
+ ". Only batches up to the confirmed stage can be deleted."
);
}
batch.setDeleted(true);
groupBatchRepository.save(batch);
}
}