346 lines
14 KiB
Java
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);
|
|
}
|
|
|
|
}
|