completed nflow integration into onboarding

This commit is contained in:
2025-09-22 22:24:01 +02:00
parent 1db27e22b2
commit 7b9e89eeff
18 changed files with 332 additions and 57 deletions

11
pom.xml
View File

@@ -157,10 +157,15 @@
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.13.1</version>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
</dependencies>
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
<version>8.13.36</version> <!-- Use the latest version -->
</dependency>
</dependencies>
<build>
<plugins>

View File

@@ -1,6 +1,7 @@
package zw.qantra.tm.configs;
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.joda.JodaModule;
import org.springframework.context.annotation.Bean;
@@ -16,6 +17,7 @@ public class JacksonConfig {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.registerModule(new JodaModule());
objectMapper.registerModule(new Jdk8Module());
return objectMapper;
}

View File

@@ -1,26 +1,25 @@
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.workflow.instance.QueryWorkflowInstances;
import io.nflow.engine.workflow.instance.WorkflowInstance;
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
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.dtos.VerifyRequest;
import zw.qantra.tm.domain.dtos.*;
import zw.qantra.tm.domain.services.OtpService;
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.EnumSet;
import java.util.HashMap;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -34,20 +33,46 @@ public class OnboardingController {
private final WorkflowInstanceService workflowInstanceService;
private final WorkflowInstanceFactory workflowInstanceFactory;
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")
public ResponseEntity<Object> register(@Valid @RequestBody RegisterRequest request)
throws ExecutionException, InterruptedException {
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
.setType(RegistrationWorkflow.TYPE)
.setExternalId(request.getUsername() + "-" + System.currentTimeMillis())
.setExternalId(request.getUsername() + "-" + new Instant().getMillis())
.putStateVariable("body", request)
.build();
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
CompletableFuture<Object> future = workflowUtils
.waitForState(workflowId, "sendOtp", 30000, 1000);
.waitForState(workflowId, new String[]{"sendOtp", "failed"},
30000, 1000);
Object response = future.get();
return ResponseEntity.ok(response);
@@ -56,6 +81,7 @@ public class OnboardingController {
@PostMapping("/verify")
public ResponseEntity<Object> verify(@Valid @RequestBody VerifyRequest request)
throws ExecutionException, InterruptedException {
// Create an update to move the workflow to the next state
WorkflowInstance update = new WorkflowInstance.Builder()
.setId(request.getWorkflowId())
@@ -74,14 +100,43 @@ public class OnboardingController {
workflowInstanceService.updateWorkflowInstance(update, action);
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();
return ResponseEntity.ok(response);
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
public ResponseEntity<Object> login(@Valid @RequestBody LoginRequest request) {
AuthResponse response = userService.login(request);
return ResponseEntity.ok(response);
}

View 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;
}

View File

@@ -14,4 +14,5 @@ public class PasswordRequest {
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
private Long workflowId;
}

View File

@@ -10,8 +10,9 @@ public class RegisterRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
private String password;
private String photoUrl;
private Long workflowId;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")

View File

@@ -11,6 +11,7 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
public class StateResponse {
private String state;
private String status;
private Object body;
private String message;
private String workflowId;

View File

@@ -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;
}

View 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;
}

View File

@@ -25,6 +25,9 @@ public class User extends BaseEntity implements UserDetails {
@Column(unique = true, nullable = false)
private String email;
private String phone;
private String erpCustomerRef;
private String photoUrl;
private Long workflowId;
@Column(name = "first_name")
private String firstName;

View File

@@ -1,5 +1,6 @@
package zw.qantra.tm.domain.services;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -17,10 +18,16 @@ import zw.qantra.tm.utils.JwtUtils;
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
@Getter
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtils jwtUtils;
public User fetchUserByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username)
@@ -43,6 +50,8 @@ public class UserService implements UserDetailsService {
user.setFirstName(request.getFirstName());
user.setLastName(request.getLastName());
user.setPhone(request.getPhone());
user.setWorkflowId(request.getWorkflowId());
user.setPhotoUrl(request.getPhotoUrl());
User savedUser = userRepository.save(user);
String token = jwtUtils.generateToken(savedUser);

View File

@@ -1,29 +1,37 @@
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 org.joda.time.DateTime;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
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.dtos.EmailRequest;
import zw.qantra.tm.domain.dtos.VerifyRequest;
import zw.qantra.tm.domain.dtos.*;
import zw.qantra.tm.domain.dtos.erp.CustomerDto;
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.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 zw.qantra.tm.rest.RestService;
import org.springframework.http.HttpHeaders;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@Component
@@ -43,6 +51,16 @@ public class RegistrationWorkflow extends WorkflowDefinition {
private String infobipUrl;
@Value("${infobip.apikey}")
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";
@@ -50,10 +68,9 @@ public class RegistrationWorkflow extends WorkflowDefinition {
public static final State REGISTER = new State("register");
public static final State SEND_OTP = new State("sendOtp", WorkflowStateType.manual);
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 SET_PIN = new State("setPin", WorkflowStateType.normal);
public static final State UPDATE_ERP = new State("updateErp", WorkflowStateType.normal);
public static final State OTP_SUCCESS = new State("otpSuccess");
public static final State UPDATE_ERP = new State("updateErp");
public static final State FAILED = new State("failed", WorkflowStateType.manual);
public static final State DONE = new State("done", WorkflowStateType.end);
@@ -63,10 +80,9 @@ public class RegistrationWorkflow extends WorkflowDefinition {
permit(BEGIN, REGISTER);
permit(REGISTER, SEND_OTP);
permit(SEND_OTP, VERIFY_OTP);
permit(RESEND_OTP, VERIFY_OTP);
permit(VERIFY_OTP, OTP_SUCCESS, FAILED);
permit(RESEND_OTP, OTP_SUCCESS, FAILED);
permit(OTP_SUCCESS, SET_PIN, FAILED);
permit(SET_PIN, UPDATE_ERP);
permit(OTP_SUCCESS, UPDATE_ERP, FAILED);
permit(UPDATE_ERP, DONE);
}
@@ -93,7 +109,19 @@ public class RegistrationWorkflow extends WorkflowDefinition {
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);
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());
} catch (Exception e) {
e.printStackTrace();
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");
}
public void otpSuccess(StateExecution execution) {
log.info("OTP verified successfully");
public void resendOtp(StateExecution execution) {
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) {
RegisterRequest response = execution.getVariable("body", RegisterRequest.class);
return NextAction.stopInState(UPDATE_ERP, "PIN set successfully for: " + response.getUsername());
public NextAction otpSuccess(StateExecution execution) {
log.info("OTP verified successfully");
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) {
RegisterRequest response = execution.getVariable("body", RegisterRequest.class);
return NextAction.stopInState(DONE, "ERP updated successfully for: " + response.getUsername());
RegisterRequest registerRequest = execution.getVariable("registerRequest", RegisterRequest.class);
// 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) {

View File

@@ -1,10 +1,13 @@
package zw.qantra.tm.domain.workflows;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import io.nflow.engine.service.WorkflowInstanceInclude.*;
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
import org.springframework.stereotype.Component;
import io.nflow.engine.service.WorkflowInstanceService;
@@ -21,7 +24,7 @@ import static io.nflow.engine.service.WorkflowInstanceInclude.*;
public class WorkflowUtils {
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<>();
while(!future.isDone()){ // check if the future is done
@@ -31,8 +34,12 @@ public class WorkflowUtils {
1L);
try {
Thread.sleep(pollingInterval);
if(instance.state.equalsIgnoreCase(state) &&
instance.status.equals(WorkflowInstance.WorkflowInstanceStatus.manual)){
List<String> stateList = Arrays.stream(states).toList();
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"));
}
} catch (InterruptedException e) {
@@ -48,10 +55,12 @@ public class WorkflowUtils {
}
public StateResponse getStateResponse(WorkflowInstance instance, String message){
WorkflowInstanceAction action = instance.actions.get(instance.actions.size()-1);
return StateResponse.builder()
.state(instance.state)
.status(instance.status.toString())
.body(Utils.fromJson(instance.getStateVariable("body"), Map.class))
.message(instance.getStateVariable("message"))
.message(action.stateText)
.workflowId(instance.id.toString())
.externalId(instance.externalId)
.build();

View File

@@ -139,6 +139,13 @@ public class RestService {
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){
// build the request
HttpEntity entity = new HttpEntity<>(payload, headers);

View File

@@ -33,6 +33,8 @@ public class RestTemplateInterceptor
String requestBody = new String(reqBody, StandardCharsets.UTF_8);
if(!isJSONValid(requestBody)) requestBody = toBasicJson(requestBody);
logger.info("Request Headers: {}", request.getHeaders().toString());
// we don't log file data
if(request.getHeaders().getContentLength() < 10000){
logger.info("Request: {}: {}\n{}",

View File

@@ -9,8 +9,6 @@ server.servlet.context-path=/api
spring.jpa.hibernate.ddl-auto=update
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.username=postgres
@@ -31,15 +29,16 @@ sbz.aggregator.encryption-key=735470ce-8efb-4c0f-ba3f-7298f805ab33
steward.payment.processor.url=${sbz.merchant.url}
# ERPNext Configuration
erpnext.url=http://localhost:8000
erpnext.api.key=your_api_key_here
erpnext.api.secret=your_api_secret_here
erpnext.url=http://173.212.247.232:8085/api
erpnext.api.key=1102c6da0312f75
erpnext.api.secret=e3d95692bcf16c7
erpnext.username=Administrator
erpnext.password=admin
erpnext.company=Qantra Payments
erpnext.customer=Default Customer
erpnext.item=Payment Service
erpnext.income.account=5111 - Sales Income - QPAY
erpnext.cost.center=Main - QPAY
erpnext.tax.account=VAT - QPAY
erpnext.income.account=5111 - Sales Income - Q
erpnext.cost.center=Main - Q
erpnext.tax.account=VAT - Q
erpnext.tax.rate=15.0
logging.level.root=WARN

View File

@@ -1,2 +1,10 @@
# sbz.merchant.url=http://localhost:24000/v1
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

View File

@@ -9,8 +9,6 @@ server.servlet.context-path=/api
spring.jpa.hibernate.ddl-auto=update
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.username=postgres
@@ -31,15 +29,16 @@ sbz.aggregator.encryption-key=f29b64d6-e44b-42ea-a0ea-9d9f6a806dbe
steward.payment.processor.url=${sbz.merchant.url}
# ERPNext Configuration
erpnext.url=http://localhost:8000
erpnext.api.key=your_api_key_here
erpnext.api.secret=your_api_secret_here
erpnext.url=http://173.212.247.232:8085/api
erpnext.api.key=1102c6da0312f75
erpnext.api.secret=e3d95692bcf16c7
erpnext.username=Administrator
erpnext.password=admin
erpnext.company=Qantra Payments
erpnext.customer=Default Customer
erpnext.item=Payment Service
erpnext.income.account=5111 - Sales Income - QPAY
erpnext.cost.center=Main - QPAY
erpnext.tax.account=VAT - QPAY
erpnext.income.account=5111 - Sales Income - Q
erpnext.cost.center=Main - Q
erpnext.tax.account=VAT - Q
erpnext.tax.rate=15.0
# 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
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
infobip.url=https://z3m696.api.infobip.com/sms/2/text/advanced