From 1987c1855e4cf8306f7610c7c4cf85aa4b959882 Mon Sep 17 00:00:00 2001 From: Vus Khoza Date: Wed, 10 Sep 2025 23:28:19 +0200 Subject: [PATCH] resolved workflow issues --- pom.xml | 19 ++-- .../zw/qantra/tm/configs/JacksonConfig.java | 21 ++++ .../zw/qantra/tm/configs/SecurityConfig.java | 6 +- .../tm/domain/controllers/AuthController.java | 29 ++++- .../qantra/tm/domain/dtos/AuthResponse.java | 2 + .../tm/domain/dtos/PasswordRequest.java | 17 +++ .../tm/domain/dtos/RegisterRequest.java | 2 - .../qantra/tm/domain/models/BaseEntity.java | 3 + .../workflows/RegistrationWorkflow.java | 100 ++++++++++++++++++ .../tm/domain/workflows/WorkflowUtils.java | 43 ++++++++ src/main/resources/application-lab.properties | 4 + 11 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 src/main/java/zw/qantra/tm/configs/JacksonConfig.java create mode 100644 src/main/java/zw/qantra/tm/domain/dtos/PasswordRequest.java create mode 100644 src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java create mode 100644 src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java diff --git a/pom.xml b/pom.xml index 11ad67c..5c76030 100644 --- a/pom.xml +++ b/pom.xml @@ -99,13 +99,17 @@ 20231013 - org.postgresql - postgresql - runtime + joda-time + joda-time + 2.12.5 - com.h2database - h2 + com.fasterxml.jackson.datatype + jackson-datatype-joda + + + org.postgresql + postgresql runtime @@ -143,7 +147,10 @@ nflow-rest-api-spring-web 10.0.1 - + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + diff --git a/src/main/java/zw/qantra/tm/configs/JacksonConfig.java b/src/main/java/zw/qantra/tm/configs/JacksonConfig.java new file mode 100644 index 0000000..bca58ba --- /dev/null +++ b/src/main/java/zw/qantra/tm/configs/JacksonConfig.java @@ -0,0 +1,21 @@ +package zw.qantra.tm.configs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.joda.JodaModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration +public class JacksonConfig { + + @Bean + @Primary + public ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.registerModule(new JodaModule()); + return objectMapper; + } +} diff --git a/src/main/java/zw/qantra/tm/configs/SecurityConfig.java b/src/main/java/zw/qantra/tm/configs/SecurityConfig.java index 7f460b0..a403342 100644 --- a/src/main/java/zw/qantra/tm/configs/SecurityConfig.java +++ b/src/main/java/zw/qantra/tm/configs/SecurityConfig.java @@ -29,9 +29,9 @@ public class SecurityConfig { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers("/explorer/**").permitAll() - .requestMatchers("/nflow/**").permitAll() - .requestMatchers("/auth/**").permitAll() + .requestMatchers("/explorer/**").permitAll() + .requestMatchers("/nflow/**").permitAll() + .requestMatchers("/auth/**").permitAll() .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ) diff --git a/src/main/java/zw/qantra/tm/domain/controllers/AuthController.java b/src/main/java/zw/qantra/tm/domain/controllers/AuthController.java index 238c02c..930c05c 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/AuthController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/AuthController.java @@ -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 register(@Valid @RequestBody RegisterRequest request) { - AuthResponse response = userService.register(request); - return ResponseEntity.ok(response); + public ResponseEntity 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 future = workflowUtils + .waitForState(workflowId, "sendOtp", 30000, 1000); + future.get(); + return ResponseEntity.ok(Utils.toJson(future)); } @PostMapping("/login") diff --git a/src/main/java/zw/qantra/tm/domain/dtos/AuthResponse.java b/src/main/java/zw/qantra/tm/domain/dtos/AuthResponse.java index 04779c1..ebff706 100644 --- a/src/main/java/zw/qantra/tm/domain/dtos/AuthResponse.java +++ b/src/main/java/zw/qantra/tm/domain/dtos/AuthResponse.java @@ -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"; diff --git a/src/main/java/zw/qantra/tm/domain/dtos/PasswordRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/PasswordRequest.java new file mode 100644 index 0000000..d823019 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/PasswordRequest.java @@ -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; +} diff --git a/src/main/java/zw/qantra/tm/domain/dtos/RegisterRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/RegisterRequest.java index 7096cd1..1721ecc 100644 --- a/src/main/java/zw/qantra/tm/domain/dtos/RegisterRequest.java +++ b/src/main/java/zw/qantra/tm/domain/dtos/RegisterRequest.java @@ -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") diff --git a/src/main/java/zw/qantra/tm/domain/models/BaseEntity.java b/src/main/java/zw/qantra/tm/domain/models/BaseEntity.java index fc922cd..2e3b0f8 100644 --- a/src/main/java/zw/qantra/tm/domain/models/BaseEntity.java +++ b/src/main/java/zw/qantra/tm/domain/models/BaseEntity.java @@ -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 } diff --git a/src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java b/src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java new file mode 100644 index 0000000..41f6910 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java @@ -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> violations = validator.validate(payload); + + if (!violations.isEmpty()) { + StringBuilder errorMessage = new StringBuilder("Validation failed: "); + for (ConstraintViolation 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()); + } + + +} diff --git a/src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java b/src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java new file mode 100644 index 0000000..f3197a5 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java @@ -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 waitForState(long workflowId, String state, int timeout, int pollingInterval) { + CompletableFuture 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; + } +} + diff --git a/src/main/resources/application-lab.properties b/src/main/resources/application-lab.properties index 3f6de79..986f916 100644 --- a/src/main/resources/application-lab.properties +++ b/src/main/resources/application-lab.properties @@ -43,3 +43,7 @@ erpnext.tax.account=VAT - QPAY erpnext.tax.rate=15.0 logging.level.root=WARN +nflow.db.postgresql.driver=org.postgresql.Driver +nflow.db.postgresql.url=jdbc:postgresql://173.212.247.232:5532/qpay +nflow.db.postgresql.user=postgres +nflow.db.postgresql.password=zdDZMzq6F4B4L1IUl