completed group batch feature
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
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);
|
||||
future.get();
|
||||
return getBatch(batchId, userId);
|
||||
} 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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user