resolved workflow issues

This commit is contained in:
2025-09-10 23:28:19 +02:00
parent eb27468ddb
commit 1987c1855e
11 changed files with 232 additions and 14 deletions

View File

@@ -1,5 +1,7 @@
package zw.qantra.tm.domain.controllers;
import io.nflow.engine.service.WorkflowInstanceService;
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
@@ -8,6 +10,12 @@ import zw.qantra.tm.domain.dtos.AuthResponse;
import zw.qantra.tm.domain.dtos.LoginRequest;
import zw.qantra.tm.domain.dtos.RegisterRequest;
import zw.qantra.tm.domain.services.UserService;
import zw.qantra.tm.domain.workflows.RegistrationWorkflow;
import zw.qantra.tm.domain.workflows.WorkflowUtils;
import zw.qantra.tm.utils.Utils;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@RestController
@RequestMapping("/auth")
@@ -16,11 +24,26 @@ import zw.qantra.tm.domain.services.UserService;
public class AuthController {
private final UserService userService;
private final WorkflowInstanceService workflowService;
private final WorkflowInstanceFactory workflowInstanceFactory;
private final WorkflowUtils workflowUtils;
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
AuthResponse response = userService.register(request);
return ResponseEntity.ok(response);
public ResponseEntity<Object> register(@Valid @RequestBody RegisterRequest request)
throws ExecutionException, InterruptedException {
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
.setType(RegistrationWorkflow.TYPE)
.setExternalId(request.getUsername() + "-" + System.currentTimeMillis())
.putStateVariable("metadata", request)
.build();
long workflowId = workflowService.insertWorkflowInstance(instance);
CompletableFuture<Object> future = workflowUtils
.waitForState(workflowId, "sendOtp", 30000, 1000);
future.get();
return ResponseEntity.ok(Utils.toJson(future));
}
@PostMapping("/login")

View File

@@ -2,9 +2,11 @@ package zw.qantra.tm.domain.dtos;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthResponse {
private String token;
private String type = "Bearer";

View File

@@ -0,0 +1,17 @@
package zw.qantra.tm.domain.dtos;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class PasswordRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
}

View File

@@ -11,8 +11,6 @@ public class RegisterRequest {
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
@NotBlank(message = "Email is required")

View File

@@ -1,5 +1,6 @@
package zw.qantra.tm.domain.models;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@@ -31,10 +32,12 @@ public class BaseEntity {
@CreationTimestamp
@Column(name = "created_at", nullable = true, updatable = false)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
private LocalDateTime createdAt; // Or Date, Timestamp
@UpdateTimestamp
@Column(name = "updated_at", nullable = true)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
private LocalDateTime updatedAt; // Or Date, Timestamp
}

View File

@@ -0,0 +1,100 @@
package zw.qantra.tm.domain.workflows;
import io.nflow.engine.workflow.definition.WorkflowStateType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import io.nflow.engine.workflow.definition.NextAction;
import io.nflow.engine.workflow.definition.StateExecution;
import io.nflow.engine.workflow.definition.WorkflowDefinition;
import zw.qantra.tm.domain.services.UserService;
import io.nflow.engine.workflow.curated.State;
import zw.qantra.tm.domain.dtos.AuthResponse;
import zw.qantra.tm.domain.dtos.RegisterRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import java.util.Set;
@Component
public class RegistrationWorkflow extends WorkflowDefinition {
public static final String TYPE = "registrationWorkflow";
public static final State BEGIN = new State("begin", WorkflowStateType.start);
public static final State REGISTER = new State("register", WorkflowStateType.normal);
public static final State SEND_OTP = new State("sendOtp", WorkflowStateType.manual);
public static final State VERIFY_OTP = new State("verifyOtp", WorkflowStateType.normal);
public static final State RESEND_OTP = new State("resendOtp", WorkflowStateType.manual);
public static final State SET_PIN = new State("setPin", WorkflowStateType.normal);
public static final State UPDATE_ERP = new State("updateErp", WorkflowStateType.normal);
public static final State FAILED = new State("failed", WorkflowStateType.manual);
public static final State DONE = new State("done", WorkflowStateType.end);
@Autowired
private UserService userService;
@Autowired
private Validator validator;
public RegistrationWorkflow() {
super(TYPE, BEGIN, FAILED);
permit(BEGIN, REGISTER);
permit(REGISTER, SEND_OTP);
permit(SEND_OTP, VERIFY_OTP, RESEND_OTP);
permit(RESEND_OTP, VERIFY_OTP, FAILED);
permit(VERIFY_OTP, SET_PIN, FAILED);
permit(SET_PIN, UPDATE_ERP);
permit(UPDATE_ERP, DONE);
}
public NextAction begin(StateExecution execution) {
return NextAction.moveToState(REGISTER, "Registration started");
}
// register validates most of the initial registration details
public NextAction register(StateExecution execution) {
RegisterRequest payload = execution.getVariable("metadata", RegisterRequest.class);
if(payload == null)
return NextAction.moveToState(FAILED, "No payload provided");
// Validate the payload using javax validation annotations
Set<ConstraintViolation<RegisterRequest>> violations = validator.validate(payload);
if (!violations.isEmpty()) {
StringBuilder errorMessage = new StringBuilder("Validation failed: ");
for (ConstraintViolation<RegisterRequest> violation : violations) {
errorMessage.append(violation.getMessage()).append("; ");
}
return NextAction.moveToState(FAILED, errorMessage.toString());
}
// todo: add this to the appropriate action
// AuthResponse response = userService.register(payload);
execution.setVariable("metadata", payload);
return NextAction.moveToState(SEND_OTP, "Initial registration success for: " + payload.getUsername());
}
public void sendOtp(StateExecution execution) {
RegisterRequest response = execution.getVariable("metadata", RegisterRequest.class);
}
public NextAction verifyOtp(StateExecution execution) {
return NextAction.stopInState(SET_PIN, "OTP verified successfully for: " );
}
public NextAction setPin(StateExecution execution) {
RegisterRequest response = execution.getVariable("response", RegisterRequest.class);
return NextAction.stopInState(UPDATE_ERP, "PIN set successfully for: " + response.getUsername());
}
public NextAction updateErp(StateExecution execution) {
RegisterRequest response = execution.getVariable("response", RegisterRequest.class);
return NextAction.stopInState(DONE, "ERP updated successfully for: " + response.getUsername());
}
}

View File

@@ -0,0 +1,43 @@
package zw.qantra.tm.domain.workflows;
import java.util.EnumSet;
import java.util.concurrent.CompletableFuture;
import io.nflow.engine.service.WorkflowInstanceInclude;
import org.springframework.stereotype.Component;
import io.nflow.engine.service.WorkflowInstanceService;
import io.nflow.engine.workflow.instance.WorkflowInstance;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class WorkflowUtils {
private final WorkflowInstanceService workflowInstanceService;
public CompletableFuture<Object> waitForState(long workflowId, String state, int timeout, int pollingInterval) {
CompletableFuture<Object> future = new CompletableFuture<>();
while(!future.isDone()){ // check if the future is done
WorkflowInstance instance = workflowInstanceService.getWorkflowInstance(
workflowId, EnumSet.of(WorkflowInstanceInclude.ACTIONS), 1L);
try {
if(instance.state.equalsIgnoreCase(state)){
future.complete(instance.getStateVariable("metadata"));
}
Thread.sleep(pollingInterval);
} catch (InterruptedException e) {
future.completeExceptionally(e);
}
timeout -= pollingInterval;
if(timeout <= 0){
future.complete(instance);
}
}
return future;
}
}