completed nflow integration into onboarding
This commit is contained in:
11
pom.xml
11
pom.xml
@@ -157,10 +157,15 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
<artifactId>jackson-datatype-joda</artifactId>
|
<artifactId>jackson-datatype-jdk8</artifactId>
|
||||||
<version>2.13.1</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
<dependency>
|
||||||
|
<groupId>com.googlecode.libphonenumber</groupId>
|
||||||
|
<artifactId>libphonenumber</artifactId>
|
||||||
|
<version>8.13.36</version> <!-- Use the latest version -->
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package zw.qantra.tm.configs;
|
package zw.qantra.tm.configs;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import com.fasterxml.jackson.datatype.joda.JodaModule;
|
import com.fasterxml.jackson.datatype.joda.JodaModule;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -16,6 +17,7 @@ public class JacksonConfig {
|
|||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
objectMapper.registerModule(new JavaTimeModule());
|
objectMapper.registerModule(new JavaTimeModule());
|
||||||
objectMapper.registerModule(new JodaModule());
|
objectMapper.registerModule(new JodaModule());
|
||||||
|
objectMapper.registerModule(new Jdk8Module());
|
||||||
|
|
||||||
return objectMapper;
|
return objectMapper;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
package zw.qantra.tm.domain.controllers;
|
package zw.qantra.tm.domain.controllers;
|
||||||
|
|
||||||
import io.nflow.engine.service.WorkflowInstanceInclude;
|
import io.nflow.engine.internal.dao.WorkflowInstanceDao;
|
||||||
import io.nflow.engine.service.WorkflowInstanceService;
|
import io.nflow.engine.service.WorkflowInstanceService;
|
||||||
|
import io.nflow.engine.workflow.instance.QueryWorkflowInstances;
|
||||||
import io.nflow.engine.workflow.instance.WorkflowInstance;
|
import io.nflow.engine.workflow.instance.WorkflowInstance;
|
||||||
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
||||||
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
|
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
import org.joda.time.Instant;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import zw.qantra.tm.domain.dtos.AuthResponse;
|
import zw.qantra.tm.domain.dtos.*;
|
||||||
import zw.qantra.tm.domain.dtos.LoginRequest;
|
import zw.qantra.tm.domain.services.OtpService;
|
||||||
import zw.qantra.tm.domain.dtos.RegisterRequest;
|
|
||||||
import zw.qantra.tm.domain.dtos.VerifyRequest;
|
|
||||||
import zw.qantra.tm.domain.services.UserService;
|
import zw.qantra.tm.domain.services.UserService;
|
||||||
import zw.qantra.tm.domain.workflows.RegistrationWorkflow;
|
import zw.qantra.tm.domain.workflows.RegistrationWorkflow;
|
||||||
import zw.qantra.tm.domain.workflows.WorkflowUtils;
|
import zw.qantra.tm.domain.workflows.WorkflowUtils;
|
||||||
import zw.qantra.tm.utils.Utils;
|
import zw.qantra.tm.utils.Utils;
|
||||||
|
|
||||||
import java.util.EnumSet;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
@@ -34,20 +33,46 @@ public class OnboardingController {
|
|||||||
private final WorkflowInstanceService workflowInstanceService;
|
private final WorkflowInstanceService workflowInstanceService;
|
||||||
private final WorkflowInstanceFactory workflowInstanceFactory;
|
private final WorkflowInstanceFactory workflowInstanceFactory;
|
||||||
private final WorkflowUtils workflowUtils;
|
private final WorkflowUtils workflowUtils;
|
||||||
|
private final OtpService otpService;
|
||||||
|
|
||||||
|
@PostMapping("/check/{username}")
|
||||||
|
public ResponseEntity check(@PathVariable String username) {
|
||||||
|
QueryWorkflowInstances query = new QueryWorkflowInstances.Builder()
|
||||||
|
.setExternalId(username)
|
||||||
|
.setIncludeCurrentStateVariables(true)
|
||||||
|
.setIncludeActions(true)
|
||||||
|
.build();
|
||||||
|
List<WorkflowInstance> workflowInstanceList =
|
||||||
|
new ArrayList<>(workflowInstanceService.listWorkflowInstances(query));
|
||||||
|
if(!workflowInstanceList.isEmpty()){
|
||||||
|
WorkflowInstance instance = workflowInstanceList.get(0);
|
||||||
|
WorkflowInstanceAction action = instance.actions.get(0);
|
||||||
|
WorkflowStatusDto statusDto = new WorkflowStatusDto();
|
||||||
|
statusDto.setWorkflowId(instance.id);
|
||||||
|
statusDto.setState(instance.state);
|
||||||
|
statusDto.setStateText(instance.stateText);
|
||||||
|
statusDto.setStatus(instance.status.toString());
|
||||||
|
statusDto.setStage(action.state);
|
||||||
|
return ResponseEntity.ok(statusDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<Object> register(@Valid @RequestBody RegisterRequest request)
|
public ResponseEntity<Object> register(@Valid @RequestBody RegisterRequest request)
|
||||||
throws ExecutionException, InterruptedException {
|
throws ExecutionException, InterruptedException {
|
||||||
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
||||||
.setType(RegistrationWorkflow.TYPE)
|
.setType(RegistrationWorkflow.TYPE)
|
||||||
.setExternalId(request.getUsername() + "-" + System.currentTimeMillis())
|
.setExternalId(request.getUsername() + "-" + new Instant().getMillis())
|
||||||
.putStateVariable("body", request)
|
.putStateVariable("body", request)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
|
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
|
||||||
|
|
||||||
CompletableFuture<Object> future = workflowUtils
|
CompletableFuture<Object> future = workflowUtils
|
||||||
.waitForState(workflowId, "sendOtp", 30000, 1000);
|
.waitForState(workflowId, new String[]{"sendOtp", "failed"},
|
||||||
|
30000, 1000);
|
||||||
Object response = future.get();
|
Object response = future.get();
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
@@ -56,6 +81,7 @@ public class OnboardingController {
|
|||||||
@PostMapping("/verify")
|
@PostMapping("/verify")
|
||||||
public ResponseEntity<Object> verify(@Valid @RequestBody VerifyRequest request)
|
public ResponseEntity<Object> verify(@Valid @RequestBody VerifyRequest request)
|
||||||
throws ExecutionException, InterruptedException {
|
throws ExecutionException, InterruptedException {
|
||||||
|
|
||||||
// Create an update to move the workflow to the next state
|
// Create an update to move the workflow to the next state
|
||||||
WorkflowInstance update = new WorkflowInstance.Builder()
|
WorkflowInstance update = new WorkflowInstance.Builder()
|
||||||
.setId(request.getWorkflowId())
|
.setId(request.getWorkflowId())
|
||||||
@@ -74,14 +100,43 @@ public class OnboardingController {
|
|||||||
workflowInstanceService.updateWorkflowInstance(update, action);
|
workflowInstanceService.updateWorkflowInstance(update, action);
|
||||||
|
|
||||||
CompletableFuture<Object> future = workflowUtils
|
CompletableFuture<Object> future = workflowUtils
|
||||||
.waitForState(request.getWorkflowId(), "verifyOtp", 30000, 1000);
|
.waitForState(request.getWorkflowId(), new String[]{"done", "failed"},
|
||||||
|
30000, 50);
|
||||||
|
Object response = future.get();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/resend")
|
||||||
|
public ResponseEntity<Object> resend(@RequestBody OtpRequest otpRequest)
|
||||||
|
throws ExecutionException, InterruptedException {
|
||||||
|
// Create an update to move the workflow to the next state
|
||||||
|
WorkflowInstance update = new WorkflowInstance.Builder()
|
||||||
|
.setId(otpRequest.getWorkflowId())
|
||||||
|
.setState("resendOtp") // The state you want to transition to
|
||||||
|
.setNextActivation(DateTime.now()) // Schedule immediate execution
|
||||||
|
.setStateVariables(new HashMap<>(){{ put("body", Utils.toJson(otpRequest)); }})
|
||||||
|
.setStateText("Resumed from manual state")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WorkflowInstanceAction action = new WorkflowInstanceAction.Builder(update)
|
||||||
|
.setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
|
||||||
|
.setExecutionEnd(DateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Apply the update
|
||||||
|
workflowInstanceService.updateWorkflowInstance(update, action);
|
||||||
|
|
||||||
|
CompletableFuture<Object> future = workflowUtils
|
||||||
|
.waitForState(otpRequest.getWorkflowId(), new String[]{"resendOtp", "failed"},
|
||||||
|
30000, 50);
|
||||||
Object response = future.get();
|
Object response = future.get();
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
|
public ResponseEntity<Object> login(@Valid @RequestBody LoginRequest request) {
|
||||||
AuthResponse response = userService.login(request);
|
AuthResponse response = userService.login(request);
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/main/java/zw/qantra/tm/domain/dtos/OtpRequest.java
Normal file
10
src/main/java/zw/qantra/tm/domain/dtos/OtpRequest.java
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class OtpRequest {
|
||||||
|
private String username;
|
||||||
|
private String phone;
|
||||||
|
private Long workflowId;
|
||||||
|
}
|
||||||
@@ -14,4 +14,5 @@ public class PasswordRequest {
|
|||||||
@NotBlank(message = "Password is required")
|
@NotBlank(message = "Password is required")
|
||||||
@Size(min = 6, message = "Password must be at least 6 characters")
|
@Size(min = 6, message = "Password must be at least 6 characters")
|
||||||
private String password;
|
private String password;
|
||||||
|
private Long workflowId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ public class RegisterRequest {
|
|||||||
@NotBlank(message = "Username is required")
|
@NotBlank(message = "Username is required")
|
||||||
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
|
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
private String password;
|
private String password;
|
||||||
|
private String photoUrl;
|
||||||
|
private Long workflowId;
|
||||||
|
|
||||||
@NotBlank(message = "Email is required")
|
@NotBlank(message = "Email is required")
|
||||||
@Email(message = "Email should be valid")
|
@Email(message = "Email should be valid")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import lombok.NoArgsConstructor;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class StateResponse {
|
public class StateResponse {
|
||||||
private String state;
|
private String state;
|
||||||
|
private String status;
|
||||||
private Object body;
|
private Object body;
|
||||||
private String message;
|
private String message;
|
||||||
private String workflowId;
|
private String workflowId;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class WorkflowStatusDto {
|
||||||
|
private Long workflowId;
|
||||||
|
private String state;
|
||||||
|
private String stateText;
|
||||||
|
private String status;
|
||||||
|
private String stage;
|
||||||
|
}
|
||||||
38
src/main/java/zw/qantra/tm/domain/dtos/erp/CustomerDto.java
Normal file
38
src/main/java/zw/qantra/tm/domain/dtos/erp/CustomerDto.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos.erp;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class CustomerDto {
|
||||||
|
@JsonProperty("doctype")
|
||||||
|
private String doctype;
|
||||||
|
|
||||||
|
@JsonProperty("customer_name")
|
||||||
|
private String customerName;
|
||||||
|
|
||||||
|
@JsonProperty("customer_group")
|
||||||
|
private String customerGroup;
|
||||||
|
|
||||||
|
@JsonProperty("territory")
|
||||||
|
private String territory;
|
||||||
|
|
||||||
|
@JsonProperty("customer_type")
|
||||||
|
private String customerType;
|
||||||
|
|
||||||
|
@JsonProperty("language")
|
||||||
|
private String language;
|
||||||
|
|
||||||
|
@JsonProperty("mobile_no")
|
||||||
|
private String mobileNo;
|
||||||
|
|
||||||
|
@JsonProperty("email_id")
|
||||||
|
private String emailId;
|
||||||
|
|
||||||
|
@JsonProperty("docstatus")
|
||||||
|
private Integer docstatus;
|
||||||
|
}
|
||||||
@@ -25,6 +25,9 @@ public class User extends BaseEntity implements UserDetails {
|
|||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
private String email;
|
private String email;
|
||||||
private String phone;
|
private String phone;
|
||||||
|
private String erpCustomerRef;
|
||||||
|
private String photoUrl;
|
||||||
|
private Long workflowId;
|
||||||
|
|
||||||
@Column(name = "first_name")
|
@Column(name = "first_name")
|
||||||
private String firstName;
|
private String firstName;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package zw.qantra.tm.domain.services;
|
package zw.qantra.tm.domain.services;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
@@ -17,10 +18,16 @@ import zw.qantra.tm.utils.JwtUtils;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserService implements UserDetailsService {
|
public class UserService implements UserDetailsService {
|
||||||
|
|
||||||
|
@Getter
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final JwtUtils jwtUtils;
|
private final JwtUtils jwtUtils;
|
||||||
|
|
||||||
|
public User fetchUserByUsername(String username) {
|
||||||
|
return userRepository.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
return userRepository.findByUsername(username)
|
return userRepository.findByUsername(username)
|
||||||
@@ -43,6 +50,8 @@ public class UserService implements UserDetailsService {
|
|||||||
user.setFirstName(request.getFirstName());
|
user.setFirstName(request.getFirstName());
|
||||||
user.setLastName(request.getLastName());
|
user.setLastName(request.getLastName());
|
||||||
user.setPhone(request.getPhone());
|
user.setPhone(request.getPhone());
|
||||||
|
user.setWorkflowId(request.getWorkflowId());
|
||||||
|
user.setPhotoUrl(request.getPhotoUrl());
|
||||||
|
|
||||||
User savedUser = userRepository.save(user);
|
User savedUser = userRepository.save(user);
|
||||||
String token = jwtUtils.generateToken(savedUser);
|
String token = jwtUtils.generateToken(savedUser);
|
||||||
|
|||||||
@@ -1,29 +1,37 @@
|
|||||||
package zw.qantra.tm.domain.workflows;
|
package zw.qantra.tm.domain.workflows;
|
||||||
|
|
||||||
|
import com.google.i18n.phonenumbers.NumberParseException;
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
import com.google.i18n.phonenumbers.Phonenumber;
|
||||||
import io.nflow.engine.workflow.definition.WorkflowStateType;
|
import io.nflow.engine.workflow.definition.WorkflowStateType;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import io.nflow.engine.workflow.definition.NextAction;
|
import io.nflow.engine.workflow.definition.NextAction;
|
||||||
import io.nflow.engine.workflow.definition.StateExecution;
|
import io.nflow.engine.workflow.definition.StateExecution;
|
||||||
import io.nflow.engine.workflow.definition.WorkflowDefinition;
|
import io.nflow.engine.workflow.definition.WorkflowDefinition;
|
||||||
import zw.qantra.tm.domain.dtos.EmailRequest;
|
import zw.qantra.tm.domain.dtos.*;
|
||||||
import zw.qantra.tm.domain.dtos.VerifyRequest;
|
import zw.qantra.tm.domain.dtos.erp.CustomerDto;
|
||||||
import zw.qantra.tm.domain.models.Otp;
|
import zw.qantra.tm.domain.models.Otp;
|
||||||
|
import zw.qantra.tm.domain.models.User;
|
||||||
import zw.qantra.tm.domain.services.OtpService;
|
import zw.qantra.tm.domain.services.OtpService;
|
||||||
import zw.qantra.tm.domain.services.UserService;
|
import zw.qantra.tm.domain.services.UserService;
|
||||||
import io.nflow.engine.workflow.curated.State;
|
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.ConstraintViolation;
|
||||||
import jakarta.validation.Validator;
|
import jakarta.validation.Validator;
|
||||||
import zw.qantra.tm.rest.RestService;
|
import zw.qantra.tm.rest.RestService;
|
||||||
|
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -43,6 +51,16 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
private String infobipUrl;
|
private String infobipUrl;
|
||||||
@Value("${infobip.apikey}")
|
@Value("${infobip.apikey}")
|
||||||
private String infobipApiKey;
|
private String infobipApiKey;
|
||||||
|
@Value("${erpnext.url}")
|
||||||
|
private String erpnextUrl;
|
||||||
|
@Value("${erpnext.api.key}")
|
||||||
|
private String erpnextApiKey;
|
||||||
|
@Value("${erpnext.api.secret}")
|
||||||
|
private String erpnextApiSecret;
|
||||||
|
@Value("${erpnext.username}")
|
||||||
|
private String erpnextUsername;
|
||||||
|
@Value("${erpnext.password}")
|
||||||
|
private String erpnextPassword;
|
||||||
|
|
||||||
public static final String TYPE = "registrationWorkflow";
|
public static final String TYPE = "registrationWorkflow";
|
||||||
|
|
||||||
@@ -50,10 +68,9 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
public static final State REGISTER = new State("register");
|
public static final State REGISTER = new State("register");
|
||||||
public static final State SEND_OTP = new State("sendOtp", WorkflowStateType.manual);
|
public static final State SEND_OTP = new State("sendOtp", WorkflowStateType.manual);
|
||||||
public static final State VERIFY_OTP = new State("verifyOtp");
|
public static final State VERIFY_OTP = new State("verifyOtp");
|
||||||
public static final State OTP_SUCCESS = new State("otpSuccess", WorkflowStateType.manual);
|
|
||||||
public static final State RESEND_OTP = new State("resendOtp", WorkflowStateType.manual);
|
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 OTP_SUCCESS = new State("otpSuccess");
|
||||||
public static final State UPDATE_ERP = new State("updateErp", WorkflowStateType.normal);
|
public static final State UPDATE_ERP = new State("updateErp");
|
||||||
public static final State FAILED = new State("failed", WorkflowStateType.manual);
|
public static final State FAILED = new State("failed", WorkflowStateType.manual);
|
||||||
public static final State DONE = new State("done", WorkflowStateType.end);
|
public static final State DONE = new State("done", WorkflowStateType.end);
|
||||||
|
|
||||||
@@ -63,10 +80,9 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
permit(BEGIN, REGISTER);
|
permit(BEGIN, REGISTER);
|
||||||
permit(REGISTER, SEND_OTP);
|
permit(REGISTER, SEND_OTP);
|
||||||
permit(SEND_OTP, VERIFY_OTP);
|
permit(SEND_OTP, VERIFY_OTP);
|
||||||
|
permit(RESEND_OTP, VERIFY_OTP);
|
||||||
permit(VERIFY_OTP, OTP_SUCCESS, FAILED);
|
permit(VERIFY_OTP, OTP_SUCCESS, FAILED);
|
||||||
permit(RESEND_OTP, OTP_SUCCESS, FAILED);
|
permit(OTP_SUCCESS, UPDATE_ERP, FAILED);
|
||||||
permit(OTP_SUCCESS, SET_PIN, FAILED);
|
|
||||||
permit(SET_PIN, UPDATE_ERP);
|
|
||||||
permit(UPDATE_ERP, DONE);
|
permit(UPDATE_ERP, DONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +109,19 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
return NextAction.moveToState(FAILED, errorMessage.toString());
|
return NextAction.moveToState(FAILED, errorMessage.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: add this to the appropriate action
|
// update phone number format
|
||||||
|
PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
|
||||||
|
try {
|
||||||
|
String phone = payload.getPhone();
|
||||||
|
Phonenumber.PhoneNumber zimProto = phoneUtil.parse(phone, "ZW");
|
||||||
|
phone = String.valueOf(zimProto.getCountryCode()) + String.valueOf(zimProto.getNationalNumber());
|
||||||
|
payload.setPhone(phone);
|
||||||
|
} catch (NumberParseException e) {
|
||||||
|
System.err.println("NumberParseException was thrown: " + e.toString());
|
||||||
|
return NextAction.moveToState(FAILED, "Invalid phone number");
|
||||||
|
}
|
||||||
|
|
||||||
|
execution.setVariable("registerRequest", payload);
|
||||||
execution.setVariable("body", payload);
|
execution.setVariable("body", payload);
|
||||||
|
|
||||||
return NextAction.moveToState(SEND_OTP, "Initial registration success for: " + payload.getUsername());
|
return NextAction.moveToState(SEND_OTP, "Initial registration success for: " + payload.getUsername());
|
||||||
@@ -117,6 +145,7 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
|
|
||||||
log.info("otp generated successfully: {}", otp.getOtp());
|
log.info("otp generated successfully: {}", otp.getOtp());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
log.error("Error sending OTP: {}", e.getMessage(), e);
|
log.error("Error sending OTP: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,18 +161,93 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
return NextAction.moveToState(OTP_SUCCESS, "OTP verified successfully");
|
return NextAction.moveToState(OTP_SUCCESS, "OTP verified successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void otpSuccess(StateExecution execution) {
|
public void resendOtp(StateExecution execution) {
|
||||||
log.info("OTP verified successfully");
|
OtpRequest otpRequest = execution.getVariable("body", OtpRequest.class);
|
||||||
|
Otp otp = otpService.generateOtp(otpRequest.getUsername(), "REGISTRATION");
|
||||||
|
String string = getSMSString(otpRequest.getPhone(), otp.getOtp());
|
||||||
|
|
||||||
|
log.info("otp generated successfully: {}", otp.getOtp());
|
||||||
}
|
}
|
||||||
|
|
||||||
public NextAction setPin(StateExecution execution) {
|
public NextAction otpSuccess(StateExecution execution) {
|
||||||
RegisterRequest response = execution.getVariable("body", RegisterRequest.class);
|
log.info("OTP verified successfully");
|
||||||
return NextAction.stopInState(UPDATE_ERP, "PIN set successfully for: " + response.getUsername());
|
log.info("Begin user registration");
|
||||||
|
|
||||||
|
VerifyRequest request = execution.getVariable("body", VerifyRequest.class);
|
||||||
|
try {
|
||||||
|
RegisterRequest registerRequest = execution.getVariable("registerRequest", RegisterRequest.class);
|
||||||
|
|
||||||
|
registerRequest.setWorkflowId(request.getWorkflowId());
|
||||||
|
execution.setVariable("registerRequest", registerRequest);
|
||||||
|
|
||||||
|
userService.register(registerRequest);
|
||||||
|
}catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return NextAction.moveToState(FAILED, "Failed to register user: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextAction.moveToState(UPDATE_ERP, "User creation successfully for: " + request.getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
public NextAction updateErp(StateExecution execution) {
|
public NextAction updateErp(StateExecution execution) {
|
||||||
RegisterRequest response = execution.getVariable("body", RegisterRequest.class);
|
RegisterRequest registerRequest = execution.getVariable("registerRequest", RegisterRequest.class);
|
||||||
return NextAction.stopInState(DONE, "ERP updated successfully for: " + response.getUsername());
|
|
||||||
|
// login to erp
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.add("Content-Type", "application/json");
|
||||||
|
headers.add("Accept", "application/json");
|
||||||
|
|
||||||
|
Map<String, String> loginRequest = Map.of(
|
||||||
|
"usr", erpnextUsername,
|
||||||
|
"pwd", erpnextPassword);
|
||||||
|
|
||||||
|
String sid = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
ResponseEntity response = restService.postAsEntity(
|
||||||
|
erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class);
|
||||||
|
|
||||||
|
sid = response.getHeaders().getFirst("Set-Cookie");
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return NextAction.retryAfter(DateTime.now().plusSeconds(30), "Failed to login to ERP: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// create customer in erp
|
||||||
|
String basicAuth = "Basic " + Base64.getEncoder().encodeToString(
|
||||||
|
(erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8));
|
||||||
|
headers.add("Authorization", basicAuth);
|
||||||
|
headers.add("Cookie", sid);
|
||||||
|
|
||||||
|
CustomerDto customerDto = CustomerDto.builder()
|
||||||
|
.doctype("Customer")
|
||||||
|
.customerName(registerRequest.getFirstName() + " " + registerRequest.getLastName())
|
||||||
|
.customerGroup("Commercial")
|
||||||
|
.territory("All Territories")
|
||||||
|
.customerType("Individual")
|
||||||
|
.language("en")
|
||||||
|
.mobileNo(registerRequest.getPhone())
|
||||||
|
.emailId(registerRequest.getEmail())
|
||||||
|
.docstatus(1)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
LinkedHashMap response = restService.postAs(
|
||||||
|
erpnextUrl + "/v2/document/Customer", customerDto, headers, LinkedHashMap.class);
|
||||||
|
|
||||||
|
LinkedHashMap data = (LinkedHashMap) response.get("data");
|
||||||
|
String customerId = (String) data.get("name");
|
||||||
|
|
||||||
|
User user = userService.fetchUserByUsername(registerRequest.getUsername());
|
||||||
|
user.setErpCustomerRef(customerId);
|
||||||
|
userService.getUserRepository().save(user);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
return NextAction.retryAfter(DateTime.now().plusSeconds(30), "Failed to update ERP: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextAction.stopInState(DONE, "ERP updated successfully for: " + registerRequest.getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
String getSMSString(String to, String otp) {
|
String getSMSString(String to, String otp) {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package zw.qantra.tm.domain.workflows;
|
package zw.qantra.tm.domain.workflows;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import io.nflow.engine.service.WorkflowInstanceInclude.*;
|
import io.nflow.engine.service.WorkflowInstanceInclude.*;
|
||||||
|
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import io.nflow.engine.service.WorkflowInstanceService;
|
import io.nflow.engine.service.WorkflowInstanceService;
|
||||||
@@ -21,7 +24,7 @@ import static io.nflow.engine.service.WorkflowInstanceInclude.*;
|
|||||||
public class WorkflowUtils {
|
public class WorkflowUtils {
|
||||||
private final WorkflowInstanceService workflowInstanceService;
|
private final WorkflowInstanceService workflowInstanceService;
|
||||||
|
|
||||||
public CompletableFuture<Object> waitForState(long workflowId, String state, int timeout, int pollingInterval) {
|
public CompletableFuture<Object> waitForState(long workflowId, String[] states, int timeout, int pollingInterval) {
|
||||||
CompletableFuture<Object> future = new CompletableFuture<>();
|
CompletableFuture<Object> future = new CompletableFuture<>();
|
||||||
|
|
||||||
while(!future.isDone()){ // check if the future is done
|
while(!future.isDone()){ // check if the future is done
|
||||||
@@ -31,8 +34,12 @@ public class WorkflowUtils {
|
|||||||
1L);
|
1L);
|
||||||
try {
|
try {
|
||||||
Thread.sleep(pollingInterval);
|
Thread.sleep(pollingInterval);
|
||||||
if(instance.state.equalsIgnoreCase(state) &&
|
List<String> stateList = Arrays.stream(states).toList();
|
||||||
instance.status.equals(WorkflowInstance.WorkflowInstanceStatus.manual)){
|
List<WorkflowInstance.WorkflowInstanceStatus> statusList =
|
||||||
|
Arrays.asList(WorkflowInstance.WorkflowInstanceStatus.manual,
|
||||||
|
WorkflowInstance.WorkflowInstanceStatus.finished);
|
||||||
|
if(stateList.contains(instance.state) &&
|
||||||
|
statusList.contains(instance.status)){
|
||||||
future.complete(getStateResponse(instance, "success"));
|
future.complete(getStateResponse(instance, "success"));
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
@@ -48,10 +55,12 @@ public class WorkflowUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public StateResponse getStateResponse(WorkflowInstance instance, String message){
|
public StateResponse getStateResponse(WorkflowInstance instance, String message){
|
||||||
|
WorkflowInstanceAction action = instance.actions.get(instance.actions.size()-1);
|
||||||
return StateResponse.builder()
|
return StateResponse.builder()
|
||||||
.state(instance.state)
|
.state(instance.state)
|
||||||
|
.status(instance.status.toString())
|
||||||
.body(Utils.fromJson(instance.getStateVariable("body"), Map.class))
|
.body(Utils.fromJson(instance.getStateVariable("body"), Map.class))
|
||||||
.message(instance.getStateVariable("message"))
|
.message(action.stateText)
|
||||||
.workflowId(instance.id.toString())
|
.workflowId(instance.id.toString())
|
||||||
.externalId(instance.externalId)
|
.externalId(instance.externalId)
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -139,6 +139,13 @@ public class RestService {
|
|||||||
return responseEntity;
|
return responseEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <T> ResponseEntity<T> postAsEntity(String path, Object payload, HttpHeaders headers, Class<T> clazz){
|
||||||
|
// build the request
|
||||||
|
HttpEntity entity = new HttpEntity<>(payload, headers);
|
||||||
|
ResponseEntity responseEntity = this.restTemplate.exchange(path, HttpMethod.POST, entity, clazz);
|
||||||
|
return responseEntity;
|
||||||
|
}
|
||||||
|
|
||||||
public <T> T postAs(String path, Object payload, HttpHeaders headers, Class<T> clazz){
|
public <T> T postAs(String path, Object payload, HttpHeaders headers, Class<T> clazz){
|
||||||
// build the request
|
// build the request
|
||||||
HttpEntity entity = new HttpEntity<>(payload, headers);
|
HttpEntity entity = new HttpEntity<>(payload, headers);
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ public class RestTemplateInterceptor
|
|||||||
String requestBody = new String(reqBody, StandardCharsets.UTF_8);
|
String requestBody = new String(reqBody, StandardCharsets.UTF_8);
|
||||||
if(!isJSONValid(requestBody)) requestBody = toBasicJson(requestBody);
|
if(!isJSONValid(requestBody)) requestBody = toBasicJson(requestBody);
|
||||||
|
|
||||||
|
logger.info("Request Headers: {}", request.getHeaders().toString());
|
||||||
|
|
||||||
// we don't log file data
|
// we don't log file data
|
||||||
if(request.getHeaders().getContentLength() < 10000){
|
if(request.getHeaders().getContentLength() < 10000){
|
||||||
logger.info("Request: {}: {}\n{}",
|
logger.info("Request: {}: {}\n{}",
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ server.servlet.context-path=/api
|
|||||||
|
|
||||||
spring.jpa.hibernate.ddl-auto=update
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||||
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
|
|
||||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
|
||||||
|
|
||||||
spring.datasource.url=jdbc:postgresql://173.212.247.232:5532/qpay?useLegacyDatetimeCode=false
|
spring.datasource.url=jdbc:postgresql://173.212.247.232:5532/qpay?useLegacyDatetimeCode=false
|
||||||
spring.datasource.username=postgres
|
spring.datasource.username=postgres
|
||||||
@@ -31,15 +29,16 @@ sbz.aggregator.encryption-key=735470ce-8efb-4c0f-ba3f-7298f805ab33
|
|||||||
steward.payment.processor.url=${sbz.merchant.url}
|
steward.payment.processor.url=${sbz.merchant.url}
|
||||||
|
|
||||||
# ERPNext Configuration
|
# ERPNext Configuration
|
||||||
erpnext.url=http://localhost:8000
|
erpnext.url=http://173.212.247.232:8085/api
|
||||||
erpnext.api.key=your_api_key_here
|
erpnext.api.key=1102c6da0312f75
|
||||||
erpnext.api.secret=your_api_secret_here
|
erpnext.api.secret=e3d95692bcf16c7
|
||||||
|
erpnext.username=Administrator
|
||||||
|
erpnext.password=admin
|
||||||
erpnext.company=Qantra Payments
|
erpnext.company=Qantra Payments
|
||||||
erpnext.customer=Default Customer
|
|
||||||
erpnext.item=Payment Service
|
erpnext.item=Payment Service
|
||||||
erpnext.income.account=5111 - Sales Income - QPAY
|
erpnext.income.account=5111 - Sales Income - Q
|
||||||
erpnext.cost.center=Main - QPAY
|
erpnext.cost.center=Main - Q
|
||||||
erpnext.tax.account=VAT - QPAY
|
erpnext.tax.account=VAT - Q
|
||||||
erpnext.tax.rate=15.0
|
erpnext.tax.rate=15.0
|
||||||
|
|
||||||
logging.level.root=WARN
|
logging.level.root=WARN
|
||||||
|
|||||||
@@ -1,2 +1,10 @@
|
|||||||
# sbz.merchant.url=http://localhost:24000/v1
|
# sbz.merchant.url=http://localhost:24000/v1
|
||||||
sbz.merchant.url=https://api.stewardpay.co.zw/lab/v2
|
sbz.merchant.url=https://api.stewardpay.co.zw/lab/v2
|
||||||
|
|
||||||
|
spring.datasource.url=jdbc:postgresql://localhost:5432/qpay?useLegacyDatetimeCode=false
|
||||||
|
spring.datasource.username=postgres
|
||||||
|
spring.datasource.password=example
|
||||||
|
|
||||||
|
nflow.db.postgresql.url=jdbc:postgresql://localhost:5432/qpay
|
||||||
|
nflow.db.postgresql.user=postgres
|
||||||
|
nflow.db.postgresql.password=example
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ server.servlet.context-path=/api
|
|||||||
|
|
||||||
spring.jpa.hibernate.ddl-auto=update
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||||
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
|
|
||||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
|
||||||
|
|
||||||
spring.datasource.url=jdbc:postgresql://173.212.247.232:5532/qpay?useLegacyDatetimeCode=false
|
spring.datasource.url=jdbc:postgresql://173.212.247.232:5532/qpay?useLegacyDatetimeCode=false
|
||||||
spring.datasource.username=postgres
|
spring.datasource.username=postgres
|
||||||
@@ -31,15 +29,16 @@ sbz.aggregator.encryption-key=f29b64d6-e44b-42ea-a0ea-9d9f6a806dbe
|
|||||||
steward.payment.processor.url=${sbz.merchant.url}
|
steward.payment.processor.url=${sbz.merchant.url}
|
||||||
|
|
||||||
# ERPNext Configuration
|
# ERPNext Configuration
|
||||||
erpnext.url=http://localhost:8000
|
erpnext.url=http://173.212.247.232:8085/api
|
||||||
erpnext.api.key=your_api_key_here
|
erpnext.api.key=1102c6da0312f75
|
||||||
erpnext.api.secret=your_api_secret_here
|
erpnext.api.secret=e3d95692bcf16c7
|
||||||
|
erpnext.username=Administrator
|
||||||
|
erpnext.password=admin
|
||||||
erpnext.company=Qantra Payments
|
erpnext.company=Qantra Payments
|
||||||
erpnext.customer=Default Customer
|
|
||||||
erpnext.item=Payment Service
|
erpnext.item=Payment Service
|
||||||
erpnext.income.account=5111 - Sales Income - QPAY
|
erpnext.income.account=5111 - Sales Income - Q
|
||||||
erpnext.cost.center=Main - QPAY
|
erpnext.cost.center=Main - Q
|
||||||
erpnext.tax.account=VAT - QPAY
|
erpnext.tax.account=VAT - Q
|
||||||
erpnext.tax.rate=15.0
|
erpnext.tax.rate=15.0
|
||||||
|
|
||||||
# JWT Configuration
|
# JWT Configuration
|
||||||
@@ -47,6 +46,10 @@ jwt.secret=your-super-secret-jwt-key-here-make-it-very-long-and-secure-in-produc
|
|||||||
jwt.expiration=86400000
|
jwt.expiration=86400000
|
||||||
|
|
||||||
logging.level.root=WARN
|
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
|
||||||
|
|
||||||
# Email Configuration
|
# Email Configuration
|
||||||
infobip.url=https://z3m696.api.infobip.com/sms/2/text/advanced
|
infobip.url=https://z3m696.api.infobip.com/sms/2/text/advanced
|
||||||
|
|||||||
Reference in New Issue
Block a user