From b8d61412e0fcf0812380f13114b01f3e3d9921a3 Mon Sep 17 00:00:00 2001
From: nikpawar89
Date: Thu, 3 Aug 2017 18:47:48 +0530
Subject: [PATCH 01/30] validation fixed
---
...tBureauCommandFromApiJsonDeserializer.java | 38 +++++++++-
...ProductCommandFromApiJsonDeserializer.java | 74 ++++++++++++++-----
...roductMappingWritePlatformServiceImpl.java | 6 +-
...CreditBureauWritePlatflormServiceImpl.java | 7 +-
4 files changed, 99 insertions(+), 26 deletions(-)
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/serialization/CreditBureauCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/serialization/CreditBureauCommandFromApiJsonDeserializer.java
index fdc9acc37..895ea7757 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/serialization/CreditBureauCommandFromApiJsonDeserializer.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/serialization/CreditBureauCommandFromApiJsonDeserializer.java
@@ -42,7 +42,7 @@ import com.google.gson.reflect.TypeToken;
@Component
public class CreditBureauCommandFromApiJsonDeserializer {
- private final Set supportedParameters = new HashSet<>(Arrays.asList("alias", "is_active"));
+ private final Set supportedParameters = new HashSet<>(Arrays.asList("alias", "is_active","creditBureauId"));
private final FromJsonHelper fromApiJsonHelper;
@@ -79,6 +79,42 @@ public class CreditBureauCommandFromApiJsonDeserializer {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
+
+
+
+ public void validateForUpdate(final String json)
+ {
+ if (StringUtils.isBlank(json)) {
+ throw new InvalidJsonException();
+ }
+
+ final Type typeOfMap = new TypeToken
+
+ Optionally, two-factor authentication can be enabled by using
+ -Ptwofactor=enabled on gradle build.
+ Details of the authentication workflow with two-factor authentication enabled can be found here.
+
The platform has been configured to reject plain HTTP requests and
to expect all API requests to be made over
+
+
+
+
Two-Factor Authentication
+
+ Two-Factor authentication is supported by requesting & verifying
+ one-time passwords(OTP). OTPs are sent via SMS & email.
+
+
+ By default, two-factor authentication is disabled by default.
+ More information on how to enable TFA can be found here.
+
+
+ Two-factor authentication workflow:
+
+
User authticates via BasicAuth / oAauth
+
Client requests a list of supported OTP delivery methods for the authenticated user(Get Delivery Methods)
+
User selects an OTP delivery method and client sends a request for OTP(Request OTP)
+
User receives an OTP and the client sends it for verification(Validate OTP)
+
If the OTP is valid, an access token is returned
+
The access token is sent in following requestes to the server as a header Fineract-Platform-TFA-Token
The following section describes the way to configure two-factor authentication
+
Two-Factor Authentication has to be enabled by either building with Gradle arguments
+ -Ptwofactor=enabled or enabling the twofactor profile via env. variable
+
+
In order for SMS to be enabled an SMS bridge has to be setup with the message-gateway service.
diff --git a/api-docs/apidocs.css b/api-docs/apidocs.css
index f8816a7de..21bc11c27 100644
--- a/api-docs/apidocs.css
+++ b/api-docs/apidocs.css
@@ -745,4 +745,8 @@ ul.field li {
tt {
font-size: 9.5pt;
+}
+
+ol.normalli li {
+ list-style-type: decimal;
}
\ No newline at end of file
diff --git a/fineract-provider/build.gradle b/fineract-provider/build.gradle
index 771fcde58..5a2b8b31c 100644
--- a/fineract-provider/build.gradle
+++ b/fineract-provider/build.gradle
@@ -193,17 +193,33 @@ if (project.hasProperty('env') && project.getProperty('env') == 'dev') {
/* Enable Oauth2 authentication based on environment, default to HTTP basic auth */
if (project.hasProperty('security') && project.getProperty('security') == 'oauth') {
- copy {
- from './properties/oauth/'
- into 'src/main/resources/'
- include '*.properties'
- }
+ if(project.hasProperty('twofactor') && project.getProperty('twofactor') == 'enabled') {
+ copy {
+ from './properties/oauth/twofactor/'
+ into 'src/main/resources/'
+ include '*.properties'
+ }
+ } else {
+ copy {
+ from './properties/oauth/'
+ into 'src/main/resources/'
+ include '*.properties'
+ }
+ }
} else {
- copy {
- from './properties/basicauth/'
- into 'src/main/resources/'
- include '*.properties'
- }
+ if(project.hasProperty('twofactor') && project.getProperty('twofactor') == 'enabled') {
+ copy {
+ from './properties/basicauth/twofactor/'
+ into 'src/main/resources/'
+ include '*.properties'
+ }
+ } else {
+ copy {
+ from './properties/basicauth/'
+ into 'src/main/resources/'
+ include '*.properties'
+ }
+ }
}
task dist(type:Zip){
diff --git a/fineract-provider/properties/basicauth/twofactor/application.properties b/fineract-provider/properties/basicauth/twofactor/application.properties
new file mode 100644
index 000000000..5b4d49668
--- /dev/null
+++ b/fineract-provider/properties/basicauth/twofactor/application.properties
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+spring.profiles.default=basicauth
+spring.profiles.active=basicauth,twofactor
\ No newline at end of file
diff --git a/fineract-provider/properties/oauth/twofactor/application.properties b/fineract-provider/properties/oauth/twofactor/application.properties
new file mode 100644
index 000000000..12cbfbe80
--- /dev/null
+++ b/fineract-provider/properties/oauth/twofactor/application.properties
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+spring.profiles.default=basicauth
+spring.profiles.active=oauth,twofactor
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index 0ad36120c..93bd160d4 100755
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -2929,7 +2929,7 @@ public class CommandWrapperBuilder {
this.href = "/smscampaigns/"+resourceId;
return this;
}
-
+
public CommandWrapperBuilder holdAmount(final Long accountId) {
this.actionName = "HOLDAMOUNT";
this.entityName = "SAVINGSACCOUNT";
@@ -3041,4 +3041,18 @@ public class CommandWrapperBuilder {
this.json = "{}";
return this;
}
+
+ public CommandWrapperBuilder invalidateTwoFactorAccessToken() {
+ this.actionName = "INVALIDATE";
+ this.entityName = "TWOFACTOR_ACCESSTOKEN";
+ this.href = "/twofactor/invalidate";
+ return this;
+ }
+
+ public CommandWrapperBuilder updateTwoFactorConfiguration() {
+ this.actionName = "UPDATE";
+ this.entityName = "TWOFACTOR_CONFIGURATION";
+ this.href = "/twofactor/configure";
+ return this;
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
index 4abbb4b77..f2eab6429 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
@@ -85,5 +85,12 @@ public interface ConfigurationDomainService {
Long getDailyTPTLimit();
void removeGlobalConfigurationPropertyDataFromCache(String propertyName);
-
+
+ boolean isSMSOTPDeliveryEnabled();
+
+ boolean isEmailOTPDeliveryEnabled();
+
+ Integer retrieveOTPCharacterLength();
+
+ Integer retrieveOTPLiveTime();
}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
index 95170f82e..3d51ddd9e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
@@ -287,6 +287,43 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
configurations.remove(key);
}
+ @Override
+ public boolean isSMSOTPDeliveryEnabled() {
+ final String propertyName = "use-sms-for-2fa";
+ final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName);
+ return property.isEnabled();
+ }
+
+ @Override
+ public boolean isEmailOTPDeliveryEnabled() {
+ final String propertyName = "use-email-for-2fa";
+ final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName);
+ return property.isEnabled();
+ }
+
+ @Override
+ public Integer retrieveOTPCharacterLength() {
+ final String propertyName = "otp-character-length";
+ final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName);
+ int defaultValue = 6;
+ int value = property.getValue().intValue();
+ if(value < 1)
+ return defaultValue;
+ return value;
+ }
+
+ @Override
+ public Integer retrieveOTPLiveTime() {
+ final String propertyName = "otp-validity-period";
+ final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName);
+ int defaultValue = 300;
+ int value = property.getValue().intValue();
+ if(value < 1) {
+ return defaultValue;
+ }
+ return value;
+ }
+
private GlobalConfigurationPropertyData getGlobalConfigurationPropertyData(final String propertyName) {
String identifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
String key = identifier + "_" + propertyName;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/AbstractApplicationConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/AbstractApplicationConfiguration.java
index 7823a0dd7..dd8fc2043 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/AbstractApplicationConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/AbstractApplicationConfiguration.java
@@ -40,7 +40,8 @@ import org.springframework.context.annotation.PropertySource;
* and MariaDB4j (because those differ in the subclasses).
*/
@Configuration
-@Import({ WebXmlConfiguration.class, WebXmlOauthConfiguration.class, WebFrontEndConfiguration.class })
+@Import({ WebXmlConfiguration.class, WebXmlOauthConfiguration.class, WebFrontEndConfiguration.class,
+ WebTwoFactorXmlConfiguration.class })
@ImportResource({ "classpath*:META-INF/spring/appContext.xml" })
@PropertySource(value="classpath:META-INF/spring/jdbc.properties")
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class,
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/WebTwoFactorXmlConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/WebTwoFactorXmlConfiguration.java
new file mode 100644
index 000000000..e8f16882d
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/WebTwoFactorXmlConfiguration.java
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.core.boot;
+
+import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
+import org.springframework.boot.context.embedded.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+
+@Configuration
+public class WebTwoFactorXmlConfiguration {
+
+ @Bean
+ public FilterRegistrationBean twoFactorFilterBean(TwoFactorAuthenticationFilter filter) {
+ FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
+ registrationBean.setEnabled(false);
+ return registrationBean;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/AuthenticationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/AuthenticationApiResource.java
index 163835931..e5c20ed6e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/AuthenticationApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/AuthenticationApiResource.java
@@ -30,8 +30,10 @@ import javax.ws.rs.core.MediaType;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
+import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
import org.apache.fineract.infrastructure.security.data.AuthenticatedUserData;
import org.apache.fineract.infrastructure.security.service.SpringSecurityPlatformSecurityContext;
+import org.apache.fineract.infrastructure.security.service.TwoFactorUtils;
import org.apache.fineract.useradministration.data.RoleData;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.Role;
@@ -56,15 +58,17 @@ public class AuthenticationApiResource {
private final DaoAuthenticationProvider customAuthenticationProvider;
private final ToApiJsonSerializer apiJsonSerializerService;
private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext;
+ private final TwoFactorUtils twoFactorUtils;
@Autowired
public AuthenticationApiResource(
@Qualifier("customAuthenticationProvider") final DaoAuthenticationProvider customAuthenticationProvider,
final ToApiJsonSerializer apiJsonSerializerService,
- final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext) {
+ final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext, TwoFactorUtils twoFactorUtils) {
this.customAuthenticationProvider = customAuthenticationProvider;
this.apiJsonSerializerService = apiJsonSerializerService;
this.springSecurityPlatformSecurityContext = springSecurityPlatformSecurityContext;
+ this.twoFactorUtils = twoFactorUtils;
}
@POST
@@ -100,12 +104,16 @@ public class AuthenticationApiResource {
final EnumOptionData organisationalRole = principal.organisationalRoleData();
+ boolean isTwoFactorRequired = twoFactorUtils.isTwoFactorAuthEnabled() && !
+ principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
if (this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal)) {
- authenticatedUserData = new AuthenticatedUserData(username, principal.getId(), new String(base64EncodedAuthenticationKey));
+ authenticatedUserData = new AuthenticatedUserData(username, principal.getId(),
+ new String(base64EncodedAuthenticationKey), isTwoFactorRequired);
} else {
authenticatedUserData = new AuthenticatedUserData(username, officeId, officeName, staffId, staffDisplayName,
- organisationalRole, roles, permissions, principal.getId(), new String(base64EncodedAuthenticationKey));
+ organisationalRole, roles, permissions, principal.getId(),
+ new String(base64EncodedAuthenticationKey), isTwoFactorRequired);
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorApiResource.java
new file mode 100644
index 000000000..482906af1
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorApiResource.java
@@ -0,0 +1,132 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.api;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.commands.service.CommandWrapperBuilder;
+import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
+import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
+import org.apache.fineract.infrastructure.security.data.AccessTokenData;
+import org.apache.fineract.infrastructure.security.data.OTPDeliveryMethod;
+import org.apache.fineract.infrastructure.security.data.OTPMetadata;
+import org.apache.fineract.infrastructure.security.data.OTPRequest;
+import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.infrastructure.security.service.TwoFactorService;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.context.annotation.Scope;
+import org.springframework.stereotype.Component;
+
+@Path("/twofactor")
+@Component
+@Profile("twofactor")
+@Scope("singleton")
+public class TwoFactorApiResource {
+
+
+ private final ToApiJsonSerializer otpRequestSerializer;
+ private final ToApiJsonSerializer otpDeliveryMethodSerializer;
+ private final ToApiJsonSerializer accessTokenSerializer;
+ private final DefaultToApiJsonSerializer> toApiJsonSerializer;
+
+ private final PlatformSecurityContext context;
+ private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
+ private final TwoFactorService twoFactorService;
+
+
+
+ @Autowired
+ public TwoFactorApiResource(ToApiJsonSerializer otpRequestSerializer,
+ ToApiJsonSerializer otpDeliveryMethodSerializer,
+ ToApiJsonSerializer accessTokenSerializer,
+ DefaultToApiJsonSerializer> toApiJsonSerializer,
+ PlatformSecurityContext context,
+ PortfolioCommandSourceWritePlatformService
+ commandsSourceWritePlatformService,
+ TwoFactorService twoFactorService) {
+ this.otpRequestSerializer = otpRequestSerializer;
+ this.otpDeliveryMethodSerializer = otpDeliveryMethodSerializer;
+ this.accessTokenSerializer = accessTokenSerializer;
+ this.toApiJsonSerializer = toApiJsonSerializer;
+ this.context = context;
+ this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
+ this.twoFactorService = twoFactorService;
+ }
+
+
+ @GET
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String getOTPDeliveryMethods(@Context final UriInfo uriInfo) {
+ AppUser user = context.authenticatedUser();
+
+ List otpDeliveryMethods = twoFactorService.getDeliveryMethodsForUser(user);
+ return this.otpDeliveryMethodSerializer.serialize(otpDeliveryMethods);
+ }
+
+ @POST
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String requestToken(@QueryParam("deliveryMethod") final String deliveryMethod,
+ @QueryParam("extendedToken") @DefaultValue("false") boolean extendedAccessToken,
+ @Context final UriInfo uriInfo) {
+ final AppUser user = context.authenticatedUser();
+
+ final OTPRequest request = twoFactorService.createNewOTPToken(user, deliveryMethod, extendedAccessToken);
+ return this.otpRequestSerializer.serialize(request.getMetadata());
+ }
+
+ @Path("validate")
+ @POST
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String validate(@QueryParam("token") final String token) {
+ final AppUser user = context.authenticatedUser();
+
+ TFAccessToken accessToken = twoFactorService.createAccessTokenFromOTP(user, token);
+
+ return accessTokenSerializer.serialize(accessToken.toTokenData());
+ }
+
+ @Path("invalidate")
+ @POST
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String updateConfiguration(final String apiRequestBodyAsJson) {
+ final CommandWrapper commandRequest = new CommandWrapperBuilder()
+ .invalidateTwoFactorAccessToken().withJson(apiRequestBodyAsJson).build();
+ final CommandProcessingResult result = this.commandsSourceWritePlatformService.
+ logCommandSource(commandRequest);
+
+ return this.toApiJsonSerializer.serialize(result);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorConfigurationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorConfigurationApiResource.java
new file mode 100644
index 000000000..13fd69ec3
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorConfigurationApiResource.java
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.api;
+
+
+import java.util.Map;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.commands.service.CommandWrapperBuilder;
+import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.infrastructure.security.service.TwoFactorConfigurationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.context.annotation.Scope;
+import org.springframework.stereotype.Component;
+
+@Path("/twofactor/configure")
+@Consumes({ MediaType.APPLICATION_JSON })
+@Produces({ MediaType.APPLICATION_JSON })
+@Component
+@Scope("singleton")
+@Profile("twofactor")
+public class TwoFactorConfigurationApiResource {
+
+ private final String resourceNameForPermissions = "TWOFACTOR_CONFIG";
+
+ private final PlatformSecurityContext context;
+ private final TwoFactorConfigurationService configurationService;
+ private final DefaultToApiJsonSerializer> toApiJsonSerializer;
+ private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
+
+ @Autowired
+ public TwoFactorConfigurationApiResource(PlatformSecurityContext context,
+ TwoFactorConfigurationService configurationService,
+ DefaultToApiJsonSerializer> toApiJsonSerializer,
+ PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService) {
+ this.context = context;
+ this.configurationService = configurationService;
+ this.toApiJsonSerializer = toApiJsonSerializer;
+ this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
+ }
+
+
+ @GET
+ public String retrieveAll() {
+ this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
+ Map configurationMap = configurationService.retrieveAll();
+ return toApiJsonSerializer.serialize(configurationMap);
+ }
+
+ @PUT
+ public String updateConfiguration(final String apiRequestBodyAsJson) {
+ final CommandWrapper commandRequest = new CommandWrapperBuilder()
+ .updateTwoFactorConfiguration().withJson(apiRequestBodyAsJson).build();
+ final CommandProcessingResult result = this.commandsSourceWritePlatformService.
+ logCommandSource(commandRequest);
+
+ return this.toApiJsonSerializer.serialize(result);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java
index 67f061680..d05f589e2 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java
@@ -30,8 +30,10 @@ import javax.ws.rs.core.MediaType;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
+import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
import org.apache.fineract.infrastructure.security.data.AuthenticatedOauthUserData;
import org.apache.fineract.infrastructure.security.service.SpringSecurityPlatformSecurityContext;
+import org.apache.fineract.infrastructure.security.service.TwoFactorUtils;
import org.apache.fineract.useradministration.data.RoleData;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.Role;
@@ -56,14 +58,17 @@ public class UserDetailsApiResource {
private final ResourceServerTokenServices tokenServices;
private final ToApiJsonSerializer apiJsonSerializerService;
private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext;
+ private final TwoFactorUtils twoFactorUtils;
@Autowired
public UserDetailsApiResource(@Qualifier("tokenServices") final ResourceServerTokenServices tokenServices,
final ToApiJsonSerializer apiJsonSerializerService,
- final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext) {
+ final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext,
+ final TwoFactorUtils twoFactorUtils) {
this.tokenServices = tokenServices;
this.apiJsonSerializerService = apiJsonSerializerService;
this.springSecurityPlatformSecurityContext = springSecurityPlatformSecurityContext;
+ this.twoFactorUtils = twoFactorUtils;
}
@GET
@@ -96,12 +101,16 @@ public class UserDetailsApiResource {
final EnumOptionData organisationalRole = principal.organisationalRoleData();
+ final boolean requireTwoFactorAuth = twoFactorUtils.isTwoFactorAuthEnabled()
+ && !principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
if (this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal)) {
- authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(), principal.getId(), accessToken);
+ authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(),
+ principal.getId(), accessToken, requireTwoFactorAuth);
} else {
- authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(), officeId, officeName, staffId, staffDisplayName,
- organisationalRole, roles, permissions, principal.getId(), accessToken);
+ authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(),
+ officeId, officeName, staffId, staffDisplayName, organisationalRole, roles,
+ permissions, principal.getId(), accessToken, requireTwoFactorAuth);
}
return this.apiJsonSerializerService.serialize(authenticatedUserData);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/InvalidateTFAccessTokenCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/InvalidateTFAccessTokenCommandHandler.java
new file mode 100644
index 000000000..1ede412aa
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/InvalidateTFAccessTokenCommandHandler.java
@@ -0,0 +1,108 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.command;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.ApiParameterError;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
+import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
+import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.infrastructure.security.service.TwoFactorService;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+
+
+@Service
+@CommandType(entity = "TWOFACTOR_ACCESSTOKEN", action = "INVALIDATE")
+@Profile("twofactor")
+public class InvalidateTFAccessTokenCommandHandler implements NewCommandSourceHandler {
+
+
+ private final TwoFactorService twoFactorService;
+ private final PlatformSecurityContext securityContext;
+ private final FromJsonHelper fromJsonHelper;
+
+ @Autowired
+ public InvalidateTFAccessTokenCommandHandler(TwoFactorService twoFactorService,
+ PlatformSecurityContext securityContext,
+ FromJsonHelper fromJsonHelper) {
+ this.twoFactorService = twoFactorService;
+ this.securityContext = securityContext;
+ this.fromJsonHelper = fromJsonHelper;
+ }
+
+ @Transactional
+ @Override
+ public CommandProcessingResult processCommand(JsonCommand command) {
+ validateJson(command.json());
+
+ final AppUser user = securityContext.authenticatedUser();
+
+ final TFAccessToken accessToken = twoFactorService.invalidateAccessToken(user, command);
+
+ return new CommandProcessingResultBuilder()
+ .withCommandId(command.commandId())
+ .withResourceIdAsString(accessToken.getToken())
+ .build();
+ }
+
+ private void validateJson(String json) {
+ if (StringUtils.isBlank(json)) {
+ throw new InvalidJsonException();
+ }
+
+ final Type typeOfMap = new TypeToken>() {}.getType();
+ this.fromJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
+ new HashSet<>(Collections.singletonList("token")));
+ final JsonElement element = this.fromJsonHelper.parse(json);
+
+ final List dataValidationErrors = new ArrayList<>();
+ final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
+ .resource(TwoFactorConstants.ACCESSTOKEN_RESOURCE_NAME);
+
+ final String token = this.fromJsonHelper.extractStringNamed("token", element);
+ baseDataValidator.reset().parameter("token").value(token).notNull().notBlank();
+
+ if(!dataValidationErrors.isEmpty()) {
+ throw new PlatformApiDataValidationException(dataValidationErrors);
+ }
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/UpdateTwoFactorConfigCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/UpdateTwoFactorConfigCommandHandler.java
new file mode 100644
index 000000000..b8c4e60e1
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/UpdateTwoFactorConfigCommandHandler.java
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.command;
+
+import java.util.Map;
+
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
+import org.apache.fineract.infrastructure.security.data.TwoFactorConfigurationValidator;
+import org.apache.fineract.infrastructure.security.service.TwoFactorConfigurationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@CommandType(entity = "TWOFACTOR_CONFIGURATION", action = "UPDATE")
+@Profile("twofactor")
+public class UpdateTwoFactorConfigCommandHandler implements NewCommandSourceHandler {
+
+ private final TwoFactorConfigurationService configurationService;
+ private final TwoFactorConfigurationValidator dataValidator;
+
+ @Autowired
+ public UpdateTwoFactorConfigCommandHandler(TwoFactorConfigurationService configurationService,
+ TwoFactorConfigurationValidator dataValidator) {
+ this.configurationService = configurationService;
+ this.dataValidator = dataValidator;
+ }
+
+ @Transactional
+ @Override
+ public CommandProcessingResult processCommand(final JsonCommand command) {
+ this.dataValidator.validateForUpdate(command.json());
+ final Map changes = configurationService.update(command);
+ return new CommandProcessingResultBuilder()
+ .withCommandId(command.commandId())
+ .with(changes)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConfigurationConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConfigurationConstants.java
new file mode 100644
index 000000000..22818dea5
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConfigurationConstants.java
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.constants;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class TwoFactorConfigurationConstants {
+
+ public static final String RESOURCE_NAME = "TWOFACTOR_CONFIGURATION";
+
+ public static final String ENABLE_EMAIL_DELIVERY = "otp-delivery-email-enable";
+ public static final String EMAIL_SUBJECT = "otp-delivery-email-subject";
+ public static final String EMAIL_BODY = "otp-delivery-email-body";
+
+ public static final String ENABLE_SMS_DELIVERY = "otp-delivery-sms-enable";
+ public static final String SMS_PROVIDER_ID = "otp-delivery-sms-provider";
+ public static final String SMS_MESSAGE_TEXT = "otp-delivery-sms-text";
+
+ public static final String OTP_TOKEN_LIVE_TIME = "otp-token-live-time";
+ public static final String OTP_TOKEN_LENGTH = "otp-token-length";
+
+ public static final String ACCESS_TOKEN_LIVE_TIME = "access-token-live-time";
+ public static final String ACCESS_TOKEN_LIVE_TIME_EXTENDED = "access-token-live-time-extended";
+
+ public static final Set REQUEST_DATA_PARAMETERS =
+ new HashSet<>(Arrays.asList(ENABLE_EMAIL_DELIVERY, EMAIL_SUBJECT, EMAIL_BODY,
+ ENABLE_SMS_DELIVERY, SMS_PROVIDER_ID, SMS_MESSAGE_TEXT, OTP_TOKEN_LIVE_TIME,
+ OTP_TOKEN_LENGTH, ACCESS_TOKEN_LIVE_TIME, ACCESS_TOKEN_LIVE_TIME_EXTENDED));
+
+ public static final List STRING_PARAMETERS =
+ Arrays.asList(EMAIL_SUBJECT, EMAIL_BODY, SMS_MESSAGE_TEXT);
+
+ public static final List BOOLEAN_PARAMETERS =
+ Arrays.asList(ENABLE_EMAIL_DELIVERY, ENABLE_SMS_DELIVERY);
+
+ public static final List NUMBER_PARAMETERS =
+ Arrays.asList(SMS_PROVIDER_ID, OTP_TOKEN_LIVE_TIME, OTP_TOKEN_LENGTH,
+ ACCESS_TOKEN_LIVE_TIME, ACCESS_TOKEN_LIVE_TIME_EXTENDED);
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConstants.java
new file mode 100644
index 000000000..7997c3986
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConstants.java
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.constants;
+
+public class TwoFactorConstants {
+
+ public static final String ACCESSTOKEN_RESOURCE_NAME = "TWOFACTOR_ACCESSTOKEN";
+
+ public static final String SMS_DELIVERY_METHOD_NAME = "sms";
+ public static final String EMAIL_DELIVERY_METHOD_NAME = "email";
+
+ public static final String BYPASS_TWO_FACTOR_PERMISSION = "BYPASS_TWOFACTOR";
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AccessTokenData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AccessTokenData.java
new file mode 100644
index 000000000..ebd27f7d1
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AccessTokenData.java
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.data;
+
+import org.joda.time.DateTime;
+
+public class AccessTokenData {
+
+ private final String token;
+
+ private final DateTime validFrom;
+ private final DateTime validTo;
+
+ public AccessTokenData(String token, DateTime validFrom, DateTime validTo) {
+ this.token = token;
+ this.validFrom = validFrom;
+ this.validTo = validTo;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public DateTime getValidFrom() {
+ return validFrom;
+ }
+
+ public DateTime getValidTo() {
+ return validTo;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedOauthUserData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedOauthUserData.java
index 830fc6750..116daa8ba 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedOauthUserData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedOauthUserData.java
@@ -54,6 +54,9 @@ public class AuthenticatedOauthUserData {
@SuppressWarnings("unused")
private final boolean shouldRenewPassword;
+ @SuppressWarnings("unused")
+ private final boolean isTwoFactorAuthenticationRequired;
+
public AuthenticatedOauthUserData(final String username, final Collection permissions) {
this.username = username;
this.userId = null;
@@ -67,11 +70,13 @@ public class AuthenticatedOauthUserData {
this.roles = null;
this.permissions = permissions;
this.shouldRenewPassword = false;
+ this.isTwoFactorAuthenticationRequired = false;
}
public AuthenticatedOauthUserData(final String username, final Long officeId, final String officeName, final Long staffId,
final String staffDisplayName, final EnumOptionData organisationalRole, final Collection roles,
- final Collection permissions, final Long userId, final String accessToken) {
+ final Collection permissions, final Long userId, final String accessToken,
+ final boolean isTwoFactorAuthenticationRequired) {
this.username = username;
this.officeId = officeId;
this.officeName = officeName;
@@ -84,9 +89,11 @@ public class AuthenticatedOauthUserData {
this.roles = roles;
this.permissions = permissions;
this.shouldRenewPassword = false;
+ this.isTwoFactorAuthenticationRequired = isTwoFactorAuthenticationRequired;
}
- public AuthenticatedOauthUserData(final String username, final Long userId, final String accessToken) {
+ public AuthenticatedOauthUserData(final String username, final Long userId, final String accessToken,
+ final boolean isTwoFactorAuthenticationRequired) {
this.username = username;
this.officeId = null;
this.officeName = null;
@@ -99,5 +106,6 @@ public class AuthenticatedOauthUserData {
this.roles = null;
this.permissions = null;
this.shouldRenewPassword = true;
+ this.isTwoFactorAuthenticationRequired = isTwoFactorAuthenticationRequired;
}
}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedUserData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedUserData.java
index 513f5dffc..c4713fcfb 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedUserData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedUserData.java
@@ -54,6 +54,9 @@ public class AuthenticatedUserData {
@SuppressWarnings("unused")
private final boolean shouldRenewPassword;
+ @SuppressWarnings("unused")
+ private final boolean isTwoFactorAuthenticationRequired;
+
public AuthenticatedUserData(final String username, final Collection permissions) {
this.username = username;
this.userId = null;
@@ -67,11 +70,13 @@ public class AuthenticatedUserData {
this.roles = null;
this.permissions = permissions;
this.shouldRenewPassword = false;
+ this.isTwoFactorAuthenticationRequired = false;
}
public AuthenticatedUserData(final String username, final Long officeId, final String officeName, final Long staffId,
final String staffDisplayName, final EnumOptionData organisationalRole, final Collection roles,
- final Collection permissions, final Long userId, final String base64EncodedAuthenticationKey) {
+ final Collection permissions, final Long userId, final String base64EncodedAuthenticationKey,
+ final boolean isTwoFactorAuthenticationRequired) {
this.username = username;
this.officeId = officeId;
this.officeName = officeName;
@@ -84,9 +89,11 @@ public class AuthenticatedUserData {
this.roles = roles;
this.permissions = permissions;
this.shouldRenewPassword = false;
+ this.isTwoFactorAuthenticationRequired = isTwoFactorAuthenticationRequired;
}
- public AuthenticatedUserData(final String username, final Long userId, final String base64EncodedAuthenticationKey) {
+ public AuthenticatedUserData(final String username, final Long userId, final String base64EncodedAuthenticationKey,
+ final boolean isTwoFactorAuthenticationRequired) {
this.username = username;
this.officeId = null;
this.officeName = null;
@@ -99,5 +106,6 @@ public class AuthenticatedUserData {
this.roles = null;
this.permissions = null;
this.shouldRenewPassword = true;
+ this.isTwoFactorAuthenticationRequired = isTwoFactorAuthenticationRequired;
}
}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPDeliveryMethod.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPDeliveryMethod.java
new file mode 100644
index 000000000..f05c7ae83
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPDeliveryMethod.java
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.data;
+
+public class OTPDeliveryMethod {
+
+ private final String name;
+ private final String target;
+
+ public OTPDeliveryMethod(String name, String target) {
+ this.name = name;
+ this.target = target;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getTarget() {
+ return target;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPMetadata.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPMetadata.java
new file mode 100644
index 000000000..0365fffe4
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPMetadata.java
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.data;
+
+import org.joda.time.DateTime;
+
+public class OTPMetadata {
+
+ private final DateTime requestTime;
+ private final int tokenLiveTimeInSec;
+ private final boolean extendedAccessToken;
+ private final OTPDeliveryMethod deliveryMethod;
+
+ public OTPMetadata(DateTime requestTime, int tokenLiveTimeInSec,
+ boolean extendedAccessToken, OTPDeliveryMethod deliveryMethod) {
+ this.requestTime = requestTime;
+ this.tokenLiveTimeInSec = tokenLiveTimeInSec;
+ this.extendedAccessToken = extendedAccessToken;
+ this.deliveryMethod = deliveryMethod;
+ }
+
+ public DateTime getRequestTime() {
+ return requestTime;
+ }
+
+ public int getTokenLiveTimeInSec() {
+ return tokenLiveTimeInSec;
+ }
+
+ public boolean isExtendedAccessToken() {
+ return extendedAccessToken;
+ }
+
+ public OTPDeliveryMethod getDeliveryMethod() {
+ return deliveryMethod;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPRequest.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPRequest.java
new file mode 100644
index 000000000..f9f3e2433
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPRequest.java
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.data;
+
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.joda.time.DateTime;
+
+public class OTPRequest {
+
+ private final String token;
+ private final OTPMetadata metadata;
+
+ public OTPRequest(String token, OTPMetadata metadata) {
+ this.token = token;
+ this.metadata = metadata;
+ }
+
+ public static OTPRequest create(String token, int tokenLiveTimeInSec, boolean extendedAccessToken,
+ OTPDeliveryMethod deliveryMethod) {
+ final OTPMetadata metadata = new OTPMetadata(DateUtils.getLocalDateTimeOfTenant().toDateTime(),
+ tokenLiveTimeInSec, extendedAccessToken, deliveryMethod);
+ return new OTPRequest(token, metadata);
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public OTPMetadata getMetadata() {
+ return metadata;
+ }
+
+ public boolean isValid() {
+ DateTime expireTime = metadata.getRequestTime().plusSeconds(metadata.getTokenLiveTimeInSec());
+ return DateTime.now().isBefore(expireTime);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TwoFactorConfigurationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TwoFactorConfigurationValidator.java
new file mode 100644
index 000000000..41cf69bd4
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TwoFactorConfigurationValidator.java
@@ -0,0 +1,120 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.data;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.fineract.infrastructure.core.data.ApiParameterError;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
+import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.infrastructure.security.constants.TwoFactorConfigurationConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+
+@Component
+@Profile("twofactor")
+public class TwoFactorConfigurationValidator {
+
+ private final FromJsonHelper fromJsonHelper;
+
+ @Autowired
+ public TwoFactorConfigurationValidator(FromJsonHelper fromJsonHelper) {
+ this.fromJsonHelper = fromJsonHelper;
+ }
+
+ public void validateForUpdate(final String json) {
+ if (StringUtils.isBlank(json)) {
+ throw new InvalidJsonException();
+ }
+
+ boolean atLeastOneParameterPassedForUpdate = false;
+ final Type typeOfMap = new TypeToken>() {}.getType();
+ this.fromJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
+ TwoFactorConfigurationConstants.REQUEST_DATA_PARAMETERS);
+ final JsonElement element = this.fromJsonHelper.parse(json);
+
+ final List dataValidationErrors = new ArrayList<>();
+ final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
+ .resource(TwoFactorConfigurationConstants.RESOURCE_NAME);
+
+
+ for(String parameterName : TwoFactorConfigurationConstants.BOOLEAN_PARAMETERS) {
+ if(this.fromJsonHelper.parameterExists(parameterName, element)) {
+ atLeastOneParameterPassedForUpdate = true;
+ validateBooleanParameter(parameterName, element, baseDataValidator);
+ }
+ }
+
+ for(String parameterName : TwoFactorConfigurationConstants.STRING_PARAMETERS) {
+ if(this.fromJsonHelper.parameterExists(parameterName, element)) {
+ atLeastOneParameterPassedForUpdate = true;
+ validateStringParameter(parameterName, element, baseDataValidator);
+ }
+ }
+
+ for(String parameterName : TwoFactorConfigurationConstants.NUMBER_PARAMETERS) {
+ if (this.fromJsonHelper.parameterExists(parameterName, element)) {
+ atLeastOneParameterPassedForUpdate = true;
+ validateNumberParameter(parameterName, element, baseDataValidator);
+ }
+ }
+
+ if(!atLeastOneParameterPassedForUpdate) {
+ final Object forceError = null;
+ baseDataValidator.reset().anyOfNotNull(forceError);
+ }
+
+ throwExceptionIfValidationWarningsExist(dataValidationErrors);
+ }
+
+ private void throwExceptionIfValidationWarningsExist(
+ final List dataValidationErrors) {
+ if(!dataValidationErrors.isEmpty()) {
+ throw new PlatformApiDataValidationException(dataValidationErrors);
+ }
+ }
+
+ private void validateBooleanParameter(final String name, final JsonElement element,
+ final DataValidatorBuilder baseDataValidator) {
+ final String value = this.fromJsonHelper.extractStringNamed(name, element);
+ baseDataValidator.reset().parameter(name).value(value).notNull().trueOrFalseRequired(value);
+ }
+
+ private void validateStringParameter(final String name, final JsonElement element,
+ final DataValidatorBuilder baseDataValidator) {
+ final String value = this.fromJsonHelper.extractStringNamed(name, element);
+ baseDataValidator.reset().parameter(name).value(value).notBlank().notExceedingLengthOf(1000);
+ }
+
+ private void validateNumberParameter(final String name, final JsonElement element,
+ final DataValidatorBuilder baseDataValidator) {
+ final Integer value = this.fromJsonHelper.extractIntegerSansLocaleNamed(name, element);
+ baseDataValidator.reset().parameter(name).value(value).notNull().integerGreaterThanZero();
+ }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/OTPRequestRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/OTPRequestRepository.java
new file mode 100644
index 000000000..fc11300da
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/OTPRequestRepository.java
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.domain;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.fineract.infrastructure.security.data.OTPRequest;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Repository;
+import org.springframework.util.Assert;
+
+@Repository
+@Profile("twofactor")
+public class OTPRequestRepository {
+
+ private final ConcurrentHashMap OTPrequests = new ConcurrentHashMap<>();
+
+
+ public OTPRequest getOTPRequestForUser(AppUser user) {
+ Assert.notNull(user);
+
+ return this.OTPrequests.get(user.getId());
+ }
+
+ public void addOTPRequest(AppUser user, OTPRequest request) {
+ Assert.notNull(user);
+ Assert.notNull(request);
+ this.OTPrequests.put(user.getId(), request);
+ }
+
+ public void deleteOTPRequestForUser(AppUser user) {
+ this.OTPrequests.remove(user.getId());
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java
new file mode 100644
index 000000000..694046f96
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java
@@ -0,0 +1,137 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.domain;
+
+import java.util.Date;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+import javax.persistence.UniqueConstraint;
+
+import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.security.data.AccessTokenData;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.joda.time.DateTime;
+import org.joda.time.LocalDateTime;
+
+@Entity
+@Table(name = "twofactor_access_token",
+ uniqueConstraints = {@UniqueConstraint(columnNames = { "token", "appuser_id" }, name = "token_appuser_UNIQUE")})
+public class TFAccessToken extends AbstractPersistableCustom {
+
+ @Column(name = "token", nullable = false, length = 32)
+ private String token;
+
+ @ManyToOne
+ @JoinColumn(name = "appuser_id", nullable = false)
+ private AppUser user;
+
+ @Temporal(TemporalType.TIMESTAMP)
+ @Column(name = "valid_from", nullable = false)
+ private Date validFrom;
+
+ @Temporal(TemporalType.TIMESTAMP)
+ @Column(name = "valid_to", nullable = false)
+ private Date validTo;
+
+ @Column(name = "enabled", nullable = false)
+ private boolean enabled;
+
+ public TFAccessToken() {
+ }
+
+ public static TFAccessToken create(String token, AppUser user, int tokenLiveTimeInSec) {
+ DateTime validFrom = DateUtils.getLocalDateTimeOfTenant().toDateTime();
+ DateTime validTo = validFrom.plusSeconds(tokenLiveTimeInSec);
+
+ return new TFAccessToken(token, user, validFrom.toDate(), validTo.toDate(), true);
+ }
+
+ public TFAccessToken(String token, AppUser user, Date validFrom, Date validTo, boolean enabled) {
+ this.token = token;
+ this.user = user;
+ this.validFrom = validFrom;
+ this.validTo = validTo;
+ this.enabled = enabled;
+ }
+
+ public boolean isValid() {
+ return this.enabled && isDateInTheFuture(getValidToDate())
+ && isDateInThePast(getValidFromDate());
+ }
+
+ public AccessTokenData toTokenData() {
+ return new AccessTokenData(this.token, getValidFromDate().toDateTime(),
+ getValidToDate().toDateTime());
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public AppUser getUser() {
+ return user;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public LocalDateTime getValidFromDate() {
+ return new LocalDateTime(validFrom);
+ }
+
+ public LocalDateTime getValidToDate() {
+ return new LocalDateTime(validTo);
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public void setUser(AppUser user) {
+ this.user = user;
+ }
+
+ public void setValidFrom(Date validFrom) {
+ this.validFrom = validFrom;
+ }
+
+ public void setValidTo(Date validTo) {
+ this.validTo = validTo;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ private boolean isDateInTheFuture(LocalDateTime dateTime) {
+ return dateTime.isAfter(DateUtils.getLocalDateTimeOfTenant());
+ }
+
+ private boolean isDateInThePast(LocalDateTime dateTime) {
+ return dateTime.isBefore(DateUtils.getLocalDateTimeOfTenant());
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessTokenRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessTokenRepository.java
new file mode 100644
index 000000000..cbdbc8fcf
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessTokenRepository.java
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.domain;
+
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.context.annotation.Profile;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+@Profile("twofactor")
+public interface TFAccessTokenRepository extends JpaRepository, JpaSpecificationExecutor {
+
+ TFAccessToken findByUserAndToken(AppUser user, String token);
+
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfiguration.java
new file mode 100644
index 000000000..e45af00ee
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfiguration.java
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.domain;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+
+import org.apache.commons.lang.BooleanUtils;
+import org.apache.commons.lang.math.NumberUtils;
+import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import org.apache.fineract.infrastructure.security.constants.TwoFactorConfigurationConstants;
+
+@Entity
+@Table(name = "twofactor_configuration",
+ uniqueConstraints = {@UniqueConstraint(columnNames = { "name" }, name = "name_UNIQUE")})
+public class TwoFactorConfiguration extends AbstractPersistableCustom {
+
+ @Column(name = "name", nullable = false, length = 32)
+ private String name;
+
+ @Column(name = "value", nullable = true, length = 1024)
+ private String value;
+
+ public String getName() {
+ return name;
+ }
+
+ public String getStringValue() {
+ return value;
+ }
+
+ public Boolean getBooleanValue() {
+ return BooleanUtils.toBooleanObject(value);
+ }
+
+ public Integer getIntegerValue() {
+ try {
+ return NumberUtils.createInteger(value);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ public Object getObjectValue() {
+ if(TwoFactorConfigurationConstants.NUMBER_PARAMETERS.contains(name)) {
+ return getIntegerValue();
+ }
+ if(TwoFactorConfigurationConstants.BOOLEAN_PARAMETERS.contains(name)) {
+ return getBooleanValue();
+ }
+
+ return getStringValue();
+ }
+
+ public void setStringValue(String value) {
+ this.value = value;
+ }
+
+ public void setBooleanValue(boolean value) {
+ this.value = String.valueOf(value);
+ }
+
+ public void setIntegerValue(long value) {
+ this.value = String.valueOf(value);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfigurationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfigurationRepository.java
new file mode 100644
index 000000000..0407f0cb2
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfigurationRepository.java
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.domain;
+
+import java.util.List;
+
+import org.springframework.context.annotation.Profile;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+@Profile("twofactor")
+public interface TwoFactorConfigurationRepository extends
+ JpaRepository, JpaSpecificationExecutor {
+
+ TwoFactorConfiguration findByName(final String name);
+
+ List findAll();
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/AccessTokenInvalidIException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/AccessTokenInvalidIException.java
new file mode 100644
index 000000000..4c39bd10f
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/AccessTokenInvalidIException.java
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+
+public class AccessTokenInvalidIException extends AbstractPlatformDomainRuleException {
+
+ public AccessTokenInvalidIException() {
+ super("error.msg.twofactor.access.token.invalid", "The provided access token is invalid");
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPDeliveryMethodInvalidException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPDeliveryMethodInvalidException.java
new file mode 100644
index 000000000..0fe02376e
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPDeliveryMethodInvalidException.java
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+
+public class OTPDeliveryMethodInvalidException extends AbstractPlatformDomainRuleException {
+
+ public OTPDeliveryMethodInvalidException() {
+ super("error.msg.twofactor.otp.delivery.invalid", "The requested OTP delivery method " +
+ "is not supported or not currently unavailable.");
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPTokenInvalidException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPTokenInvalidException.java
new file mode 100644
index 000000000..12d11e9f4
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPTokenInvalidException.java
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+
+public class OTPTokenInvalidException extends AbstractPlatformDomainRuleException {
+
+ public OTPTokenInvalidException() {
+ super("error.msg.twofactor.otp.token.invalid", "The provided one time token is invalid");
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java
new file mode 100644
index 000000000..2f63eba6b
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java
@@ -0,0 +1,82 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.filter;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+
+/**
+ * A dummy {@link TwoFactorAuthenticationFilter} filter used when 'twofactor'
+ * environment profile is not active.
+ *
+ * This filter adds 'TWOFACTOR_AUTHENTICATED' authority to every authenticated
+ * platform user.
+ */
+@Service(value = "twoFactorAuthFilter")
+@Profile("!twofactor")
+public class InsecureTwoFactorAuthenticationFilter extends TwoFactorAuthenticationFilter {
+
+ public InsecureTwoFactorAuthenticationFilter() {
+ super(null);
+ }
+
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
+
+ SecurityContext context = SecurityContextHolder.getContext();
+ Authentication authentication = null;
+ if(context != null) {
+ authentication = context.getAuthentication();
+ }
+
+ // Add two-factor authenticated authority if user is authenticated
+ if(authentication != null && authentication.isAuthenticated()) {
+ AppUser user = (AppUser) authentication.getPrincipal();
+
+ if(user == null) {
+ return;
+ }
+
+ List updatedAuthorities = new ArrayList<>(authentication.getAuthorities());
+ updatedAuthorities.add(new SimpleGrantedAuthority("TWOFACTOR_AUTHENTICATED"));
+ UsernamePasswordAuthenticationToken updatedAuthentication =
+ new UsernamePasswordAuthenticationToken(authentication.getPrincipal(),
+ authentication.getCredentials(), updatedAuthorities);
+ context.setAuthentication(updatedAuthentication);
+ }
+
+ chain.doFilter(req, res);
+ }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java
new file mode 100644
index 000000000..6db141e65
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java
@@ -0,0 +1,139 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.filter;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
+import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
+import org.apache.fineract.infrastructure.security.service.TwoFactorService;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.stereotype.Service;
+import org.springframework.web.filter.GenericFilterBean;
+
+
+/**
+ * This filter is responsible for handling two-factor authentication.
+ * The filter is enabled when 'twofactor' environment profile is active, otherwise
+ * {@link InsecureTwoFactorAuthenticationFilter} is used.
+ *
+ * This filter validates an access-token provided as a header 'Fineract-Platform-TFA-Token'.
+ * If a valid token is provided, a 'TWOFACTOR_AUTHENTICATED' authority is added to the current
+ * authentication.
+ * If an invalid(non-existent or invalid) token is provided, 403 response is returned.
+ *
+ * An authenticated platform user with permission 'BYPASS_TWOFACTOR' will always be granted
+ * 'TWOFACTOR_AUTHENTICATED' authority regardless of the value of the 'Fineract-Platform-TFA-Token'
+ * header.
+ */
+@Service(value = "twoFactorAuthFilter")
+@Profile("twofactor")
+public class TwoFactorAuthenticationFilter extends GenericFilterBean {
+
+ private final TwoFactorService twoFactorService;
+
+ @Autowired
+ public TwoFactorAuthenticationFilter(TwoFactorService twoFactorService) {
+ this.twoFactorService = twoFactorService;
+ }
+
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+ throws IOException, ServletException {
+
+ final HttpServletRequest request = (HttpServletRequest) req;
+ final HttpServletResponse response = (HttpServletResponse) res;
+
+ SecurityContext context = SecurityContextHolder.getContext();
+ Authentication authentication = null;
+ if(context != null) {
+ authentication = context.getAuthentication();
+ }
+
+ // Process two-factor only when user is authenticated
+ if(authentication != null && authentication.isAuthenticated()) {
+ AppUser user = (AppUser) authentication.getPrincipal();
+
+ if(user == null) {
+ return;
+ }
+
+ if(!user.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION)) {
+ // User can't bypass two-factor auth, check two-factor access token
+ String token = request.getHeader("Fineract-Platform-TFA-Token");
+ if(token != null) {
+ TFAccessToken accessToken = twoFactorService.fetchAccessTokenForUser(user, token);
+ // Token is non-existent or invalid
+ if(accessToken == null || !accessToken.isValid()) {
+ response.addHeader("WWW-Authenticate",
+ "Basic realm=\"Fineract Platform API Two Factor\"");
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
+ "Invalid two-factor access token provided");
+ return;
+ }
+ } else {
+ // No token provided
+ chain.doFilter(req, res);
+ return;
+ }
+ }
+
+ List updatedAuthorities = new ArrayList<>(authentication.getAuthorities());
+ updatedAuthorities.add(new SimpleGrantedAuthority("TWOFACTOR_AUTHENTICATED"));
+ final Authentication updatedAuthentication = createUpdatedAuthentication(authentication,
+ updatedAuthorities);
+ context.setAuthentication(updatedAuthentication);
+ }
+
+ chain.doFilter(req, res);
+ }
+
+ private Authentication createUpdatedAuthentication(final Authentication currentAuthentication,
+ final List updatedAuthorities) {
+
+ final UsernamePasswordAuthenticationToken authentication = new
+ UsernamePasswordAuthenticationToken(currentAuthentication.getPrincipal(),
+ currentAuthentication.getCredentials(), updatedAuthorities);
+
+ if(currentAuthentication instanceof OAuth2Authentication) {
+ final OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) currentAuthentication;
+ return new OAuth2Authentication(oAuth2Authentication.getOAuth2Request(), authentication);
+ }
+
+ return authentication;
+ }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AccessTokenGenerationService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AccessTokenGenerationService.java
new file mode 100644
index 000000000..e477593e9
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AccessTokenGenerationService.java
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.service;
+
+public interface AccessTokenGenerationService {
+
+ String generateRandomToken();
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/RandomOTPGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/RandomOTPGenerator.java
new file mode 100644
index 000000000..d3bf5518d
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/RandomOTPGenerator.java
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.service;
+
+public class RandomOTPGenerator {
+
+ private static final String allowedCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVQXYZ";
+ private final int tokenLength;
+
+ public RandomOTPGenerator(int tokenLength) {
+ this.tokenLength = tokenLength;
+ }
+
+ public String generate() {
+ StringBuilder builder = new StringBuilder();
+ for(int i = 0; i < tokenLength; i++) {
+ builder.append(allowedCharacters.charAt((int) (Math.random() * (allowedCharacters.length()))));
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationService.java
new file mode 100644
index 000000000..aaf9354ef
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationService.java
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.service;
+
+import java.util.Map;
+
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.security.data.OTPRequest;
+import org.apache.fineract.useradministration.domain.AppUser;
+
+public interface TwoFactorConfigurationService {
+
+
+ Map retrieveAll();
+
+ boolean isSMSEnabled();
+ Integer getSMSProviderId();
+ String getSmsText();
+
+ boolean isEmailEnabled();
+ String getEmailSubject();
+ String getEmailBody();
+
+ String getFormattedEmailSubjectFor(AppUser user, OTPRequest request);
+ String getFormattedEmailBodyFor(AppUser user, OTPRequest request);
+ String getFormattedSmsTextFor(AppUser user, OTPRequest request);
+
+ Integer getOTPTokenLength();
+ Integer getOTPTokenLiveTime();
+
+ Integer getAccessTokenLiveTime();
+ Integer getAccessTokenExtendedLiveTime();
+
+ Map update(JsonCommand command);
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationServiceImpl.java
new file mode 100644
index 000000000..bd4c108d8
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationServiceImpl.java
@@ -0,0 +1,304 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.service;
+
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.security.constants.TwoFactorConfigurationConstants;
+import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
+import org.apache.fineract.infrastructure.security.data.OTPRequest;
+import org.apache.fineract.infrastructure.security.domain.TwoFactorConfiguration;
+import org.apache.fineract.infrastructure.security.domain.TwoFactorConfigurationRepository;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.joda.time.LocalDateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+
+import com.github.mustachejava.DefaultMustacheFactory;
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+
+@Service
+@Profile("twofactor")
+public class TwoFactorConfigurationServiceImpl implements TwoFactorConfigurationService {
+
+ private static final String DEFAULT_EMAIL_SUBJECT = "Fineract Two-Factor Authentication Token";
+ private static final String DEFAULT_EMAIL_BODY = "Hello {username}.\n" +
+ "Your OTP login token is {token}.";
+ private static final String DEFAULT_SMS_TEXT = "Your authentication token for Fineract is " +
+ "{token}.";
+
+ private final TwoFactorConfigurationRepository configurationRepository;
+
+
+ @Autowired
+ public TwoFactorConfigurationServiceImpl(TwoFactorConfigurationRepository configurationRepository) {
+ this.configurationRepository = configurationRepository;
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()")
+ public Map retrieveAll() {
+ List configurationList = configurationRepository.findAll();
+ Map configurationMap = new HashMap<>();
+ for(final TwoFactorConfiguration configuration : configurationList) {
+ configurationMap.put(configuration.getName(), configuration.getObjectValue());
+ }
+ return configurationMap;
+ }
+
+ @Override
+ @CacheEvict(value = "tfConfig", allEntries = true)
+ public Map update(JsonCommand command) {
+ Map actualChanges = new HashMap<>();
+
+
+ for(final String parameterName : TwoFactorConfigurationConstants.BOOLEAN_PARAMETERS) {
+ TwoFactorConfiguration configuration = configurationRepository.findByName(parameterName);
+ if(configuration == null) {
+ continue;
+ }
+
+ if(command.isChangeInBooleanParameterNamed(parameterName, configuration.getBooleanValue())) {
+ final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(parameterName);
+ actualChanges.put(parameterName, newValue);
+ configuration.setBooleanValue(newValue);
+ configurationRepository.save(configuration);
+ }
+ }
+
+ for(final String parameterName : TwoFactorConfigurationConstants.STRING_PARAMETERS) {
+ TwoFactorConfiguration configuration = configurationRepository.findByName(parameterName);
+ if(configuration == null) {
+ continue;
+ }
+
+ if(command.isChangeInStringParameterNamed(parameterName, configuration.getStringValue())) {
+ final String newValue = command.stringValueOfParameterNamed(parameterName).trim();
+ actualChanges.put(parameterName, newValue);
+ configuration.setStringValue(newValue);
+ configurationRepository.save(configuration);
+ }
+ }
+
+ for(final String parameterName : TwoFactorConfigurationConstants.NUMBER_PARAMETERS) {
+ TwoFactorConfiguration configuration = configurationRepository.findByName(parameterName);
+ if(configuration == null) {
+ continue;
+ }
+
+ if(command.isChangeInIntegerSansLocaleParameterNamed(parameterName, configuration.getIntegerValue())) {
+ final Long newValue = command.longValueOfParameterNamed(parameterName);
+ actualChanges.put(parameterName, newValue);
+ configuration.setIntegerValue(newValue);
+ configurationRepository.save(configuration);
+ }
+ }
+
+ if(!actualChanges.isEmpty()) {
+ configurationRepository.flush();
+ }
+
+ return actualChanges;
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|smsEnabled'")
+ public boolean isSMSEnabled() {
+ return getBooleanConfig(TwoFactorConfigurationConstants.ENABLE_SMS_DELIVERY, false);
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|smsProvider'")
+ public Integer getSMSProviderId() {
+ Integer value = getIntegerConfig(TwoFactorConfigurationConstants.SMS_PROVIDER_ID,
+ null);
+ if(value < 1) {
+ return null;
+ }
+ return value;
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|smsText'")
+ public String getSmsText() {
+ return getStringConfig(TwoFactorConfigurationConstants.SMS_MESSAGE_TEXT, DEFAULT_SMS_TEXT);
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|emailEnabled'")
+ public boolean isEmailEnabled() {
+ return getBooleanConfig(TwoFactorConfigurationConstants.ENABLE_EMAIL_DELIVERY, false);
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|emailSubject'")
+ public String getEmailSubject() {
+ return getStringConfig(TwoFactorConfigurationConstants.EMAIL_SUBJECT, DEFAULT_EMAIL_SUBJECT);
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|emailBody'")
+ public String getEmailBody() {
+ return getStringConfig(TwoFactorConfigurationConstants.EMAIL_BODY, DEFAULT_EMAIL_BODY);
+ }
+
+ @Override
+ public String getFormattedEmailSubjectFor(AppUser user, OTPRequest request) {
+ final Map templateData = processTemplateDataFor(user, request);
+ return compileTextTemplate(getEmailSubject(), TwoFactorConstants.EMAIL_DELIVERY_METHOD_NAME, templateData);
+ }
+
+ @Override
+ public String getFormattedEmailBodyFor(AppUser user, OTPRequest request) {
+ final Map templateData = processTemplateDataFor(user, request);
+ return compileTextTemplate(getEmailBody(), TwoFactorConstants.EMAIL_DELIVERY_METHOD_NAME, templateData);
+ }
+
+ @Override
+ public String getFormattedSmsTextFor(AppUser user, OTPRequest request) {
+ final Map templateData = processTemplateDataFor(user, request);
+ return compileTextTemplate(getSmsText(), TwoFactorConstants.SMS_DELIVERY_METHOD_NAME, templateData);
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|otpLength'")
+ public Integer getOTPTokenLength() {
+ Integer defaultValue = 1;
+ return getIntegerConfig(TwoFactorConfigurationConstants.OTP_TOKEN_LENGTH,
+ defaultValue);
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|otpTime'")
+ public Integer getOTPTokenLiveTime() {
+ Integer defaultValue = 300;
+ Integer value = getIntegerConfig(TwoFactorConfigurationConstants.OTP_TOKEN_LIVE_TIME,
+ defaultValue);
+ if(value < 1) {
+ return defaultValue;
+ }
+ return value;
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|tokenTime'")
+ public Integer getAccessTokenLiveTime() {
+ Integer defaultValue = 86400;
+ Integer value = getIntegerConfig(TwoFactorConfigurationConstants.ACCESS_TOKEN_LIVE_TIME,
+ defaultValue);
+ if(value < 1) {
+ return defaultValue;
+ }
+ return value;
+ }
+
+ @Override
+ @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|tokenExtendedTime'")
+ public Integer getAccessTokenExtendedLiveTime() {
+ Integer defaultValue = 604800;
+ Integer value = getIntegerConfig(TwoFactorConfigurationConstants.ACCESS_TOKEN_LIVE_TIME_EXTENDED,
+ defaultValue);
+ if(value < 1) {
+ return defaultValue;
+ }
+ return value;
+ }
+
+ private boolean getBooleanConfig(final String name, final boolean defaultValue) {
+ final TwoFactorConfiguration configuration =
+ configurationRepository.findByName(name);
+ Boolean value = configuration.getBooleanValue();
+ if(value == null) {
+ return defaultValue;
+ }
+ return value;
+ }
+
+ private String getStringConfig(final String name, final String defaultValue) {
+ final TwoFactorConfiguration configuration =
+ configurationRepository.findByName(name);
+ String value = configuration.getStringValue();
+ if(value == null) {
+ return defaultValue;
+ }
+ return value;
+ }
+
+ private Integer getIntegerConfig(final String name, final Integer defaultValue) {
+ final TwoFactorConfiguration configuration =
+ configurationRepository.findByName(name);
+ Integer value = configuration.getIntegerValue();
+ if(value == null) {
+ return defaultValue;
+ }
+ return value;
+ }
+
+ private Map processTemplateDataFor(AppUser user, OTPRequest request) {
+ Map templateData = new HashMap<>();
+
+ templateData.put("username", user.getUsername());
+ templateData.put("email", user.getEmail());
+ templateData.put("firstname", user.getFirstname());
+ templateData.put("lastname", user.getLastname());
+ if(user.getStaff() != null && user.getStaff().mobileNo() != null) {
+ templateData.put("mobileno", user.getStaff().mobileNo());
+ }
+
+ templateData.put("token", request.getToken());
+ templateData.put("tokenlivetime", request.getMetadata().getTokenLiveTimeInSec());
+
+ DateTimeFormatter timeFormatter = DateTimeFormat.forPattern("HH:mm:ss");
+ DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("dd.MM.yyyy");
+
+ final LocalDateTime requestTime = request.getMetadata().getRequestTime().toLocalDateTime();
+ final LocalDateTime expireTime = requestTime.plusSeconds(request.getMetadata().getTokenLiveTimeInSec());
+
+ templateData.put("requestdate", requestTime.toLocalDate().toString(dateFormatter));
+ templateData.put("requesttime", requestTime.toLocalTime().toString(timeFormatter));
+
+ templateData.put("expiredate", expireTime.toLocalDate().toString(dateFormatter));
+ templateData.put("expiretime", expireTime.toLocalTime().toString(timeFormatter));
+
+ return templateData;
+ }
+
+ private String compileTextTemplate(final String template, final String name,
+ final Map params) {
+ final MustacheFactory mf = new DefaultMustacheFactory();
+ final Mustache mustache = mf.compile(new StringReader(template), name);
+
+ final StringWriter stringWriter = new StringWriter();
+ mustache.execute(stringWriter, params);
+
+ return stringWriter.toString();
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorService.java
new file mode 100644
index 000000000..da556d0b5
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorService.java
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.service;
+
+import java.util.List;
+
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.security.data.OTPDeliveryMethod;
+import org.apache.fineract.infrastructure.security.data.OTPRequest;
+import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
+import org.apache.fineract.useradministration.domain.AppUser;
+
+public interface TwoFactorService {
+
+ List getDeliveryMethodsForUser(AppUser user);
+
+ OTPRequest createNewOTPToken(AppUser user, String deliveryMethodName, boolean extendedAccessToken);
+
+ TFAccessToken createAccessTokenFromOTP(AppUser user, String otpToken);
+
+ void validateTwoFactorAccessToken(AppUser user, String token);
+
+ TFAccessToken fetchAccessTokenForUser(AppUser user, String token);
+
+ TFAccessToken invalidateAccessToken(AppUser user, JsonCommand command);
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorServiceImpl.java
new file mode 100644
index 000000000..64361c20c
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorServiceImpl.java
@@ -0,0 +1,229 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.domain.EmailDetail;
+import org.apache.fineract.infrastructure.core.service.PlatformEmailService;
+import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
+import org.apache.fineract.infrastructure.security.data.OTPDeliveryMethod;
+import org.apache.fineract.infrastructure.security.data.OTPRequest;
+import org.apache.fineract.infrastructure.security.domain.OTPRequestRepository;
+import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
+import org.apache.fineract.infrastructure.security.domain.TFAccessTokenRepository;
+import org.apache.fineract.infrastructure.security.exception.AccessTokenInvalidIException;
+import org.apache.fineract.infrastructure.security.exception.OTPDeliveryMethodInvalidException;
+import org.apache.fineract.infrastructure.security.exception.OTPTokenInvalidException;
+import org.apache.fineract.infrastructure.sms.domain.SmsMessage;
+import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository;
+import org.apache.fineract.infrastructure.sms.scheduler.SmsMessageScheduledJobService;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+
+@Service
+@Profile("twofactor")
+public class TwoFactorServiceImpl implements TwoFactorService {
+
+
+
+ private final AccessTokenGenerationService accessTokenGenerationService;
+ private final PlatformEmailService emailService;
+ private final SmsMessageScheduledJobService smsMessageScheduledJobService;
+
+ private final OTPRequestRepository otpRequestRepository;
+ private final TFAccessTokenRepository tfAccessTokenRepository;
+ private final SmsMessageRepository smsMessageRepository;
+
+ private final TwoFactorConfigurationService configurationService;
+
+ @Autowired
+ public TwoFactorServiceImpl(AccessTokenGenerationService accessTokenGenerationService,
+ PlatformEmailService emailService,
+ SmsMessageScheduledJobService smsMessageScheduledJobService,
+ OTPRequestRepository otpRequestRepository,
+ TFAccessTokenRepository tfAccessTokenRepository,
+ SmsMessageRepository smsMessageRepository,
+ TwoFactorConfigurationService configurationService) {
+ this.accessTokenGenerationService = accessTokenGenerationService;
+ this.emailService = emailService;
+ this.smsMessageScheduledJobService = smsMessageScheduledJobService;
+ this.otpRequestRepository = otpRequestRepository;
+ this.tfAccessTokenRepository = tfAccessTokenRepository;
+ this.smsMessageRepository = smsMessageRepository;
+ this.configurationService = configurationService;
+ }
+
+
+ @Override
+ public List getDeliveryMethodsForUser(final AppUser user) {
+ List deliveryMethods = new ArrayList<>();
+
+ OTPDeliveryMethod smsMethod = getSMSDeliveryMethodForUser(user);
+ if(smsMethod != null) {
+ deliveryMethods.add(smsMethod);
+ }
+ OTPDeliveryMethod emailDelivery = getEmailDeliveryMethodForUser(user);
+ if(emailDelivery != null) {
+ deliveryMethods.add(emailDelivery);
+ }
+
+ return deliveryMethods;
+ }
+
+ @Override
+ public OTPRequest createNewOTPToken(final AppUser user, final String deliveryMethodName,
+ final boolean extendedAccessToken) {
+ if(TwoFactorConstants.SMS_DELIVERY_METHOD_NAME.equalsIgnoreCase(deliveryMethodName)) {
+ OTPDeliveryMethod smsDelivery = getSMSDeliveryMethodForUser(user);
+ if(smsDelivery == null) {
+ throw new OTPDeliveryMethodInvalidException();
+ }
+ final OTPRequest request = generateNewToken(smsDelivery, extendedAccessToken);
+ final String smsText = configurationService.getFormattedSmsTextFor(user, request);
+ SmsMessage smsMessage = SmsMessage.pendingSms(null, null, null, user.getStaff(), smsText,
+ user.getStaff().mobileNo(), null);
+ this.smsMessageRepository.save(smsMessage);
+ smsMessageScheduledJobService.sendTriggeredMessage(Collections.singleton(smsMessage),
+ configurationService.getSMSProviderId());
+ otpRequestRepository.addOTPRequest(user, request);
+ return request;
+ } else if(TwoFactorConstants.EMAIL_DELIVERY_METHOD_NAME.equalsIgnoreCase(deliveryMethodName)) {
+ OTPDeliveryMethod emailDelivery = getEmailDeliveryMethodForUser(user);
+ if(emailDelivery == null) {
+ throw new OTPDeliveryMethodInvalidException();
+ }
+ final OTPRequest request = generateNewToken(emailDelivery, extendedAccessToken);
+ final String emailSubject = configurationService.getFormattedEmailSubjectFor(user, request);
+ final String emailBody = configurationService.getFormattedEmailBodyFor(user, request);
+ final EmailDetail emailData = new EmailDetail(emailSubject, emailBody, user.getEmail(),
+ user.getFirstname() + " " + user.getLastname());
+ emailService.sendDefinedEmail(emailData);
+ otpRequestRepository.addOTPRequest(user, request);
+ return request;
+ }
+
+ throw new OTPDeliveryMethodInvalidException();
+ }
+
+ @Override
+ @CachePut(value = "userTFAccessToken",
+ key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil)" +
+ ".getTenant().getTenantIdentifier().concat(#user.username).concat(#result.token + 'tok')")
+ public TFAccessToken createAccessTokenFromOTP(final AppUser user, final String otpToken) {
+
+ OTPRequest otpRequest = otpRequestRepository.getOTPRequestForUser(user);
+ if(otpRequest == null || !otpRequest.isValid() || !otpRequest.getToken().equalsIgnoreCase(otpToken)) {
+ throw new OTPTokenInvalidException();
+ }
+
+ otpRequestRepository.deleteOTPRequestForUser(user);
+
+ String token = accessTokenGenerationService.generateRandomToken();
+ int liveTime;
+ if(otpRequest.getMetadata().isExtendedAccessToken()) {
+ liveTime = configurationService.getAccessTokenExtendedLiveTime();
+ } else {
+ liveTime = configurationService.getAccessTokenLiveTime();
+ }
+ TFAccessToken accessToken = TFAccessToken.create(token, user, liveTime);
+ tfAccessTokenRepository.save(accessToken);
+ return accessToken;
+ }
+
+ @Override
+ public void validateTwoFactorAccessToken(AppUser user, String token) {
+ TFAccessToken accessToken = fetchAccessTokenForUser(user, token);
+
+ if(accessToken == null || !accessToken.isValid()) {
+ throw new AccessTokenInvalidIException();
+ }
+ }
+
+ @Override
+ @CacheEvict(value = "userTFAccessToken",
+ key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil)" +
+ ".getTenant().getTenantIdentifier().concat(#user.username).concat(#result.token + 'tok')")
+ public TFAccessToken invalidateAccessToken(final AppUser user, final JsonCommand command) {
+
+ final String token = command.stringValueOfParameterNamed("token");
+ final TFAccessToken accessToken = fetchAccessTokenForUser(user, token);
+
+ if(accessToken == null || !accessToken.isValid()) {
+ throw new AccessTokenInvalidIException();
+ }
+
+ accessToken.setEnabled(false);
+ tfAccessTokenRepository.save(accessToken);
+
+ return accessToken;
+ }
+
+ @Override
+ @Cacheable(value = "userTFAccessToken",
+ key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil)" +
+ ".getTenant().getTenantIdentifier().concat(#user.username).concat(#token + 'tok')")
+ public TFAccessToken fetchAccessTokenForUser(final AppUser user, final String token) {
+ return tfAccessTokenRepository.findByUserAndToken(user, token);
+ }
+
+ private OTPDeliveryMethod getSMSDeliveryMethodForUser(final AppUser user) {
+ if(!configurationService.isSMSEnabled()) {
+ return null;
+ }
+
+ if(configurationService.getSMSProviderId() == null) {
+ return null;
+ }
+
+ if(user.getStaff() == null) {
+ return null;
+ }
+ String mobileNo = user.getStaff().mobileNo();
+ if(StringUtils.isBlank(mobileNo)) {
+ return null;
+ }
+
+ return new OTPDeliveryMethod(TwoFactorConstants.SMS_DELIVERY_METHOD_NAME, mobileNo);
+ }
+
+ private OTPDeliveryMethod getEmailDeliveryMethodForUser(final AppUser user) {
+ if(!configurationService.isEmailEnabled()) {
+ return null;
+ }
+
+ return new OTPDeliveryMethod(TwoFactorConstants.EMAIL_DELIVERY_METHOD_NAME, user.getEmail());
+ }
+
+ private OTPRequest generateNewToken(final OTPDeliveryMethod deliveryMethod, final boolean extendedAccessToken) {
+ int tokenLiveTime = configurationService.getOTPTokenLiveTime();
+ int otpLength = configurationService.getOTPTokenLength();
+ String token = new RandomOTPGenerator(otpLength).generate();
+ return OTPRequest.create(token, tokenLiveTime, extendedAccessToken, deliveryMethod);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorUtils.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorUtils.java
new file mode 100644
index 000000000..89292463b
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorUtils.java
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.service;
+
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+@Component
+public class TwoFactorUtils {
+
+ private static final String TWO_FACTOR_PROFILE_NAME = "twofactor";
+
+ private final Environment environment;
+
+ @Autowired
+ public TwoFactorUtils(Environment environment) {
+ this.environment = environment;
+ }
+
+
+ public boolean isTwoFactorAuthEnabled() {
+ for(final String profile : this.environment.getActiveProfiles()) {
+ if(TWO_FACTOR_PROFILE_NAME.equals(profile)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/UUIDAccessTokenGenerationService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/UUIDAccessTokenGenerationService.java
new file mode 100644
index 000000000..12e09c4a7
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/UUIDAccessTokenGenerationService.java
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.service;
+
+import java.util.UUID;
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class UUIDAccessTokenGenerationService implements AccessTokenGenerationService {
+
+ @Override
+ public String generateRandomToken() {
+ return UUID.randomUUID().toString().replaceAll("-", "");
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java
index 9c8a86f86..d5bcfd522 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java
@@ -491,6 +491,23 @@ public class AppUser extends AbstractPersistableCustom implements Platform
return hasNotPermission;
}
+ /**
+ * Checks whether the user has a given permission explicitly.
+ *
+ * @param permissionCode the permission code to check for.
+ * @return whether the user has the specified permission
+ */
+ public boolean hasSpecificPermissionTo(final String permissionCode) {
+ boolean hasPermission = false;
+ for (final Role role : this.roles) {
+ if(role.hasPermissionTo(permissionCode)) {
+ hasPermission = true;
+ break;
+ }
+ }
+ return hasPermission;
+ }
+
public void validateHasReadPermission(final String resourceType) {
final String authorizationMessage = "User has no authority to view " + resourceType.toLowerCase() + "s";
diff --git a/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml b/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml
index b991c6996..b5a442df5 100644
--- a/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml
+++ b/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml
@@ -48,4 +48,8 @@
overflowToDisk="false" />
+
+
\ No newline at end of file
diff --git a/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml b/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml
index f03220f79..a4db7fef5 100644
--- a/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml
+++ b/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml
@@ -42,19 +42,26 @@
method="POST" requires-channel="https" />
-
+
+
+
-
-
-
-
+
-
+
-
+
+
+
+
-
-
-
-
+
diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V336__two_factor_authentication.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V336__two_factor_authentication.sql
new file mode 100644
index 000000000..bd5d35950
--- /dev/null
+++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V336__two_factor_authentication.sql
@@ -0,0 +1,63 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements. See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership. The ASF licenses this file
+-- to you under the Apache License, Version 2.0 (the
+-- "License"); you may not use this file except in compliance
+-- with the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing,
+-- software distributed under the License is distributed on an
+-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+-- KIND, either express or implied. See the License for the
+-- specific language governing permissions and limitations
+-- under the License.
+--
+
+-- Access Token Table
+
+CREATE TABLE `twofactor_access_token` (
+ `id` bigint(20) NOT NULL AUTO_INCREMENT,
+ `token` varchar(32) NOT NULL,
+ `appuser_id` bigint(20) NOT NULL,
+ `valid_from` datetime NOT NULL,
+ `valid_to` datetime NOT NULL,
+ `enabled` bit(1) NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `token_appuser_UNIQUE` (`token`,`appuser_id`),
+ KEY `user` (`appuser_id`),
+ KEY `token` (`token`),
+ CONSTRAINT `fk_2fa_access_token_user_id` FOREIGN KEY (`appuser_id`) REFERENCES `m_appuser` (`id`)
+);
+
+-- Configuration
+
+CREATE TABLE `twofactor_configuration` (
+ `id` bigint(20) NOT NULL AUTO_INCREMENT,
+ `name` varchar(40) NOT NULL,
+ `value` varchar(1024) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `key_UNIQUE` (`name`)
+);
+
+INSERT INTO `twofactor_configuration` (`name`, `value`) VALUES
+ ('otp-delivery-email-enable', 'true'),
+ ('otp-delivery-email-subject', 'Fineract Two-Factor Authentication Token'),
+ ('otp-delivery-email-body', 'Hello {{username}}.\nYour OTP login token is {{token}}.'),
+ ('otp-delivery-sms-enable', 'false'),
+ ('otp-delivery-sms-provider', '1'),
+ ('otp-delivery-sms-text', 'Your authentication token for Fineract is {{token}}.'),
+ ('otp-token-live-time', '300'),
+ ('otp-token-length', '5'),
+ ('access-token-live-time', '86400'),
+ ('access-token-live-time-extended', '604800');
+
+
+INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES
+ ('authorisation', 'INVALIDATE_TWOFACTOR_ACCESSTOKEN', 'TWOFACTOR_ACCESSTOKEN', 'INVALIDATE', '0'),
+ ('configuration', 'READ_TWOFACTOR_CONFIGURATION', 'TWOFACTOR_CONFIGURATION', 'READ', '0'),
+ ('configuration', 'UPDATE_TWOFACTOR_CONFIGURATION', 'TWOFACTOR_CONFIGURATION', 'UPDATE', '0'),
+ ('special', 'BYPASS_TWOFACTOR', NULL, NULL, '0');
\ No newline at end of file
From 81b2ba1c311e928b30cc80858ff9828fba591d62 Mon Sep 17 00:00:00 2001
From: avikganguly01
Date: Fri, 22 Sep 2017 22:16:22 +0530
Subject: [PATCH 03/30] Fineract-521 Undo Transaction
---
.../portfolio/savings/domain/SavingsAccount.java | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
index e87470193..3f2da48e0 100755
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
@@ -1148,9 +1148,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
final List dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType().resourceName() + transactionAction);
- if (this.allowOverdraft) {
- baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("results.in.balance.exceeding.overdraft.limit");
- } else {
+ if (!this.allowOverdraft) {
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("results.in.balance.going.negative");
}
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
@@ -1160,6 +1158,13 @@ public class SavingsAccount extends AbstractPersistableCustom {
lastSavingsDate = transaction.transactionLocalDate();
}
+
+ BigDecimal withdrawalFee = null;
+ BigDecimal transactionAmount = null;
+ if(isOverdraft()) {
+ if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { throw new InsufficientAccountBalanceException(
+ "transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); }
+ }
}
protected boolean isAccountLocked(final LocalDate transactionDate) {
From 2e00bff5575053ca3953bd0d23dc4ae680118fc9 Mon Sep 17 00:00:00 2001
From: nazeer shaik
Date: Tue, 31 Oct 2017 19:10:14 +0530
Subject: [PATCH 04/30] FINERACT-467 : equal amortization
---
.../loanaccount/api/LoanApiConstants.java | 2 +
.../loanaccount/data/LoanAccountData.java | 41 ++++-----
.../portfolio/loanaccount/domain/Loan.java | 21 ++++-
.../domain/AbstractLoanScheduleGenerator.java | 9 +-
.../domain/LoanApplicationTerms.java | 87 ++++++++++++-------
.../domain/LoanScheduleModel.java | 7 ++
.../service/LoanScheduleAssembler.java | 29 ++++++-
...ateLoanScheduleQueryFromApiJsonHelper.java | 2 +-
...anApplicationCommandFromApiJsonHelper.java | 37 +++++++-
.../service/LoanReadPlatformServiceImpl.java | 6 +-
.../loanproduct/LoanProductConstants.java | 2 +
.../api/LoanProductsApiResource.java | 2 +-
.../loanproduct/data/LoanProductData.java | 25 ++++--
.../loanproduct/domain/LoanProduct.java | 21 ++++-
.../domain/LoanProductRelatedDetail.java | 24 ++++-
...ortizationUnsupportedFeatureException.java | 30 +++++++
.../LoanProductDataValidator.java | 43 ++++++++-
.../LoanProductReadPlatformServiceImpl.java | 7 +-
.../core_db/V337__equal_amortization.sql | 21 +++++
.../LoanProductRelatedDetailTestHelper.java | 4 +-
20 files changed, 330 insertions(+), 90 deletions(-)
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/exception/EqualAmortizationUnsupportedFeatureException.java
create mode 100644 fineract-provider/src/main/resources/sql/migrations/core_db/V337__equal_amortization.sql
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java
index 6e5376a79..c8480a431 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java
@@ -124,4 +124,6 @@ public interface LoanApiConstants {
public static final String topupAmount = "topupAmount";
public static final String datatables = "datatables";
+
+ public static final String isEqualAmortizationParam = "isEqualAmortization";
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java
index 5109c3e71..ad21674fb 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java
@@ -213,6 +213,7 @@ public class LoanAccountData {
private final Integer maximumGap;
private List datatables = null;
+ private final Boolean isEqualAmortization;
/**
* Used to produce a {@link LoanAccountData} with only collateral options
@@ -331,7 +332,7 @@ public class LoanAccountData {
final Long closureLoanId = null;
final String closureLoanAccountNo = null;
final BigDecimal topupAmount = null;
-
+ final boolean isEqualAmortization = false;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group,
loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName,
loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, principal,
@@ -350,7 +351,7 @@ public class LoanAccountData {
maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges,
isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule,
createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap,
- maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount);
+ maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization);
}
@@ -471,7 +472,7 @@ public class LoanAccountData {
final Long closureLoanId = null;
final String closureLoanAccountNo = null;
final BigDecimal topupAmount = null;
-
+ final boolean isEqualAmortization = false;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group,
loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName,
loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, principal,
@@ -490,7 +491,7 @@ public class LoanAccountData {
maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges,
isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule,
createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap,
- maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount);
+ maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization);
}
@@ -520,7 +521,7 @@ public class LoanAccountData {
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule,
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup,
- acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount);
+ acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization);
}
/**
@@ -642,7 +643,7 @@ public class LoanAccountData {
final Long closureLoanId = null;
final String closureLoanAccountNo = null;
final BigDecimal topupAmount = null;
-
+ final boolean isEqualAmortization = false;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group,
loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName,
loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, principal,
@@ -661,7 +662,7 @@ public class LoanAccountData {
maxOutstandingBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges,
isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule,
createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap,
- maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount);
+ maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization);
}
@@ -691,7 +692,7 @@ public class LoanAccountData {
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule,
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup,
- acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount);
+ acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization);
}
@@ -821,7 +822,6 @@ public class LoanAccountData {
final Long closureLoanId = null;
final String closureLoanAccountNo = null;
final BigDecimal topupAmount = null;
-
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group,
loanType, product.getId(), product.getName(), product.getDescription(), product.isLinkedToFloatingInterestRates(),
product.getFundId(), product.getFundName(), loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName,
@@ -846,7 +846,7 @@ public class LoanAccountData {
originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods,
product.isVariableInstallmentsAllowed(), product.getMinimumGapBetweenInstallments(),
product.getMaximumGapBetweenInstallments(), subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId,
- closureLoanAccountNo, topupAmount);
+ closureLoanAccountNo, topupAmount, product.isEqualAmortization());
}
public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) {
@@ -906,7 +906,7 @@ public class LoanAccountData {
product.toLoanInterestRecalculationData(), acc.originalSchedule, acc.createStandingInstructionAtDisbursement,
paidInAdvance, acc.interestRatesPeriods, product.isVariableInstallmentsAllowed(),
product.getMinimumGapBetweenInstallments(), product.getMaximumGapBetweenInstallments(), acc.subStatus, acc.canUseForTopup,
- acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount);
+ acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, product.isEqualAmortization());
}
@@ -938,7 +938,7 @@ public class LoanAccountData {
final LoanInterestRecalculationData interestRecalculationData, final Boolean createStandingInstructionAtDisbursement,
final Boolean isVariableInstallmentsAllowed, Integer minimumGap, Integer maximumGap, final EnumOptionData subStatus,
final boolean canUseForTopup, final boolean isTopup, final Long closureLoanId, final String closureLoanAccountNo,
- final BigDecimal topupAmount) {
+ final BigDecimal topupAmount, final boolean isEqualAmortization) {
final LoanScheduleData repaymentSchedule = null;
final Collection transactions = null;
@@ -994,7 +994,7 @@ public class LoanAccountData {
outstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges,
isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule,
createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap,
- maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount);
+ maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization);
}
/*
@@ -1047,7 +1047,7 @@ public class LoanAccountData {
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule,
acc.createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, acc.isVariableInstallmentsAllowed,
acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, clientActiveLoanOptions, acc.isTopup,
- acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount);
+ acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization);
}
public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions,
@@ -1089,7 +1089,7 @@ public class LoanAccountData {
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule,
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup,
- acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount);
+ acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization);
}
public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map memberLoanCycle) {
@@ -1154,7 +1154,7 @@ public class LoanAccountData {
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule,
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup,
- acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount);
+ acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization);
}
@@ -1188,7 +1188,7 @@ public class LoanAccountData {
acc.isInterestRecalculationEnabled, interestRecalculationData, acc.originalSchedule,
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup,
- acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount);
+ acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization);
}
public static LoanAccountData withLoanCalendarData(final LoanAccountData acc, final CalendarData calendarData) {
@@ -1216,7 +1216,7 @@ public class LoanAccountData {
acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData,
acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup,
- acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount);
+ acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization);
}
public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) {
@@ -1245,7 +1245,7 @@ public class LoanAccountData {
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, originalSchedule,
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup,
- acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount);
+ acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization);
}
private LoanAccountData(final Long id, //
@@ -1295,7 +1295,7 @@ public class LoanAccountData {
final Collection interestRatesPeriods, final Boolean isVariableInstallmentsAllowed,
final Integer minimumGap, final Integer maximumGap, final EnumOptionData subStatus, final Boolean canUseForTopup,
final Collection clientActiveLoanOptions, final boolean isTopup,
- final Long closureLoanId, final String closureLoanAccountNo, final BigDecimal topupAmount) {
+ final Long closureLoanId, final String closureLoanAccountNo, final BigDecimal topupAmount, final boolean isEqualAmortization) {
this.id = id;
this.accountNo = accountNo;
@@ -1477,6 +1477,7 @@ public class LoanAccountData {
this.closureLoanId = closureLoanId;
this.closureLoanAccountNo = closureLoanAccountNo;
this.topupAmount = topupAmount;
+ this.isEqualAmortization = isEqualAmortization;
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index 480b08108..193810d50 100755
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -115,6 +115,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignme
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
@@ -2616,10 +2617,24 @@ public class Loan extends AbstractPersistableCustom {
final RoundingMode roundingMode = MoneyHelper.getRoundingMode();
final MathContext mc = new MathContext(8, roundingMode);
- final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(scheduleGeneratorDTO);
-
- final LoanScheduleGenerator loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(interestMethod);
+ LoanScheduleGenerator loanScheduleGenerator = null;
+ if (loanApplicationTerms.isEqualAmortization()) {
+ if (loanApplicationTerms.getInterestMethod().isDecliningBalnce()) {
+ final LoanScheduleGenerator decliningLoanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(
+ InterestMethod.DECLINING_BALANCE);
+ Set loanCharges = charges();
+ LoanScheduleModel loanSchedule = decliningLoanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, scheduleGeneratorDTO.getHolidayDetailDTO());
+
+ loanApplicationTerms.updateTotalInterestDue(Money.of(loanApplicationTerms.getCurrency(), loanSchedule.getTotalInterestCharged()));
+
+
+ }
+ loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(InterestMethod.FLAT);
+ } else {
+ loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(loanApplicationTerms.getInterestMethod());
+ }
+
final LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, loanApplicationTerms, charges(),
scheduleGeneratorDTO.getHolidayDetailDTO());
return loanSchedule;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java
index 19a203a5c..fb5c12ab0 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java
@@ -67,7 +67,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
final LoanScheduleParams loanScheduleRecalculationDTO = null;
return generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, loanScheduleRecalculationDTO);
}
-
+
private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
final Set loanCharges, final HolidayDetailDTO holidayDetailDTO, final LoanScheduleParams loanScheduleParams) {
@@ -114,10 +114,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// Determine the total interest owed over the full loan for FLAT
// interest method .
- if (!scheduleParams.isPartialUpdate()) {
+ if (!scheduleParams.isPartialUpdate() && !loanApplicationTerms.isEqualAmortization()) {
Money totalInterestChargedForFullLoanTerm = loanApplicationTerms.calculateTotalInterestCharged(
- this.paymentPeriodsInOneYearCalculator, mc);
+ this.paymentPeriodsInOneYearCalculator, mc);
+
loanApplicationTerms.updateTotalInterestDue(totalInterestChargedForFullLoanTerm);
+
}
boolean isFirstRepayment = true;
@@ -177,6 +179,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
isFirstRepayment = false;
}
+
while (!scheduleParams.getOutstandingBalance().isZero() || !scheduleParams.getDisburseDetailMap().isEmpty()) {
LocalDate previousRepaymentDate = scheduleParams.getActualRepaymentDate();
scheduleParams.setActualRepaymentDate(this.scheduledDateGenerator.generateNextRepaymentDate(
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
index d5f08cb33..24b166ea5 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
@@ -210,7 +210,7 @@ public final class LoanApplicationTerms {
private Money totalInterestAccounted;
private int periodsCompleted = 0;
private int extraPeriods = 0;
-
+ private boolean isEqualAmortization;
@@ -234,7 +234,8 @@ public final class LoanApplicationTerms {
final LoanPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy, final Calendar loanCalendar,
BigDecimal approvedAmount, List loanTermVariations,
Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final Integer numberOfdays,
- boolean isSkipRepaymentOnFirstDayofMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod) {
+ boolean isSkipRepaymentOnFirstDayofMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod,
+ final boolean isEqualAmortization) {
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
@@ -249,7 +250,7 @@ public final class LoanApplicationTerms {
compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf,
preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper,
isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfdays, isSkipRepaymentOnFirstDayofMonth, holidayDetailDTO,
- allowCompoundingOnEod);
+ allowCompoundingOnEod, isEqualAmortization);
}
@@ -317,6 +318,7 @@ public final class LoanApplicationTerms {
final DaysInYearType daysInYearType = loanProductRelatedDetail.fetchDaysInYearType();
final boolean isInterestRecalculationEnabled = loanProductRelatedDetail.isInterestRecalculationEnabled();
final boolean isInterestChargedFromDateSameAsDisbursalDateEnabled = false;
+ final boolean isEqualAmortization = loanProductRelatedDetail.isEqualAmortization();
return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
repaymentEvery, repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod,
interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate,
@@ -328,7 +330,7 @@ public final class LoanApplicationTerms {
recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment,
installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount,
loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfdays,
- isSkipRepaymentOnFirstDayofMonth, holidayDetailDTO, allowCompoundingOnEod);
+ isSkipRepaymentOnFirstDayofMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization);
}
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
@@ -375,7 +377,7 @@ public final class LoanApplicationTerms {
}
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
final boolean isInterestChargedFromDateSameAsDisbursalDateEnabled = false;
-
+ final boolean isEqualAmortization = loanProductRelatedDetail.isEqualAmortization();
return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
repaymentEvery, repaymentPeriodFrequencyType, null, null, amortizationMethod, interestMethod, interestRatePerPeriod,
interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
@@ -387,7 +389,7 @@ public final class LoanApplicationTerms {
compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf,
loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper,
isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfdays, isSkipRepaymentOnFirstDayofMonth, holidayDetailDTO,
- allowCompoundingOnEod);
+ allowCompoundingOnEod, isEqualAmortization);
}
@@ -413,7 +415,7 @@ public final class LoanApplicationTerms {
applicationTerms.approvedPrincipal.getAmount(), loanTermVariations, applicationTerms.calendarHistoryDataWrapper,
applicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled, applicationTerms.numberOfDays,
applicationTerms.isSkipRepaymentOnFirstDayOfMonth, applicationTerms.holidayDetailDTO,
- applicationTerms.allowCompoundingOnEod);
+ applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization);
}
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
@@ -437,7 +439,7 @@ public final class LoanApplicationTerms {
BigDecimal approvedAmount, List loanTermVariations,
final CalendarHistoryDataWrapper calendarHistoryDataWrapper, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled,
final Integer numberOfdays, final boolean isSkipRepaymentOnFirstDayofMonth, final HolidayDetailDTO holidayDetailDTO,
- final boolean allowCompoundingOnEod) {
+ final boolean allowCompoundingOnEod, final boolean isEqualAmortization) {
this.currency = currency;
this.loanTermFrequency = loanTermFrequency;
@@ -509,6 +511,7 @@ public final class LoanApplicationTerms {
this.totalPrincipalAccountedForInterestCalcualtion = principal.zero();
this.totalInterestAccounted = principal.zero();
this.totalPrincipalAccounted = principal.zero();
+ this.isEqualAmortization = isEqualAmortization;
}
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
@@ -667,27 +670,36 @@ public final class LoanApplicationTerms {
Money interestForInstallment = this.principal.zero();
Money interestBroughtForwardDueToGrace = cumulatingInterestPaymentDueToGrace.copy();
+ InterestMethod interestMethod = this.interestMethod;
- switch (this.interestMethod) {
+ if (this.isEqualAmortization() && this.totalInterestDue != null) {
+ interestMethod = InterestMethod.FLAT;
+ }
+ switch (interestMethod) {
case FLAT:
-
- switch (this.amortizationMethod) {
- case EQUAL_INSTALLMENTS:
- // average out outstanding interest over remaining
- // instalments where interest is applicable
- interestForInstallment = calculateTotalFlatInterestForInstallmentAveragingOutGracePeriods(calculator, periodNumber,
- mc);
- break;
- case EQUAL_PRINCIPAL:
- // interest follows time-value of money and is brought
- // forward to next applicable interest payment period
- final PrincipalInterest result = calculateTotalFlatInterestForPeriod(calculator, periodNumber, mc,
- interestBroughtForwardDueToGrace);
- interestForInstallment = result.interest();
- interestBroughtForwardDueToGrace = result.interestPaymentDueToGrace();
- break;
- case INVALID:
- break;
+ if (this.isEqualAmortization() && this.totalInterestDue != null && this.interestMethod.isDecliningBalnce()) {
+ interestForInstallment = flatInterestPerInstallment(mc, this.totalInterestDue);
+ } else {
+ switch (this.amortizationMethod) {
+ case EQUAL_INSTALLMENTS:
+ // average out outstanding interest over remaining
+ // instalments where interest is applicable
+ interestForInstallment = calculateTotalFlatInterestForInstallmentAveragingOutGracePeriods(calculator,
+ periodNumber, mc);
+ break;
+ case EQUAL_PRINCIPAL:
+ // interest follows time-value of money and is
+ // brought
+ // forward to next applicable interest payment
+ // period
+ final PrincipalInterest result = calculateTotalFlatInterestForPeriod(calculator, periodNumber, mc,
+ interestBroughtForwardDueToGrace);
+ interestForInstallment = result.interest();
+ interestBroughtForwardDueToGrace = result.interestPaymentDueToGrace();
+ break;
+ case INVALID:
+ break;
+ }
}
break;
case DECLINING_BALANCE:
@@ -868,13 +880,18 @@ public final class LoanApplicationTerms {
private Money calculateTotalInterestPerInstallmentWithoutGrace(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) {
final Money totalInterestForLoanTerm = calculateTotalFlatInterestDueWithoutGrace(calculator, mc);
- Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(Long.valueOf(this.actualNumberOfRepayments) - defaultToZeroIfNull(this.excludePeriodsForCalculation), mc.getRoundingMode());
+ return flatInterestPerInstallment(mc, totalInterestForLoanTerm);
+ }
+
+ private Money flatInterestPerInstallment(final MathContext mc, final Money totalInterestForLoanTerm) {
+ Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(Long.valueOf(this.actualNumberOfRepayments)
+ - defaultToZeroIfNull(this.excludePeriodsForCalculation), mc.getRoundingMode());
if (this.excludePeriodsForCalculation < this.periodsCompleted) {
Money interestLeft = this.totalInterestDue.minus(this.totalInterestAccounted);
interestPerInstallment = interestLeft.dividedBy(Long.valueOf(this.actualNumberOfRepayments)
- defaultToZeroIfNull(this.periodsCompleted), mc.getRoundingMode());
}
-
+
return interestPerInstallment;
}
@@ -1320,7 +1337,7 @@ public final class LoanApplicationTerms {
this.interestCalculationPeriodMethod, this.allowPartialPeriodInterestCalcualtion, this.repaymentEvery,
this.repaymentPeriodFrequencyType, this.numberOfRepayments, this.principalGrace, this.recurringMoratoriumOnPrincipalPeriods, this.interestPaymentGrace,
this.interestChargingGrace, this.amortizationMethod, this.inArrearsTolerance.getAmount(), this.graceOnArrearsAgeing,
- this.daysInMonthType.getValue(), this.daysInYearType.getValue(), this.interestRecalculationEnabled);
+ this.daysInMonthType.getValue(), this.daysInYearType.getValue(), this.interestRecalculationEnabled, this.isEqualAmortization);
}
public Integer getLoanTermFrequency() {
@@ -1746,5 +1763,13 @@ public final class LoanApplicationTerms {
public void setSeedDate(LocalDate seedDate) {
this.seedDate = seedDate;
}
-
+
+ public boolean isEqualAmortization() {
+ return isEqualAmortization;
+ }
+
+ public void setEqualAmortization(boolean isEqualAmortization) {
+ this.isEqualAmortization = isEqualAmortization;
+ }
+
}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java
index 22f8bac03..29c092eb3 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java
@@ -120,4 +120,11 @@ public final class LoanScheduleModel {
public BigDecimal getTotalPenaltyChargesCharged() {
return this.totalPenaltyChargesCharged;
}
+
+
+ public BigDecimal getTotalInterestCharged() {
+ return this.totalInterestCharged;
+ }
+
+
}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
index 095da8aea..c3f48a850 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
@@ -89,6 +89,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.apache.fineract.portfolio.loanaccount.exception.MinDaysBetweenDisbursalAndFirstRepaymentViolationException;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
@@ -201,6 +202,11 @@ public class LoanScheduleAssembler {
final Integer amortizationType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType", element);
final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(amortizationType);
+
+ boolean isEqualAmortization = false;
+ if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) {
+ isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam, element);
+ }
// interest terms
final Integer interestType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element);
@@ -432,6 +438,8 @@ public class LoanScheduleAssembler {
HolidayStatusType.ACTIVE.getValue());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
+
+
return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod,
interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
@@ -442,7 +450,7 @@ public class LoanScheduleAssembler {
recalculationFrequencyType, restCalendarInstance, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType,
principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanProduct.preCloseInterestCalculationStrategy(),
calendar, BigDecimal.ZERO, loanTermVariations, isInterestChargedFromDateSameAsDisbursalDateEnabled,numberOfDays, isSkipMeetingOnFirstDay, detailDTO,
- allowCompoundingOnEod);
+ allowCompoundingOnEod, isEqualAmortization);
}
private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery,
@@ -609,13 +617,26 @@ public class LoanScheduleAssembler {
List disbursementDetails) {
final Set loanCharges = this.loanChargeAssembler.fromParsedJson(element, disbursementDetails);
-
- final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod());
+
final RoundingMode roundingMode = MoneyHelper.getRoundingMode();
final MathContext mc = new MathContext(8, roundingMode);
-
HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
+
+ LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod());
+ if (loanApplicationTerms.isEqualAmortization()) {
+ if (loanApplicationTerms.getInterestMethod().isDecliningBalnce()) {
+ final LoanScheduleGenerator decliningLoanScheduleGenerator = this.loanScheduleFactory
+ .create(InterestMethod.DECLINING_BALANCE);
+ LoanScheduleModel loanSchedule = decliningLoanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, detailDTO);
+
+ loanApplicationTerms.updateTotalInterestDue(Money.of(loanApplicationTerms.getCurrency(), loanSchedule.getTotalInterestCharged()));
+
+ }
+ loanScheduleGenerator = this.loanScheduleFactory.create(InterestMethod.FLAT);
+ } else {
+ loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod());
+ }
return loanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, detailDTO);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java
index 57e0c74aa..d33ea1df1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java
@@ -69,7 +69,7 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper {
LoanProductConstants.graceOnArrearsAgeingParameterName, LoanApiConstants.createStandingInstructionAtDisbursementParameterName,
LoanApiConstants.isFloatingInterestRateParameterName, LoanApiConstants.interestRateDifferentialParameterName,
LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName,
- LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables));
+ LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables, LoanApiConstants.isEqualAmortizationParam));
private final FromJsonHelper fromApiJsonHelper;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java
index 198f52405..466ec016c 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java
@@ -37,7 +37,6 @@ import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterExc
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.accountdetails.domain.AccountType;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
-import org.apache.fineract.portfolio.client.api.ClientApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
@@ -45,6 +44,7 @@ import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
+import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.joda.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;
@@ -93,7 +93,7 @@ public final class LoanApplicationCommandFromApiJsonHelper {
LoanApiConstants.linkAccountIdParameterName, LoanApiConstants.disbursementDataParameterName,
LoanApiConstants.emiAmountParameterName, LoanApiConstants.maxOutstandingBalanceParameterName,
LoanProductConstants.graceOnArrearsAgeingParameterName, LoanApiConstants.createStandingInstructionAtDisbursementParameterName,
- LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables));
+ LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables, LoanApiConstants.isEqualAmortizationParam));
private final FromJsonHelper fromApiJsonHelper;
private final CalculateLoanScheduleQueryFromApiJsonHelper apiJsonHelper;
@@ -119,7 +119,7 @@ public final class LoanApplicationCommandFromApiJsonHelper {
final String loanTypeParameterName = "loanType";
final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element);
baseDataValidator.reset().parameter(loanTypeParameterName).value(loanTypeStr).notNull();
-
+
if (!StringUtils.isBlank(loanTypeStr)) {
final AccountType loanType = AccountType.fromName(loanTypeStr);
baseDataValidator.reset().parameter(loanTypeParameterName).value(loanType.getValue()).inMinMaxRange(1, 3);
@@ -160,6 +160,15 @@ public final class LoanApplicationCommandFromApiJsonHelper {
}
}
+
+ boolean isEqualAmortization = false;
+ if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) {
+ isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam, element);
+ baseDataValidator.reset().parameter(LoanApiConstants.isEqualAmortizationParam).value(isEqualAmortization).ignoreIfNull()
+ .validateForBooleanValue();
+ if (isEqualAmortization && loanProduct.isInterestRecalculationEnabled()) { throw new EqualAmortizationUnsupportedFeatureException(
+ "interest.recalculation", "interest recalculation"); }
+ }
final Long productId = this.fromApiJsonHelper.extractLongNamed("productId", element);
baseDataValidator.reset().parameter("productId").value(productId).notNull().integerGreaterThanZero();
@@ -235,6 +244,8 @@ public final class LoanApplicationCommandFromApiJsonHelper {
.inMinMaxRange(0, 1);
if (loanProduct.isLinkedToFloatingInterestRate()) {
+ if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate",
+ "floating interest rate"); }
if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) {
baseDataValidator
.reset()
@@ -461,6 +472,7 @@ public final class LoanApplicationCommandFromApiJsonHelper {
unsupportedParameterList.add(LoanApiConstants.emiAmountParameterName);
throw new UnsupportedParameterException(unsupportedParameterList);
}
+ if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); }
final BigDecimal emiAnount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName,
element);
baseDataValidator.reset().parameter(LoanApiConstants.emiAmountParameterName).value(emiAnount).ignoreIfNull().positiveAmount();
@@ -531,6 +543,15 @@ public final class LoanApplicationCommandFromApiJsonHelper {
final String accountNo = this.fromApiJsonHelper.extractStringNamed(accountNoParameterName, element);
baseDataValidator.reset().parameter(accountNoParameterName).value(accountNo).notBlank().notExceedingLengthOf(20);
}
+
+ boolean isEqualAmortization = existingLoanApplication.getLoanProductRelatedDetail().isEqualAmortization();
+ if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isEqualAmortizationParam, element)) {
+ isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.isEqualAmortizationParam, element);
+ baseDataValidator.reset().parameter(LoanProductConstants.isEqualAmortizationParam).value(isEqualAmortization).ignoreIfNull()
+ .validateForBooleanValue();
+ if (isEqualAmortization && loanProduct.isInterestRecalculationEnabled()) { throw new EqualAmortizationUnsupportedFeatureException(
+ "interest.recalculation", "interest recalculation"); }
+ }
final String externalIdParameterName = "externalId";
if (this.fromApiJsonHelper.parameterExists(externalIdParameterName, element)) {
@@ -632,6 +653,8 @@ public final class LoanApplicationCommandFromApiJsonHelper {
}
if (loanProduct.isLinkedToFloatingInterestRate()) {
+ if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate",
+ "floating interest rate"); }
if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) {
baseDataValidator
.reset()
@@ -907,6 +930,7 @@ public final class LoanApplicationCommandFromApiJsonHelper {
unsupportedParameterList.add(LoanApiConstants.emiAmountParameterName);
throw new UnsupportedParameterException(unsupportedParameterList);
}
+ if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); }
final BigDecimal emiAnount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName,
element);
baseDataValidator.reset().parameter(LoanApiConstants.emiAmountParameterName).value(emiAnount).ignoreIfNull().positiveAmount();
@@ -1076,11 +1100,18 @@ public final class LoanApplicationCommandFromApiJsonHelper {
final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement);
if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.disbursementDataParameterName, element) && expectedDisbursement != null
&& totalPrincipal != null) {
+
BigDecimal tatalDisbursement = BigDecimal.ZERO;
final JsonArray variationArray = this.fromApiJsonHelper.extractJsonArrayNamed(LoanApiConstants.disbursementDataParameterName,
element);
List expectedDisbursementDates = new ArrayList<>();
if (variationArray != null && variationArray.size() > 0) {
+ if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) {
+ boolean isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam,
+ element);
+ if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("tranche.disbursal",
+ "tranche disbursal"); }
+ }
int i = 0;
do {
final JsonObject jsonObject = variationArray.get(i).getAsJsonObject();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
index ab6c31044..8c2994256 100755
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
@@ -563,7 +563,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
+ " l.nominal_interest_rate_per_period as interestRatePerPeriod, l.annual_nominal_interest_rate as annualInterestRate, "
+ " l.repayment_period_frequency_enum as repaymentFrequencyType, l.interest_period_frequency_enum as interestRateFrequencyType, "
+ " l.term_frequency as termFrequency, l.term_period_frequency_enum as termPeriodFrequencyType, "
- + " l.amortization_method_enum as amortizationType, l.interest_method_enum as interestType, l.interest_calculated_in_period_enum as interestCalculationPeriodType,"
+ + " l.amortization_method_enum as amortizationType, l.interest_method_enum as interestType, l.is_equal_amortization as isEqualAmortization, l.interest_calculated_in_period_enum as interestCalculationPeriodType,"
+ " l.allow_partial_period_interest_calcualtion as allowPartialPeriodInterestCalcualtion,"
+ " l.loan_status_id as lifeCycleStatusId, l.loan_transaction_strategy_id as transactionStrategyId, "
+ " lps.name as transactionStrategyName, "
@@ -790,7 +790,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final int amortizationTypeInt = JdbcSupport.getInteger(rs, "amortizationType");
final int interestTypeInt = JdbcSupport.getInteger(rs, "interestType");
final int interestCalculationPeriodTypeInt = JdbcSupport.getInteger(rs, "interestCalculationPeriodType");
-
+ final boolean isEqualAmortization = rs.getBoolean("isEqualAmortization");
final EnumOptionData amortizationType = LoanEnumerations.amortizationType(amortizationTypeInt);
final EnumOptionData interestType = LoanEnumerations.interestType(interestTypeInt);
final EnumOptionData interestCalculationPeriodType = LoanEnumerations
@@ -965,7 +965,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
loanProductCounter, multiDisburseLoan, canDefineInstallmentAmount, fixedEmiAmount, outstandingLoanBalance, inArrears,
graceOnArrearsAgeing, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, createStandingInstructionAtDisbursement, isvariableInstallmentsAllowed, minimumGap,
- maximumGap, loanSubStatus, canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount);
+ maximumGap, loanSubStatus, canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization);
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java
index 5022d013c..317f7f435 100755
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java
@@ -132,5 +132,7 @@ public interface LoanProductConstants {
public static final String canUseForTopup = "canUseForTopup";
+ public static final String isEqualAmortizationParam = "isEqualAmortization";
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java
index 80f1c3e20..5a56c5a18 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java
@@ -91,7 +91,7 @@ public class LoanProductsApiResource {
"interestCalculationPeriodTypeOptions", "transactionProcessingStrategyOptions", "chargeOptions", "accountingOptions",
"accountingRuleOptions", "accountingMappingOptions", "floatingRateOptions", "isLinkedToFloatingInterestRates",
"floatingRatesId", "interestRateDifferential", "minDifferentialLendingRate", "defaultDifferentialLendingRate",
- "maxDifferentialLendingRate", "isFloatingInterestRateCalculationAllowed", LoanProductConstants.canUseForTopup));
+ "maxDifferentialLendingRate", "isFloatingInterestRateCalculationAllowed", LoanProductConstants.canUseForTopup, LoanProductConstants.isEqualAmortizationParam));
private final Set PRODUCT_MIX_DATA_PARAMETERS = new HashSet<>(Arrays.asList("productId", "productName", "restrictedProducts",
"allowedProducts", "productOptions"));
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java
index d95fcce0c..31729b321 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java
@@ -183,6 +183,7 @@ public class LoanProductData {
private final Boolean accountMovesOutOfNPAOnlyOnArrearsCompletion;
private LoanProductConfigurableAttributes allowAttributeOverrides;
private final boolean syncExpectedWithDisbursementDate;
+ private final boolean isEqualAmortization;
/**
* Used when returning lookup information about loan product for dropdowns.
@@ -258,7 +259,7 @@ public class LoanProductData {
final LoanProductConfigurableAttributes loanProductConfigurableAttributes = null;
final boolean syncExpectedWithDisbursementDate = false;
final boolean canUseForTopup = false;
-
+ final boolean isEqualAmortization = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType,
@@ -273,7 +274,7 @@ public class LoanProductData {
loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId, floatingRateName,
interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate,
isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
- syncExpectedWithDisbursementDate, canUseForTopup);
+ syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization);
}
@@ -350,6 +351,7 @@ public class LoanProductData {
final LoanProductConfigurableAttributes loanProductConfigurableAttributes = null;
final boolean syncExpectedWithDisbursementDate = false;
final boolean canUseForTopup = false;
+ final boolean isEqualAmortization = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -365,7 +367,7 @@ public class LoanProductData {
loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId, floatingRateName,
interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate,
isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
- syncExpectedWithDisbursementDate, canUseForTopup);
+ syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization);
}
@@ -449,6 +451,7 @@ public class LoanProductData {
final LoanProductConfigurableAttributes loanProductConfigurableAttributes = null;
final boolean syncExpectedWithDisbursementDate = false;
final boolean canUseForTopup = false;
+ final boolean isEqualAmortization = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -464,7 +467,7 @@ public class LoanProductData {
installmentAmountInMultiplesOf, loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId,
floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate,
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
- syncExpectedWithDisbursementDate, canUseForTopup);
+ syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization);
}
@@ -542,6 +545,7 @@ public class LoanProductData {
final LoanProductConfigurableAttributes loanProductConfigurableAttributes = null;
final boolean syncExpectedWithDisbursementDate = false;
final boolean canUseForTopup = false;
+ final boolean isEqualAmortization = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -557,7 +561,7 @@ public class LoanProductData {
installmentAmountInMultiplesOf, loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId,
floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate,
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
- syncExpectedWithDisbursementDate, canUseForTopup);
+ syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization);
}
@@ -571,7 +575,7 @@ public class LoanProductData {
productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings;
return productData;
}
-
+
public LoanProductData(final Long id, final String name, final String shortName, final String description, final CurrencyData currency,
final BigDecimal principal, final BigDecimal minPrincipal, final BigDecimal maxPrincipal, final BigDecimal tolerance,
final Integer numberOfRepayments, final Integer minNumberOfRepayments, final Integer maxNumberOfRepayments,
@@ -596,7 +600,7 @@ public class LoanProductData {
BigDecimal minDifferentialLendingRate, BigDecimal defaultDifferentialLendingRate, BigDecimal maxDifferentialLendingRate,
boolean isFloatingInterestRateCalculationAllowed, final boolean isVariableInstallmentsAllowed,
final Integer minimumGapBetweenInstallments, final Integer maximumGapBetweenInstallments,
- final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup) {
+ final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup, final boolean isEqualAmortization) {
this.id = id;
this.name = name;
this.shortName = shortName;
@@ -700,6 +704,7 @@ public class LoanProductData {
this.preClosureInterestCalculationStrategyOptions = null;
this.syncExpectedWithDisbursementDate = syncExpectedWithDisbursementDate;
this.canUseForTopup = canUseForTopup;
+ this.isEqualAmortization = isEqualAmortization;
}
@@ -836,6 +841,7 @@ public class LoanProductData {
this.preClosureInterestCalculationStrategyOptions = preCloseInterestCalculationStrategyOptions;
this.syncExpectedWithDisbursementDate = productData.syncExpectedWithDisbursementDate;
this.canUseForTopup = productData.canUseForTopup;
+ this.isEqualAmortization = productData.isEqualAmortization;
}
private Collection nullIfEmpty(final Collection charges) {
@@ -1196,4 +1202,9 @@ public class LoanProductData {
public BigDecimal getInterestRateDifferential() {
return this.interestRateDifferential;
}
+
+ public boolean isEqualAmortization() {
+ return isEqualAmortization;
+ }
+
}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java
index 05173e455..443dd4f28 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java
@@ -182,6 +182,9 @@ public class LoanProduct extends AbstractPersistableCustom {
@Column(name = "can_use_for_topup", nullable = false)
private boolean canUseForTopup = false;
+
+ @Column(name = "is_equal_amortization", nullable = false)
+ private boolean isEqualAmortization = false;
public static LoanProduct assembleFromJson(final Fund fund, final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy,
final List productCharges, final JsonCommand command, final AprCalculator aprCalculator, FloatingRate floatingRate) {
@@ -331,6 +334,9 @@ public class LoanProduct extends AbstractPersistableCustom {
final boolean canUseForTopup = command.parameterExists(LoanProductConstants.canUseForTopup)
? command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.canUseForTopup)
: false;
+
+ final boolean isEqualAmortization = command.parameterExists(LoanProductConstants.isEqualAmortizationParam) ? command
+ .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.isEqualAmortizationParam) : false;
return new LoanProduct(fund, loanTransactionProcessingStrategy, name, shortName, description, currency, principal, minPrincipal,
maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType,
@@ -345,7 +351,7 @@ public class LoanProduct extends AbstractPersistableCustom {
installmentAmountInMultiplesOf, loanConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRate,
interestRateDifferential, minDifferentialLendingRate, maxDifferentialLendingRate, defaultDifferentialLendingRate,
isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGapBetweenInstallments,
- maximumGapBetweenInstallments, syncExpectedWithDisbursementDate, canUseForTopup);
+ maximumGapBetweenInstallments, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization);
}
@@ -575,7 +581,7 @@ public class LoanProduct extends AbstractPersistableCustom {
BigDecimal minDifferentialLendingRate, BigDecimal maxDifferentialLendingRate, BigDecimal defaultDifferentialLendingRate,
Boolean isFloatingInterestRateCalculationAllowed, final Boolean isVariableInstallmentsAllowed,
final Integer minimumGapBetweenInstallments, final Integer maximumGapBetweenInstallments,
- final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup) {
+ final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup, final boolean isEqualAmortization) {
this.fund = fund;
this.transactionProcessingStrategy = transactionProcessingStrategy;
this.name = name.trim();
@@ -607,7 +613,7 @@ public class LoanProduct extends AbstractPersistableCustom {
interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod,
considerPartialPeriodInterest, repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods,
graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing,
- daysInMonthType.getValue(), daysInYearType.getValue(), isInterestRecalculationEnabled);
+ daysInMonthType.getValue(), daysInYearType.getValue(), isInterestRecalculationEnabled, isEqualAmortization);
this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings();
@@ -652,6 +658,7 @@ public class LoanProduct extends AbstractPersistableCustom {
this.syncExpectedWithDisbursementDate =
syncExpectedWithDisbursementDate;
this.canUseForTopup = canUseForTopup;
+ this.isEqualAmortization = isEqualAmortization;
}
public MonetaryCurrency getCurrency() {
@@ -1365,4 +1372,12 @@ public class LoanProduct extends AbstractPersistableCustom {
return this.canUseForTopup;
}
+ public boolean isEqualAmortization() {
+ return isEqualAmortization;
+ }
+
+ public void setEqualAmortization(boolean isEqualAmortization) {
+ this.isEqualAmortization = isEqualAmortization;
+ }
+
}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java
index 901967ee8..25ce67d52 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java
@@ -127,6 +127,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
@Column(name = "interest_recalculation_enabled")
private boolean isInterestRecalculationEnabled;
+
+ @Column(name = "is_equal_amortization", nullable = false)
+ private boolean isEqualAmortization = false;
public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal,
final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType,
@@ -135,13 +138,13 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
final Integer repaymentEvery, final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer numberOfRepayments,
final Integer graceOnPrincipalPayment, final Integer recurringMoratoriumOnPrincipalPeriods, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged,
final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance, final Integer graceOnArrearsAgeing,
- final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled) {
+ final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization) {
return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType,
nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion,
repaymentEvery, repaymentPeriodFrequencyType, numberOfRepayments, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment,
graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType,
- isInterestRecalculationEnabled);
+ isInterestRecalculationEnabled, isEqualAmortization);
}
protected LoanProductRelatedDetail() {
@@ -155,7 +158,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
final Integer repayEvery, final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfRepayments,
final Integer graceOnPrincipalPayment, final Integer recurringMoratoriumOnPrincipalPeriods, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged,
final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance, final Integer graceOnArrearsAgeing,
- final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled) {
+ final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization) {
this.currency = currency;
this.principal = defaultPrincipal;
this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod;
@@ -181,6 +184,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
this.daysInMonthType = daysInMonthType;
this.daysInYearType = daysInYearType;
this.isInterestRecalculationEnabled = isInterestRecalculationEnabled;
+ this.isEqualAmortization = isEqualAmortization;
}
private Integer defaultToNullIfZero(final Integer value) {
@@ -493,6 +497,12 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
actualChanges.put(LoanProductConstants.isInterestRecalculationEnabledParameterName, newValue);
this.isInterestRecalculationEnabled = newValue;
}
+
+ if (command.isChangeInBooleanParameterNamed(LoanProductConstants.isEqualAmortizationParam, this.isEqualAmortization)) {
+ final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.isEqualAmortizationParam);
+ actualChanges.put(LoanProductConstants.isEqualAmortizationParam, newValue);
+ this.isEqualAmortization = newValue;
+ }
validateRepaymentPeriodWithGraceSettings();
@@ -648,5 +658,13 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
public boolean isAllowPartialPeriodInterestCalcualtion() {
return this.allowPartialPeriodInterestCalcualtion;
}
+
+ public boolean isEqualAmortization() {
+ return isEqualAmortization;
+ }
+
+ public void setEqualAmortization(boolean isEqualAmortization) {
+ this.isEqualAmortization = isEqualAmortization;
+ }
}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/exception/EqualAmortizationUnsupportedFeatureException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/exception/EqualAmortizationUnsupportedFeatureException.java
new file mode 100644
index 000000000..8df7ec737
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/exception/EqualAmortizationUnsupportedFeatureException.java
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanproduct.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+
+@SuppressWarnings("serial")
+public class EqualAmortizationUnsupportedFeatureException extends AbstractPlatformDomainRuleException {
+
+ public EqualAmortizationUnsupportedFeatureException(String code, String property) {
+ super("error.msg.equal.amortization.does.not.support." + code, "Equal Amortization does not support " + property, property);
+ }
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java
index 8eb132c46..04bfa7f06 100755
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java
@@ -45,9 +45,9 @@ import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
-import org.apache.fineract.portfolio.loanproduct.domain.LoanProductConfigurableAttributes;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
+import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -108,7 +108,7 @@ public final class LoanProductDataValidator {
LoanProductConstants.recalculationRestFrequencyWeekdayParamName,
LoanProductConstants.recalculationRestFrequencyNthDayParamName, LoanProductConstants.recalculationRestFrequencyOnDayParamName,
LoanProductConstants.isCompoundingToBePostedAsTransactionParamName, LoanProductConstants.allowCompoundingOnEodParamName,
- LoanProductConstants.canUseForTopup));
+ LoanProductConstants.canUseForTopup, LoanProductConstants.isEqualAmortizationParam));
private static final String[] supportedloanConfigurableAttributes = {LoanProductConstants.amortizationTypeParamName,
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyIdParamName,
@@ -148,6 +148,13 @@ public final class LoanProductDataValidator {
final Long fundId = this.fromApiJsonHelper.extractLongNamed("fundId", element);
baseDataValidator.reset().parameter("fundId").value(fundId).ignoreIfNull().integerGreaterThanZero();
}
+
+ boolean isEqualAmortization = false;
+ if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isEqualAmortizationParam, element)) {
+ isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.isEqualAmortizationParam, element);
+ baseDataValidator.reset().parameter(LoanProductConstants.isEqualAmortizationParam).value(isEqualAmortization).ignoreIfNull()
+ .validateForBooleanValue();
+ }
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.minimumDaysBetweenDisbursalAndFirstRepayment, element)) {
final Long minimumDaysBetweenDisbursalAndFirstRepayment = this.fromApiJsonHelper.extractLongNamed(
@@ -314,6 +321,8 @@ public final class LoanProductDataValidator {
if (isInterestRecalculationEnabled != null) {
if (isInterestRecalculationEnabled.booleanValue()) {
+ if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("interest.recalculation",
+ "interest recalculation"); }
validateInterestRecalculationParams(element, baseDataValidator, null);
}
}
@@ -321,7 +330,9 @@ public final class LoanProductDataValidator {
// interest rates
if (this.fromApiJsonHelper.parameterExists("isLinkedToFloatingInterestRates", element)
&& this.fromApiJsonHelper.extractBooleanNamed("isLinkedToFloatingInterestRates", element) == true) {
- if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) {
+ if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate",
+ "floating interest rate"); }
+ if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) {
baseDataValidator
.reset()
.parameter("interestRatePerPeriod")
@@ -641,6 +652,13 @@ public final class LoanProductDataValidator {
private void validateVariableInstallmentSettings(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.allowVariableInstallmentsParamName, element)
&& this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.allowVariableInstallmentsParamName, element)) {
+
+ boolean isEqualAmortization = false;
+ if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isEqualAmortizationParam, element)) {
+ isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.isEqualAmortizationParam, element);
+ }
+ if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("variable.installment",
+ "variable installment"); }
Long minimumGapBetweenInstallments = null;
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.minimumGapBetweenInstallments, element)) {
@@ -721,6 +739,13 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(LoanProductConstants.multiDisburseLoanParameterName).value(multiDisburseLoan)
.ignoreIfNull().validateForBooleanValue();
}
+
+ boolean isEqualAmortization = false;
+ if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isEqualAmortizationParam, element)) {
+ isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.isEqualAmortizationParam, element);
+ }
+ if (isEqualAmortization && multiDisburseLoan) { throw new EqualAmortizationUnsupportedFeatureException("tranche.disbursal",
+ "tranche disbursal"); }
if (multiDisburseLoan) {
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.outstandingLoanBalanceParameterName, element)) {
@@ -1114,6 +1139,13 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName)
.value(npaChangeConfig).notNull().isOneOfTheseValues(true, false);
}
+
+ boolean isEqualAmortization = loanProduct.isEqualAmortization();
+ if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isEqualAmortizationParam, element)) {
+ isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.isEqualAmortizationParam, element);
+ baseDataValidator.reset().parameter(LoanProductConstants.isEqualAmortizationParam).value(isEqualAmortization).ignoreIfNull()
+ .validateForBooleanValue();
+ }
// Interest recalculation settings
Boolean isInterestRecalculationEnabled = loanProduct.isInterestRecalculationEnabled();
@@ -1126,6 +1158,8 @@ public final class LoanProductDataValidator {
if (isInterestRecalculationEnabled != null) {
if (isInterestRecalculationEnabled) {
+ if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("interest.recalculation",
+ "interest recalculation"); }
validateInterestRecalculationParams(element, baseDataValidator, loanProduct);
}
}
@@ -1136,6 +1170,9 @@ public final class LoanProductDataValidator {
isLinkedToFloatingInterestRates = this.fromApiJsonHelper.extractBooleanNamed("isLinkedToFloatingInterestRates", element);
}
if (isLinkedToFloatingInterestRates) {
+ if(isEqualAmortization){
+ throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", "floating interest rate");
+ }
if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) {
baseDataValidator
.reset()
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java
index 078d74f89..00808fa84 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java
@@ -227,7 +227,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
+ "lp.allow_variabe_installments as isVariableIntallmentsAllowed, "
+ "lvi.minimum_gap as minimumGap, "
+ "lvi.maximum_gap as maximumGap, "
- + "lp.can_use_for_topup as canUseForTopup "
+ + "lp.can_use_for_topup as canUseForTopup, lp.is_equal_amortization as isEqualAmortization "
+ " from m_product_loan lp "
+ " left join m_fund f on f.id = lp.fund_id "
+ " left join m_product_loan_recalculation_details lpr on lpr.product_id=lp.id "
@@ -308,7 +308,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final int amortizationTypeId = JdbcSupport.getInteger(rs, "amortizationMethod");
final EnumOptionData amortizationType = LoanEnumerations.amortizationType(amortizationTypeId);
-
+ final boolean isEqualAmortization = rs.getBoolean("isEqualAmortization");
+
final Integer interestRateFrequencyTypeId = JdbcSupport.getInteger(rs, "interestRatePerPeriodFreq");
final EnumOptionData interestRateFrequencyType = LoanEnumerations.interestRateFrequencyType(interestRateFrequencyTypeId);
@@ -464,7 +465,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
installmentAmountInMultiplesOf, allowAttributeOverrides, isLinkedToFloatingInterestRates, floatingRateId,
floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate,
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableIntallmentsAllowed, minimumGap,
- maximumGap, syncExpectedWithDisbursementDate, canUseForTopup);
+ maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization);
}
}
diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V337__equal_amortization.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V337__equal_amortization.sql
new file mode 100644
index 000000000..a32ccf44e
--- /dev/null
+++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V337__equal_amortization.sql
@@ -0,0 +1,21 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements. See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership. The ASF licenses this file
+-- to you under the Apache License, Version 2.0 (the
+-- "License"); you may not use this file except in compliance
+-- with the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing,
+-- software distributed under the License is distributed on an
+-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+-- KIND, either express or implied. See the License for the
+-- specific language governing permissions and limitations
+-- under the License.
+--
+
+ALTER TABLE `m_product_loan` ADD COLUMN `is_equal_amortization` TINYINT(1) NOT NULL DEFAULT '0';
+ALTER TABLE `m_loan` ADD COLUMN `is_equal_amortization` TINYINT(1) NOT NULL DEFAULT '0';
\ No newline at end of file
diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java
index c42957838..c202ae99f 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java
@@ -153,10 +153,10 @@ public class LoanProductRelatedDetailTestHelper {
final Integer daysInYearType = DaysInYearType.ACTUAL.getValue();
final boolean isInterestRecalculationEnabled = false;
final boolean considerPartialPeriodInterest = false;
-
+ final boolean isEqualAmortization = false;
return new LoanProductRelatedDetail(currency, defaultPrincipal, defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType,
defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, considerPartialPeriodInterest, repayEvery,
repaymentFrequencyType, defaultNumberOfRepayments, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged,
- amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled);
+ amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, isEqualAmortization);
}
}
\ No newline at end of file
From 8f30c2102f9f3526ac274f48e11ab2a5f91a86ff Mon Sep 17 00:00:00 2001
From: nazeer shaik
Date: Thu, 2 Nov 2017 16:24:28 +0530
Subject: [PATCH 05/30] notification sms
---
.../campaigns/constants/CampaignType.java | 12 +-
.../constants/SmsCampaignEnumerations.java | 3 +
.../campaigns/sms/data/SmsCampaignData.java | 16 +-
.../campaigns/sms/domain/SmsCampaign.java | 36 +-
.../sms/domain/SmsCampaignRepository.java | 2 +
.../serialization/SmsCampaignValidator.java | 38 +-
.../service/SmsCampaignDomainServiceImpl.java | 23 +-
...mpaignDropdownReadPlatformServiceImpl.java | 3 -
.../SmsCampaignReadPlatformServiceImpl.java | 7 +-
...msCampaignWritePlatformServiceJpaImpl.java | 49 +-
...pertiesCommandFromApiJsonDeserializer.java | 6 +
.../service/ExternalServicesConstants.java | 36 +
...ServicesPropertiesReadPlatformService.java | 3 +
...icesPropertiesReadPlatformServiceImpl.java | 35 +
...ternalServicesReadPlatformServiceImpl.java | 4 +
.../infrastructure/gcm/GcmConstants.java | 242 +++++
.../api/DeviceRegistrationApiConstants.java | 25 +
.../api/DeviceRegistrationApiResource.java | 160 ++++
.../gcm/domain/DeviceRegistration.java | 85 ++
.../gcm/domain/DeviceRegistrationData.java | 47 +
.../domain/DeviceRegistrationRepository.java | 36 +
.../DeviceRegistrationRepositoryWrapper.java | 61 ++
.../infrastructure/gcm/domain/Message.java | 317 +++++++
.../gcm/domain/MulticastResult.java | 151 ++++
.../gcm/domain/Notification.java | 330 +++++++
.../domain/NotificationConfigurationData.java | 49 ++
.../infrastructure/gcm/domain/Result.java | 187 ++++
.../infrastructure/gcm/domain/Sender.java | 832 ++++++++++++++++++
.../DeviceRegistrationNotFoundException.java | 38 +
.../exception/InvalidRequestException.java | 66 ++
...DeviceRegistrationReadPlatformService.java | 32 +
...ceRegistrationReadPlatformServiceImpl.java | 125 +++
...eviceRegistrationWritePlatformService.java | 30 +
...eRegistrationWritePlatformServiceImpl.java | 123 +++
.../service/NotificationSenderService.java | 133 +++
.../infrastructure/sms/domain/SmsMessage.java | 32 +-
.../sms/domain/SmsMessageAssembler.java | 4 +-
.../SmsMessageScheduledJobServiceImpl.java | 36 +-
.../portfolio/client/data/ClientData.java | 6 +
...eRegistrationWritePlatformServiceImpl.java | 3 +-
.../V336__sms_campaign_notification.sql | 48 +
41 files changed, 3409 insertions(+), 62 deletions(-)
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/GcmConstants.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiConstants.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiResource.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistration.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationData.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepository.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepositoryWrapper.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Message.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/MulticastResult.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Notification.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/NotificationConfigurationData.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Result.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Sender.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/DeviceRegistrationNotFoundException.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/InvalidRequestException.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformService.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformServiceImpl.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformService.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformServiceImpl.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/NotificationSenderService.java
create mode 100644 fineract-provider/src/main/resources/sql/migrations/core_db/V336__sms_campaign_notification.sql
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/constants/CampaignType.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/constants/CampaignType.java
index b6a0aaf7c..421882a34 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/constants/CampaignType.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/constants/CampaignType.java
@@ -21,7 +21,7 @@ package org.apache.fineract.infrastructure.campaigns.constants;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
public enum CampaignType {
- INVALID(0, "campaignType.invalid"), SMS(1, "campaignType.sms");
+ INVALID(0, "campaignType.invalid"), SMS(1, "campaignType.sms"), NOTIFICATION(2, "campaignType.notification");
private Integer value;
private String code;
@@ -48,6 +48,9 @@ public enum CampaignType {
case 1:
type = SMS;
break;
+ case 2:
+ type = NOTIFICATION;
+ break;
}
return type;
}
@@ -66,6 +69,9 @@ public enum CampaignType {
case SMS:
optionData = new EnumOptionData(CampaignType.SMS.getValue().longValue(), CampaignType.SMS.getCode(), "SMS");
break;
+ case NOTIFICATION:
+ optionData = new EnumOptionData(CampaignType.NOTIFICATION.getValue().longValue(), CampaignType.NOTIFICATION.getCode(), "NOTIFICATION");
+ break;
}
return optionData;
}
@@ -73,4 +79,8 @@ public enum CampaignType {
public boolean isSms() {
return this.value.equals(CampaignType.SMS.getValue());
}
+
+ public boolean isNotificaion() {
+ return this.value.equals(CampaignType.NOTIFICATION.getValue());
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/constants/SmsCampaignEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/constants/SmsCampaignEnumerations.java
index 99de55ecc..6e74932e9 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/constants/SmsCampaignEnumerations.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/constants/SmsCampaignEnumerations.java
@@ -61,6 +61,9 @@ public class SmsCampaignEnumerations {
case SMS:
optionData = new EnumOptionData(CampaignType.SMS.getValue().longValue(), CampaignType.SMS.getCode(), "SMS");
break;
+ case NOTIFICATION:
+ optionData = new EnumOptionData(CampaignType.NOTIFICATION.getValue().longValue(), CampaignType.NOTIFICATION.getCode(), "NOTIFICATION");
+ break;
}
return optionData;
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/data/SmsCampaignData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/data/SmsCampaignData.java
index 8a6e6a310..cbd9bd550 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/data/SmsCampaignData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/data/SmsCampaignData.java
@@ -44,6 +44,8 @@ public class SmsCampaignData {
private final DateTime recurrenceStartDate;
private final String recurrence;
private final Long providerId;
+ private final boolean isNotification;
+
private final Collection smsProviderOptions;
@@ -70,7 +72,7 @@ public class SmsCampaignData {
final Collection smsProviderOptions, final Collection campaignTypeOptions,
final Collection triggerTypeOptions, final Collection months,
final Collection weekDays, final Collection frequencyTypeOptions,
- final Collection periodFrequencyOptions) {
+ final Collection periodFrequencyOptions, final boolean isNotification) {
this.id = id;
this.campaignName = campaignName;
this.campaignType = campaignType;
@@ -90,6 +92,7 @@ public class SmsCampaignData {
} else {
this.lastTriggerDate = null;
}
+ this.isNotification = isNotification;
this.smsCampaignTimeLine = smsCampaignTimeLine;
this.recurrenceStartDate = recurrenceStartDate;
this.recurrence = recurrence;
@@ -109,7 +112,7 @@ public class SmsCampaignData {
final Long runReportId, final String reportName, final String paramValue, final EnumOptionData campaignStatus,
final String message, final DateTime nextTriggerDate, final LocalDate lastTriggerDate,
final SmsCampaignTimeLine smsCampaignTimeLine, final DateTime recurrenceStartDate, final String recurrence,
- final Long providerId) {
+ final Long providerId, final boolean isNotification) {
final Collection businessRulesOptions = null;
final Collection smsProviderOptions = null;
final Collection campaignTypeOptions = null;
@@ -122,7 +125,7 @@ public class SmsCampaignData {
return new SmsCampaignData(id, campaignName, campaignType, triggerType, runReportId,
reportName, paramValue, campaignStatus, message, nextTriggerDate, lastTriggerDate, smsCampaignTimeLine,
recurrenceStartDate, recurrence, providerId, businessRulesOptions, smsProviderOptions, campaignTypeOptions,
- triggerTypeOptions, months, weekDays, frequencyTypeOptions, periodFrequencyOptions);
+ triggerTypeOptions, months, weekDays, frequencyTypeOptions, periodFrequencyOptions, isNotification);
}
public static SmsCampaignData template(final Collection smsProviderOptions,
@@ -145,10 +148,11 @@ public class SmsCampaignData {
final EnumOptionData triggerType = null;
final String reportName = null;
final Long providerId = null;
+ final boolean isNotification = false;
return new SmsCampaignData(id, campaignName, campaignType, triggerType, runReportId,
reportName, paramValue, campaignStatus, message, nextTriggerDate, lastTriggerDate, smsCampaignTimeLine,
recurrenceStartDate, recurrence, providerId, businessRulesOptions, smsProviderOptions, campaignTypeOptions,
- triggerTypeOptions, months, weekDays, frequencyTypeOptions, periodFrequencyOptions);
+ triggerTypeOptions, months, weekDays, frequencyTypeOptions, periodFrequencyOptions, isNotification);
}
public Long getId() {
@@ -203,4 +207,8 @@ public class SmsCampaignData {
return this.providerId;
}
+ public boolean isNotification() {
+ return this.isNotification;
+ }
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaign.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaign.java
index 3e1610e89..1c023e441 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaign.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaign.java
@@ -67,7 +67,7 @@ public class SmsCampaign extends AbstractPersistableCustom {
@Column(name = "campaign_trigger_type", nullable = false)
private Integer triggerType; //defines direct, scheduled, transaction
- @Column(name = "provider_id", nullable = false)
+ @Column(name = "provider_id", nullable = true)//null for notifications
private Long providerId; // defined provider details
@ManyToOne
@@ -124,13 +124,16 @@ public class SmsCampaign extends AbstractPersistableCustom {
@Column(name = "is_visible", nullable = true)
private boolean isVisible;
+
+ @Column(name = "is_notification", nullable = true)
+ private boolean isNotification;
public SmsCampaign() {}
private SmsCampaign(final String campaignName, final Integer campaignType,
final Integer triggerType, final Report businessRuleId, final Long providerId, final String paramValue,
final String message, final LocalDate submittedOnDate, final AppUser submittedBy, final String recurrence,
- final LocalDateTime localDateTime) {
+ final LocalDateTime localDateTime, final boolean isNotification) {
this.campaignName = campaignName;
this.campaignType = campaignType;
this.triggerType = SmsCampaignTriggerType.fromInt(triggerType).getValue();
@@ -149,6 +152,7 @@ public class SmsCampaign extends AbstractPersistableCustom {
} else {
this.recurrenceStartDate = recurrenceStartDate.toDate();
}
+ this.isNotification = isNotification;
}
public static SmsCampaign instance(final AppUser submittedBy, final Report report, final JsonCommand command) {
@@ -156,7 +160,15 @@ public class SmsCampaign extends AbstractPersistableCustom {
final String campaignName = command.stringValueOfParameterNamed(SmsCampaignValidator.campaignName);
final Long campaignType = command.longValueOfParameterNamed(SmsCampaignValidator.campaignType);
final Long triggerType = command.longValueOfParameterNamed(SmsCampaignValidator.triggerType);
- final Long providerId = command.longValueOfParameterNamed(SmsCampaignValidator.providerId);
+ boolean isNotification = false;
+ if(command.parameterExists(SmsCampaignValidator.isNotificationParamName)){
+ isNotification = command.booleanPrimitiveValueOfParameterNamed(SmsCampaignValidator.isNotificationParamName);
+ }
+ Long providerId = null;
+ if(!isNotification){
+ providerId = command.longValueOfParameterNamed(SmsCampaignValidator.providerId);
+ }
+
final String paramValue = command.jsonFragment(SmsCampaignValidator.paramValue);
final String message = command.stringValueOfParameterNamed(SmsCampaignValidator.message);
@@ -165,6 +177,7 @@ public class SmsCampaign extends AbstractPersistableCustom {
submittedOnDate = command.localDateValueOfParameterNamed(SmsCampaignValidator.submittedOnDateParamName);
}
String recurrence = null;
+
LocalDateTime recurrenceStartDate = new LocalDateTime();
if (SmsCampaignTriggerType.fromInt(triggerType.intValue()).isSchedule()) {
@@ -184,7 +197,7 @@ public class SmsCampaign extends AbstractPersistableCustom {
}
return new SmsCampaign(campaignName, campaignType.intValue(), triggerType.intValue(), report,
- providerId, paramValue, message, submittedOnDate, submittedBy, recurrence, recurrenceStartDate);
+ providerId, paramValue, message, submittedOnDate, submittedBy, recurrence, recurrenceStartDate, isNotification);
}
public Map update(JsonCommand command) {
@@ -232,6 +245,11 @@ public class SmsCampaign extends AbstractPersistableCustom {
final Long newValue = command.longValueOfParameterNamed(SmsCampaignValidator.providerId);
actualChanges.put(SmsCampaignValidator.providerId, newValue);
}
+ if (command.isChangeInBooleanParameterNamed(SmsCampaignValidator.isNotificationParamName, this.isNotification)) {
+ final Boolean newValue = command.booleanObjectValueOfParameterNamed(SmsCampaignValidator.isNotificationParamName);
+ this.isNotification = newValue;
+ actualChanges.put(SmsCampaignValidator.isNotificationParamName, newValue);
+ }
if (SmsCampaignTriggerType.fromInt(this.triggerType).isSchedule()) {
final String dateFormatAsInput = command.dateFormat();
@@ -558,4 +576,14 @@ public class SmsCampaign extends AbstractPersistableCustom {
}
return recurrenceBuilder.toString();
}
+
+ public boolean isNotification() {
+ return this.isNotification;
+ }
+
+ public void setNotification(boolean isNotification) {
+ this.isNotification = isNotification;
+ }
+
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaignRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaignRepository.java
index bc3bb7d08..cb1cf1c9f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaignRepository.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaignRepository.java
@@ -32,6 +32,8 @@ public interface SmsCampaignRepository extends JpaRepository,
Collection findByCampaignTypeAndTriggerTypeAndStatus(final Integer campaignType, final Integer triggerType,
final Integer status);
+
+ Collection findByTriggerTypeAndStatus(final Integer triggerType, final Integer status);
Collection findByTriggerType(final Integer triggerType) ;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/serialization/SmsCampaignValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/serialization/SmsCampaignValidator.java
index dcc9a77c3..d9eb56fd0 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/serialization/SmsCampaignValidator.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/serialization/SmsCampaignValidator.java
@@ -28,12 +28,16 @@ import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignTriggerType;
+import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaign;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration;
+import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationRepositoryWrapper;
import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType;
+import org.apache.fineract.portfolio.client.domain.Client;
import org.joda.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -67,19 +71,21 @@ public class SmsCampaignValidator {
public static final String frequencyParamName = "frequency";
public static final String intervalParamName = "interval";
public static final String repeatsOnDayParamName = "repeatsOnDay";
+ public static final String isNotificationParamName = "isNotification";
private final FromJsonHelper fromApiJsonHelper;
+ private final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository;
protected static final Set supportedParams = new HashSet<>(Arrays.asList(campaignName, campaignType,
localeParamName,
dateFormatParamName, runReportId, paramValue, message, recurrenceStartDate, activationDateParamName, submittedOnDateParamName,
closureDateParamName, recurrenceParamName, providerId, triggerType, frequencyParamName, intervalParamName,
- repeatsOnDayParamName, triggerEntityType, triggerActionType, dateTimeFormat));
+ repeatsOnDayParamName, triggerEntityType, triggerActionType, dateTimeFormat, isNotificationParamName));
protected static final Set supportedParamsForUpdate = new HashSet<>(Arrays.asList(campaignName, campaignType,
localeParamName,
dateFormatParamName, runReportId, paramValue, message, recurrenceStartDate, activationDateParamName, recurrenceParamName,
- providerId, triggerType, triggerEntityType, triggerActionType, dateTimeFormat));
+ providerId, triggerType, triggerEntityType, triggerActionType, dateTimeFormat, isNotificationParamName));
protected static final Set ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
dateFormatParamName,
@@ -92,8 +98,9 @@ public class SmsCampaignValidator {
protected static final Set PREVIEW_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(paramValue, message));
@Autowired
- public SmsCampaignValidator(FromJsonHelper fromApiJsonHelper) {
+ public SmsCampaignValidator(FromJsonHelper fromApiJsonHelper, final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository) {
this.fromApiJsonHelper = fromApiJsonHelper;
+ this.deviceRegistrationRepository = deviceRegistrationRepository;
}
public void validateCreate(String json) {
@@ -159,6 +166,12 @@ public class SmsCampaignValidator {
element);
baseDataValidator.reset().parameter(SmsCampaignValidator.submittedOnDateParamName).value(submittedOnDate).notNull();
}
+
+ if (this.fromApiJsonHelper.parameterExists(SmsCampaignValidator.isNotificationParamName, element)) {
+ final Boolean isNotification = this.fromApiJsonHelper.extractBooleanNamed(SmsCampaignValidator.isNotificationParamName,
+ element);
+ baseDataValidator.reset().parameter(SmsCampaignValidator.submittedOnDateParamName).trueOrFalseRequired(isNotification);
+ }
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@@ -214,7 +227,11 @@ public class SmsCampaignValidator {
}
}
}
-
+ if (this.fromApiJsonHelper.parameterExists(SmsCampaignValidator.isNotificationParamName, element)) {
+ final Boolean isNotification = this.fromApiJsonHelper.extractBooleanNamed(SmsCampaignValidator.isNotificationParamName,
+ element);
+ baseDataValidator.reset().parameter(SmsCampaignValidator.submittedOnDateParamName).trueOrFalseRequired(isNotification);
+ }
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@@ -303,4 +320,17 @@ public class SmsCampaignValidator {
private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
}
+
+ public boolean isValidNotificationOrSms(Client client, SmsCampaign smsCampaign, Object mobileNo) {
+ if (smsCampaign.isNotification()) {
+ if (client != null) {
+ DeviceRegistration deviceRegistration = this.deviceRegistrationRepository
+ .findDeviceRegistrationByClientId(client.getId());
+ return (deviceRegistration != null);
+ }
+ return false;
+ }
+ return (mobileNo != null);
+ }
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java
index 3b0e0aef4..7bf79ad49 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java
@@ -35,6 +35,7 @@ import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignTri
import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaign;
import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaignRepository;
import org.apache.fineract.infrastructure.campaigns.sms.exception.SmsRuntimeException;
+import org.apache.fineract.infrastructure.campaigns.sms.serialization.SmsCampaignValidator;
import org.apache.fineract.infrastructure.sms.domain.SmsMessage;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository;
import org.apache.fineract.infrastructure.sms.scheduler.SmsMessageScheduledJobService;
@@ -77,13 +78,15 @@ public class SmsCampaignDomainServiceImpl implements SmsCampaignDomainService {
private final GroupRepository groupRepository;
private final SmsMessageScheduledJobService smsMessageScheduledJobService;
+ private final SmsCampaignValidator smsCampaignValidator;
@Autowired
public SmsCampaignDomainServiceImpl(final SmsCampaignRepository smsCampaignRepository, final SmsMessageRepository smsMessageRepository,
final BusinessEventNotifierService businessEventNotifierService, final OfficeRepository officeRepository,
final SmsCampaignWritePlatformService smsCampaignWritePlatformCommandHandler,
final GroupRepository groupRepository,
- final SmsMessageScheduledJobService smsMessageScheduledJobService){
+ final SmsMessageScheduledJobService smsMessageScheduledJobService,
+ final SmsCampaignValidator smsCampaignValidator){
this.smsCampaignRepository = smsCampaignRepository;
this.smsMessageRepository = smsMessageRepository;
this.businessEventNotifierService = businessEventNotifierService;
@@ -91,6 +94,7 @@ public class SmsCampaignDomainServiceImpl implements SmsCampaignDomainService {
this.smsCampaignWritePlatformCommandHandler = smsCampaignWritePlatformCommandHandler;
this.groupRepository = groupRepository;
this.smsMessageScheduledJobService = smsMessageScheduledJobService ;
+ this.smsCampaignValidator = smsCampaignValidator;
}
@PostConstruct
@@ -217,10 +221,13 @@ public class SmsCampaignDomainServiceImpl implements SmsCampaignDomainService {
String message = this.smsCampaignWritePlatformCommandHandler.compileSmsTemplate(
smsCampaign.getMessage(), smsCampaign.getCampaignName(), smsParams);
Object mobileNo = smsParams.get("mobileNo");
- if (mobileNo != null) {
+ if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) {
+ String mobileNumber = null;
+ if(mobileNo != null){
+ mobileNumber = mobileNo.toString();
+ }
SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, message,
- mobileNo.toString(), smsCampaign);
- this.smsMessageRepository.save(smsMessage);
+ mobileNumber, smsCampaign, smsCampaign.isNotification());
Collection messages = new ArrayList<>();
messages.add(smsMessage);
Map> smsDataMap = new HashMap<>();
@@ -273,9 +280,13 @@ public class SmsCampaignDomainServiceImpl implements SmsCampaignDomainService {
String message = this.smsCampaignWritePlatformCommandHandler
.compileSmsTemplate(smsCampaign.getMessage(), smsCampaign.getCampaignName(), smsParams);
Object mobileNo = smsParams.get("mobileNo");
- if (mobileNo != null) {
+ if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) {
+ String mobileNumber = null;
+ if(mobileNo != null){
+ mobileNumber = mobileNo.toString();
+ }
SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, message,
- mobileNo.toString(), smsCampaign);
+ mobileNumber, smsCampaign, smsCampaign.isNotification());
this.smsMessageRepository.save(smsMessage);
Collection messages = new ArrayList<>();
messages.add(smsMessage);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDropdownReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDropdownReadPlatformServiceImpl.java
index d974d7ab3..69536f89e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDropdownReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDropdownReadPlatformServiceImpl.java
@@ -30,7 +30,6 @@ import org.apache.fineract.infrastructure.campaigns.helper.SmsConfigUtils;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignEnumerations;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignTriggerType;
import org.apache.fineract.infrastructure.campaigns.sms.data.SmsProviderData;
-import org.apache.fineract.infrastructure.campaigns.sms.exception.ConnectionFailureException;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.calendar.domain.CalendarWeekDaysType;
import org.apache.fineract.portfolio.calendar.service.CalendarEnumerations;
@@ -81,10 +80,8 @@ public class SmsCampaignDropdownReadPlatformServiceImpl implements SmsCampaignDr
new ParameterizedTypeReference>() {});
smsProviderOptions = responseOne.getBody();
if (!responseOne.getStatusCode().equals(HttpStatus.OK)) {
- throw new ConnectionFailureException(hostName);
}
} catch (Exception e) {
- throw new ConnectionFailureException(hostName);
}
return smsProviderOptions;
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignReadPlatformServiceImpl.java
index 94215b0a4..5e15272db 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignReadPlatformServiceImpl.java
@@ -219,7 +219,8 @@ public class SmsCampaignReadPlatformServiceImpl implements SmsCampaignReadPlatfo
sql.append("acu.username as activatedByUsername, ");
sql.append("sc.approvedon_date as activatedOnDate, ");
sql.append("sr.report_name as reportName, ");
- sql.append("provider_id as providerId ");
+ sql.append("provider_id as providerId, ");
+ sql.append("sc.is_notification as isNotification ");
sql.append("from sms_campaign sc ");
sql.append("left join m_appuser sbu on sbu.id = sc.submittedon_userid ");
sql.append("left join m_appuser acu on acu.id = sc.approvedon_userid ");
@@ -265,9 +266,9 @@ public class SmsCampaignReadPlatformServiceImpl implements SmsCampaignReadPlatfo
activatedByUsername, closedOnDate, closedByUsername);
final String reportName = rs.getString("reportName");
final Long providerId = rs.getLong("providerId");
-
+ final Boolean isNotification = rs.getBoolean("isNotification");
return SmsCampaignData.instance(id, campaignName, campaignTypeEnum, triggerTypeEnum, runReportId, reportName, paramValue, status, message, nextTriggerDate, lastTriggerDate,
- smsCampaignTimeLine, recurrenceStartDate, recurrence, providerId);
+ smsCampaignTimeLine, recurrenceStartDate, recurrence, providerId, isNotification);
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignWritePlatformServiceJpaImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignWritePlatformServiceJpaImpl.java
index 0084ed6f9..976a6d75b 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignWritePlatformServiceJpaImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignWritePlatformServiceJpaImpl.java
@@ -30,7 +30,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
-import org.apache.fineract.infrastructure.campaigns.constants.CampaignType;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignStatus;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignTriggerType;
import org.apache.fineract.infrastructure.campaigns.sms.data.CampaignPreviewData;
@@ -56,6 +55,8 @@ import org.apache.fineract.infrastructure.dataqueries.domain.ReportRepository;
import org.apache.fineract.infrastructure.dataqueries.exception.ReportNotFoundException;
import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
+import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration;
+import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationRepositoryWrapper;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
@@ -109,6 +110,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
private final ReadReportingService readReportingService;
private final GenericDataService genericDataService;
private final FromJsonHelper fromJsonHelper;
+ private final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository;
private final SmsMessageScheduledJobService smsMessageScheduledJobService;
@@ -118,7 +120,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
final SmsMessageRepository smsMessageRepository, final ClientRepositoryWrapper clientRepositoryWrapper,
final ReadReportingService readReportingService, final GenericDataService genericDataService,
final FromJsonHelper fromJsonHelper, final GroupRepository groupRepository,
- final SmsMessageScheduledJobService smsMessageScheduledJobService) {
+ final SmsMessageScheduledJobService smsMessageScheduledJobService, final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository) {
this.context = context;
this.smsCampaignRepository = smsCampaignRepository;
this.smsCampaignValidator = smsCampaignValidator;
@@ -130,6 +132,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
this.fromJsonHelper = fromJsonHelper;
this.groupRepository = groupRepository;
this.smsMessageScheduledJobService = smsMessageScheduledJobService ;
+ this.deviceRegistrationRepository = deviceRegistrationRepository;
}
@Transactional
@@ -228,10 +231,14 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
Object mobileNo = entry.get("mobileNo");
Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId.longValue());
- if (mobileNo != null) {
+ if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) {
// String countryCode = this.smsReadPlatformService.retrieveCountryCode(client.getOffice().getId()).getCountryCode();
- SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, mobileNo.toString(),
- smsCampaign);
+ String mobileNumber = null;
+ if(mobileNo != null){
+ mobileNumber = mobileNo.toString();
+ }
+ SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, mobileNumber,
+ smsCampaign, smsCampaign.isNotification());
this.smsMessageRepository.save(smsMessage);
}
}
@@ -242,6 +249,8 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
}
+
+
@Override
public void insertDirectCampaignIntoSmsOutboundTable(final Loan loan, final SmsCampaign smsCampaign) {
try {
@@ -278,9 +287,13 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
String textMessage = this.compileSmsTemplate(smsCampaign.getMessage(), smsCampaign.getCampaignName(), entry);
Object mobileNo = entry.get("mobileNo");
- if (mobileNo != null) {
- SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, mobileNo.toString(),
- smsCampaign);
+ if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) {
+ String mobileNumber = null;
+ if(mobileNo != null){
+ mobileNumber = mobileNo.toString();
+ }
+ SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, mobileNumber,
+ smsCampaign, smsCampaign.isNotification());
smsMessage.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue());
this.smsMessageRepository.save(smsMessage);
Collection messages = new ArrayList<>() ;
@@ -320,9 +333,13 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
smsCampaign.getCampaignName(), entry);
Object mobileNo = entry.get("mobileNo");
- if (mobileNo != null) {
+ if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) {
+ String mobileNumber = null;
+ if(mobileNo != null){
+ mobileNumber = mobileNo.toString();
+ }
SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage,
- mobileNo.toString(), smsCampaign);
+ mobileNumber, smsCampaign, smsCampaign.isNotification());
smsMessage.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue());
this.smsMessageRepository.save(smsMessage);
Collection messages = new ArrayList<>();
@@ -360,9 +377,13 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
smsCampaign.getCampaignName(), entry);
Object mobileNo = entry.get("mobileNo");
- if (mobileNo != null) {
+ if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) {
+ String mobileNumber = null;
+ if(mobileNo != null){
+ mobileNumber = mobileNo.toString();
+ }
SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage,
- mobileNo.toString(), smsCampaign);
+ mobileNumber, smsCampaign, smsCampaign.isNotification());
smsMessage.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue());
this.smsMessageRepository.save(smsMessage);
Collection messages = new ArrayList<>();
@@ -634,8 +655,8 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
@Override
@CronTarget(jobName = JobName.UPDATE_SMS_OUTBOUND_WITH_CAMPAIGN_MESSAGE)
public void storeTemplateMessageIntoSmsOutBoundTable() throws JobExecutionException {
- final Collection smsCampaignDataCollection = this.smsCampaignRepository.findByCampaignTypeAndTriggerTypeAndStatus(
- CampaignType.SMS.getValue(), SmsCampaignTriggerType.SCHEDULE.getValue(), SmsCampaignStatus.ACTIVE.getValue());
+ final Collection smsCampaignDataCollection = this.smsCampaignRepository.findByTriggerTypeAndStatus(
+ SmsCampaignTriggerType.SCHEDULE.getValue(), SmsCampaignStatus.ACTIVE.getValue());
if (smsCampaignDataCollection != null) {
for (SmsCampaign smsCampaign : smsCampaignDataCollection) {
LocalDateTime tenantDateNow = tenantDateTime();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java
index 9b5aeb2d5..3168ef489 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java
@@ -24,6 +24,7 @@ import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.fineract.infrastructure.configuration.exception.ExternalServiceConfigurationNotFoundException;
+import org.apache.fineract.infrastructure.configuration.service.ExternalServicesConstants.NOTIFICATION_JSON_INPUT_PARAMS;
import org.apache.fineract.infrastructure.configuration.service.ExternalServicesConstants.S3_JSON_INPUT_PARAMS;
import org.apache.fineract.infrastructure.configuration.service.ExternalServicesConstants.SMS_JSON_INPUT_PARAMS;
import org.apache.fineract.infrastructure.configuration.service.ExternalServicesConstants.SMTP_JSON_INPUT_PARAMS;
@@ -40,6 +41,7 @@ public class ExternalServicesPropertiesCommandFromApiJsonDeserializer {
private final Set S3SupportedParameters = S3_JSON_INPUT_PARAMS.getAllValues();
private final Set SMTPSupportedParameters = SMTP_JSON_INPUT_PARAMS.getAllValues();
private final Set SMSSupportedParameters = SMS_JSON_INPUT_PARAMS.getAllValues();
+ private final Set NotificationSupportedParameters = NOTIFICATION_JSON_INPUT_PARAMS.getAllValues();
private final FromJsonHelper fromApiJsonHelper;
@Autowired
@@ -64,6 +66,10 @@ public class ExternalServicesPropertiesCommandFromApiJsonDeserializer {
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.SMSSupportedParameters);
break;
+ case "NOTIFICATION":
+ this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.NotificationSupportedParameters);
+ break;
+
default:
throw new ExternalServiceConfigurationNotFoundException(externalServiceName);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java
index 44a56ca7e..f123c1923 100755
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java
@@ -41,6 +41,11 @@ public class ExternalServicesConstants {
public static final String SMS_END_POINT = "end_point";
public static final String SMS_TENANT_APP_KEY = "tenant_app_key";
+ public static final String NOTIFICATION_SERVICE_NAME = "NOTIFICATION";
+ public static final String NOTIFICATION_SERVER_KEY = "server_key";
+ public static final String NOTIFICATION_GCM_END_POINT = "gcm_end_point";
+ public static final String NOTIFICATION_FCM_END_POINT = "fcm_end_point";
+
public static enum EXTERNALSERVICEPROPERTIES_JSON_INPUT_PARAMS {
EXTERNAL_SERVICE_ID("external_service_id"), NAME("name"), VALUE("value");
@@ -164,5 +169,36 @@ public class ExternalServicesConstants {
return this.value;
}
}
+
+ public static enum NOTIFICATION_JSON_INPUT_PARAMS {
+ SERVER_KEY("server_key"), GCM_END_POINT("gcm_end_point"), FCM_END_POINT("fcm_end_point");
+
+ private final String value;
+
+ private NOTIFICATION_JSON_INPUT_PARAMS(final String value) {
+ this.value = value;
+ }
+
+ private static final Set values = new HashSet<>();
+
+ static {
+ for (final NOTIFICATION_JSON_INPUT_PARAMS type : NOTIFICATION_JSON_INPUT_PARAMS.values()) {
+ values.add(type.value);
+ }
+ }
+
+ public static Set getAllValues() {
+ return values;
+ }
+
+ @Override
+ public String toString() {
+ return name().toString().replaceAll("_", " ");
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformService.java
index 2d4683570..25f4b8e76 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformService.java
@@ -24,6 +24,7 @@ import org.apache.fineract.infrastructure.campaigns.sms.data.MessageGatewayConfi
import org.apache.fineract.infrastructure.configuration.data.ExternalServicesPropertiesData;
import org.apache.fineract.infrastructure.configuration.data.S3CredentialsData;
import org.apache.fineract.infrastructure.configuration.data.SMTPCredentialsData;
+import org.apache.fineract.infrastructure.gcm.domain.NotificationConfigurationData;
public interface ExternalServicesPropertiesReadPlatformService {
@@ -34,5 +35,7 @@ public interface ExternalServicesPropertiesReadPlatformService {
MessageGatewayConfigurationData getSMSGateway();
Collection retrieveOne(String serviceName);
+
+ NotificationConfigurationData getNotificationConfiguration();
}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java
index ccd401231..bf6270850 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java
@@ -28,6 +28,7 @@ import org.apache.fineract.infrastructure.configuration.data.S3CredentialsData;
import org.apache.fineract.infrastructure.configuration.data.SMTPCredentialsData;
import org.apache.fineract.infrastructure.configuration.exception.ExternalServiceConfigurationNotFoundException;
import org.apache.fineract.infrastructure.core.service.RoutingDataSource;
+import org.apache.fineract.infrastructure.gcm.domain.NotificationConfigurationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -176,6 +177,10 @@ public class ExternalServicesPropertiesReadPlatformServiceImpl implements Extern
serviceNameToUse = ExternalServicesConstants.SMS_SERVICE_NAME;
break;
+ case "NOTIFICATION":
+ serviceNameToUse = ExternalServicesConstants.NOTIFICATION_SERVICE_NAME;
+ break;
+
default:
throw new ExternalServiceConfigurationNotFoundException(serviceName);
}
@@ -185,5 +190,35 @@ public class ExternalServicesPropertiesReadPlatformServiceImpl implements Extern
return this.jdbcTemplate.query(sql, mapper, new Object[] {});
}
+
+ private static final class NotificationDataExtractor implements ResultSetExtractor {
+
+ @Override
+ public NotificationConfigurationData extractData(final ResultSet rs) throws SQLException, DataAccessException {
+ String serverKey = null;
+ String gcmEndPoint = null;
+ String fcmEndPoint = null;
+ while (rs.next()) {
+ if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.NOTIFICATION_SERVER_KEY )) {
+ serverKey = rs.getString("value");
+ } else if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.NOTIFICATION_GCM_END_POINT )) {
+ gcmEndPoint = rs.getString("value");
+ } else if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.NOTIFICATION_FCM_END_POINT )) {
+ fcmEndPoint = rs.getString("value");
+ }
+ }
+ return new NotificationConfigurationData(null, serverKey, gcmEndPoint, fcmEndPoint);
+ }
+ }
+
+
+ @Override
+ public NotificationConfigurationData getNotificationConfiguration() {
+ final ResultSetExtractor resultSetExtractor = new NotificationDataExtractor();
+ final String sql = "SELECT esp.name, esp.value FROM c_external_service_properties esp inner join c_external_service es on esp.external_service_id = es.id where es.name = '"
+ + ExternalServicesConstants.NOTIFICATION_SERVICE_NAME + "'";
+ final NotificationConfigurationData notificationConfigurationData = this.jdbcTemplate.query(sql, resultSetExtractor, new Object[] {});
+ return notificationConfigurationData;
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java
index 44f41647b..9bab493ea 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java
@@ -58,6 +58,10 @@ public class ExternalServicesReadPlatformServiceImpl implements ExternalServices
serviceNameToUse = ExternalServicesConstants.SMS_SERVICE_NAME;
break;
+ case "NOTIFICATION":
+ serviceNameToUse = ExternalServicesConstants.NOTIFICATION_SERVICE_NAME;
+ break;
+
default:
throw new ExternalServiceConfigurationNotFoundException(serviceName);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/GcmConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/GcmConstants.java
new file mode 100644
index 000000000..fa3b9bb65
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/GcmConstants.java
@@ -0,0 +1,242 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm;
+
+/**
+ * Constants used on GCM service communication.
+ */
+public final class GcmConstants {
+
+
+ /**
+ * Title for notification
+ */
+ public static final String title = "Hello !";
+
+ /**
+ * icon for notification.
+ */
+ public static final String defaultIcon = "default";
+
+
+ /**
+ * Parameter for to field.
+ */
+ public static final String PARAM_TO = "to";
+
+ /**
+ * Prefix of the topic.
+ */
+ public static final String TOPIC_PREFIX = "/topics/";
+
+ /**
+ * HTTP parameter for registration id.
+ */
+ //public static final String PARAM_REGISTRATION_ID = "registration_id";
+
+ /**
+ * HTTP parameter for collapse key.
+ */
+ public static final String PARAM_COLLAPSE_KEY = "collapse_key";
+
+ /**
+ * HTTP parameter for delaying the message delivery if the device is idle.
+ */
+ public static final String PARAM_DELAY_WHILE_IDLE = "delay_while_idle";
+
+ /**
+ * HTTP parameter for telling gcm to validate the message without actually
+ * sending it.
+ */
+ public static final String PARAM_DRY_RUN = "dry_run";
+
+ /**
+ * HTTP parameter for package name that can be used to restrict message
+ * delivery by matching against the package name used to generate the
+ * registration id.
+ */
+ public static final String PARAM_RESTRICTED_PACKAGE_NAME = "restricted_package_name";
+
+ /**
+ * Parameter used to set the message time-to-live.
+ */
+ public static final String PARAM_TIME_TO_LIVE = "time_to_live";
+
+ /**
+ * Parameter used to set the message priority.
+ */
+ public static final String PARAM_PRIORITY = "priority";
+
+ /**
+ * Parameter used to set the content available (iOS only)
+ */
+ public static final String PARAM_CONTENT_AVAILABLE = "content_available";
+
+ /**
+ * Value used to set message priority to normal.
+ */
+ public static final String MESSAGE_PRIORITY_NORMAL = "normal";
+
+ /**
+ * Value used to set message priority to high.
+ */
+ public static final String MESSAGE_PRIORITY_HIGH = "high";
+
+ /**
+ * A particular message could not be sent because the GCM servers were not
+ * available. Used only on JSON requests, as in plain text requests
+ * unavailability is indicated by a 503 response.
+ */
+ public static final String ERROR_UNAVAILABLE = "Unavailable";
+
+ /**
+ * A particular message could not be sent because the GCM servers
+ * encountered an error. Used only on JSON requests, as in plain text
+ * requests internal errors are indicated by a 500 response.
+ */
+ public static final String ERROR_INTERNAL_SERVER_ERROR = "InternalServerError";
+
+ /**
+ * Token returned by GCM when the requested registration id has a canonical
+ * value.
+ */
+ public static final String TOKEN_CANONICAL_REG_ID = "registration_id";
+
+ /**
+ * JSON-only field representing the registration ids.
+ */
+ public static final String JSON_REGISTRATION_IDS = "registration_ids";
+
+ /**
+ * JSON-only field representing the to recipient.
+ */
+ public static final String JSON_TO = "to";
+
+ /**
+ * JSON-only field representing the payload data.
+ */
+ public static final String JSON_PAYLOAD = "data";
+
+ /**
+ * JSON-only field representing the notification payload.
+ */
+ public static final String JSON_NOTIFICATION = "notification";
+
+ /**
+ * JSON-only field representing the notification title.
+ */
+ public static final String JSON_NOTIFICATION_TITLE = "title";
+
+ /**
+ * JSON-only field representing the notification body.
+ */
+ public static final String JSON_NOTIFICATION_BODY = "body";
+
+ /**
+ * JSON-only field representing the notification icon.
+ */
+ public static final String JSON_NOTIFICATION_ICON = "icon";
+
+ /**
+ * JSON-only field representing the notification sound.
+ */
+ public static final String JSON_NOTIFICATION_SOUND = "sound";
+
+ /**
+ * JSON-only field representing the notification badge.
+ */
+ public static final String JSON_NOTIFICATION_BADGE = "badge";
+
+ /**
+ * JSON-only field representing the notification tag.
+ */
+ public static final String JSON_NOTIFICATION_TAG = "tag";
+
+ /**
+ * JSON-only field representing the notification color.
+ */
+ public static final String JSON_NOTIFICATION_COLOR = "color";
+
+ /**
+ * JSON-only field representing the notification click action.
+ */
+ public static final String JSON_NOTIFICATION_CLICK_ACTION = "click_action";
+
+ /**
+ * JSON-only field representing the notification body localization key.
+ */
+ public static final String JSON_NOTIFICATION_BODY_LOC_KEY = "body_loc_key";
+
+ /**
+ * JSON-only field representing the notification body localization values.
+ */
+ public static final String JSON_NOTIFICATION_BODY_LOC_ARGS = "body_loc_args";
+
+ /**
+ * JSON-only field representing the notification title localization key.
+ */
+ public static final String JSON_NOTIFICATION_TITLE_LOC_KEY = "title_loc_key";
+
+ /**
+ * JSON-only field representing the notification title localization values.
+ */
+ public static final String JSON_NOTIFICATION_TITLE_LOC_ARGS = "title_loc_args";
+
+ /**
+ * JSON-only field representing the number of successful messages.
+ */
+ public static final String JSON_SUCCESS = "success";
+
+ /**
+ * JSON-only field representing the number of failed messages.
+ */
+ public static final String JSON_FAILURE = "failure";
+
+ /**
+ * JSON-only field representing the number of messages with a canonical
+ * registration id.
+ */
+ public static final String JSON_CANONICAL_IDS = "canonical_ids";
+
+ /**
+ * JSON-only field representing the id of the multicast request.
+ */
+ public static final String JSON_MULTICAST_ID = "multicast_id";
+
+ /**
+ * JSON-only field representing the result of each individual request.
+ */
+ public static final String JSON_RESULTS = "results";
+
+ /**
+ * JSON-only field representing the error field of an individual request.
+ */
+ public static final String JSON_ERROR = "error";
+
+ /**
+ * JSON-only field sent by GCM when a message was successfully sent.
+ */
+ public static final String JSON_MESSAGE_ID = "message_id";
+
+ private GcmConstants() {
+ throw new UnsupportedOperationException();
+ }
+
+ public static final Integer TIME_TO_LIVE = 30;
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiConstants.java
new file mode 100644
index 000000000..0b675cb47
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiConstants.java
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm.api;
+
+public class DeviceRegistrationApiConstants {
+ public static final String clientIdParamName = "clientId";
+ public static final String registrationIdParamName = "registrationId";
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiResource.java
new file mode 100644
index 000000000..1ba0fa737
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiResource.java
@@ -0,0 +1,160 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm.api;
+
+import java.util.Collection;
+import java.util.HashMap;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
+import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration;
+import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationData;
+import org.apache.fineract.infrastructure.gcm.service.DeviceRegistrationReadPlatformService;
+import org.apache.fineract.infrastructure.gcm.service.DeviceRegistrationWritePlatformService;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Scope;
+import org.springframework.stereotype.Component;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+@Path("/device/registration")
+@Component
+@Scope("singleton")
+public class DeviceRegistrationApiResource {
+
+ private final PlatformSecurityContext context;
+ private final DeviceRegistrationWritePlatformService deviceRegistrationWritePlatformService;
+ private final DefaultToApiJsonSerializer toApiJsonSerializer;
+ private final DeviceRegistrationReadPlatformService deviceRegistrationReadPlatformService;
+
+ @Autowired
+ public DeviceRegistrationApiResource(PlatformSecurityContext context,
+ final DefaultToApiJsonSerializer toApiJsonSerializer,
+ final DeviceRegistrationReadPlatformService deviceRegistrationReadPlatformService,
+ final DeviceRegistrationWritePlatformService deviceRegistrationWritePlatformService) {
+ this.context = context;
+ this.toApiJsonSerializer = toApiJsonSerializer;
+ this.deviceRegistrationReadPlatformService = deviceRegistrationReadPlatformService;
+ this.deviceRegistrationWritePlatformService = deviceRegistrationWritePlatformService;
+ }
+
+ @POST
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String registerDevice(final String apiRequestBodyAsJson) {
+ this.context.authenticatedUser();
+ Gson gson = new Gson();
+ JsonObject json = new Gson().fromJson(apiRequestBodyAsJson, JsonObject.class);
+ Long clientId = json.get(DeviceRegistrationApiConstants.clientIdParamName).getAsLong();
+ String registrationId = json.get(DeviceRegistrationApiConstants.registrationIdParamName).getAsString();
+ DeviceRegistration deviceRegistration = this.deviceRegistrationWritePlatformService.registerDevice(clientId, registrationId);
+ String response = gson.toJson(deviceRegistration);
+ return response;
+ }
+
+ @GET
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String retrieveAllDeviceRegistrations(@Context final UriInfo uriInfo) {
+
+ this.context.authenticatedUser();
+
+ Collection deviceRegistrationDataList = this.deviceRegistrationReadPlatformService
+ .retrieveAllDeviceRegiistrations();
+
+ return this.toApiJsonSerializer.serialize(deviceRegistrationDataList);
+ }
+
+ @GET
+ @Path("client/{clientId}")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String retrieveDeviceRegistrationByClientId(@PathParam("clientId") final Long clientId, @Context final UriInfo uriInfo) {
+
+ this.context.authenticatedUser();
+
+ DeviceRegistrationData deviceRegistrationData = this.deviceRegistrationReadPlatformService
+ .retrieveDeviceRegiistrationByClientId(clientId);
+
+ return this.toApiJsonSerializer.serialize(deviceRegistrationData);
+ }
+
+ @GET
+ @Path("{id}")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String retrieveDeviceRegiistration(@PathParam("id") final Long id, @Context final UriInfo uriInfo) {
+
+ this.context.authenticatedUser();
+
+ DeviceRegistrationData deviceRegistrationData = this.deviceRegistrationReadPlatformService.retrieveDeviceRegiistration(id);
+
+ return this.toApiJsonSerializer.serialize(deviceRegistrationData);
+ }
+
+ @PUT
+ @Path("{id}")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String updateDeviceRegistration(@PathParam("id") final Long id, final String apiRequestBodyAsJson) {
+
+ this.context.authenticatedUser();
+
+ Gson gson = new Gson();
+ JsonObject json = new Gson().fromJson(apiRequestBodyAsJson, JsonObject.class);
+ Long clientId = json.get(DeviceRegistrationApiConstants.clientIdParamName).getAsLong();
+ String registrationId = json.get(DeviceRegistrationApiConstants.registrationIdParamName).getAsString();
+ DeviceRegistration deviceRegistration = this.deviceRegistrationWritePlatformService.updateDeviceRegistration(id, clientId,
+ registrationId);
+ String response = gson.toJson(deviceRegistration);
+ return response;
+ }
+
+ @DELETE
+ @Path("{id}")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String delete(@PathParam("id") final Long id) {
+
+ this.context.authenticatedUser();
+ this.deviceRegistrationWritePlatformService.deleteDeviceRegistration(id);
+ return responseMap(id);
+
+ }
+
+ public String responseMap(Long id){
+ HashMap responseMap = new HashMap<>();
+ responseMap.put("resource", id);
+ return new Gson().toJson(responseMap);
+ }
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistration.java
new file mode 100644
index 000000000..c4ba7b330
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistration.java
@@ -0,0 +1,85 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm.domain;
+
+import java.util.Date;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.portfolio.client.domain.Client;
+
+@Entity
+@Table(name = "client_device_registration")
+public class DeviceRegistration extends AbstractPersistableCustom {
+
+ @OneToOne
+ @JoinColumn(name = "client_id", nullable = false, unique = true)
+ private Client client;
+
+ @Column(name = "registration_id", nullable = false, unique = true)
+ private String registrationId;
+
+ @Column(name = "updatedon_date", nullable = false)
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date updatedOnDate;
+
+ private DeviceRegistration(final Client client, final String registrationId) {
+ this.client = client;
+ this.registrationId = registrationId;
+ this.updatedOnDate = DateUtils.getLocalDateTimeOfTenant().toDate();
+ }
+
+ public static DeviceRegistration instance(final Client client,
+ final String registrationId) {
+ return new DeviceRegistration(client, registrationId);
+ }
+
+ public Client getClient() {
+ return this.client;
+ }
+
+ public void setClient(Client client) {
+ this.client = client;
+ }
+
+ public String getRegistrationId() {
+ return this.registrationId;
+ }
+
+ public void setRegistrationId(String registrationId) {
+ this.registrationId = registrationId;
+ }
+
+ public Date getUpdatedOnDate() {
+ return this.updatedOnDate;
+ }
+
+ public void setUpdatedOnDate(Date updatedOnDate) {
+ this.updatedOnDate = updatedOnDate;
+ }
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationData.java
new file mode 100644
index 000000000..3397a9bd3
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationData.java
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm.domain;
+
+import java.util.Date;
+
+import org.apache.fineract.portfolio.client.data.ClientData;
+
+public class DeviceRegistrationData {
+
+ public Long id;
+ public ClientData clientData;
+ public String registrationId;
+ public Date updatedOnDate;
+
+ private DeviceRegistrationData(final Long id, final ClientData clientData,
+ final String registrationId, final Date updatedOnDate) {
+ this.id = id;
+ this.clientData = clientData;
+ this.registrationId = registrationId;
+ this.updatedOnDate = updatedOnDate;
+ }
+
+ public static DeviceRegistrationData instance(final Long id,
+ final ClientData clientData, final String registrationId,
+ final Date updatedOnDate) {
+ return new DeviceRegistrationData(id, clientData, registrationId,
+ updatedOnDate);
+ }
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepository.java
new file mode 100644
index 000000000..003311762
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepository.java
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm.domain;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+public interface DeviceRegistrationRepository extends
+ JpaRepository,
+ JpaSpecificationExecutor {
+
+ public static final String FIND_DEVICE_REGISTRATION_BY_CLIENT = "select dr from DeviceRegistration dr where dr.client.id =:clientId ";
+
+ @Query(FIND_DEVICE_REGISTRATION_BY_CLIENT)
+ DeviceRegistration findDeviceRegistrationByClientId(
+ @Param("clientId") Long clientId);
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepositoryWrapper.java
new file mode 100644
index 000000000..a36b476ee
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepositoryWrapper.java
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm.domain;
+
+import org.apache.fineract.infrastructure.gcm.exception.DeviceRegistrationNotFoundException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DeviceRegistrationRepositoryWrapper {
+
+ private final DeviceRegistrationRepository repository;
+
+ @Autowired
+ public DeviceRegistrationRepositoryWrapper(
+ DeviceRegistrationRepository repository) {
+ this.repository = repository;
+ }
+
+ public DeviceRegistration findOneWithNotFoundDetection(
+ final Long deviceRegistrationId) {
+ final DeviceRegistration deviceRegistration = this.repository
+ .findOne(deviceRegistrationId);
+ if (deviceRegistration == null) {
+ throw new DeviceRegistrationNotFoundException(deviceRegistrationId);
+ }
+ return deviceRegistration;
+ }
+
+ public void save(final DeviceRegistration deviceRegistration) {
+ this.repository.save(deviceRegistration);
+ }
+
+ public void delete(final DeviceRegistration deviceRegistration) {
+ this.repository.delete(deviceRegistration);
+ }
+
+ public void saveAndFlush(final DeviceRegistration deviceRegistration) {
+ this.repository.saveAndFlush(deviceRegistration);
+ }
+
+ public DeviceRegistration findDeviceRegistrationByClientId(Long clientId) {
+ return this.repository.findDeviceRegistrationByClientId(clientId);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Message.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Message.java
new file mode 100644
index 000000000..34234dcb8
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Message.java
@@ -0,0 +1,317 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm.domain;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.fineract.infrastructure.gcm.GcmConstants;
+
+/**
+ * GCM message.
+ *
+ *
+ * Instances of this class are immutable and should be created using a
+ * {@link Builder}. Examples:
+ *
+ * Simplest message:
+ *
+ *
+ *
+ * Notification notification = new Notification.Builder("myicon")
+ * .title("Hello world!")
+ * .body("Here is a more detailed description")
+ * .build();
+ *
+ *
+ *
+ */
+public final class Notification implements Serializable {
+
+ private final String title;
+ private final String body;
+ private final String icon;
+ private final String sound;
+ private final Integer badge;
+ private final String tag;
+ private final String color;
+ private final String clickAction;
+ private final String bodyLocKey;
+ private final List bodyLocArgs;
+ private final String titleLocKey;
+ private final List titleLocArgs;
+
+ public static final class Builder {
+
+ // required parameters
+ private final String icon;
+
+ // optional parameters
+ private String title;
+ private String body;
+ private String sound;
+ private Integer badge;
+ private String tag;
+ private String color;
+ private String clickAction;
+ private String bodyLocKey;
+ private List bodyLocArgs;
+ private String titleLocKey;
+ private List titleLocArgs;
+
+ public Builder(String icon) {
+ this.icon = icon;
+ this.sound = "default"; // the only currently supported value
+ }
+
+ /**
+ * Sets the title property.
+ */
+ public Builder title(String value) {
+ title = value;
+ return this;
+ }
+
+ /**
+ * Sets the body property.
+ */
+ public Builder body(String value) {
+ body = value;
+ return this;
+ }
+
+ /**
+ * Sets the sound property (default value is {@literal default}).
+ */
+ public Builder sound(String value) {
+ sound = value;
+ return this;
+ }
+
+ /**
+ * Sets the badge property.
+ */
+ public Builder badge(int value) {
+ badge = value;
+ return this;
+ }
+
+ /**
+ * Sets the tag property.
+ */
+ public Builder tag(String value) {
+ tag = value;
+ return this;
+ }
+
+ /**
+ * Sets the color property in {@literal #rrggbb} format.
+ */
+ public Builder color(String value) {
+ color = value;
+ return this;
+ }
+
+ /**
+ * Sets the click action property.
+ */
+ public Builder clickAction(String value) {
+ clickAction = value;
+ return this;
+ }
+
+ /**
+ * Sets the body localization key property.
+ */
+ public Builder bodyLocKey(String value) {
+ bodyLocKey = value;
+ return this;
+ }
+
+ /**
+ * Sets the body localization values property.
+ */
+ public Builder bodyLocArgs(List value) {
+ bodyLocArgs = Collections.unmodifiableList(value);
+ return this;
+ }
+
+ /**
+ * Sets the title localization key property.
+ */
+ public Builder titleLocKey(String value) {
+ titleLocKey = value;
+ return this;
+ }
+
+ /**
+ * Sets the title localization values property.
+ */
+ public Builder titleLocArgs(List value) {
+ titleLocArgs = Collections.unmodifiableList(value);
+ return this;
+ }
+
+ public Notification build() {
+ return new Notification(this);
+ }
+
+ }
+
+ private Notification(Builder builder) {
+ title = builder.title;
+ body = builder.body;
+ icon = builder.icon;
+ sound = builder.sound;
+ badge = builder.badge;
+ tag = builder.tag;
+ color = builder.color;
+ clickAction = builder.clickAction;
+ bodyLocKey = builder.bodyLocKey;
+ bodyLocArgs = builder.bodyLocArgs;
+ titleLocKey = builder.titleLocKey;
+ titleLocArgs = builder.titleLocArgs;
+ }
+
+ /**
+ * Gets the title.
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Gets the body.
+ */
+ public String getBody() {
+ return body;
+ }
+
+ /**
+ * Gets the icon.
+ */
+ public String getIcon() {
+ return icon;
+ }
+
+ /**
+ * Gets the sound.
+ */
+ public String getSound() {
+ return sound;
+ }
+
+ /**
+ * Gets the badge.
+ */
+ public Integer getBadge() {
+ return badge;
+ }
+
+ /**
+ * Gets the tag.
+ */
+ public String getTag() {
+ return tag;
+ }
+
+ /**
+ * Gets the color.
+ */
+ public String getColor() {
+ return color;
+ }
+
+ /**
+ * Gets the click action.
+ */
+ public String getClickAction() {
+ return clickAction;
+ }
+
+ /**
+ * Gets the body localization key.
+ */
+ public String getBodyLocKey() {
+ return bodyLocKey;
+ }
+
+ /**
+ * Gets the body localization values list, which is immutable.
+ */
+ public List getBodyLocArgs() {
+ return bodyLocArgs;
+ }
+
+ /**
+ * Gets the title localization key.
+ */
+ public String getTitleLocKey() {
+ return titleLocKey;
+ }
+
+ /**
+ * Gets the title localization values list, which is immutable.
+ */
+ public List getTitleLocArgs() {
+ return titleLocArgs;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder("Notification(");
+ if (title != null) {
+ builder.append("title=").append(title).append(", ");
+ }
+ if (body != null) {
+ builder.append("body=").append(body).append(", ");
+ }
+ if (icon != null) {
+ builder.append("icon=").append(icon).append(", ");
+ }
+ if (sound != null) {
+ builder.append("sound=").append(sound).append(", ");
+ }
+ if (badge != null) {
+ builder.append("badge=").append(badge).append(", ");
+ }
+ if (tag != null) {
+ builder.append("tag=").append(tag).append(", ");
+ }
+ if (color != null) {
+ builder.append("color=").append(color).append(", ");
+ }
+ if (clickAction != null) {
+ builder.append("clickAction=").append(clickAction).append(", ");
+ }
+ if (bodyLocKey != null) {
+ builder.append("bodyLocKey=").append(bodyLocKey).append(", ");
+ }
+ if (bodyLocArgs != null) {
+ builder.append("bodyLocArgs=").append(bodyLocArgs).append(", ");
+ }
+ if (titleLocKey != null) {
+ builder.append("titleLocKey=").append(titleLocKey).append(", ");
+ }
+ if (titleLocArgs != null) {
+ builder.append("titleLocArgs=").append(titleLocArgs).append(", ");
+ }
+ if (builder.charAt(builder.length() - 1) == ' ') {
+ builder.delete(builder.length() - 2, builder.length());
+ }
+ builder.append(")");
+ return builder.toString();
+ }
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/NotificationConfigurationData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/NotificationConfigurationData.java
new file mode 100644
index 000000000..2d4acc93f
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/NotificationConfigurationData.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm.domain;
+
+public class NotificationConfigurationData {
+
+ private final Long id;
+ private final String serverKey;
+ private final String gcmEndPoint;
+ private final String fcmEndPoint;
+ public NotificationConfigurationData(Long id, String serverKey,final String gcmEndPoint,final String fcmEndPoint) {
+ this.id = id;
+ this.serverKey = serverKey;
+ this.gcmEndPoint = gcmEndPoint;
+ this.fcmEndPoint = fcmEndPoint;
+ }
+ public Long getId() {
+ return id;
+ }
+ public String getServerKey() {
+ return serverKey;
+ }
+
+ public String getGcmEndPoint() {
+ return gcmEndPoint;
+ }
+ public String getFcmEndPoint() {
+ return fcmEndPoint;
+ }
+
+
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Result.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Result.java
new file mode 100644
index 000000000..76aafa8ef
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Result.java
@@ -0,0 +1,187 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm.domain;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Result of a GCM message request that returned HTTP status code 200.
+ *
+ *
+ * If the message is successfully created, the {@link #getMessageId()} returns
+ * the message id and {@link #getErrorCodeName()} returns {@literal null};
+ * otherwise, {@link #getMessageId()} returns {@literal null} and
+ * {@link #getErrorCodeName()} returns the code of the error.
+ *
+ *
+ * There are cases when a request is accept and the message successfully
+ * created, but GCM has a canonical registration id for that device. In this
+ * case, the server should update the registration id to avoid rejected requests
+ * in the future.
+ *
+ *
+ * In a nutshell, the workflow to handle a result is:
+ *
+ *
+ * - Call {@link #getMessageId()}:
+ * - {@literal null} means error, call {@link #getErrorCodeName()}
+ * - non-{@literal null} means the message was created:
+ * - Call {@link #getCanonicalRegistrationId()}
+ * - if it returns {@literal null}, do nothing.
+ * - otherwise, update the server datastore with the new id.
+ *
+ */
+public final class Result implements Serializable {
+
+ private final String messageId;
+ private final String canonicalRegistrationId;
+ private final String errorCode;
+ private final Integer success;
+ private final Integer failure;
+ private final List failedRegistrationIds;
+ private final int status;
+
+ public static final class Builder {
+
+ // optional parameters
+ private String messageId;
+ private String canonicalRegistrationId;
+ private String errorCode;
+ private Integer success;
+ private Integer failure;
+ private List failedRegistrationIds;
+ private int status;
+
+ public Builder canonicalRegistrationId(String value) {
+ canonicalRegistrationId = value;
+ return this;
+ }
+
+ public Builder messageId(String value) {
+ messageId = value;
+ return this;
+ }
+
+ public Builder errorCode(String value) {
+ errorCode = value;
+ return this;
+ }
+
+ public Builder success(Integer value) {
+ success = value;
+ return this;
+ }
+
+ public Builder failure(Integer value) {
+ failure = value;
+ return this;
+ }
+
+ public Builder status(int value) {
+ status = value;
+ return this;
+ }
+
+ public Builder failedRegistrationIds(List value) {
+ failedRegistrationIds = value;
+ return this;
+ }
+
+ public Result build() {
+ return new Result(this);
+ }
+ }
+
+ private Result(Builder builder) {
+ canonicalRegistrationId = builder.canonicalRegistrationId;
+ messageId = builder.messageId;
+ errorCode = builder.errorCode;
+ success = builder.success;
+ failure = builder.failure;
+ failedRegistrationIds = builder.failedRegistrationIds;
+ status = builder.status;
+ }
+
+ /**
+ * Gets the message id, if any.
+ */
+ public String getMessageId() {
+ return messageId;
+ }
+
+ /**
+ * Gets the canonical registration id, if any.
+ */
+ public String getCanonicalRegistrationId() {
+ return canonicalRegistrationId;
+ }
+
+ /**
+ * Gets the error code, if any.
+ */
+ public String getErrorCodeName() {
+ return errorCode;
+ }
+
+ public Integer getSuccess() {
+ return success;
+ }
+
+ public Integer getFailure() {
+ return failure;
+ }
+
+ public List getFailedRegistrationIds() {
+ return failedRegistrationIds;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder("[");
+ if (messageId != null) {
+ builder.append(" messageId=").append(messageId);
+ }
+ if (canonicalRegistrationId != null) {
+ builder.append(" canonicalRegistrationId=").append(
+ canonicalRegistrationId);
+ }
+ if (errorCode != null) {
+ builder.append(" errorCode=").append(errorCode);
+ }
+ if (success != null) {
+ builder.append(" groupSuccess=").append(success);
+ }
+ if (failure != null) {
+ builder.append(" groupFailure=").append(failure);
+ }
+ if (failedRegistrationIds != null) {
+ builder.append(" failedRegistrationIds=").append(
+ failedRegistrationIds);
+ }
+ return builder.append(" ]").toString();
+ }
+
+ public int getStatus() {
+ return this.status;
+ }
+
+
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Sender.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Sender.java
new file mode 100644
index 000000000..cc9970e4d
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Sender.java
@@ -0,0 +1,832 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.gcm.domain;
+
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_CANONICAL_IDS;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_ERROR;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_FAILURE;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_MESSAGE_ID;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_MULTICAST_ID;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_BADGE;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_BODY;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_BODY_LOC_ARGS;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_BODY_LOC_KEY;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_CLICK_ACTION;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_COLOR;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_ICON;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_SOUND;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_TAG;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_TITLE;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_TITLE_LOC_ARGS;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_TITLE_LOC_KEY;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_PAYLOAD;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_REGISTRATION_IDS;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_TO;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_RESULTS;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_SUCCESS;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_COLLAPSE_KEY;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_DELAY_WHILE_IDLE;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_DRY_RUN;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_PRIORITY;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_CONTENT_AVAILABLE;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_RESTRICTED_PACKAGE_NAME;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_TIME_TO_LIVE;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.TOKEN_CANONICAL_REG_ID;
+import static org.apache.fineract.infrastructure.gcm.GcmConstants.TOPIC_PREFIX;
+
+import org.apache.fineract.infrastructure.gcm.GcmConstants;
+import org.apache.fineract.infrastructure.gcm.exception.InvalidRequestException;
+/*import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;*/
+
+
+
+
+
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import java.io.BufferedReader;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Helper class to send messages to the GCM service using an API Key.
+ */
+public class Sender {
+
+ protected static final String UTF8 = "UTF-8";
+
+ /**
+ * Initial delay before first retry, without jitter.
+ */
+ protected static final int BACKOFF_INITIAL_DELAY = 1000;
+ /**
+ * Maximum delay before a retry.
+ */
+ protected static final int MAX_BACKOFF_DELAY = 1024000;
+
+ protected final Random random = new Random();
+ protected static final Logger logger = Logger.getLogger(Sender.class
+ .getName());
+
+ private final String key;
+
+ private String endpoint;
+
+ private int connectTimeout;
+ private int readTimeout;
+
+ /**
+ * Full options constructor.
+ *
+ * @param key
+ * FCM Server Key obtained through the Firebase Web Console.
+ * @param endpoint
+ * Endpoint to use when sending the message.
+ */
+ public Sender(String key, String endpoint) {
+ this.key = nonNull(key);
+ this.endpoint = nonNull(endpoint);
+ }
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ /**
+ * Set the underlying URLConnection's connect timeout (in milliseconds). A
+ * timeout value of 0 specifies an infinite timeout.
+ *
+ * Default is the system's default timeout.
+ *
+ * @see java.net.URLConnection#setConnectTimeout(int)
+ */
+ public final void setConnectTimeout(int connectTimeout) {
+ if (connectTimeout < 0) {
+ throw new IllegalArgumentException("timeout can not be negative");
+ }
+ this.connectTimeout = connectTimeout;
+ }
+
+ /**
+ * Set the underlying URLConnection's read timeout (in milliseconds). A
+ * timeout value of 0 specifies an infinite timeout.
+ *
+ * Default is the system's default timeout.
+ *
+ * @see java.net.URLConnection#setReadTimeout(int)
+ */
+ public final void setReadTimeout(int readTimeout) {
+ if (readTimeout < 0) {
+ throw new IllegalArgumentException("timeout can not be negative");
+ }
+ this.readTimeout = readTimeout;
+ }
+
+ /**
+ * Sends a message to one device, retrying in case of unavailability.
+ *
+ *
+ * Note: this method uses exponential back-off to retry in
+ * case of service unavailability and hence could block the calling thread
+ * for many seconds.
+ *
+ * @param message
+ * message to be sent, including the device's registration id.
+ * @param to
+ * registration token, notification key, or topic where the
+ * message will be sent.
+ * @param retries
+ * number of retries in case of service unavailability errors.
+ *
+ * @return result of the request (see its javadoc for more details).
+ *
+ * @throws IllegalArgumentException
+ * if to is {@literal null}.
+ * @throws InvalidRequestException
+ * if GCM didn't returned a 200 or 5xx status.
+ * @throws IOException
+ * if message could not be sent.
+ */
+ public Result send(Message message, String to, int retries)
+ throws IOException {
+ int attempt = 0;
+ Result result;
+ int backoff = BACKOFF_INITIAL_DELAY;
+ boolean tryAgain;
+ do {
+ attempt++;
+ if (logger.isLoggable(Level.FINE)) {
+ logger.fine("Attempt #" + attempt + " to send message "
+ + message + " to regIds " + to);
+ }
+ result = sendNoRetry(message, to);
+ tryAgain = result == null && attempt <= retries;
+ if (tryAgain) {
+ int sleepTime = backoff / 2 + random.nextInt(backoff);
+ sleep(sleepTime);
+ if (2 * backoff < MAX_BACKOFF_DELAY) {
+ backoff *= 2;
+ }
+ }
+ } while (tryAgain);
+ if (result == null) {
+ throw new IOException("Could not send message after " + attempt
+ + " attempts");
+ }
+ return result;
+ }
+
+ /**
+ * Sends a message without retrying in case of service unavailability. See
+ * {@link #send(Message, String, int)} for more info.
+ *
+ * @return result of the post, or {@literal null} if the GCM service was
+ * unavailable or any network exception caused the request to fail,
+ * or if the response contains more than one result.
+ *
+ * @throws InvalidRequestException
+ * if GCM didn't returned a 200 status.
+ * @throws IllegalArgumentException
+ * if to is {@literal null}.
+ */
+ public Result sendNoRetry(Message message, String to) throws IOException {
+ nonNull(to);
+ Map