diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index bc0c43e2d..9a87171cc 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -2629,6 +2629,30 @@ + Two-Factor Authentication + twofactor + Request OTP + List OTP delivery methods + + + + + + twofactor/validate + Validate OTP + + + + + + + twofactor/invalidate + Invalidate Access Token + + + + + User users Create a User @@ -2942,6 +2966,16 @@ external service Configuration + + Two-Factor Configuration + twofactor/configure + + Retrieve + Two-Factor Configuration + Update + Two-Factor Configuration + + @@ -3930,6 +3964,11 @@ Default authentication is using HTTP Basic Auth. Oauth2 can be enabled by using -Psecurity=oauth option on gradle build command , refer the platform setup wiki for additional details.

+

+ 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: +

    +
  1. User authticates via BasicAuth / oAauth
  2. +
  3. Client requests a list of supported OTP delivery methods for the authenticated user(Get Delivery Methods)
  4. +
  5. User selects an OTP delivery method and client sends a request for OTP(Request OTP)
  6. +
  7. User receives an OTP and the client sends it for verification(Validate OTP)
  8. +
  9. If the OTP is valid, an access token is returned
  10. +
  11. The access token is sent in following requestes to the server as a header Fineract-Platform-TFA-Token
  12. +
  13. On session end, the access token should be invalidatedInvalidate Access Token)
  14. +
+

+

+ Two-Factor authentication and delivery methods can be configured via + the /twofactor/configure endpoint. +

+
+
+ +
+
+ +   +
+
+

Get Delivery Methods

+

Returns a list of possible OTP delivery methods for the current user

+

Requires first-factor authenticated user.

+
+
+ GET https://DomainName/api/v1/twofactor + +[ + { + "name": "sms", + "target": "08888888888" + }, + { + "name": "email", + "target": "user@example.com" + } +] +
+
+ +   +
+
+

Request OTP

+

Requests an OTP.

+

Requires first-factor authenticated user.

+
Arguments
+
+
deliveryMethod
+
+ String mandatory, the delivery method name +
+
extendedToken
+
+ boolean optional, whether to request an extended token, default false +
+
+
+
+ POST https://DomainName/api/v1/twofactor?deliveryMethod=sms&extendedToken=false + +{ + "requestTime": 1500000000000, + "tokenLiveTimeInSec": 300, + "extendedAccessToken": false, + "deliveryMethod": { + "name": "sms", + "target": "08888888888" + } +} +
+
+ +   +
+
+

Validate OTP

+

Validates an OTP. If the OTP is valid, an access token is created.

+

The returned access token is later sent as a header Fineract-Platform-TFA-Token.

+

Requires first-factor authenticated user.

+
Arguments
+
+
token
+
+ String mandatory, the OTP to validate +
+
+
+
+ POST https://DomainName/api/v1/twofactor/validate?token=YYYYY + +{ + "token": "cb0bb6e33fc540709d50a16eb2e555f9", + "validFrom": 1501530702801, + "validTo": 1501617102801 +} +
+
+ +   +
+
+

Invalidate Access Token

+

Invalidates an access token.

+

Two factor access tokens should be invalidated on logout.

+

Requires fully authenticated user.

+
+
+ POST https://DomainName/api/v1/twofactor/invalidate + +{ + "token": "cb0bb6e33fc540709d50a16eb2e555f9" +} + +{ + "resourceIdentifier": "cb0bb6e33fc540709d50a16eb2e555f9" +} +
+
 
@@ -19391,6 +19566,73 @@ Content-Type: application/json
+   +
+
+

Two-Factor Configuration

+

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.

+
+
+
+
+ +   +
+
+

Retrieve Two-Factor Configuration

+

Returns available two-factor configuration.

+
+
+ GET https://DomainName/api/v1/twofactor/configure + + +{ + "otp-delivery-email-body": "Hello {{username}}.\n\nYour OTP login token is {{token}}.", + "otp-delivery-sms-enable": true, + "otp-delivery-sms-provider": 6, + "otp-delivery-email-subject": "Fineract Two-Factor Authentication Token", + "otp-token-length": 5, + "access-token-live-time-extended": 604800, + "otp-delivery-email-enable": true, + "otp-token-live-time": 300, + "otp-delivery-sms-text": "Your authentication token for Fineract is {{token}}.", + "access-token-live-time": 86400 +} + +
+
+ +   +
+
+

Update Two-Factor Configuration

+

Update two-factor configuration.

+
+
+ PUT https://DomainName/api/v1/twofactor/configure + + +{ + "otp-delivery-sms-provider": 7 + "otp-delivery-sms-enable": false +} + + +{ + "changes": { + "otp-delivery-sms-enable": false, + "otp-delivery-sms-provider": 7 + } +} + +
+
+  
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 cf6febab2..2ddf09c36 100644 --- a/fineract-provider/build.gradle +++ b/fineract-provider/build.gradle @@ -195,17 +195,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/integrationTest/java/org/apache/fineract/integrationtests/common/HolidayHelper.java b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/HolidayHelper.java index d8b90adfd..b1a5e64bd 100644 --- a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/HolidayHelper.java +++ b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/HolidayHelper.java @@ -59,7 +59,7 @@ public class HolidayHelper { map.put("fromDate", "01 April 2013"); map.put("toDate", "01 April 2013"); map.put("repaymentsRescheduledTo", "08 April 2013"); - + map.put("reschedulingType", 2); String HolidayCreateJson = new Gson().toJson(map); System.out.println(HolidayCreateJson); return HolidayCreateJson; 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..ae53bb78c 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"; @@ -3039,6 +3039,97 @@ public class CommandWrapperBuilder { this.entityId = adHocId; this.href = "/adhocquery/" + adHocId; this.json = "{}"; + return this ; + } + public CommandWrapperBuilder createEmail() { + this.actionName = "CREATE"; + this.entityName = "EMAIL"; + this.entityId = null; + this.href = "/emailcampaigns/template"; + return this; + } + + public CommandWrapperBuilder updateEmail(final Long resourceId) { + this.actionName = "UPDATE"; + this.entityName = "EMAIL"; + this.entityId = resourceId; + this.href = "/emailcampaigns/"+resourceId; + return this; + } + + public CommandWrapperBuilder deleteEmail(final Long resourceId) { + this.actionName = "DELETE"; + this.entityName = "EMAIL"; + this.entityId = resourceId; + this.href = "/emailcampaigns/"+resourceId; + return this; + } + + public CommandWrapperBuilder createEmailCampaign() { + this.actionName = "CREATE"; + this.entityName = "EMAIL_CAMPAIGN"; + this.entityId = null; + this.href = "/emailcampaigns/campaign"; + return this; + } + + public CommandWrapperBuilder updateEmailCampaign(final Long resourceId) { + this.actionName = "UPDATE"; + this.entityName = "EMAIL_CAMPAIGN"; + this.entityId = resourceId; + this.href = "/emailcampaigns/"+resourceId; + return this; + } + + public CommandWrapperBuilder deleteEmailCampaign(final Long resourceId) { + this.actionName = "DELETE"; + this.entityName = "EMAIL_CAMPAIGN"; + this.entityId = resourceId; + this.href = "/emailcampaigns/"+resourceId; + return this; + } + + public CommandWrapperBuilder activateEmailCampaign(final Long resourceId) { + this.actionName = "ACTIVATE"; + this.entityName = "EMAIL_CAMPAIGN"; + this.entityId = resourceId; + this.href = "/emailcampaigns/"+resourceId + "?command=activate"; + return this; + } + + public CommandWrapperBuilder closeEmailCampaign(final Long resourceId) { + this.actionName = "CLOSE"; + this.entityName = "EMAIL_CAMPAIGN"; + this.entityId = resourceId; + this.href = "/emailcampaigns/"+resourceId + "?command=close"; + return this; + } + public CommandWrapperBuilder reactivateEmailCampaign(final Long resourceId) { + this.actionName = "REACTIVATE"; + this.entityName = "EMAIL_CAMPAIGN"; + this.entityId = resourceId; + this.href = "/emailcampaigns/"+resourceId + "?command=reactivate"; + return this; + } + + public CommandWrapperBuilder updateEmailConfiguration() { + this.actionName = "UPDATE"; + this.entityName = "EMAIL_CONFIGURATION"; + this.href = "/emailcampaigns/configuration/"; + 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/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/email/EmailApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/EmailApiConstants.java new file mode 100644 index 000000000..1307467ac --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/EmailApiConstants.java @@ -0,0 +1,54 @@ +/** + * 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.campaigns.email; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class EmailApiConstants { + + public static final String RESOURCE_NAME = "email"; + + // general + public static final String localeParamName = "locale"; + public static final String dateFormatParamName = "dateFormat"; + + // request parameters + public static final String idParamName = "id"; + public static final String groupIdParamName = "groupId"; + public static final String clientIdParamName = "clientId"; + public static final String staffIdParamName = "staffId"; + public static final String subjectParamName = "emailSubject"; + public static final String messageParamName = "emailMessage"; + + // response parameters + public static final String statusParamName = "status"; + + public static final Set CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, + dateFormatParamName, groupIdParamName, clientIdParamName, staffIdParamName, messageParamName)); + + public static final Set UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(messageParamName)); + + public static final String SMTP_SERVER = "SMTP_SERVER"; + public static final String SMTP_PORT = "SMTP_PORT"; + public static final String SMTP_USERNAME = "SMTP_USERNAME"; + public static final String SMTP_PASSWORD = "SMTP_PASSWORD"; + +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/ScheduledEmailConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/ScheduledEmailConstants.java new file mode 100644 index 000000000..950f232f0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/ScheduledEmailConstants.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.campaigns.email; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class ScheduledEmailConstants { + + // define the API resource entity name + public static final String SCHEDULED_EMAIL_ENTITY_NAME = "SCHEDULEDEMAIL"; + + // general API resource request parameter constants + public static final String LOCALE_PARAM_NAME = "locale"; + public static final String DATE_FORMAT_PARAM_NAME = "dateFormat"; + + // parameter constants for create/update entity API request + public static final String NAME_PARAM_NAME = "name"; + public static final String DESCRIPTION_PARAM_NAME = "description"; + public static final String START_DATE_TIME_PARAM_NAME = "startDateTime"; + public static final String RECURRENCE_PARAM_NAME = "recurrence"; + public static final String EMAIL_RECIPIENTS_PARAM_NAME = "emailRecipients"; + public static final String EMAIL_SUBJECT_PARAM_NAME = "emailSubject"; + public static final String EMAIL_MESSAGE_PARAM_NAME = "emailMessage"; + public static final String EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME = "emailAttachmentFileFormatId"; + public static final String STRETCHY_REPORT_ID_PARAM_NAME = "stretchyReportId"; + public static final String STRETCHY_REPORT_PARAM_MAP_PARAM_NAME = "stretchyReportParamMap"; + public static final String IS_ACTIVE_PARAM_NAME = "isActive"; + + // other parameter constants + public static final String ID_PARAM_NAME = "id"; + public static final String EMAIL_ATTACHMENT_FILE_FORMAT_PARAM_NAME = "emailAttachmentFileFormat"; + public static final String PREVIOUS_RUN_DATE_TIME_PARAM_NAME = "previousRunDateTime"; + public static final String NEXT_RUN_DATE_TIME_PARAM_NAME = "nextRunDateTime"; + public static final String PREVIOUS_RUN_STATUS = "previousRunStatus"; + public static final String PREVIOUS_RUN_ERROR_LOG = "previousRunErrorLog"; + public static final String PREVIOUS_RUN_ERROR_MESSAGE = "previousRunErrorMessage"; + public static final String NUMBER_OF_RUNS = "numberOfRuns"; + public static final String STRETCHY_REPORT_PARAM_NAME = "stretchyReport"; + + // list of permitted parameters for the create report mailing job request + public static final Set CREATE_REQUEST_PARAMETERS = new HashSet<>(Arrays.asList(LOCALE_PARAM_NAME, DATE_FORMAT_PARAM_NAME, + NAME_PARAM_NAME, DESCRIPTION_PARAM_NAME, RECURRENCE_PARAM_NAME, EMAIL_RECIPIENTS_PARAM_NAME, EMAIL_SUBJECT_PARAM_NAME, + EMAIL_MESSAGE_PARAM_NAME, EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME, STRETCHY_REPORT_ID_PARAM_NAME, + STRETCHY_REPORT_PARAM_MAP_PARAM_NAME, IS_ACTIVE_PARAM_NAME, START_DATE_TIME_PARAM_NAME)); + + // list of permitted parameters for the update report mailing job request + public static final Set UPDATE_REQUEST_PARAMETERS = new HashSet<>(Arrays.asList(LOCALE_PARAM_NAME, DATE_FORMAT_PARAM_NAME, + NAME_PARAM_NAME, DESCRIPTION_PARAM_NAME, RECURRENCE_PARAM_NAME, EMAIL_RECIPIENTS_PARAM_NAME, EMAIL_SUBJECT_PARAM_NAME, + EMAIL_MESSAGE_PARAM_NAME, EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME, STRETCHY_REPORT_ID_PARAM_NAME, + STRETCHY_REPORT_PARAM_MAP_PARAM_NAME, IS_ACTIVE_PARAM_NAME, START_DATE_TIME_PARAM_NAME)); + + // list of parameters that represent the properties of a report mailing job + public static final Set REPORT_MAILING_JOB_DATA_PARAMETERS = new HashSet<>(Arrays.asList(ID_PARAM_NAME, NAME_PARAM_NAME, + DESCRIPTION_PARAM_NAME, RECURRENCE_PARAM_NAME, EMAIL_RECIPIENTS_PARAM_NAME, EMAIL_SUBJECT_PARAM_NAME, EMAIL_MESSAGE_PARAM_NAME, + EMAIL_ATTACHMENT_FILE_FORMAT_PARAM_NAME, STRETCHY_REPORT_PARAM_NAME, STRETCHY_REPORT_PARAM_MAP_PARAM_NAME, IS_ACTIVE_PARAM_NAME, + START_DATE_TIME_PARAM_NAME, PREVIOUS_RUN_DATE_TIME_PARAM_NAME, NEXT_RUN_DATE_TIME_PARAM_NAME, PREVIOUS_RUN_STATUS, + PREVIOUS_RUN_ERROR_LOG, PREVIOUS_RUN_ERROR_MESSAGE, NUMBER_OF_RUNS)); + + // report mailing job configuration names + public static final String GMAIL_SMTP_SERVER = "GMAIL_SMTP_SERVER"; + public static final String GMAIL_SMTP_PORT = "GMAIL_SMTP_PORT"; + public static final String GMAIL_SMTP_USERNAME = "GMAIL_SMTP_USERNAME"; + public static final String GMAIL_SMTP_PASSWORD = "GMAIL_SMTP_PASSWORD"; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/api/EmailApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/api/EmailApiResource.java new file mode 100644 index 000000000..6bdae7b62 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/api/EmailApiResource.java @@ -0,0 +1,185 @@ +/** + * 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.campaigns.email.api; + +import org.apache.fineract.accounting.journalentry.api.DateParam; +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.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.core.service.SearchParameters; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailData; +import org.apache.fineract.infrastructure.campaigns.email.service.EmailReadPlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import java.util.Collection; +import java.util.Date; + +@Path("/email") +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +@Component +@Scope("singleton") +public class EmailApiResource { + + private final String resourceNameForPermissions = "Email"; + private final PlatformSecurityContext context; + private final EmailReadPlatformService readPlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + + @Autowired + public EmailApiResource(final PlatformSecurityContext context, final EmailReadPlatformService readPlatformService, + final DefaultToApiJsonSerializer toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper, + final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService) { + this.context = context; + this.readPlatformService = readPlatformService; + this.toApiJsonSerializer = toApiJsonSerializer; + this.apiRequestParameterHelper = apiRequestParameterHelper; + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + } + + @GET + public String retrieveAllEmails(@Context final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + final Collection emailMessages = this.readPlatformService.retrieveAll(); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, emailMessages); + } + + @GET + @Path("pendingEmail") + public String retrievePendingEmail(@QueryParam("sqlSearch") final String sqlSearch, @QueryParam("offset") final Integer offset, + @QueryParam("limit") final Integer limit, @QueryParam("orderBy") final String orderBy, + @QueryParam("sortOrder") final String sortOrder, @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + final SearchParameters searchParameters = SearchParameters.forEmailCampaign(sqlSearch, offset, limit, orderBy, sortOrder); + Collection emailMessages = this.readPlatformService.retrieveAllPending(searchParameters); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, emailMessages); + } + + @GET + @Path("sentEmail") + public String retrieveSentEmail(@QueryParam("sqlSearch") final String sqlSearch, @QueryParam("offset") final Integer offset, + @QueryParam("limit") final Integer limit, @QueryParam("orderBy") final String orderBy, + @QueryParam("sortOrder") final String sortOrder, @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + + final SearchParameters searchParameters = SearchParameters.forEmailCampaign(sqlSearch, offset, limit, orderBy, sortOrder); + Collection emailMessages = this.readPlatformService.retrieveAllSent(searchParameters); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, emailMessages); + } + + @GET + @Path("messageByStatus") + public String retrieveAllEmailByStatus(@QueryParam("sqlSearch") final String sqlSearch, @QueryParam("offset") final Integer offset, + @QueryParam("limit") final Integer limit, @QueryParam("status") final Integer status, + @QueryParam("orderBy") final String orderBy, @QueryParam("sortOrder") final String sortOrder, + @QueryParam("fromDate") final DateParam fromDateParam, @QueryParam("toDate") final DateParam toDateParam, + @QueryParam("locale") final String locale, @QueryParam("dateFormat") final String dateFormat, @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + Date fromDate = null; + if (fromDateParam != null) { + fromDate = fromDateParam.getDate("fromDate", dateFormat, locale); + } + Date toDate = null; + if (toDateParam != null) { + toDate = toDateParam.getDate("toDate", dateFormat, locale); + } + Page emailMessages = this.readPlatformService.retrieveEmailByStatus(limit, status, fromDate, toDate); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, emailMessages); + } + + @GET + @Path("failedEmail") + public String retrieveFailedEmail(@QueryParam("sqlSearch") final String sqlSearch, @QueryParam("offset") final Integer offset, + @QueryParam("limit") final Integer limit, @QueryParam("orderBy") final String orderBy, + @QueryParam("sortOrder") final String sortOrder, @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + + final SearchParameters searchParameters = SearchParameters.forEmailCampaign(sqlSearch, offset, limit, orderBy, sortOrder); + Collection emailMessages = this.readPlatformService.retrieveAllFailed(searchParameters); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, emailMessages); + } + + @POST + public String create(final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().createEmail().withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @GET + @Path("{resourceId}") + public String retrieveOne(@PathParam("resourceId") final Long resourceId, @Context final UriInfo uriInfo) { + + final EmailData emailMessage = this.readPlatformService.retrieveOne(resourceId); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, emailMessage); + } + + @PUT + @Path("{resourceId}") + public String update(@PathParam("resourceId") final Long resourceId, final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateEmail(resourceId).withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @DELETE + @Path("{resourceId}") + public String delete(@PathParam("resourceId") final Long resourceId) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteEmail(resourceId).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/api/EmailCampaignApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/api/EmailCampaignApiResource.java new file mode 100644 index 000000000..243eefc86 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/api/EmailCampaignApiResource.java @@ -0,0 +1,230 @@ +/** + * 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.campaigns.email.api; + +import com.google.gson.JsonElement; +import org.apache.commons.lang.StringUtils; +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.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.api.JsonQuery; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.core.service.SearchParameters; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.campaigns.email.data.PreviewCampaignMessage; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailBusinessRulesData; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignData; +import org.apache.fineract.infrastructure.campaigns.email.service.EmailCampaignReadPlatformService; +import org.apache.fineract.infrastructure.campaigns.email.service.EmailCampaignWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; +import java.util.Collection; +import java.util.HashSet; + +/** + * Created with IntelliJ IDEA. + * User: andrew + * Date: 19-5-14 + * Time: 15:17 + * To change this template use File | Settings | File Templates. + */ +@Path("/email/campaign") +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +@Component +@Scope("singleton") +public class EmailCampaignApiResource { + + + //change name to email campaign + private final String resourceNameForPermissions = "EMAIL_CAMPAIGN"; + + private final PlatformSecurityContext context; + + private final DefaultToApiJsonSerializer toApiJsonSerializer; + + private final ApiRequestParameterHelper apiRequestParameterHelper; + + private final EmailCampaignReadPlatformService emailCampaignReadPlatformService; + private final FromJsonHelper fromJsonHelper; + + + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final DefaultToApiJsonSerializer emailCampaignDataDefaultToApiJsonSerializer; + private final EmailCampaignWritePlatformService emailCampaignWritePlatformService; + + private final DefaultToApiJsonSerializer previewCampaignMessageDefaultToApiJsonSerializer; + + + @Autowired + public EmailCampaignApiResource(final PlatformSecurityContext context,final DefaultToApiJsonSerializer toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper, + final EmailCampaignReadPlatformService emailCampaignReadPlatformService, final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, + final DefaultToApiJsonSerializer emailCampaignDataDefaultToApiJsonSerializer, + final FromJsonHelper fromJsonHelper, final EmailCampaignWritePlatformService emailCampaignWritePlatformService, + final DefaultToApiJsonSerializer previewCampaignMessageDefaultToApiJsonSerializer) { + this.context = context; + this.toApiJsonSerializer = toApiJsonSerializer; + this.apiRequestParameterHelper = apiRequestParameterHelper; + this.emailCampaignReadPlatformService = emailCampaignReadPlatformService; + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + this.emailCampaignDataDefaultToApiJsonSerializer = emailCampaignDataDefaultToApiJsonSerializer; + this.fromJsonHelper = fromJsonHelper; + this.emailCampaignWritePlatformService = emailCampaignWritePlatformService; + this.previewCampaignMessageDefaultToApiJsonSerializer = previewCampaignMessageDefaultToApiJsonSerializer; + } + + + @GET + @Path("{resourceId}") + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveOneCampaign(@PathParam("resourceId") final Long resourceId,@Context final UriInfo uriInfo){ + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + + EmailCampaignData emailCampaignData = this.emailCampaignReadPlatformService.retrieveOne(resourceId); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.emailCampaignDataDefaultToApiJsonSerializer.serialize(settings,emailCampaignData); + + } + + @GET + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveAllCampaign(@Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + + final Collection emailCampaignDataCollection = this.emailCampaignReadPlatformService.retrieveAllCampaign(); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.emailCampaignDataDefaultToApiJsonSerializer.serialize(settings,emailCampaignDataCollection); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String createCampaign(final String apiRequestBodyAsJson,@Context final UriInfo uriInfo){ + + final CommandWrapper commandRequest = new CommandWrapperBuilder().createEmailCampaign().withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @PUT + @Path("{resourceId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String updateCampaign(@PathParam("resourceId") final Long campaignId,final String apiRequestBodyAsJson,@Context final UriInfo uriInfo){ + + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateEmailCampaign(campaignId).withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{resourceId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String activate(@PathParam("resourceId") final Long campaignId, @QueryParam("command") final String commandParam, + final String apiRequestBodyAsJson){ + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); + + CommandProcessingResult result = null; + CommandWrapper commandRequest = null; + if (is(commandParam, "activate")) { + commandRequest = builder.activateEmailCampaign(campaignId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + }else if (is(commandParam, "close")){ + commandRequest = builder.closeEmailCampaign(campaignId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + }else if (is(commandParam, "reactivate")){ + commandRequest = builder.reactivateEmailCampaign(campaignId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("preview") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String preview(final String apiRequestBodyAsJson,@Context final UriInfo uriInfo){ + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + + PreviewCampaignMessage campaignMessage = null; + final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson); + final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper); + campaignMessage = this.emailCampaignWritePlatformService.previewMessage(query); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.previewCampaignMessageDefaultToApiJsonSerializer.serialize(settings,campaignMessage, new HashSet()); + + } + + + @GET() + @Path("template") + public String template(@Context final UriInfo uriInfo){ + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + + final Collection emailBusinessRulesDataCollection = this.emailCampaignReadPlatformService.retrieveAll(); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings,emailBusinessRulesDataCollection); + } + + @GET + @Path("template/{resourceId}") + public String retrieveOneTemplate(@PathParam("resourceId") final Long resourceId,@Context final UriInfo uriInfo){ + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + + final EmailBusinessRulesData emailBusinessRulesData = this.emailCampaignReadPlatformService.retrieveOneTemplate(resourceId); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings,emailBusinessRulesData); + + } + + @DELETE + @Path("{resourceId}") + public String delete(@PathParam("resourceId") final Long resourceId) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteEmailCampaign(resourceId).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + private boolean is(final String commandParam, final String commandValue) { + return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/api/EmailConfigurationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/api/EmailConfigurationApiResource.java new file mode 100644 index 000000000..e95669401 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/api/EmailConfigurationApiResource.java @@ -0,0 +1,94 @@ +/** + * 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.campaigns.email.api; + +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.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailConfigurationData; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailData; +import org.apache.fineract.infrastructure.campaigns.email.service.EmailConfigurationReadPlatformService; +import org.apache.fineract.infrastructure.campaigns.email.service.EmailReadPlatformService; +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 javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; +import java.util.Collection; + + +@Path("/email/configuration") +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +@Component +@Scope("singleton") +public class EmailConfigurationApiResource { + + private final String resourceNameForPermissions = "EMAIL_CONFIGURATION"; + private final PlatformSecurityContext context; + private final EmailReadPlatformService readPlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final EmailConfigurationReadPlatformService emailConfigurationReadPlatformService; + + @Autowired + public EmailConfigurationApiResource(final PlatformSecurityContext context, final EmailReadPlatformService readPlatformService, + final DefaultToApiJsonSerializer toApiJsonSerializer, + final ApiRequestParameterHelper apiRequestParameterHelper, + final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, + final EmailConfigurationReadPlatformService emailConfigurationReadPlatformService) { + this.context = context; + this.readPlatformService = readPlatformService; + this.toApiJsonSerializer = toApiJsonSerializer; + this.apiRequestParameterHelper = apiRequestParameterHelper; + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + this.emailConfigurationReadPlatformService = emailConfigurationReadPlatformService; + } + + @GET + public String retrieveAll(@Context final UriInfo uriInfo){ + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + + final Collection configuration = this.emailConfigurationReadPlatformService.retrieveAll(); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + + return this.toApiJsonSerializer.serialize(settings, configuration); + } + + + @PUT + public String updateConfiguration(@Context final UriInfo uriInfo, final String apiRequestBodyAsJson){ + + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateEmailConfiguration().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/campaigns/email/data/EmailBusinessRulesData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailBusinessRulesData.java new file mode 100644 index 000000000..2d7dfb15e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailBusinessRulesData.java @@ -0,0 +1,96 @@ +/** + * 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.campaigns.email.data; + +import java.util.Map; + +public class EmailBusinessRulesData { + + @SuppressWarnings("unused") + private final Long reportId; + + @SuppressWarnings("unused") + private final String reportName; + + @SuppressWarnings("unused") + private final String reportType; + + @SuppressWarnings("unused") + private final String reportSubType; + + @SuppressWarnings("unused") + private final String reportDescription; + + @SuppressWarnings("unused") + private final Map reportParamName; + + + + public EmailBusinessRulesData(final Long reportId, final String reportName,final String reportType, final Map reportParamName, + final String reportSubType, final String reportDescription) { + this.reportId = reportId; + this.reportName = reportName; + this.reportType = reportType; + this.reportParamName = reportParamName; + this.reportDescription = reportDescription; + this.reportSubType = reportSubType; + } + + + public static EmailBusinessRulesData instance(final Long reportId, final String reportName, final String reportType, final Map reportParamName, + final String reportSubType, final String reportDescription){ + return new EmailBusinessRulesData(reportId,reportName,reportType,reportParamName,reportSubType,reportDescription); + } + + public Map getReportParamName() { + return reportParamName; + } + + public String getReportType() { + return reportType; + } + + public String getReportName() { + return reportName; + } + + public Long getReportId() { + return reportId; + } + + public String getReportDescription() { + return reportDescription; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + EmailBusinessRulesData that = (EmailBusinessRulesData) o; + + return reportId != null ? reportId.equals(that.reportId) : that.reportId == null; + + } + + @Override + public int hashCode() { + return reportId != null ? reportId.hashCode() : 0; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailCampaignData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailCampaignData.java new file mode 100644 index 000000000..02ff74e72 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailCampaignData.java @@ -0,0 +1,144 @@ +/** + * 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.campaigns.email.data; + +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; + +public class EmailCampaignData { + + @SuppressWarnings("unused") + private Long id; + @SuppressWarnings("unused") + private final String campaignName; + @SuppressWarnings("unused") + private final Integer campaignType; + @SuppressWarnings("unused") + private final Long businessRuleId; + @SuppressWarnings("unused") + private final String paramValue; + @SuppressWarnings("unused") + private final EnumOptionData campaignStatus; + @SuppressWarnings("unused") + private final String emailSubject; + @SuppressWarnings("unused") + private final String emailMessage; + @SuppressWarnings("unused") + private final String emailAttachmentFileFormat; + @SuppressWarnings("unused") + private final Long stretchyReportId; + @SuppressWarnings("unused") + private final String stretchyReportParamMap; + @SuppressWarnings("unused") + private final DateTime nextTriggerDate; + @SuppressWarnings("unused") + private final LocalDate lastTriggerDate; + @SuppressWarnings("unused") + private final EmailCampaignTimeLine emailCampaignTimeLine; + + @SuppressWarnings("unused") + private final DateTime recurrenceStartDate; + + private final String recurrence; + + private EmailCampaignData(final Long id, final String campaignName, final Integer campaignType, final Long businessRuleId, + final String paramValue, final EnumOptionData campaignStatus, final String emailSubject, + final String message, final String emailAttachmentFileFormat, final Long stretchyReportId, + final String stretchyReportParamMap, final DateTime nextTriggerDate, final LocalDate lastTriggerDate, + final EmailCampaignTimeLine emailCampaignTimeLine, final DateTime recurrenceStartDate, final String recurrence) { + this.id = id; + this.campaignName = campaignName; + this.campaignType = campaignType; + this.businessRuleId = businessRuleId; + this.paramValue = paramValue; + this.campaignStatus =campaignStatus; + this.emailSubject = emailSubject; + this.emailMessage = message; + this.emailAttachmentFileFormat = emailAttachmentFileFormat; + this.stretchyReportId = stretchyReportId; + this.stretchyReportParamMap = stretchyReportParamMap; + if(nextTriggerDate !=null){ + this.nextTriggerDate = nextTriggerDate; + }else{ + this.nextTriggerDate = null; + } + if(lastTriggerDate !=null){ + this.lastTriggerDate = lastTriggerDate; + }else{ + this.lastTriggerDate = null; + } + this.emailCampaignTimeLine =emailCampaignTimeLine; + this.recurrenceStartDate = recurrenceStartDate; + this.recurrence = recurrence; + } + + public static EmailCampaignData instance(final Long id, final String campaignName, final Integer campaignType, final Long runReportId, + final String paramValue, final EnumOptionData campaignStatus, final String emailSubject, + final String message, final String emailAttachmentFileFormat, final Long stretchyReportId, + final String stretchyReportParamMap, final DateTime nextTriggerDate, final LocalDate lastTriggerDate, + final EmailCampaignTimeLine emailCampaignTimeLine, + final DateTime recurrenceStartDate, final String recurrence){ + return new EmailCampaignData(id,campaignName,campaignType,runReportId,paramValue, + campaignStatus,emailSubject,message,emailAttachmentFileFormat,stretchyReportId,stretchyReportParamMap,nextTriggerDate,lastTriggerDate,emailCampaignTimeLine,recurrenceStartDate,recurrence); + } + + + public Long getId() { + return id; + } + + public String getCampaignName() { + return this.campaignName; + } + + public Integer getCampaignType() { + return this.campaignType; + } + + public Long getRunReportId() { + return this.businessRuleId; + } + + public String getParamValue() { + return this.paramValue; + } + + public EnumOptionData getCampaignStatus() { + return this.campaignStatus; + } + + public String getEmailSubject() { return this.emailSubject; } + + public String getMessage() { + return this.emailMessage; + } + + public DateTime getNextTriggerDate() { + return this.nextTriggerDate; + } + + public LocalDate getLastTriggerDate() { + return this.lastTriggerDate; + } + + public String getRecurrence() {return this.recurrence;} + + public DateTime getRecurrenceStartDate() {return this.recurrenceStartDate;} +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailCampaignTimeLine.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailCampaignTimeLine.java new file mode 100644 index 000000000..920462690 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailCampaignTimeLine.java @@ -0,0 +1,40 @@ +/** + * 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.campaigns.email.data; + +import org.joda.time.LocalDate; + +public class EmailCampaignTimeLine { + private final LocalDate submittedOnDate; + private final String submittedByUsername; + private final LocalDate activatedOnDate; + private final String activatedByUsername; + private final LocalDate closedOnDate; + private final String closedByUsername; + + public EmailCampaignTimeLine(final LocalDate submittedOnDate, final String submittedByUsername, + final LocalDate activatedOnDate, final String activatedByUsername, final LocalDate closedOnDate, final String closedByUsername) { + this.submittedOnDate = submittedOnDate; + this.submittedByUsername = submittedByUsername; + this.activatedOnDate = activatedOnDate; + this.activatedByUsername = activatedByUsername; + this.closedOnDate = closedOnDate; + this.closedByUsername = closedByUsername; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailCampaignValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailCampaignValidator.java new file mode 100644 index 000000000..d85f38693 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailCampaignValidator.java @@ -0,0 +1,259 @@ +/** + * 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.campaigns.email.data; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import org.apache.commons.lang.StringUtils; +import org.joda.time.LocalDate; +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.campaigns.email.domain.EmailCampaignType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Type; +import java.util.*; + +@Component +public class EmailCampaignValidator { + + + + public static final String RESOURCE_NAME = "email"; + public static final String campaignName = "campaignName"; + public static final String campaignType = "campaignType"; + public static final String businessRuleId = "businessRuleId"; + public static final String stretchyReportId = "stretchyReportId"; + public static final String stretchyReportParamMap = "stretchyReportParamMap"; + public static final String paramValue = "paramValue"; + public static final String emailSubject = "emailSubject"; + public static final String emailMessage = "emailMessage"; + public static final String emailAttachmentFileFormatId = "emailAttachmentFileFormatId"; + public static final String activationDateParamName = "activationDate"; + public static final String recurrenceStartDate = "recurrenceStartDate"; + public static final String submittedOnDateParamName = "submittedOnDate"; + public static final String closureDateParamName = "closureDate"; + public static final String recurrenceParamName = "recurrence"; + public static final String statusParamName = "status"; + + public static final String localeParamName = "locale"; + public static final String dateFormatParamName = "dateFormat"; + + + private final FromJsonHelper fromApiJsonHelper; + + + public static final Set supportedParams = new HashSet(Arrays.asList(campaignName, campaignType,localeParamName,dateFormatParamName, + businessRuleId,paramValue,emailMessage,recurrenceStartDate,activationDateParamName,submittedOnDateParamName,closureDateParamName,recurrenceParamName, + emailSubject,stretchyReportId,stretchyReportParamMap,emailAttachmentFileFormatId)); + + public static final Set supportedParamsForUpdate = new HashSet<>(Arrays.asList(campaignName, campaignType,localeParamName,dateFormatParamName, + businessRuleId,paramValue,emailMessage,recurrenceStartDate,activationDateParamName,recurrenceParamName)); + + public static final Set ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet(Arrays.asList(localeParamName, + dateFormatParamName, activationDateParamName)); + + public static final Set CLOSE_REQUEST_DATA_PARAMETERS = new HashSet(Arrays.asList(localeParamName, + dateFormatParamName, closureDateParamName)); + + public static final Set PREVIEW_REQUEST_DATA_PARAMETERS= new HashSet(Arrays.asList(paramValue,emailMessage)); + + @Autowired + public EmailCampaignValidator(FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; + } + + + public void validateCreate(String json){ + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, EmailCampaignValidator.supportedParams); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final List dataValidationErrors = new ArrayList(); + + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(EmailCampaignValidator.RESOURCE_NAME); + + final String campaignName = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.campaignName,element); + baseDataValidator.reset().parameter(EmailCampaignValidator.campaignName).value(campaignName).notBlank().notExceedingLengthOf(100); + + + final Long campaignType = this.fromApiJsonHelper.extractLongNamed(EmailCampaignValidator.campaignType,element); + baseDataValidator.reset().parameter(EmailCampaignValidator.campaignType).value(campaignType).notNull().integerGreaterThanZero(); + + if(campaignType.intValue() == EmailCampaignType.SCHEDULE.getValue()){ + final String recurrenceParamName = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.recurrenceParamName, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.recurrenceParamName).value(recurrenceParamName).notBlank(); + + final String recurrenceStartDate = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.recurrenceStartDate, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.recurrenceStartDate).value(recurrenceStartDate).notBlank(); + } + + final Long businessRuleId = this.fromApiJsonHelper.extractLongNamed(EmailCampaignValidator.businessRuleId,element); + baseDataValidator.reset().parameter(EmailCampaignValidator.businessRuleId).value(businessRuleId).notNull().integerGreaterThanZero(); + + final String emailSubject = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.emailSubject, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.emailSubject).value(emailSubject).notBlank().notExceedingLengthOf(50); + + final String emailMessage = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.emailMessage, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.emailMessage).value(emailMessage).notBlank().notExceedingLengthOf(480); + + final String paramValue = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.paramValue, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.paramValue).value(paramValue).notBlank(); + + + + if (this.fromApiJsonHelper.parameterExists(EmailCampaignValidator.submittedOnDateParamName, element)) { + final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(EmailCampaignValidator.submittedOnDateParamName, + element); + baseDataValidator.reset().parameter(EmailCampaignValidator.submittedOnDateParamName).value(submittedOnDate).notNull(); + } + + + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + + } + + public void validateForUpdate(String json){ + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, EmailCampaignValidator.supportedParamsForUpdate); + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final List dataValidationErrors = new ArrayList(); + + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(EmailCampaignValidator.RESOURCE_NAME); + + final String campaignName = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.campaignName,element); + baseDataValidator.reset().parameter(EmailCampaignValidator.campaignName).value(campaignName).notBlank().notExceedingLengthOf(100); + + + final Long campaignType = this.fromApiJsonHelper.extractLongNamed(EmailCampaignValidator.campaignType,element); + baseDataValidator.reset().parameter(EmailCampaignValidator.campaignType).value(campaignType).notNull().integerGreaterThanZero(); + + if(campaignType.intValue() == EmailCampaignType.SCHEDULE.getValue()){ + final String recurrenceParamName = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.recurrenceParamName, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.recurrenceParamName).value(recurrenceParamName).notBlank(); + + final String recurrenceStartDate = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.recurrenceStartDate, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.recurrenceStartDate).value(recurrenceStartDate).notBlank(); + } + + final Long runReportId = this.fromApiJsonHelper.extractLongNamed(EmailCampaignValidator.businessRuleId,element); + baseDataValidator.reset().parameter(EmailCampaignValidator.businessRuleId).value(runReportId).notNull().integerGreaterThanZero(); + + final String message = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.emailMessage, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.emailMessage).value(message).notBlank().notExceedingLengthOf(480); + + final String paramValue = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.paramValue, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.paramValue).value(paramValue).notBlank(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + + + } + + public void validatePreviewMessage(String json){ + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, EmailCampaignValidator.PREVIEW_REQUEST_DATA_PARAMETERS); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(EmailCampaignValidator.RESOURCE_NAME); + + final String paramValue = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.paramValue, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.paramValue).value(paramValue).notBlank(); + + final String message = this.fromApiJsonHelper.extractStringNamed(EmailCampaignValidator.emailMessage, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.emailMessage).value(message).notBlank().notExceedingLengthOf(480); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + + + } + + public void validateClosedDate(String json){ + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, EmailCampaignValidator.CLOSE_REQUEST_DATA_PARAMETERS); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(EmailCampaignValidator.RESOURCE_NAME); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(EmailCampaignValidator.closureDateParamName, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.closureDateParamName).value(closeDate).notNull(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + public void validateActivation(String json){ + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, EmailCampaignValidator.ACTIVATION_REQUEST_DATA_PARAMETERS); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(EmailCampaignValidator.RESOURCE_NAME); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(EmailCampaignValidator.activationDateParamName, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.activationDateParamName).value(activationDate).notNull(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void ValidateClosure(String json){ + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, EmailCampaignValidator.CLOSE_REQUEST_DATA_PARAMETERS); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(EmailCampaignValidator.RESOURCE_NAME); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(EmailCampaignValidator.closureDateParamName, element); + baseDataValidator.reset().parameter(EmailCampaignValidator.closureDateParamName).value(closeDate).notNull(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailConfigurationData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailConfigurationData.java new file mode 100644 index 000000000..6d005ea10 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailConfigurationData.java @@ -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. + */ +package org.apache.fineract.infrastructure.campaigns.email.data; + +/** + * Immutable data object representing an Email configuration. + */ +public class EmailConfigurationData { + @SuppressWarnings("unused") + private final Long id; + + private final String name; + + private final String value; + + /** + * @return an instance of the EmailConfigurationData class + **/ + public static EmailConfigurationData instance(Long id, String name, String value) { + return new EmailConfigurationData(id, name, value); + } + + /** + * EmailConfigurationData constructor + **/ + private EmailConfigurationData(Long id, String name, String value) { + this.id = id; + this.name = name; + this.value = value; + } + + /** + * @return the id + */ + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailConfigurationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailConfigurationValidator.java new file mode 100644 index 000000000..3bb267eea --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailConfigurationValidator.java @@ -0,0 +1,111 @@ +/** + * 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.campaigns.email.data; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +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.campaigns.email.EmailApiConstants; +import org.apache.fineract.infrastructure.campaigns.email.exception.EmailConfigurationSMTPUsernameNotValid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Type; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Component +public class EmailConfigurationValidator { + + private final FromJsonHelper fromApiJsonHelper; + + private static final String EMAIL_REGEX = "^[\\w!#$%&’*+/=?`{|}~^-]+(?:\\.[\\w!#$%&’*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$"; + private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); + + public static final Set supportedParams = new HashSet(Arrays.asList(EmailApiConstants.SMTP_PORT,EmailApiConstants.SMTP_PASSWORD, + EmailApiConstants.SMTP_USERNAME,EmailApiConstants.SMTP_SERVER)); + + @Autowired + private EmailConfigurationValidator(final FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; + } + + + + public void validateUpdateConfiguration(String json){ + + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, EmailConfigurationValidator.supportedParams); + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final List dataValidationErrors = new ArrayList<>(); + + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(EmailApiConstants.RESOURCE_NAME); + + final String smtpUsername = this.fromApiJsonHelper.extractStringNamed(EmailApiConstants.SMTP_USERNAME,element); + baseDataValidator.reset().parameter(EmailApiConstants.SMTP_USERNAME).value(smtpUsername).notBlank().notExceedingLengthOf(150); + + + final String smtpPassword = this.fromApiJsonHelper.extractStringNamed(EmailApiConstants.SMTP_PASSWORD,element); + baseDataValidator.reset().parameter(EmailApiConstants.SMTP_PASSWORD).value(smtpPassword).notBlank().notExceedingLengthOf(50); + + final String smtpServer = this.fromApiJsonHelper.extractStringNamed(EmailApiConstants.SMTP_SERVER,element); + baseDataValidator.reset().parameter(EmailApiConstants.SMTP_SERVER).value(smtpServer).notBlank().notExceedingLengthOf(100); + + final Long smtpPort = this.fromApiJsonHelper.extractLongNamed(EmailApiConstants.SMTP_PORT,element); + baseDataValidator.reset().parameter(EmailApiConstants.SMTP_PORT).value(smtpPort).notNull().integerGreaterThanZero(); + + this.throwExceptionIfValidationWarningsExist(dataValidationErrors); + + + + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + + public boolean isValidEmail(String email) { + // this is the easiest check + if (email == null) { + return false; + }else if (email.endsWith(".")) { + return false; + } + + // Check the whole email address structure + Matcher emailMatcher = EMAIL_PATTERN.matcher(email); + + // check if the Matcher matches the email pattern + if (!emailMatcher.matches()) { + return false; + } + + return true; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailData.java new file mode 100644 index 000000000..280b4ea25 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailData.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.campaigns.email.data; + +import org.joda.time.LocalDate; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.dataqueries.data.ReportData; + +import java.util.List; + +/** + * Immutable data object representing a SMS message. + */ +public class EmailData { + + private final Long id; + private final Long groupId; + private final Long clientId; + private final Long staffId; + private final EnumOptionData status; + private final String emailAddress; + private final String emailSubject; + private final String emailMessage; + private final EnumOptionData emailAttachmentFileFormat; + private final ReportData stretchyReport; + private final String stretchyReportParamMap; + private final List emailAttachmentFileFormatOptions; + private final List stretchyReportParamDateOptions; + private final String campaignName; + private final LocalDate sentDate; + private final String errorMessage; + + + public static EmailData instance(final Long id,final Long groupId, final Long clientId, final Long staffId, final EnumOptionData status, + final String emailAddress, final String emailSubject, + final String message, final EnumOptionData emailAttachmentFileFormat, final ReportData stretchyReport, + final String stretchyReportParamMap, final List emailAttachmentFileFormatOptions, + final List stretchyReportParamDateOptions, final String campaignName, final LocalDate sentDate,final String errorMessage) { + return new EmailData(id, groupId, clientId, staffId, status, emailAddress, emailSubject, message, + emailAttachmentFileFormat,stretchyReport,stretchyReportParamMap,emailAttachmentFileFormatOptions, + stretchyReportParamDateOptions,campaignName,sentDate,errorMessage); + } + + private EmailData(final Long id,final Long groupId, final Long clientId, final Long staffId, final EnumOptionData status, final String emailAddress, final String emailSubject, final String message, + final EnumOptionData emailAttachmentFileFormat, final ReportData stretchyReport, final String stretchyReportParamMap, + final List emailAttachmentFileFormatOptions, final List stretchyReportParamDateOptions, + final String campaignName,final LocalDate sentDate,final String errorMessage) { + this.id = id; + this.groupId = groupId; + this.clientId = clientId; + this.staffId = staffId; + this.status = status; + this.emailAddress = emailAddress; + this.emailSubject = emailSubject; + this.emailMessage = message; + this.emailAttachmentFileFormat = emailAttachmentFileFormat; + this.stretchyReport = stretchyReport; + this.stretchyReportParamMap = stretchyReportParamMap; + this.emailAttachmentFileFormatOptions = emailAttachmentFileFormatOptions; + this.stretchyReportParamDateOptions = stretchyReportParamDateOptions; + this.campaignName = campaignName; + this.sentDate = sentDate; + this.errorMessage = errorMessage; + } + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @return the groupId + */ + public Long getGroupId() { + return groupId; + } + + /** + * @return the clientId + */ + public Long getClientId() { + return clientId; + } + + /** + * @return the staffId + */ + public Long getStaffId() { + return staffId; + } + + /** + * @return the status + */ + public EnumOptionData getStatus() { + return status; + } + + public String getErrorMessage() {return this.errorMessage;} + + /** + * @return the emailAddress + */ + public String getEmailAddress() { + return emailAddress; + } + + /** + * @return the message + */ + public String getMessage() { + return emailMessage; + } + + public String getCampaignName() {return this.campaignName;} + + public LocalDate getSentDate() { return this.sentDate; } + + public String getEmailSubject() { + return emailSubject; + } + + public EnumOptionData getEmailAttachmentFileFormat() { + return emailAttachmentFileFormat; + } + + public ReportData getStretchyReport() { + return stretchyReport; + } + + public String getStretchyReportParamMap() { + return stretchyReportParamMap; + } + + public List getEmailAttachmentFileFormatOptions() { + return emailAttachmentFileFormatOptions; + } + + public List getStretchyReportParamDateOptions() { + return stretchyReportParamDateOptions; + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailDataValidator.java new file mode 100644 index 000000000..c7f05dafc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailDataValidator.java @@ -0,0 +1,307 @@ +/** + * 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.campaigns.email.data; + +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.TypeReference; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import org.apache.commons.lang.StringUtils; +import org.joda.time.LocalDateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +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.campaigns.email.EmailApiConstants; +import org.apache.fineract.infrastructure.campaigns.email.ScheduledEmailConstants; +import org.apache.fineract.infrastructure.campaigns.email.domain.ScheduledEmailAttachmentFileFormat; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Type; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Component +public final class EmailDataValidator { + + private final FromJsonHelper fromApiJsonHelper; + private static final String EMAIL_REGEX = "^[\\w!#$%&’*+/=?`{|}~^-]+(?:\\.[\\w!#$%&’*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$"; + private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); + + @Autowired + public EmailDataValidator(final FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; + } + + /** + * validate the request to create a new report mailing job + * + * @param jsonCommand -- the JSON command object (instance of the JsonCommand class) + * @return None + **/ + public void validateCreateRequest(final JsonCommand jsonCommand) { + final String jsonString = jsonCommand.json(); + final JsonElement jsonElement = jsonCommand.parsedJson(); + + if (StringUtils.isBlank(jsonString)) { + throw new InvalidJsonException(); + } + + final Type typeToken = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeToken, jsonString, + ScheduledEmailConstants.CREATE_REQUEST_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors). + resource(StringUtils.lowerCase(ScheduledEmailConstants.SCHEDULED_EMAIL_ENTITY_NAME)); + + final String name = this.fromApiJsonHelper.extractStringNamed(ScheduledEmailConstants.NAME_PARAM_NAME, jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.NAME_PARAM_NAME).value(name).notBlank().notExceedingLengthOf(100); + + final String startDateTime = this.fromApiJsonHelper.extractStringNamed(ScheduledEmailConstants.START_DATE_TIME_PARAM_NAME, + jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.START_DATE_TIME_PARAM_NAME).value(startDateTime).notBlank(); + + final Integer stretchyReportId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(ScheduledEmailConstants.STRETCHY_REPORT_ID_PARAM_NAME, + jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.STRETCHY_REPORT_ID_PARAM_NAME).value(stretchyReportId).notNull(). + integerGreaterThanZero(); + + final String emailRecipients = this.fromApiJsonHelper.extractStringNamed(ScheduledEmailConstants.EMAIL_RECIPIENTS_PARAM_NAME, jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.EMAIL_RECIPIENTS_PARAM_NAME).value(emailRecipients).notBlank(); + + final String emailSubject = this.fromApiJsonHelper.extractStringNamed(ScheduledEmailConstants.EMAIL_SUBJECT_PARAM_NAME, jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.EMAIL_SUBJECT_PARAM_NAME).value(emailSubject).notBlank().notExceedingLengthOf(100); + + final String emailMessage = this.fromApiJsonHelper.extractStringNamed(ScheduledEmailConstants.EMAIL_MESSAGE_PARAM_NAME, jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.EMAIL_MESSAGE_PARAM_NAME).value(emailMessage).notBlank(); + + if (this.fromApiJsonHelper.parameterExists(ScheduledEmailConstants.IS_ACTIVE_PARAM_NAME, jsonElement)) { + final Boolean isActive = this.fromApiJsonHelper.extractBooleanNamed(ScheduledEmailConstants.IS_ACTIVE_PARAM_NAME, + jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.IS_ACTIVE_PARAM_NAME).value(isActive).notNull(); + } + + final Integer emailAttachmentFileFormatId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed( + ScheduledEmailConstants.EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME, jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME). + value(emailAttachmentFileFormatId).notNull(); + + if (emailAttachmentFileFormatId != null) { + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME).value(emailAttachmentFileFormatId). + isOneOfTheseValues(ScheduledEmailAttachmentFileFormat.validValues()); + } + + final String dateFormat = jsonCommand.dateFormat(); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.DATE_FORMAT_PARAM_NAME).value(dateFormat).notBlank(); + + if (StringUtils.isNotEmpty(dateFormat)) { + + try { + final DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(dateFormat).withLocale(jsonCommand.extractLocale()); + + // try to parse the date time string + LocalDateTime.parse(startDateTime, dateTimeFormatter); + } + + catch(IllegalArgumentException ex) { + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.DATE_FORMAT_PARAM_NAME).value(dateFormat).failWithCode("invalid.date.format"); + } + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + /** + * validate the request to update a report mailing job + * + * @param jsonCommand -- the JSON command object (instance of the JsonCommand class) + * @return None + **/ + public void validateUpdateRequest(final JsonCommand jsonCommand) { + final String jsonString = jsonCommand.json(); + final JsonElement jsonElement = jsonCommand.parsedJson(); + + if (StringUtils.isBlank(jsonString)) { + throw new InvalidJsonException(); + } + + final Type typeToken = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeToken, jsonString, + ScheduledEmailConstants.UPDATE_REQUEST_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors). + resource(StringUtils.lowerCase(ScheduledEmailConstants.SCHEDULED_EMAIL_ENTITY_NAME)); + + if (this.fromApiJsonHelper.parameterExists(ScheduledEmailConstants.NAME_PARAM_NAME, jsonElement)) { + final String name = this.fromApiJsonHelper.extractStringNamed(ScheduledEmailConstants.NAME_PARAM_NAME, jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.NAME_PARAM_NAME).value(name).notBlank().notExceedingLengthOf(100); + } + + if (this.fromApiJsonHelper.parameterExists(ScheduledEmailConstants.STRETCHY_REPORT_ID_PARAM_NAME, jsonElement)) { + final Integer stretchyReportId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(ScheduledEmailConstants.STRETCHY_REPORT_ID_PARAM_NAME, + jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.STRETCHY_REPORT_ID_PARAM_NAME).value(stretchyReportId).notNull(). + integerGreaterThanZero(); + } + + if (this.fromApiJsonHelper.parameterExists(ScheduledEmailConstants.EMAIL_RECIPIENTS_PARAM_NAME, jsonElement)) { + final String emailRecipients = this.fromApiJsonHelper.extractStringNamed(ScheduledEmailConstants.EMAIL_RECIPIENTS_PARAM_NAME, jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.EMAIL_RECIPIENTS_PARAM_NAME).value(emailRecipients).notBlank(); + } + + if (this.fromApiJsonHelper.parameterExists(ScheduledEmailConstants.EMAIL_SUBJECT_PARAM_NAME, jsonElement)) { + final String emailSubject = this.fromApiJsonHelper.extractStringNamed(ScheduledEmailConstants.EMAIL_SUBJECT_PARAM_NAME, jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.EMAIL_SUBJECT_PARAM_NAME).value(emailSubject).notBlank().notExceedingLengthOf(100); + } + + if (this.fromApiJsonHelper.parameterExists(ScheduledEmailConstants.EMAIL_MESSAGE_PARAM_NAME, jsonElement)) { + final String emailMessage = this.fromApiJsonHelper.extractStringNamed(ScheduledEmailConstants.EMAIL_MESSAGE_PARAM_NAME, jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.EMAIL_MESSAGE_PARAM_NAME).value(emailMessage).notBlank(); + } + + if (this.fromApiJsonHelper.parameterExists(ScheduledEmailConstants.IS_ACTIVE_PARAM_NAME, jsonElement)) { + final Boolean isActive = this.fromApiJsonHelper.extractBooleanNamed(ScheduledEmailConstants.IS_ACTIVE_PARAM_NAME, + jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.IS_ACTIVE_PARAM_NAME).value(isActive).notNull(); + } + + if (this.fromApiJsonHelper.parameterExists(ScheduledEmailConstants.EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME, jsonElement)) { + final Integer emailAttachmentFileFormatId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed( + ScheduledEmailConstants.EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME, jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME). + value(emailAttachmentFileFormatId).notNull(); + + if (emailAttachmentFileFormatId != null) { + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME).value(emailAttachmentFileFormatId). + isOneOfTheseValues(ScheduledEmailAttachmentFileFormat.validValues()); + } + } + + if (this.fromApiJsonHelper.parameterExists(ScheduledEmailConstants.START_DATE_TIME_PARAM_NAME, jsonElement)) { + final String dateFormat = jsonCommand.dateFormat(); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.DATE_FORMAT_PARAM_NAME).value(dateFormat).notBlank(); + + final String startDateTime = this.fromApiJsonHelper.extractStringNamed(ScheduledEmailConstants.START_DATE_TIME_PARAM_NAME, + jsonElement); + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.START_DATE_TIME_PARAM_NAME).value(startDateTime).notBlank(); + + if (StringUtils.isNotEmpty(dateFormat)) { + + try { + final DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(dateFormat).withLocale(jsonCommand.extractLocale()); + + // try to parse the date time string + LocalDateTime.parse(startDateTime, dateTimeFormatter); + } + + catch(IllegalArgumentException ex) { + dataValidatorBuilder.reset().parameter(ScheduledEmailConstants.DATE_FORMAT_PARAM_NAME).value(dateFormat).failWithCode("invalid.date.format"); + } + } + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + /** + * check if string is a valid email address + * + * @param email -- string to be validated + * @return true if string is a valid email address + **/ + public boolean isValidEmail(String email) { + // this is the easiest check + if (email == null) { + return false; + } + + // this is another easy check + if (email.endsWith(".")) { + return false; + } + + // Check the whole email address structure + Matcher emailMatcher = EMAIL_PATTERN.matcher(email); + + // check if the Matcher matches the email pattern + if (!emailMatcher.matches()) { + return false; + } + + return true; + } + + /** + * Validate the email recipients string + * + * @param emailRecipients -- the email recipients string to be validated + * @return a hashset containing valid email addresses + **/ + public Set validateEmailRecipients(String emailRecipients) { + Set emailRecipientsSet = new HashSet<>(); + + if (emailRecipients != null) { + String[] split = emailRecipients.split(","); + + for (String emailAddress : split) { + emailAddress = emailAddress.trim(); + + if (this.isValidEmail(emailAddress)) { + emailRecipientsSet.add(emailAddress); + } + } + } + + return emailRecipientsSet; + } + + /** + * validate the stretchy report param json string + * + * @param stretchyReportParamMap -- json string to be validated + * @return if string is valid or empty, a HashMap object, else null + **/ + public HashMap validateStretchyReportParamMap(String stretchyReportParamMap) { + HashMap stretchyReportParamHashMap = new HashMap<>(); + + if (!StringUtils.isEmpty(stretchyReportParamMap)) { + try { + stretchyReportParamHashMap = new ObjectMapper().readValue(stretchyReportParamMap, new TypeReference>(){}); + } + + catch(Exception e) { + stretchyReportParamHashMap = null; + } + } + + return stretchyReportParamHashMap; + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailMessageWithAttachmentData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailMessageWithAttachmentData.java new file mode 100644 index 000000000..935d339d5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailMessageWithAttachmentData.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.campaigns.email.data; + +import java.io.File; +import java.util.List; +import java.util.Set; + +public class EmailMessageWithAttachmentData { + + private final String to; + private final String text; + private final String subject; + private final List attachments; + + private EmailMessageWithAttachmentData(final String to, final String text, final String subject, final List attachments) { + this.to = to; + this.text = text; + this.subject = subject; + this.attachments = attachments; + } + + + public static EmailMessageWithAttachmentData createNew (final String to, final String text, final String subject, final List attachments){ + return new EmailMessageWithAttachmentData(to,text,subject,attachments); + } + + public String getTo() {return this.to;} + + public String getText() {return this.text;} + + public String getSubject() {return this.subject;} + + public List getAttachments() { + return this.attachments; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/PreviewCampaignMessage.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/PreviewCampaignMessage.java new file mode 100644 index 000000000..06ff76455 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/PreviewCampaignMessage.java @@ -0,0 +1,40 @@ +/** + * 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.campaigns.email.data; + +public class PreviewCampaignMessage { + + @SuppressWarnings("unused") + private final String campaignMessage; + + private final Integer totalNumberOfMessages; + + public PreviewCampaignMessage(String campaignMessage, Integer totalNumberOfMessages) { + this.campaignMessage = campaignMessage; + this.totalNumberOfMessages = totalNumberOfMessages; + } + + public String getCampaignMessage() { + return campaignMessage; + } + + public Integer getTotalNumberOfMessages() { + return totalNumberOfMessages; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/ScheduledEmailEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/ScheduledEmailEnumerations.java new file mode 100644 index 000000000..d2843a3e3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/ScheduledEmailEnumerations.java @@ -0,0 +1,56 @@ +/** + * 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.campaigns.email.data; + +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.campaigns.email.domain.ScheduledEmailAttachmentFileFormat; +import org.apache.fineract.infrastructure.campaigns.email.domain.ScheduledEmailStretchyReportParamDateOption; + +public class ScheduledEmailEnumerations { + public static EnumOptionData emailAttachementFileFormat(final Integer emailAttachementFileFormatId) { + return emailAttachementFileFormat(ScheduledEmailAttachmentFileFormat.instance(emailAttachementFileFormatId)); + } + + public static EnumOptionData emailAttachementFileFormat(final String emailAttachementFileFormatString) { + return emailAttachementFileFormat(ScheduledEmailAttachmentFileFormat.instance(emailAttachementFileFormatString)); + } + + public static EnumOptionData emailAttachementFileFormat(final ScheduledEmailAttachmentFileFormat emailAttachementFileFormat) { + EnumOptionData enumOptionData = null; + + if (emailAttachementFileFormat != null) { + enumOptionData = new EnumOptionData(emailAttachementFileFormat.getId().longValue(), emailAttachementFileFormat.getCode(), + emailAttachementFileFormat.getValue()); + } + + return enumOptionData; + } + + public static EnumOptionData stretchyReportDateOption(final ScheduledEmailStretchyReportParamDateOption + reportMailingJobStretchyReportParamDateOption) { + EnumOptionData enumOptionData = null; + + if (reportMailingJobStretchyReportParamDateOption != null) { + enumOptionData = new EnumOptionData(reportMailingJobStretchyReportParamDateOption.getId().longValue(), + reportMailingJobStretchyReportParamDateOption.getCode(), reportMailingJobStretchyReportParamDateOption.getValue()); + } + + return enumOptionData; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaign.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaign.java new file mode 100644 index 000000000..ef87c76a8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaign.java @@ -0,0 +1,538 @@ +/** + * 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.campaigns.email.domain; + +import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.dataqueries.domain.Report; +import org.apache.fineract.infrastructure.campaigns.email.ScheduledEmailConstants; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignValidator; +import org.apache.fineract.portfolio.client.api.ClientApiConstants; +import org.apache.fineract.useradministration.domain.AppUser; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +import javax.persistence.*; +import java.security.InvalidParameterException; +import java.util.*; + +@Entity +@Table(name = "scheduled_email_campaign") +public class EmailCampaign extends AbstractPersistableCustom { + + @Column(name = "campaign_name", nullable = false) + private String campaignName; + + @Column(name = "campaign_type", nullable = false) + private Integer campaignType; + + @ManyToOne + @JoinColumn(name = "businessRule_id", nullable = false) + private Report businessRuleId ; + + @Column(name = "param_value") + private String paramValue; + + @Column(name = "status_enum", nullable = false) + private Integer status; + + @Column(name = "email_subject", nullable = false) + private String emailSubject; + + @Column(name = "email_message", nullable = false) + private String emailMessage; + + @Column(name = "email_attachment_file_format", nullable = false) + private String emailAttachmentFileFormat; + + @ManyToOne + @JoinColumn(name = "stretchy_report_id", nullable = false) + private Report stretchyReport; + + @Column(name = "stretchy_report_param_map", nullable = true) + private String stretchyReportParamMap; + + @Column(name = "closedon_date", nullable = true) + @Temporal(TemporalType.DATE) + private Date closureDate; + + @ManyToOne(optional = true) + @JoinColumn(name = "closedon_userid", nullable = true) + private AppUser closedBy; + + @Column(name = "submittedon_date", nullable = true) + @Temporal(TemporalType.DATE) + private Date submittedOnDate; + + @ManyToOne(optional = true) + @JoinColumn(name = "submittedon_userid", nullable = true) + private AppUser submittedBy; + + @Column(name = "approvedon_date", nullable = true) + @Temporal(TemporalType.DATE) + private Date approvedOnDate; + + @ManyToOne(optional = true) + @JoinColumn(name = "approvedon_userid", nullable = true) + private AppUser approvedBy; + + @Column(name = "recurrence", nullable = false) + private String recurrence; + + @Column(name = "next_trigger_date", nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date nextTriggerDate; + + @Column(name = "last_trigger_date", nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date lastTriggerDate; + + @Column(name = "recurrence_start_date", nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date recurrenceStartDate; + + @Column(name="is_visible",nullable = true) + private boolean isVisible; + + @Column(name = "previous_run_status", nullable = true) + private String previousRunStatus; + + @Column(name = "previous_run_error_log", nullable = true) + private String previousRunErrorLog; + + @Column(name = "previous_run_error_message", nullable = true) + private String previousRunErrorMessage; + + public EmailCampaign() { + } + + private EmailCampaign(final String campaignName, final Integer campaignType, final Report businessRuleId, final String paramValue, + final String emailSubject, final String emailMessage,final LocalDate submittedOnDate, final AppUser submittedBy, + final Report stretchyReport, final String stretchyReportParamMap, final ScheduledEmailAttachmentFileFormat emailAttachmentFileFormat, + final String recurrence, final LocalDateTime localDateTime) { + this.campaignName = campaignName; + this.campaignType = EmailCampaignType.fromInt(campaignType).getValue(); + this.businessRuleId = businessRuleId; + this.paramValue = paramValue; + this.status = EmailCampaignStatus.PENDING.getValue(); + this.emailSubject = emailSubject; + this.emailMessage = emailMessage; + this.emailAttachmentFileFormat = emailAttachmentFileFormat.getValue(); + this.stretchyReport = stretchyReport; + this.stretchyReportParamMap = stretchyReportParamMap; + this.submittedOnDate = submittedOnDate.toDate(); + this.submittedBy = submittedBy; + this.recurrence = recurrence; + LocalDateTime recurrenceStartDate = new LocalDateTime(); + this.isVisible = true; + if(localDateTime != null){ + this.recurrenceStartDate = localDateTime.toDate(); + }else{ + this.recurrenceStartDate = recurrenceStartDate.toDate(); + } + + } + + public static EmailCampaign instance(final AppUser submittedBy, final Report businessRuleId, final Report stretchyReport, final JsonCommand command){ + + final String campaignName = command.stringValueOfParameterNamed(EmailCampaignValidator.campaignName); + final Long campaignType = command.longValueOfParameterNamed(EmailCampaignValidator.campaignType); + + final String paramValue = command.stringValueOfParameterNamed(EmailCampaignValidator.paramValue); + final String emailSubject = command.stringValueOfParameterNamed(EmailCampaignValidator.emailSubject); + final String emailMessage = command.stringValueOfParameterNamed(EmailCampaignValidator.emailMessage); + final String stretchyReportParamMap = command.stringValueOfParameterNamed(ScheduledEmailConstants.STRETCHY_REPORT_PARAM_MAP_PARAM_NAME); + final Integer emailAttachmentFileFormatId = command.integerValueOfParameterNamed(ScheduledEmailConstants.EMAIL_ATTACHMENT_FILE_FORMAT_ID_PARAM_NAME); + final ScheduledEmailAttachmentFileFormat emailAttachmentFileFormat; + if (emailAttachmentFileFormatId!=null) { + emailAttachmentFileFormat = ScheduledEmailAttachmentFileFormat.instance(emailAttachmentFileFormatId); + }else{emailAttachmentFileFormat = ScheduledEmailAttachmentFileFormat.instance(2);} + LocalDate submittedOnDate = new LocalDate(); + if (command.hasParameter(EmailCampaignValidator.submittedOnDateParamName)) { + submittedOnDate = command.localDateValueOfParameterNamed(EmailCampaignValidator.submittedOnDateParamName); + } + + final String recurrence = command.stringValueOfParameterNamed(EmailCampaignValidator.recurrenceParamName); + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); + + LocalDateTime recurrenceStartDate = new LocalDateTime(); + if(EmailCampaignType.fromInt(campaignType.intValue()).isSchedule()) { + if(command.hasParameter(EmailCampaignValidator.recurrenceStartDate)){ + recurrenceStartDate = LocalDateTime.parse(command.stringValueOfParameterNamed(EmailCampaignValidator.recurrenceStartDate),fmt); + } + } else{recurrenceStartDate = null;} + + + return new EmailCampaign(campaignName,campaignType.intValue(),businessRuleId,paramValue,emailSubject,emailMessage, + submittedOnDate,submittedBy,stretchyReport,stretchyReportParamMap,emailAttachmentFileFormat,recurrence,recurrenceStartDate); + } + + public Map update(JsonCommand command){ + + final Map actualChanges = new LinkedHashMap<>(5); + + if (command.isChangeInStringParameterNamed(EmailCampaignValidator.campaignName, this.campaignName)) { + final String newValue = command.stringValueOfParameterNamed(EmailCampaignValidator.campaignName); + actualChanges.put(EmailCampaignValidator.campaignName, newValue); + this.campaignName = StringUtils.defaultIfEmpty(newValue, null); + } + if (command.isChangeInStringParameterNamed(EmailCampaignValidator.emailMessage, this.emailMessage)) { + final String newValue = command.stringValueOfParameterNamed(EmailCampaignValidator.emailMessage); + actualChanges.put(EmailCampaignValidator.emailMessage, newValue); + this.emailMessage = StringUtils.defaultIfEmpty(newValue, null); + } + if (command.isChangeInStringParameterNamed(EmailCampaignValidator.paramValue, this.paramValue)) { + final String newValue = command.stringValueOfParameterNamed(EmailCampaignValidator.paramValue); + actualChanges.put(EmailCampaignValidator.paramValue, newValue); + this.paramValue = StringUtils.defaultIfEmpty(newValue, null); + } + if (command.isChangeInIntegerParameterNamed(EmailCampaignValidator.campaignType, this.campaignType)) { + final Integer newValue = command.integerValueOfParameterNamed(EmailCampaignValidator.campaignType); + actualChanges.put(EmailCampaignValidator.campaignType, EmailCampaignType.fromInt(newValue)); + this.campaignType = EmailCampaignType.fromInt(newValue).getValue(); + } + if (command.isChangeInLongParameterNamed(EmailCampaignValidator.businessRuleId, (this.businessRuleId != null) ? this.businessRuleId.getId() : null)) { + final String newValue = command.stringValueOfParameterNamed(EmailCampaignValidator.businessRuleId); + actualChanges.put(EmailCampaignValidator.businessRuleId, newValue); + } + if (command.isChangeInStringParameterNamed(EmailCampaignValidator.recurrenceParamName, this.recurrence)) { + final String newValue = command.stringValueOfParameterNamed(EmailCampaignValidator.recurrenceParamName); + actualChanges.put(EmailCampaignValidator.recurrenceParamName, newValue); + this.recurrence = StringUtils.defaultIfEmpty(newValue, null); + } + final String dateFormatAsInput = command.dateFormat(); + final String localeAsInput = command.locale(); + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); + + if (command.isChangeInLocalDateParameterNamed(EmailCampaignValidator.recurrenceStartDate, getRecurrenceStartDate())) { + final String valueAsInput = command.stringValueOfParameterNamed(EmailCampaignValidator.recurrenceStartDate); + actualChanges.put(EmailCampaignValidator.recurrenceStartDate, valueAsInput); + actualChanges.put(ClientApiConstants.dateFormatParamName, dateFormatAsInput); + actualChanges.put(ClientApiConstants.localeParamName, localeAsInput); + + final LocalDateTime newValue = LocalDateTime.parse(valueAsInput, fmt); + + this.recurrenceStartDate = newValue.toDate(); + } + + return actualChanges; + } + + + public void activate(final AppUser currentUser, final DateTimeFormatter formatter, final LocalDate activationLocalDate){ + + if(isActive()){ + //handle errors if already activated + final String defaultUserMessage = "Cannot activate campaign. Campaign is already active."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.already.active", defaultUserMessage, + EmailCampaignValidator.activationDateParamName, activationLocalDate.toString(formatter)); + + final List dataValidationErrors = new ArrayList(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + this.approvedOnDate = activationLocalDate.toDate(); + this.approvedBy = currentUser; + this.status = EmailCampaignStatus.ACTIVE.getValue(); + + validate(); + } + + public void close(final AppUser currentUser,final DateTimeFormatter dateTimeFormatter, final LocalDate closureLocalDate){ + if(isClosed()){ + //handle errors if already activated + final String defaultUserMessage = "Cannot close campaign. Campaign already in closed state."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.already.closed", defaultUserMessage, + EmailCampaignValidator.statusParamName, EmailCampaignStatus.fromInt(this.status).getCode()); + + final List dataValidationErrors = new ArrayList(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + if(this.campaignType.intValue() == EmailCampaignType.SCHEDULE.getValue()){ + this.nextTriggerDate = null; + this.lastTriggerDate = null; + } + this.closedBy = currentUser; + this.closureDate = closureLocalDate.toDate(); + this.status = EmailCampaignStatus.CLOSED.getValue(); + validateClosureDate(); + } + + public void reactivate(final AppUser currentUser, final DateTimeFormatter dateTimeFormat,final LocalDate reactivateLocalDate){ + + if(!isClosed()){ + //handle errors if already activated + final String defaultUserMessage = "Cannot reactivate campaign. Campaign must be in closed state."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.must.be.closed", defaultUserMessage, + EmailCampaignValidator.statusParamName, EmailCampaignStatus.fromInt(this.status).getCode()); + + final List dataValidationErrors = new ArrayList(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + this.approvedOnDate = reactivateLocalDate.toDate(); + this.status = EmailCampaignStatus.ACTIVE.getValue(); + this.approvedBy =currentUser; + this.closureDate = null; + this.isVisible = true; + this.closedBy = null; + + validateReactivate(); + } + + public void delete(){ + if(!isClosed()){ + //handle errors if already activated + final String defaultUserMessage = "Cannot delete campaign. Campaign must be in closed state."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.must.be.closed", defaultUserMessage, + EmailCampaignValidator.statusParamName, EmailCampaignStatus.fromInt(this.status).getCode()); + + final List dataValidationErrors = new ArrayList(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + this.isVisible = false; + } + + + + + public boolean isActive(){ + return EmailCampaignStatus.fromInt(this.status).isActive(); + } + + public boolean isPending(){ + return EmailCampaignStatus.fromInt(this.status).isPending(); + } + + public boolean isClosed(){ + return EmailCampaignStatus.fromInt(this.status).isClosed(); + } + + public boolean isDirect(){ + return EmailCampaignType.fromInt(this.campaignType).isDirect(); + } + + public boolean isSchedule(){ + return EmailCampaignType.fromInt(this.campaignType).isSchedule(); + } + + private void validate() { + final List dataValidationErrors = new ArrayList(); + validateActivationDate(dataValidationErrors); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + + private void validateReactivate(){ + final List dataValidationErrors = new ArrayList(); + validateReactivationDate(dataValidationErrors); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + + private void validateClosureDate(){ + final List dataValidationErrors = new ArrayList(); + validateClosureDate(dataValidationErrors); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + + private void validateActivationDate(final List dataValidationErrors) { + + if (getSubmittedOnDate() != null && isDateInTheFuture(getSubmittedOnDate())) { + + final String defaultUserMessage = "submitted date cannot be in the future."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.submittedOnDate.in.the.future", + defaultUserMessage, EmailCampaignValidator.submittedOnDateParamName, this.submittedOnDate); + + dataValidationErrors.add(error); + } + + if (getActivationLocalDate() != null && getSubmittedOnDate() != null && getSubmittedOnDate().isAfter(getActivationLocalDate())) { + + final String defaultUserMessage = "submitted date cannot be after the activation date"; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.submittedOnDate.after.activation.date", + defaultUserMessage, EmailCampaignValidator.submittedOnDateParamName, this.submittedOnDate); + + dataValidationErrors.add(error); + } + + if (getActivationLocalDate() != null && isDateInTheFuture(getActivationLocalDate())) { + + final String defaultUserMessage = "Activation date cannot be in the future."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.activationDate.in.the.future", + defaultUserMessage, EmailCampaignValidator.activationDateParamName, getActivationLocalDate()); + + dataValidationErrors.add(error); + } + + } + + private void validateReactivationDate(final List dataValidationErrors){ + if (getActivationLocalDate() != null && isDateInTheFuture(getActivationLocalDate())) { + + final String defaultUserMessage = "Activation date cannot be in the future."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.activationDate.in.the.future", + defaultUserMessage, EmailCampaignValidator.activationDateParamName, getActivationLocalDate()); + + dataValidationErrors.add(error); + } + if (getActivationLocalDate() != null && getSubmittedOnDate() != null && getSubmittedOnDate().isAfter(getActivationLocalDate())) { + + final String defaultUserMessage = "submitted date cannot be after the activation date"; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.submittedOnDate.after.activation.date", + defaultUserMessage, EmailCampaignValidator.submittedOnDateParamName, this.submittedOnDate); + + dataValidationErrors.add(error); + } + if (getSubmittedOnDate() != null && isDateInTheFuture(getSubmittedOnDate())) { + + final String defaultUserMessage = "submitted date cannot be in the future."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.submittedOnDate.in.the.future", + defaultUserMessage, EmailCampaignValidator.submittedOnDateParamName, this.submittedOnDate); + + dataValidationErrors.add(error); + } + + } + + private void validateClosureDate(final List dataValidationErrors){ + if (getClosureDate() != null && isDateInTheFuture(getClosureDate())) { + final String defaultUserMessage = "closure date cannot be in the future."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.campaign.closureDate.in.the.future", + defaultUserMessage, EmailCampaignValidator.closureDateParamName, this.closureDate); + + dataValidationErrors.add(error); + } + } + + public LocalDate getSubmittedOnDate() { + return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate(this.submittedOnDate), null); + + } + public LocalDate getClosureDate() { + return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate(this.closureDate), null); + } + + public LocalDate getActivationLocalDate() { + LocalDate activationLocalDate = null; + if (this.approvedOnDate != null) { + activationLocalDate = LocalDate.fromDateFields(this.approvedOnDate); + } + return activationLocalDate; + } + private boolean isDateInTheFuture(final LocalDate localDate) { + return localDate.isAfter(DateUtils.getLocalDateOfTenant()); + } + + public Report getBusinessRuleId() { + return this.businessRuleId; + } + + public String getCampaignName() { + return this.campaignName; + } + + public String getEmailSubject() { return this.emailSubject; } + + public String getEmailMessage() { + return this.emailMessage; + } + + public String getParamValue() { + return this.paramValue; + } + + public String getRecurrence() { + return this.recurrence; + } + + public LocalDate getRecurrenceStartDate() { + return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate(this.recurrenceStartDate), null); + } + public LocalDateTime getRecurrenceStartDateTime() { + return (LocalDateTime) ObjectUtils.defaultIfNull(new LocalDateTime(this.recurrenceStartDate), null); + } + + + + public void setLastTriggerDate(Date lastTriggerDate) { + this.lastTriggerDate = lastTriggerDate; + } + + public void setNextTriggerDate(Date nextTriggerDate) { + this.nextTriggerDate = nextTriggerDate; + } + + public LocalDateTime getNextTriggerDate() { + return (LocalDateTime) ObjectUtils.defaultIfNull(new LocalDateTime(this.nextTriggerDate), null); + + } + + public Date getNextTriggerDateInDate(){ + return this.nextTriggerDate; + } + + public LocalDate getLastTriggerDate() { + return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate(this.lastTriggerDate), null); + } + + public void updateIsVisible(boolean isVisible) { + this.isVisible = isVisible; + } + + public void updateBusinessRuleId(final Report report){ + this.businessRuleId = report; + } + + public String getEmailAttachmentFileFormat() { + return this.emailAttachmentFileFormat; + } + + public Report getStretchyReport() { + return this.stretchyReport; + } + + public String getStretchyReportParamMap() { + return this.stretchyReportParamMap; + } + + public void setStretchyReportParamMap(String stretchyReportParamMap) { + this.stretchyReportParamMap = stretchyReportParamMap; + } + + public AppUser getApprovedBy() {return this.approvedBy;} + + public AppUser getClosedBy() { + return this.closedBy; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignRepository.java new file mode 100644 index 000000000..480ecca7d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignRepository.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.campaigns.email.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface EmailCampaignRepository extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignStatus.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignStatus.java new file mode 100644 index 000000000..8e396a3fe --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignStatus.java @@ -0,0 +1,71 @@ +/** + * 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.campaigns.email.domain; + +public enum EmailCampaignStatus { + + INVALID(0, "emailCampaignStatus.invalid"), // + PENDING(100, "emailCampaignStatus.pending"), // + ACTIVE(300, "emailCampaignStatus.active"), // + CLOSED(600, "emailCampaignStatus.closed"); + + private final Integer value; + private final String code; + + EmailCampaignStatus(Integer value, String code) { + this.value = value; + this.code = code; + } + + public static EmailCampaignStatus fromInt(final Integer statusValue) { + + EmailCampaignStatus enumeration = EmailCampaignStatus.INVALID; + switch (statusValue) { + case 100: + enumeration = EmailCampaignStatus.PENDING; + break; + case 300: + enumeration = EmailCampaignStatus.ACTIVE; + break; + case 600: + enumeration = EmailCampaignStatus.CLOSED; + break; + } + return enumeration; + } + + public Integer getValue() { + return value; + } + + public String getCode() { + return code; + } + public boolean isActive(){ + return this.value.equals(EmailCampaignStatus.ACTIVE.getValue()); + } + + public boolean isPending(){ + return this.value.equals(EmailCampaignStatus.PENDING.getValue()); + } + + public boolean isClosed(){ + return this.value.equals(EmailCampaignStatus.CLOSED.getValue()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignStatusEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignStatusEnumerations.java new file mode 100644 index 000000000..c7fe791c8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignStatusEnumerations.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.campaigns.email.domain; + +import org.apache.fineract.infrastructure.core.data.EnumOptionData; + +public class EmailCampaignStatusEnumerations { + public static EnumOptionData status(final Integer statusId) { + return status(EmailCampaignStatus.fromInt(statusId)); + } + + public static EnumOptionData status(final EmailCampaignStatus status) { + EnumOptionData optionData = new EnumOptionData(EmailCampaignStatus.INVALID.getValue().longValue(), + EmailCampaignStatus.INVALID.getCode(), "Invalid"); + switch (status) { + case INVALID: + optionData = new EnumOptionData(EmailCampaignStatus.INVALID.getValue().longValue(), + EmailCampaignStatus.INVALID.getCode(), "Invalid"); + break; + case PENDING: + optionData = new EnumOptionData(EmailCampaignStatus.PENDING.getValue().longValue(), + EmailCampaignStatus.PENDING.getCode(), "Pending"); + break; + case ACTIVE: + optionData = new EnumOptionData(EmailCampaignStatus.ACTIVE.getValue().longValue(), EmailCampaignStatus.ACTIVE.getCode(), + "active"); + break; + case CLOSED: + optionData = new EnumOptionData(EmailCampaignStatus.CLOSED.getValue().longValue(), + EmailCampaignStatus.CLOSED.getCode(), "closed"); + break; + + } + + return optionData; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignType.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignType.java new file mode 100644 index 000000000..f443719f8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailCampaignType.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.campaigns.email.domain; + +public enum EmailCampaignType { + DIRECT(1,"emailCampaignStatusType.direct"), + SCHEDULE(2,"emailCampaignStatusType.schedule"); + + private Integer value; + private String code; + + EmailCampaignType(Integer value, String code) { + this.value = value; + this.code = code; + } + + public Integer getValue() { + return value; + } + + public String getCode() { + return code; + } + + public static EmailCampaignType fromInt(final Integer typeValue) { + EmailCampaignType type = null; + switch (typeValue) { + case 1: + type = DIRECT; + break; + case 2: + type = SCHEDULE; + break; + } + return type; + } + + public boolean isDirect(){ + return this.value.equals(EmailCampaignType.DIRECT.getValue()); + } + + public boolean isSchedule(){ + return this.value.equals(EmailCampaignType.SCHEDULE.getValue()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailConfiguration.java new file mode 100644 index 000000000..ac451c7fa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailConfiguration.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.campaigns.email.domain; + +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +@Entity +@Table(name = "scheduled_email_configuration") +public class EmailConfiguration extends AbstractPersistableCustom { + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "value", nullable = false) + private String value; + + /** + * EmailConfiguration constructor + **/ + protected EmailConfiguration() {} + + /** + * EmailConfiguration constructor + **/ + public EmailConfiguration(String name, String value) { + this.name = name; + this.value = value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getName() { + return this.name; + } + + public String getValue() { + return this.value; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailConfigurationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailConfigurationRepository.java new file mode 100644 index 000000000..00ad729b2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailConfigurationRepository.java @@ -0,0 +1,26 @@ +/** + * 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.campaigns.email.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface EmailConfigurationRepository extends JpaRepository, JpaSpecificationExecutor { + EmailConfiguration findByName(String name); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessage.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessage.java new file mode 100644 index 000000000..6107e20b9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessage.java @@ -0,0 +1,164 @@ +/** + * 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.campaigns.email.domain; + +import org.apache.commons.lang3.StringUtils; +import org.joda.time.LocalDate; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.campaigns.email.EmailApiConstants; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +import javax.persistence.*; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +@Entity +@Table(name = "scheduled_email_messages_outbound") +public class EmailMessage extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "group_id", nullable = true) + private Group group; + + @ManyToOne + @JoinColumn(name = "client_id", nullable = true) + private Client client; + + @ManyToOne + @JoinColumn(name = "staff_id", nullable = true) + private Staff staff; + + @ManyToOne + @JoinColumn(name = "email_campaign_id", nullable = true) + private EmailCampaign emailCampaign; + + @Column(name = "status_enum", nullable = false) + private Integer statusType; + + @Column(name = "email_address", nullable = false, length = 50) + private String emailAddress; + + @Column(name = "email_subject", nullable = false, length = 50) + private String emailSubject; + + @Column(name = "message", nullable = false) + private String message; + + @Column(name = "campaign_name", nullable = true) + private String campaignName; + + @Column(name = "submittedon_date", nullable = true) + @Temporal(TemporalType.DATE) + private Date submittedOnDate; + + @Column(name = "error_message") + private String errorMessage; + + + public static EmailMessage pendingEmail(final Group group, final Client client, final Staff staff,final EmailCampaign emailCampaign, final String emailSubject, final String message, + final String emailAddress, final String campaignName) { + return new EmailMessage(group, client, staff,emailCampaign,EmailMessageStatusType.PENDING, emailSubject, message, emailAddress,campaignName); + } + + public static EmailMessage instance(final Group group, final Client client, final Staff staff, final EmailCampaign emailCampaign, final EmailMessageStatusType statusType, + final String emailSubject, final String message, final String sourceAddress, final String emailAddress, final String campaignName) { + return new EmailMessage(group, client, staff,emailCampaign, statusType, emailSubject, message, emailAddress, campaignName); + } + + protected EmailMessage() { + // + } + + private EmailMessage(final Group group, final Client client, final Staff staff, final EmailCampaign emailCampaign, final EmailMessageStatusType statusType, + final String emailSubject, final String message, final String emailAddress, final String campaignName) { + this.group = group; + this.client = client; + this.staff = staff; + this.emailCampaign = emailCampaign; + this.statusType = statusType.getValue(); + this.emailAddress = emailAddress; + this.emailSubject = emailSubject; + this.message = message; + this.campaignName = campaignName; + this.submittedOnDate = LocalDate.now().toDate(); + } + + public Map update(final JsonCommand command) { + + final Map actualChanges = new LinkedHashMap<>(1); + + if (command.isChangeInStringParameterNamed(EmailApiConstants.messageParamName, this.message)) { + final String newValue = command.stringValueOfParameterNamed(EmailApiConstants.messageParamName); + actualChanges.put(EmailApiConstants.messageParamName, newValue); + this.message = StringUtils.defaultIfEmpty(newValue, null); + } + + return actualChanges; + } + + + public Group getGroup() { + return group; + } + + public Client getClient() { + return client; + } + + public Staff getStaff() { + return staff; + } + + public Integer getStatusType() { + return statusType; + } + + + public String getEmailAddress() {return this.emailAddress;} + + public String getEmailSubject() {return emailSubject; } + + public String getMessage() { + return message; + } + + + public void setStatusType(final Integer statusType) { + this.statusType = statusType; + } + + public String getCampaignName() { + return this.campaignName; + } + + public Date getSubmittedOnDate() { + return this.submittedOnDate; + } + + public EmailCampaign getEmailCampaign() {return this.emailCampaign;} + + public void updateErrorMessage(final String errorMessage) {this.errorMessage = errorMessage;} + + public boolean isPending(){ return EmailMessageStatusType.fromInt(this.statusType).isPending();} + public boolean isSent(){ return EmailMessageStatusType.fromInt(this.statusType).isSent();} +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageAssembler.java new file mode 100644 index 000000000..beed4bb33 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageAssembler.java @@ -0,0 +1,92 @@ +/** + * 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.campaigns.email.domain; + +import com.google.gson.JsonElement; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.campaigns.email.EmailApiConstants; +import org.apache.fineract.infrastructure.campaigns.email.exception.EmailNotFoundException; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class EmailMessageAssembler { + + private final EmailMessageRepository emailMessageRepository; + private final GroupRepositoryWrapper groupRepository; + private final ClientRepositoryWrapper clientRepository; + private final StaffRepositoryWrapper staffRepository; + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public EmailMessageAssembler(final EmailMessageRepository emailMessageRepository, final GroupRepositoryWrapper groupRepositoryWrapper, + final ClientRepositoryWrapper clientRepository, final StaffRepositoryWrapper staffRepository, + final FromJsonHelper fromApiJsonHelper) { + this.emailMessageRepository = emailMessageRepository; + this.groupRepository = groupRepositoryWrapper; + this.clientRepository = clientRepository; + this.staffRepository = staffRepository; + this.fromApiJsonHelper = fromApiJsonHelper; + } + + public EmailMessage assembleFromJson(final JsonCommand command) { + + final JsonElement element = command.parsedJson(); + + String emailAddress = null; + + Group group = null; + if (this.fromApiJsonHelper.parameterExists(EmailApiConstants.groupIdParamName, element)) { + final Long groupId = this.fromApiJsonHelper.extractLongNamed(EmailApiConstants.groupIdParamName, element); + group = this.groupRepository.findOneWithNotFoundDetection(groupId); + } + + Client client = null; + if (this.fromApiJsonHelper.parameterExists(EmailApiConstants.clientIdParamName, element)) { + final Long clientId = this.fromApiJsonHelper.extractLongNamed(EmailApiConstants.clientIdParamName, element); + client = this.clientRepository.findOneWithNotFoundDetection(clientId); + emailAddress = client.emailAddress(); + } + + Staff staff = null; + if (this.fromApiJsonHelper.parameterExists(EmailApiConstants.staffIdParamName, element)) { + final Long staffId = this.fromApiJsonHelper.extractLongNamed(EmailApiConstants.staffIdParamName, element); + staff = this.staffRepository.findOneWithNotFoundDetection(staffId); + emailAddress = staff.emailAddress(); + } + + final String message = this.fromApiJsonHelper.extractStringNamed(EmailApiConstants.messageParamName, element); + final String emailSubject = this.fromApiJsonHelper.extractStringNamed(EmailApiConstants.subjectParamName, element); + + return EmailMessage.pendingEmail(group, client, staff,null,emailSubject, message,emailAddress,null); + } + + public EmailMessage assembleFromResourceId(final Long resourceId) { + final EmailMessage email = this.emailMessageRepository.findOne(resourceId); + if (email == null) { throw new EmailNotFoundException(resourceId); } + return email; + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageEnumerations.java new file mode 100644 index 000000000..76d9b6174 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageEnumerations.java @@ -0,0 +1,58 @@ +/** + * 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.campaigns.email.domain; + +import org.apache.fineract.infrastructure.core.data.EnumOptionData; + +public class EmailMessageEnumerations { + + public static EnumOptionData status(final Integer statusId) { + return status(EmailMessageStatusType.fromInt(statusId)); + } + + public static EnumOptionData status(final EmailMessageStatusType status) { + EnumOptionData optionData = new EnumOptionData(EmailMessageStatusType.INVALID.getValue().longValue(), + EmailMessageStatusType.INVALID.getCode(), "Invalid"); + switch (status) { + case INVALID: + optionData = new EnumOptionData(EmailMessageStatusType.INVALID.getValue().longValue(), + EmailMessageStatusType.INVALID.getCode(), "Invalid"); + break; + case PENDING: + optionData = new EnumOptionData(EmailMessageStatusType.PENDING.getValue().longValue(), + EmailMessageStatusType.PENDING.getCode(), "Pending"); + break; + case SENT: + optionData = new EnumOptionData(EmailMessageStatusType.SENT.getValue().longValue(), EmailMessageStatusType.SENT.getCode(), + "Sent"); + break; + case DELIVERED: + optionData = new EnumOptionData(EmailMessageStatusType.DELIVERED.getValue().longValue(), + EmailMessageStatusType.DELIVERED.getCode(), "Delivered"); + break; + case FAILED: + optionData = new EnumOptionData(EmailMessageStatusType.FAILED.getValue().longValue(), EmailMessageStatusType.FAILED.getCode(), + "Failed"); + break; + + } + + return optionData; + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageRepository.java new file mode 100644 index 000000000..b086fc2cd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageRepository.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.campaigns.email.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.List; + +public interface EmailMessageRepository extends JpaRepository, JpaSpecificationExecutor { + // no extra behaviour + List findByStatusType(Integer emailMessageStatus); +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageStatusType.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageStatusType.java new file mode 100644 index 000000000..54dec8b33 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/EmailMessageStatusType.java @@ -0,0 +1,72 @@ +/** + * 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.campaigns.email.domain; + +public enum EmailMessageStatusType { + + INVALID(0, "emailMessageStatusType.invalid"), // + PENDING(100, "emailMessageStatusType.pending"), // + SENT(200, "emailMessageStatusType.sent"), // + DELIVERED(300, "emailMessageStatusType.delivered"), // + FAILED(400, "emailMessageStatusType.failed"); + + private final Integer value; + private final String code; + + public static EmailMessageStatusType fromInt(final Integer statusValue) { + + EmailMessageStatusType enumeration = EmailMessageStatusType.INVALID; + switch (statusValue) { + case 100: + enumeration = EmailMessageStatusType.PENDING; + break; + case 200: + enumeration = EmailMessageStatusType.SENT; + break; + case 300: + enumeration = EmailMessageStatusType.DELIVERED; + break; + case 400: + enumeration = EmailMessageStatusType.FAILED; + break; + } + return enumeration; + } + + EmailMessageStatusType(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public Integer getValue() { + return this.value; + } + + public String getCode() { + return this.code; + } + + public boolean isPending(){ + return this.value.equals(EmailMessageStatusType.PENDING.getValue()); + } + + public boolean isSent(){ + return this.value.equals(EmailMessageStatusType.SENT.getValue()); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/ScheduledEmailAttachmentFileFormat.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/ScheduledEmailAttachmentFileFormat.java new file mode 100644 index 000000000..677555a46 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/ScheduledEmailAttachmentFileFormat.java @@ -0,0 +1,110 @@ +/** + * 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.campaigns.email.domain; + +public enum ScheduledEmailAttachmentFileFormat { + INVALID(0, "EmailAttachmentFileFormat.invalid", "invalid"), + XLS(1, "EmailAttachmentFileFormat.xls", "xls"), + PDF(2, "EmailAttachmentFileFormat.pdf", "pdf"), + CSV(3, "EmailAttachmentFileFormat.csv", "csv"); + + private String code; + private String value; + private Integer id; + + ScheduledEmailAttachmentFileFormat(final Integer id, final String code, final String value) { + this.value = value; + this.code = code; + this.id = id; + } + + public static ScheduledEmailAttachmentFileFormat instance(final String value) { + ScheduledEmailAttachmentFileFormat emailAttachmentFileFormat = INVALID; + + switch (value) { + case "xls": + emailAttachmentFileFormat = XLS; + break; + + case "pdf": + emailAttachmentFileFormat = PDF; + break; + + case "csv": + emailAttachmentFileFormat = CSV; + break; + + default: + break; + } + + return emailAttachmentFileFormat; + } + + public static ScheduledEmailAttachmentFileFormat instance(final Integer id) { + ScheduledEmailAttachmentFileFormat emailAttachmentFileFormat = INVALID; + + switch (id) { + case 1: + emailAttachmentFileFormat = XLS; + break; + + case 2: + emailAttachmentFileFormat = PDF; + break; + + case 3: + emailAttachmentFileFormat = CSV; + break; + + default: + break; + } + + return emailAttachmentFileFormat; + } + + /** + * @return the code + */ + public String getCode() { + return code; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @return the id + */ + public Integer getId() { + return id; + } + + /** + * @return list of valid ScheduledEmailAttachmentFileFormat ids + **/ + public static Object[] validValues() { + return new Object[] { XLS.getId(), PDF.getId(), CSV.getId() }; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/ScheduledEmailStretchyReportParamDateOption.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/ScheduledEmailStretchyReportParamDateOption.java new file mode 100644 index 000000000..39b6e9b7b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/domain/ScheduledEmailStretchyReportParamDateOption.java @@ -0,0 +1,117 @@ +/** + * 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.campaigns.email.domain; + +public enum ScheduledEmailStretchyReportParamDateOption { + INVALID(0, "scheduledEmailStretchyReportParamDateOption.invalid", "invalid"), + TODAY(1, "scheduledEmailStretchyReportParamDateOption.today", "today"), + // YESTERDAY(2, "scheduledEmailStretchyReportParamDateOption.yesterday", "yesterday"), + TOMORROW(3, "scheduledEmailStretchyReportParamDateOption.tomorrow", "tomorrow"); + + private String code; + private String value; + private Integer id; + + /** + * @param id + * @param code + * @param value + */ + private ScheduledEmailStretchyReportParamDateOption(final Integer id, final String code, final String value) { + this.value = value; + this.code = code; + this.id = id; + } + + /** + * @param value + * @return + */ + public static ScheduledEmailStretchyReportParamDateOption instance(final String value) { + ScheduledEmailStretchyReportParamDateOption scheduledEmailStretchyReportParamDateOption = INVALID; + + switch (value) { + case "today": + scheduledEmailStretchyReportParamDateOption = TODAY; + break; + + // case "yesterday": + // scheduledEmailStretchyReportParamDateOption = YESTERDAY; + // break; + + case "tomorrow": + scheduledEmailStretchyReportParamDateOption = TOMORROW; + break; + } + + return scheduledEmailStretchyReportParamDateOption; + } + + /** + * @param id + * @return + */ + public static ScheduledEmailStretchyReportParamDateOption instance(final Integer id) { + ScheduledEmailStretchyReportParamDateOption scheduledEmailStretchyReportParamDateOption = INVALID; + + switch (id) { + case 1: + scheduledEmailStretchyReportParamDateOption = TODAY; + break; + + // case 2: + // scheduledEmailStretchyReportParamDateOption = YESTERDAY; + // break; + + case 3: + scheduledEmailStretchyReportParamDateOption = TOMORROW; + break; + } + + return scheduledEmailStretchyReportParamDateOption; + } + + /** + * @return the code + */ + public String getCode() { + return code; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @return the id + */ + public Integer getId() { + return id; + } + + /** + * @return list of valid ScheduledEmailAttachmentFileFormat values + **/ + public static Object[] validValues() { + return new Object[] { TODAY.value, TOMORROW.value }; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailBusinessRuleNotFound.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailBusinessRuleNotFound.java new file mode 100644 index 000000000..e8e3d3ff2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailBusinessRuleNotFound.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.campaigns.email.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class EmailBusinessRuleNotFound extends AbstractPlatformResourceNotFoundException { + + public EmailBusinessRuleNotFound(final Long resourceId) { + super("error.msg.email.business.rule.not.found", "Email business rule with identifier `" + resourceId + "` does not exist", resourceId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailCampaignMustBeClosedToBeDeletedException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailCampaignMustBeClosedToBeDeletedException.java new file mode 100644 index 000000000..4aef1bd7c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailCampaignMustBeClosedToBeDeletedException.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.campaigns.email.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class EmailCampaignMustBeClosedToBeDeletedException extends AbstractPlatformDomainRuleException { + + public EmailCampaignMustBeClosedToBeDeletedException(final Long resourceId) { + super("error.msg.email.campaign.cannot.be.deleted", + "Campaign with identifier " + resourceId + " cannot be deleted as it is not in `Closed` state.", resourceId); } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailCampaignMustBeClosedToEditException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailCampaignMustBeClosedToEditException.java new file mode 100644 index 000000000..364468786 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailCampaignMustBeClosedToEditException.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.campaigns.email.exception; + + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class EmailCampaignMustBeClosedToEditException extends AbstractPlatformDomainRuleException { + + public EmailCampaignMustBeClosedToEditException(final Long resourceId) { + super("error.msg.email.campaign.cannot.be.updated", + "Campaign with identifier " + resourceId + " cannot be updated as it is not in `Closed` state.", resourceId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailCampaignNotFound.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailCampaignNotFound.java new file mode 100644 index 000000000..0307fb1b3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailCampaignNotFound.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.campaigns.email.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class EmailCampaignNotFound extends AbstractPlatformResourceNotFoundException{ + + public EmailCampaignNotFound(final Long resourceId) { + super("error.msg.email.campaign.identifier.not.found", "EMAIL_CAMPAIGN with identifier `" + resourceId + "` does not exist", resourceId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailConfigurationNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailConfigurationNotFoundException.java new file mode 100644 index 000000000..32aa906dd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailConfigurationNotFoundException.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.campaigns.email.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +/** + * A {@link RuntimeException} thrown when a code is not found. + */ +public class EmailConfigurationNotFoundException extends AbstractPlatformResourceNotFoundException { + + public EmailConfigurationNotFoundException(final String name) { + super("error.msg.email.configuration.name.not.found", "Email configuration with name " + name + " does not exist", name); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailConfigurationSMTPUsernameNotValid.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailConfigurationSMTPUsernameNotValid.java new file mode 100644 index 000000000..34aab28e9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailConfigurationSMTPUsernameNotValid.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.campaigns.email.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + + +public class EmailConfigurationSMTPUsernameNotValid extends AbstractPlatformDomainRuleException { + + public EmailConfigurationSMTPUsernameNotValid(final String smtpUsername) { + super("error.msg.email.configuration.smtpusername.not.valid", + "SMTP username configuration with email " + "'"+smtpUsername+"'" + " is not a valid email address "); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailNotFoundException.java new file mode 100644 index 000000000..c3b63ffcc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/exception/EmailNotFoundException.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.campaigns.email.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +/** + * A {@link RuntimeException} thrown when a code is not found. + */ +public class EmailNotFoundException extends AbstractPlatformResourceNotFoundException { + + public EmailNotFoundException(final Long resourceId) { + super("error.msg.email.identifier.not.found", "Email with identifier `" + resourceId + "` does not exist", resourceId); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/ActivateEmailCampaignCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/ActivateEmailCampaignCommandHandler.java new file mode 100644 index 000000000..3215c1c59 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/ActivateEmailCampaignCommandHandler.java @@ -0,0 +1,45 @@ +/** + * 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.campaigns.email.handler; + +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.campaigns.email.service.EmailCampaignWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "EMAIL_CAMPAIGN", action = "ACTIVATE") +public class ActivateEmailCampaignCommandHandler implements NewCommandSourceHandler { + private EmailCampaignWritePlatformService emailCampaignWritePlatformService; + + @Autowired + public ActivateEmailCampaignCommandHandler(final EmailCampaignWritePlatformService emailCampaignWritePlatformService) { + this.emailCampaignWritePlatformService = emailCampaignWritePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return this.emailCampaignWritePlatformService.activateEmailCampaign(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/CloseEmailCampaignCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/CloseEmailCampaignCommandHandler.java new file mode 100644 index 000000000..6f021587c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/CloseEmailCampaignCommandHandler.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.campaigns.email.handler; + +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.campaigns.email.service.EmailCampaignWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +@CommandType(entity = "EMAIL_CAMPAIGN", action = "CLOSE") +public class CloseEmailCampaignCommandHandler implements NewCommandSourceHandler { + private final EmailCampaignWritePlatformService emailCampaignWritePlatformService; + + @Autowired + public CloseEmailCampaignCommandHandler(final EmailCampaignWritePlatformService emailCampaignWritePlatformService) { + this.emailCampaignWritePlatformService = emailCampaignWritePlatformService; + } + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return this.emailCampaignWritePlatformService.closeEmailCampaign(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/CreateEmailCampaignCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/CreateEmailCampaignCommandHandler.java new file mode 100644 index 000000000..10fce846c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/CreateEmailCampaignCommandHandler.java @@ -0,0 +1,46 @@ +/** + * 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.campaigns.email.handler; + +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.campaigns.email.service.EmailCampaignWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "EMAIL_CAMPAIGN", action = "CREATE") +public class CreateEmailCampaignCommandHandler implements NewCommandSourceHandler { + + private EmailCampaignWritePlatformService emailCampaignWritePlatformService; + + @Autowired + public CreateEmailCampaignCommandHandler(final EmailCampaignWritePlatformService emailCampaignWritePlatformService) { + this.emailCampaignWritePlatformService = emailCampaignWritePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return this.emailCampaignWritePlatformService.create(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/CreateEmailCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/CreateEmailCommandHandler.java new file mode 100644 index 000000000..8a65aae37 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/CreateEmailCommandHandler.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.campaigns.email.handler; + +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.campaigns.email.service.EmailWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "EMAIL", action = "CREATE") +public class CreateEmailCommandHandler implements NewCommandSourceHandler { + + private final EmailWritePlatformService writePlatformService; + + @Autowired + public CreateEmailCommandHandler(final EmailWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.create(command); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/DeleteEmailCampaignCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/DeleteEmailCampaignCommandHandler.java new file mode 100644 index 000000000..febd4219c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/DeleteEmailCampaignCommandHandler.java @@ -0,0 +1,45 @@ +/** + * 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.campaigns.email.handler; + +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.campaigns.email.service.EmailCampaignWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "EMAIL_CAMPAIGN", action = "DELETE") +public class DeleteEmailCampaignCommandHandler implements NewCommandSourceHandler { + + private final EmailCampaignWritePlatformService emailCampaignWritePlatformService; + + @Autowired + public DeleteEmailCampaignCommandHandler(EmailCampaignWritePlatformService emailCampaignWritePlatformService) { + this.emailCampaignWritePlatformService = emailCampaignWritePlatformService; + } + @Transactional + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return this.emailCampaignWritePlatformService.delete(command.entityId()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/DeleteEmailCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/DeleteEmailCommandHandler.java new file mode 100644 index 000000000..8608f190d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/DeleteEmailCommandHandler.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.campaigns.email.handler; + +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.campaigns.email.service.EmailWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "EMAIL", action = "DELETE") +public class DeleteEmailCommandHandler implements NewCommandSourceHandler { + + private final EmailWritePlatformService writePlatformService; + + @Autowired + public DeleteEmailCommandHandler(final EmailWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.delete(command.entityId()); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/ReactivateEmailCampaignCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/ReactivateEmailCampaignCommandHandler.java new file mode 100644 index 000000000..b8ea3e086 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/ReactivateEmailCampaignCommandHandler.java @@ -0,0 +1,45 @@ +/** + * 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.campaigns.email.handler; + +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.campaigns.email.service.EmailCampaignWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "EMAIL_CAMPAIGN", action = "REACTIVATE") +public class ReactivateEmailCampaignCommandHandler implements NewCommandSourceHandler { + + private final EmailCampaignWritePlatformService emailCampaignWritePlatformService; + + @Autowired + public ReactivateEmailCampaignCommandHandler(EmailCampaignWritePlatformService emailCampaignWritePlatformService) { + this.emailCampaignWritePlatformService = emailCampaignWritePlatformService; + } + @Transactional + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return this.emailCampaignWritePlatformService.reactivateEmailCampaign(command.entityId(),command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/UpdateEmailCampaignCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/UpdateEmailCampaignCommandHandler.java new file mode 100644 index 000000000..f02d662a4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/UpdateEmailCampaignCommandHandler.java @@ -0,0 +1,44 @@ +/** + * 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.campaigns.email.handler; + +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.campaigns.email.service.EmailCampaignWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "EMAIL_CAMPAIGN", action = "UPDATE") +public class UpdateEmailCampaignCommandHandler implements NewCommandSourceHandler { + + private final EmailCampaignWritePlatformService emailCampaignWritePlatformService; + @Autowired + public UpdateEmailCampaignCommandHandler(EmailCampaignWritePlatformService emailCampaignWritePlatformService) { + this.emailCampaignWritePlatformService = emailCampaignWritePlatformService; + } + @Transactional + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return this.emailCampaignWritePlatformService.update(command.entityId(),command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/UpdateEmailConfigurationCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/UpdateEmailConfigurationCommandHandler.java new file mode 100644 index 000000000..b468d4c2e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/handler/UpdateEmailConfigurationCommandHandler.java @@ -0,0 +1,48 @@ +/** + * 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.campaigns.email.handler; + +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.campaigns.email.service.EmailConfigurationWritePlatformService; +import org.apache.fineract.infrastructure.campaigns.email.service.EmailWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "EMAIL_CONFIGURATION", action = "UPDATE") +public class UpdateEmailConfigurationCommandHandler implements NewCommandSourceHandler { + + private final EmailConfigurationWritePlatformService writePlatformService; + + @Autowired + public UpdateEmailConfigurationCommandHandler(final EmailConfigurationWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.update(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignReadPlatformService.java new file mode 100644 index 000000000..a8fde906d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignReadPlatformService.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.campaigns.email.service; + +import org.apache.fineract.infrastructure.campaigns.email.data.EmailBusinessRulesData; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignData; + +import java.util.Collection; + +public interface EmailCampaignReadPlatformService { + + Collection retrieveAll(); + + EmailBusinessRulesData retrieveOneTemplate(Long resourceId); + + EmailCampaignData retrieveOne(Long resourceId); + + Collection retrieveAllCampaign(); + + Collection retrieveAllScheduleActiveCampaign(); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignReadPlatformServiceImpl.java new file mode 100644 index 000000000..f791d9260 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignReadPlatformServiceImpl.java @@ -0,0 +1,285 @@ +/** + * 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.campaigns.email.service; + +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.RoutingDataSource; +import org.apache.fineract.infrastructure.dataqueries.data.ReportData; +import org.apache.fineract.infrastructure.campaigns.email.data.ScheduledEmailEnumerations; +import org.apache.fineract.infrastructure.campaigns.email.exception.EmailBusinessRuleNotFound; +import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignNotFound; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailBusinessRulesData; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignData; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignTimeLine; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaignStatus; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaignStatusEnumerations; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaignType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +@Service +public class EmailCampaignReadPlatformServiceImpl implements EmailCampaignReadPlatformService { + + + private final JdbcTemplate jdbcTemplate; + + private final BusinessRuleMapper businessRuleMapper; + + private final EmailCampaignMapper emailCampaignMapper; + + @Autowired + public EmailCampaignReadPlatformServiceImpl(final RoutingDataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.businessRuleMapper = new BusinessRuleMapper(); + this.emailCampaignMapper = new EmailCampaignMapper(); + } + + + private static final class EmailCampaignMapper implements RowMapper{ + + final String schema; + + private EmailCampaignMapper() { + final StringBuilder sql = new StringBuilder(400); + sql.append("ec.id as id, "); + sql.append("ec.campaign_name as campaignName, "); + sql.append("ec.campaign_type as campaignType, "); + sql.append("ec.businessRule_id as businessRuleId, "); + sql.append("ec.email_subject as emailSubject, "); + sql.append("ec.email_message as emailMessage, "); + sql.append("ec.email_attachment_file_format as emailAttachmentFileFormat, "); + sql.append("sr.id as stretchyReportId, "); + sql.append("sr.report_name as reportName, sr.report_type as reportType, sr.report_subtype as reportSubType, "); + sql.append("sr.report_category as reportCategory, sr.report_sql as reportSql, sr.description as reportDescription, "); + sql.append("sr.core_report as coreReport, sr.use_report as useReport, "); + sql.append("ec.stretchy_report_param_map as stretchyReportParamMap, "); + sql.append("ec.param_value as paramValue, "); + sql.append("ec.status_enum as statusEnum, "); + sql.append("ec.recurrence as recurrence, "); + sql.append("ec.recurrence_start_date as recurrenceStartDate, "); + sql.append("ec.next_trigger_date as nextTriggerDate, "); + sql.append("ec.last_trigger_date as lastTriggerDate, "); + sql.append("ec.submittedon_date as submittedOnDate, "); + sql.append("sbu.username as submittedByUsername, "); + sql.append("ec.closedon_date as closedOnDate, "); + sql.append("clu.username as closedByUsername, "); + sql.append("acu.username as activatedByUsername, "); + sql.append("ec.approvedon_date as activatedOnDate "); + sql.append("from scheduled_email_campaign ec "); + sql.append("left join m_appuser sbu on sbu.id = ec.submittedon_userid "); + sql.append("left join m_appuser acu on acu.id = ec.approvedon_userid "); + sql.append("left join m_appuser clu on clu.id = ec.closedon_userid "); + sql.append("left join stretchy_report sr on ec.stretchy_report_id = sr.id"); + + this.schema = sql.toString(); + } + public String schema() { + return this.schema; + } + + @Override + public EmailCampaignData mapRow(ResultSet rs, int rowNum) throws SQLException { + final Long id = JdbcSupport.getLong(rs, "id"); + final String campaignName = rs.getString("campaignName"); + final Integer campaignType = JdbcSupport.getInteger(rs, "campaignType"); + final Long businessRuleId = JdbcSupport.getLong(rs, "businessRuleId"); + final String paramValue = rs.getString("paramValue"); + final String emailSubject = rs.getString("emailSubject"); + final String emailMessage = rs.getString("emailMessage"); + final String emailAttachmentFileFormatString = rs.getString("emailAttachmentFileFormat"); + final String stretchyReportParamMap = rs.getString("stretchyReportParamMap"); + EnumOptionData emailAttachmentFileFormat = null; + if (emailAttachmentFileFormatString != null) { + emailAttachmentFileFormat = ScheduledEmailEnumerations.emailAttachementFileFormat(emailAttachmentFileFormatString); + } + final Long reportId = JdbcSupport.getLong(rs, "stretchyReportId"); + final String reportName = rs.getString("reportName"); + final String reportType = rs.getString("reportType"); + final String reportSubType = rs.getString("reportSubType"); + final String reportCategory = rs.getString("reportCategory"); + final String reportSql = rs.getString("reportSql"); + final String reportDescription = rs.getString("reportDescription"); + final boolean coreReport = rs.getBoolean("coreReport"); + final boolean useReport = rs.getBoolean("useReport"); + + final ReportData stretchyReport = new ReportData(reportId, reportName, reportType, reportSubType, reportCategory, + reportDescription, reportSql, coreReport, useReport, null); + + final Integer statusId = JdbcSupport.getInteger(rs, "statusEnum"); + final EnumOptionData status = EmailCampaignStatusEnumerations.status(statusId); + final DateTime nextTriggerDate = JdbcSupport.getDateTime(rs, "nextTriggerDate"); + final LocalDate lastTriggerDate = JdbcSupport.getLocalDate(rs, "lastTriggerDate"); + + + final LocalDate closedOnDate = JdbcSupport.getLocalDate(rs, "closedOnDate"); + final String closedByUsername = rs.getString("closedByUsername"); + + + final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate"); + final String submittedByUsername = rs.getString("submittedByUsername"); + + final LocalDate activatedOnDate = JdbcSupport.getLocalDate(rs, "activatedOnDate"); + final String activatedByUsername = rs.getString("activatedByUsername"); + final String recurrence =rs.getString("recurrence"); + final DateTime recurrenceStartDate = JdbcSupport.getDateTime(rs, "recurrenceStartDate"); + final EmailCampaignTimeLine emailCampaignTimeLine = new EmailCampaignTimeLine(submittedOnDate,submittedByUsername, + activatedOnDate,activatedByUsername,closedOnDate,closedByUsername); + + + + return EmailCampaignData.instance(id,campaignName,campaignType,businessRuleId,paramValue,status,emailSubject,emailMessage, + emailAttachmentFileFormatString,reportId,stretchyReportParamMap,nextTriggerDate,lastTriggerDate,emailCampaignTimeLine, + recurrenceStartDate,recurrence); + } + } + + + private static final class BusinessRuleMapper implements ResultSetExtractor>{ + + final String schema; + + private BusinessRuleMapper() { + final StringBuilder sql = new StringBuilder(300); + sql.append("sr.id as id, "); + sql.append("sr.report_name as reportName, "); + sql.append("sr.report_type as reportType, "); + sql.append("sr.report_subtype as reportSubType, "); + sql.append("sr.description as description, "); + sql.append("sp.parameter_variable as params, "); + sql.append("sp.parameter_FormatType as paramType, "); + sql.append("sp.parameter_label as paramLabel, "); + sql.append("sp.parameter_name as paramName "); + sql.append("from stretchy_report sr "); + sql.append("left join stretchy_report_parameter as srp on srp.report_id = sr.id "); + sql.append("left join stretchy_parameter as sp on sp.id = srp.parameter_id "); + + this.schema = sql.toString(); + } + + public String schema(){ + return this.schema; + } + + @Override + public List extractData(ResultSet rs) throws SQLException, DataAccessException { + List emailBusinessRulesDataList = new ArrayList(); + + EmailBusinessRulesData emailBusinessRulesData = null; + + Map mapOfSameObjects = new HashMap(); + + while(rs.next()){ + final Long id = rs.getLong("id"); + emailBusinessRulesData = mapOfSameObjects.get(id); + if(emailBusinessRulesData == null){ + final String reportName = rs.getString("reportName") ; + final String reportType = rs.getString("reportType"); + final String reportSubType = rs.getString("reportSubType"); + final String paramName = rs.getString("paramName"); + final String paramLabel = rs.getString("paramLabel"); + final String description = rs.getString("description"); + + Map hashMap = new HashMap(); + hashMap.put(paramLabel,paramName); + emailBusinessRulesData = EmailBusinessRulesData.instance(id,reportName,reportType,hashMap,reportSubType,description); + mapOfSameObjects.put(id,emailBusinessRulesData); + //add to the list + emailBusinessRulesDataList.add(emailBusinessRulesData); + } + //add new paramType to the existing object + Map hashMap = new HashMap(); + final String paramName = rs.getString("paramName"); + final String paramLabel = rs.getString("paramLabel"); + hashMap.put(paramLabel,paramName); + + //get existing map and add new items to it + emailBusinessRulesData.getReportParamName().putAll(hashMap); + } + + return emailBusinessRulesDataList; + } + } + + @Override + public Collection retrieveAll() { + final String searchType = "Email"; + final String sql = "select " + this.businessRuleMapper.schema() + " where sr.report_type = ?"; + + return this.jdbcTemplate.query(sql, this.businessRuleMapper, searchType); + } + + @Override + public EmailBusinessRulesData retrieveOneTemplate(Long resourceId) { + final String searchType = "Email"; + + final String sql = "select " + this.businessRuleMapper.schema() + " where sr.report_type = ? and sr.id = ?"; + + List retrieveOne = this.jdbcTemplate.query(sql, this.businessRuleMapper, searchType,resourceId); + try{ + EmailBusinessRulesData emailBusinessRulesData = retrieveOne.get(0); + return emailBusinessRulesData; + } + catch (final IndexOutOfBoundsException e){ + throw new EmailBusinessRuleNotFound(resourceId); + } + + } + + @Override + public EmailCampaignData retrieveOne(Long resourceId) { + final Integer isVisible =1; + try{ + final String sql = "select " + this.emailCampaignMapper.schema + " where ec.id = ? and ec.is_visible = ?"; + return this.jdbcTemplate.queryForObject(sql, this.emailCampaignMapper, resourceId,isVisible); + } catch (final EmptyResultDataAccessException e) { + throw new EmailCampaignNotFound(resourceId); + } + } + + @Override + public Collection retrieveAllCampaign() { + final Integer visible = 1; + final String sql = "select " + this.emailCampaignMapper.schema() + " where ec.is_visible = ?"; + return this.jdbcTemplate.query(sql, this.emailCampaignMapper, visible); + } + + @Override + public Collection retrieveAllScheduleActiveCampaign() { + final Integer scheduleCampaignType = EmailCampaignType.SCHEDULE.getValue(); + final Integer statusEnum = EmailCampaignStatus.ACTIVE.getValue(); + final Integer visible = 1; + final String sql = "select " + this.emailCampaignMapper.schema() + " where ec.status_enum = ? and ec.campaign_type = ? and ec.is_visible = ?"; + return this.jdbcTemplate.query(sql,this.emailCampaignMapper, statusEnum,scheduleCampaignType,visible); + } + + + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignWritePlatformCommandHandlerImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignWritePlatformCommandHandlerImpl.java new file mode 100644 index 000000000..cada88b9a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignWritePlatformCommandHandlerImpl.java @@ -0,0 +1,801 @@ +/** + * 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.campaigns.email.service; + +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; +import com.google.gson.Gson; +import org.apache.commons.lang.StringUtils; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.TypeReference; +import org.joda.time.DateTimeZone; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.api.JsonQuery; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; +import org.apache.fineract.infrastructure.dataqueries.domain.*; +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.documentmanagement.contentrepository.FileSystemContentRepository; +import org.apache.fineract.infrastructure.reportmailingjob.helper.IPv4Helper; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailMessageWithAttachmentData; +import org.apache.fineract.infrastructure.campaigns.email.domain.*; +import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignMustBeClosedToBeDeletedException; +import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignMustBeClosedToEditException; +import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignNotFound; +import org.apache.fineract.infrastructure.jobs.annotation.CronTarget; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.campaigns.email.data.PreviewCampaignMessage; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignData; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignValidator; +import org.apache.fineract.portfolio.calendar.service.CalendarUtils; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import java.io.*; +import java.util.*; + +@Service +public class EmailCampaignWritePlatformCommandHandlerImpl implements EmailCampaignWritePlatformService { + + private final static Logger logger = LoggerFactory.getLogger(EmailCampaignWritePlatformCommandHandlerImpl.class); + + private final PlatformSecurityContext context; + + private final EmailCampaignRepository emailCampaignRepository; + private final EmailCampaignValidator emailCampaignValidator; + private final EmailCampaignReadPlatformService emailCampaignReadPlatformService; + private final ReportRepository reportRepository; + private final EmailMessageRepository emailMessageRepository; + private final ClientRepositoryWrapper clientRepositoryWrapper; + private final ReadReportingService readReportingService; + private final GenericDataService genericDataService; + private final FromJsonHelper fromJsonHelper; + private final LoanRepository loanRepository; + private final SavingsAccountRepository savingsAccountRepository; + private final EmailMessageJobEmailService emailMessageJobEmailService; + + @Autowired + public EmailCampaignWritePlatformCommandHandlerImpl(final PlatformSecurityContext context, + final EmailCampaignRepository emailCampaignRepository, final EmailCampaignValidator emailCampaignValidator, + final EmailCampaignReadPlatformService emailCampaignReadPlatformService, final ReportRepository reportRepository, + final EmailMessageRepository emailMessageRepository, final ClientRepositoryWrapper clientRepositoryWrapper, + final ReadReportingService readReportingService, final GenericDataService genericDataService, + final FromJsonHelper fromJsonHelper, final LoanRepository loanRepository, + final SavingsAccountRepository savingsAccountRepository, final EmailMessageJobEmailService emailMessageJobEmailService) { + this.context = context; + this.emailCampaignRepository = emailCampaignRepository; + this.emailCampaignValidator = emailCampaignValidator; + this.emailCampaignReadPlatformService = emailCampaignReadPlatformService; + this.reportRepository = reportRepository; + this.emailMessageRepository = emailMessageRepository; + this.clientRepositoryWrapper = clientRepositoryWrapper; + this.readReportingService = readReportingService; + this.genericDataService = genericDataService; + this.fromJsonHelper = fromJsonHelper; + this.loanRepository = loanRepository; + this.savingsAccountRepository = savingsAccountRepository; + this.emailMessageJobEmailService = emailMessageJobEmailService; + } + + @Transactional + @Override + public CommandProcessingResult create(JsonCommand command) { + + final AppUser currentUser = this.context.authenticatedUser(); + + this.emailCampaignValidator.validateCreate(command.json()); + + final Long businessRuleId = command.longValueOfParameterNamed(EmailCampaignValidator.businessRuleId); + + final Report businessRule = this.reportRepository.findOne(businessRuleId); + if (businessRule == null) { throw new ReportNotFoundException(businessRuleId); } + + final Long reportId = command.longValueOfParameterNamed(EmailCampaignValidator.stretchyReportId); + + final Report report = this.reportRepository.findOne(reportId); + if (report == null) { throw new ReportNotFoundException(reportId); } + // find all report parameters and store them as json string + final Set reportParameterUsages = report.getReportParameterUsages(); + final Map stretchyReportParams = new HashMap<>(); + + if (reportParameterUsages != null && !reportParameterUsages.isEmpty()) { + for (final ReportParameterUsage reportParameterUsage : reportParameterUsages) { + stretchyReportParams.put(reportParameterUsage.getReportParameterName(), ""); + } + } + + EmailCampaign emailCampaign = EmailCampaign.instance(currentUser, businessRule, report, command); + emailCampaign.setStretchyReportParamMap(new Gson().toJson(stretchyReportParams)); + + this.emailCampaignRepository.save(emailCampaign); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(emailCampaign.getId()) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult update(final Long resourceId, final JsonCommand command) { + try { + this.context.authenticatedUser(); + this.emailCampaignValidator.validateForUpdate(command.json()); + final EmailCampaign emailCampaign = this.emailCampaignRepository.findOne(resourceId); + + if (emailCampaign == null) { throw new EmailCampaignNotFound(resourceId); } + if (emailCampaign.isActive()) { throw new EmailCampaignMustBeClosedToEditException(emailCampaign.getId()); } + final Map changes = emailCampaign.update(command); + + if (changes.containsKey(EmailCampaignValidator.businessRuleId)) { + final Long newValue = command.longValueOfParameterNamed(EmailCampaignValidator.businessRuleId); + final Report reportId = this.reportRepository.findOne(newValue); + if (reportId == null) { throw new ReportNotFoundException(newValue); } + emailCampaign.updateBusinessRuleId(reportId); + + } + + if (!changes.isEmpty()) { + this.emailCampaignRepository.saveAndFlush(emailCampaign); + } + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(resourceId) // + .with(changes) // + .build(); + } catch (final DataIntegrityViolationException dve) { + handleDataIntegrityIssues(command, dve); + return CommandProcessingResult.empty(); + } + + } + + @Transactional + @Override + public CommandProcessingResult delete(final Long resourceId) { + this.context.authenticatedUser(); + final EmailCampaign emailCampaign = this.emailCampaignRepository.findOne(resourceId); + + if (emailCampaign == null) { throw new EmailCampaignNotFound(resourceId); } + if (emailCampaign.isActive()) { throw new EmailCampaignMustBeClosedToBeDeletedException(emailCampaign.getId()); } + + /* + * Do not delete but set a boolean is_visible to zero + */ + emailCampaign.delete(); + this.emailCampaignRepository.saveAndFlush(emailCampaign); + + return new CommandProcessingResultBuilder() // + .withEntityId(emailCampaign.getId()) // + .build(); + + } + + private void insertDirectCampaignIntoEmailOutboundTable(final String emailParams, final String emailSubject, + final String messageTemplate, final String campaignName, final Long campaignId) { + try { + HashMap campaignParams = new ObjectMapper().readValue(emailParams, + new TypeReference>() {}); + + HashMap queryParamForRunReport = new ObjectMapper().readValue(emailParams, + new TypeReference>() {}); + + List> runReportObject = this.getRunReportByServiceImpl(campaignParams.get("reportName"), + queryParamForRunReport); + + if (runReportObject != null) { + for (HashMap entry : runReportObject) { + String message = this.compileEmailTemplate(messageTemplate, campaignName, entry); + Integer clientId = (Integer) entry.get("id"); + EmailCampaign emailCampaign = this.emailCampaignRepository.findOne(campaignId); + Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId.longValue()); + String emailAddress = client.emailAddress(); + + if (emailAddress != null && isValidEmail(emailAddress)) { + EmailMessage emailMessage = EmailMessage.pendingEmail(null, client, null, emailCampaign, emailSubject, message, + emailAddress, campaignName); + this.emailMessageRepository.save(emailMessage); + } + } + } + } catch (final IOException e) { + // TODO throw something here + } + + } + + public static boolean isValidEmail(String email) { + + boolean isValid = true; + + try { + + InternetAddress emailO = new InternetAddress(email); + emailO.validate(); + + } catch (AddressException ex) { + + isValid = false; + } + return isValid; + } + + @Override + @CronTarget(jobName = JobName.UPDATE_EMAIL_OUTBOUND_WITH_CAMPAIGN_MESSAGE) + public void storeTemplateMessageIntoEmailOutBoundTable() throws JobExecutionException { + final Collection emailCampaignDataCollection = this.emailCampaignReadPlatformService + .retrieveAllScheduleActiveCampaign(); + if (emailCampaignDataCollection != null) { + for (EmailCampaignData emailCampaignData : emailCampaignDataCollection) { + LocalDateTime tenantDateNow = tenantDateTime(); + LocalDateTime nextTriggerDate = emailCampaignData.getNextTriggerDate().toLocalDateTime(); + + logger.info("tenant time " + tenantDateNow.toString() + " trigger time " + nextTriggerDate.toString()); + if (nextTriggerDate.isBefore(tenantDateNow)) { + insertDirectCampaignIntoEmailOutboundTable(emailCampaignData.getParamValue(), emailCampaignData.getEmailSubject(), + emailCampaignData.getMessage(), emailCampaignData.getCampaignName(), emailCampaignData.getId()); + this.updateTriggerDates(emailCampaignData.getId()); + } + } + } + } + + private void updateTriggerDates(Long campaignId) { + final EmailCampaign emailCampaign = this.emailCampaignRepository.findOne(campaignId); + if (emailCampaign == null) { throw new EmailCampaignNotFound(campaignId); } + LocalDateTime nextTriggerDate = emailCampaign.getNextTriggerDate(); + emailCampaign.setLastTriggerDate(nextTriggerDate.toDate()); + // calculate new trigger date and insert into next trigger date + + /** + * next run time has to be in the future if not calculate a new future + * date + */ + LocalDate nextRuntime = CalendarUtils.getNextRecurringDate(emailCampaign.getRecurrence(), + emailCampaign.getNextTriggerDate().toLocalDate(), nextTriggerDate.toLocalDate()); + if (nextRuntime.isBefore(DateUtils.getLocalDateOfTenant())) { // means + // next + // run + // time is + // in the + // past + // calculate + // a new + // future + // date + nextRuntime = CalendarUtils.getNextRecurringDate(emailCampaign.getRecurrence(), + emailCampaign.getNextTriggerDate().toLocalDate(), DateUtils.getLocalDateOfTenant()); + } + final LocalDateTime getTime = emailCampaign.getRecurrenceStartDateTime(); + final String dateString = nextRuntime.toString() + " " + getTime.getHourOfDay() + ":" + getTime.getMinuteOfHour() + ":" + + getTime.getSecondOfMinute(); + final DateTimeFormatter simpleDateFormat = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); + final LocalDateTime newTriggerDateWithTime = LocalDateTime.parse(dateString, simpleDateFormat); + + emailCampaign.setNextTriggerDate(newTriggerDateWithTime.toDate()); + this.emailCampaignRepository.saveAndFlush(emailCampaign); + } + + @Transactional + @Override + public CommandProcessingResult activateEmailCampaign(Long campaignId, JsonCommand command) { + final AppUser currentUser = this.context.authenticatedUser(); + + this.emailCampaignValidator.validateActivation(command.json()); + + final EmailCampaign emailCampaign = this.emailCampaignRepository.findOne(campaignId); + + if (emailCampaign == null) { throw new EmailCampaignNotFound(campaignId); } + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); + final LocalDate activationDate = command.localDateValueOfParameterNamed("activationDate"); + + emailCampaign.activate(currentUser, fmt, activationDate); + + this.emailCampaignRepository.saveAndFlush(emailCampaign); + + if (emailCampaign.isDirect()) { + insertDirectCampaignIntoEmailOutboundTable(emailCampaign.getParamValue(), emailCampaign.getEmailSubject(), + emailCampaign.getEmailMessage(), emailCampaign.getCampaignName(), emailCampaign.getId()); + } else { + if (emailCampaign.isSchedule()) { + + /** + * if recurrence start date is in the future calculate next + * trigger date if not use recurrence start date us next trigger + * date when activating + */ + LocalDate nextTriggerDate = null; + if (emailCampaign.getRecurrenceStartDateTime().isBefore(tenantDateTime())) { + nextTriggerDate = CalendarUtils.getNextRecurringDate(emailCampaign.getRecurrence(), + emailCampaign.getRecurrenceStartDate(), DateUtils.getLocalDateOfTenant()); + } else { + nextTriggerDate = emailCampaign.getRecurrenceStartDate(); + } + // to get time of tenant + final LocalDateTime getTime = emailCampaign.getRecurrenceStartDateTime(); + + final String dateString = nextTriggerDate.toString() + " " + getTime.getHourOfDay() + ":" + getTime.getMinuteOfHour() + ":" + + getTime.getSecondOfMinute(); + final DateTimeFormatter simpleDateFormat = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); + final LocalDateTime nextTriggerDateWithTime = LocalDateTime.parse(dateString, simpleDateFormat); + + emailCampaign.setNextTriggerDate(nextTriggerDateWithTime.toDate()); + this.emailCampaignRepository.saveAndFlush(emailCampaign); + } + } + + /* + * if campaign is direct insert campaign message into email outbound + * table else if its a schedule create a job process for it + */ + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(emailCampaign.getId()) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult closeEmailCampaign(Long campaignId, JsonCommand command) { + + final AppUser currentUser = this.context.authenticatedUser(); + this.emailCampaignValidator.validateClosedDate(command.json()); + + final EmailCampaign emailCampaign = this.emailCampaignRepository.findOne(campaignId); + if (emailCampaign == null) { throw new EmailCampaignNotFound(campaignId); } + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); + final LocalDate closureDate = command.localDateValueOfParameterNamed("closureDate"); + + emailCampaign.close(currentUser, fmt, closureDate); + + this.emailCampaignRepository.saveAndFlush(emailCampaign); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(emailCampaign.getId()) // + .build(); + } + + private String compileEmailTemplate(final String textMessageTemplate, final String campaignName, + final Map emailParams) { + final MustacheFactory mf = new DefaultMustacheFactory(); + final Mustache mustache = mf.compile(new StringReader(textMessageTemplate), campaignName); + + final StringWriter stringWriter = new StringWriter(); + mustache.execute(stringWriter, emailParams); + + return stringWriter.toString(); + } + + @SuppressWarnings({ "unused", "rawtypes" }) + private List> getRunReportByServiceImpl(final String reportName, final Map queryParams) + throws IOException { + final String reportType = "report"; + + List> resultList = new ArrayList>(); + final GenericResultsetData results = this.readReportingService.retrieveGenericResultSetForSmsEmailCampaign(reportName, reportType, + queryParams); + final String response = this.genericDataService.generateJsonFromGenericResultsetData(results); + resultList = new ObjectMapper().readValue(response, new TypeReference>>() {}); + // loop changes array date to string date + for (HashMap entry : resultList) { + for (Map.Entry map : entry.entrySet()) { + String key = map.getKey(); + Object ob = map.getValue(); + if (ob instanceof ArrayList && ((ArrayList) ob).size() == 3) { + String changeArrayDateToStringDate = ((ArrayList) ob).get(2).toString() + "-" + ((ArrayList) ob).get(1).toString() + "-" + + ((ArrayList) ob).get(0).toString(); + entry.put(key, changeArrayDateToStringDate); + } + } + } + return resultList; + } + + @Override + public PreviewCampaignMessage previewMessage(final JsonQuery query) { + PreviewCampaignMessage campaignMessage = null; + this.context.authenticatedUser(); + this.emailCampaignValidator.validatePreviewMessage(query.json()); + final String emailParams = this.fromJsonHelper.extractStringNamed("paramValue", query.parsedJson()); + final String textMessageTemplate = this.fromJsonHelper.extractStringNamed("emailMessage", query.parsedJson()); + + try { + HashMap campaignParams = new ObjectMapper().readValue(emailParams, + new TypeReference>() {}); + + HashMap queryParamForRunReport = new ObjectMapper().readValue(emailParams, + new TypeReference>() {}); + + List> runReportObject = this.getRunReportByServiceImpl(campaignParams.get("reportName"), + queryParamForRunReport); + + if (runReportObject != null) { + for (HashMap entry : runReportObject) { + // add string object to campaignParam object + String textMessage = this.compileEmailTemplate(textMessageTemplate, "EmailCampaign", entry); + if (!textMessage.isEmpty()) { + final Integer totalMessage = runReportObject.size(); + campaignMessage = new PreviewCampaignMessage(textMessage, totalMessage); + break; + } + } + } + } catch (final IOException e) { + // TODO throw something here + } + + return campaignMessage; + + } + + @Transactional + @Override + public CommandProcessingResult reactivateEmailCampaign(final Long campaignId, JsonCommand command) { + + this.emailCampaignValidator.validateActivation(command.json()); + + final AppUser currentUser = this.context.authenticatedUser(); + + final EmailCampaign emailCampaign = this.emailCampaignRepository.findOne(campaignId); + + if (emailCampaign == null) { throw new EmailCampaignNotFound(campaignId); } + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); + final LocalDate reactivationDate = command.localDateValueOfParameterNamed("activationDate"); + emailCampaign.reactivate(currentUser, fmt, reactivationDate); + if (emailCampaign.isSchedule()) { + + /** + * if recurrence start date is in the future calculate next trigger + * date if not use recurrence start date us next trigger date when + * activating + */ + LocalDate nextTriggerDate = null; + if (emailCampaign.getRecurrenceStartDateTime().isBefore(tenantDateTime())) { + nextTriggerDate = CalendarUtils.getNextRecurringDate(emailCampaign.getRecurrence(), emailCampaign.getRecurrenceStartDate(), + DateUtils.getLocalDateOfTenant()); + } else { + nextTriggerDate = emailCampaign.getRecurrenceStartDate(); + } + // to get time of tenant + final LocalDateTime getTime = emailCampaign.getRecurrenceStartDateTime(); + + final String dateString = nextTriggerDate.toString() + " " + getTime.getHourOfDay() + ":" + getTime.getMinuteOfHour() + ":" + + getTime.getSecondOfMinute(); + final DateTimeFormatter simpleDateFormat = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); + final LocalDateTime nextTriggerDateWithTime = LocalDateTime.parse(dateString, simpleDateFormat); + + emailCampaign.setNextTriggerDate(nextTriggerDateWithTime.toDate()); + this.emailCampaignRepository.saveAndFlush(emailCampaign); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(emailCampaign.getId()) // + .build(); + + } + + private void handleDataIntegrityIssues(@SuppressWarnings("unused") final JsonCommand command, + final DataIntegrityViolationException dve) { + final Throwable realCause = dve.getMostSpecificCause(); + + throw new PlatformDataIntegrityException("error.msg.email.campaign.unknown.data.integrity.issue", + "Unknown data integrity issue with resource: " + realCause.getMessage()); + } + + private LocalDateTime tenantDateTime() { + LocalDateTime today = new LocalDateTime(); + final FineractPlatformTenant tenant = ThreadLocalContextUtil.getTenant(); + + if (tenant != null) { + final DateTimeZone zone = DateTimeZone.forID(tenant.getTimezoneId()); + if (zone != null) { + today = new LocalDateTime(zone); + } + } + return today; + } + + @Override + @CronTarget(jobName = JobName.EXECUTE_EMAIL) + public void sendEmailMessage() throws JobExecutionException { + if (IPv4Helper.applicationIsNotRunningOnLocalMachine()) { // remove when + // testing + // locally + final List emailMessages = this.emailMessageRepository + .findByStatusType(EmailMessageStatusType.PENDING.getValue()); // retrieve + // all + // pending + // message + + for (final EmailMessage emailMessage : emailMessages) { + + if (isValidEmail(emailMessage.getEmailAddress())) { + + final EmailCampaign emailCampaign = this.emailCampaignRepository.findOne(emailMessage.getEmailCampaign().getId()); // + + final ScheduledEmailAttachmentFileFormat emailAttachmentFileFormat = ScheduledEmailAttachmentFileFormat + .instance(emailCampaign.getEmailAttachmentFileFormat()); + + final List attachmentList = new ArrayList<>(); + + final StringBuilder errorLog = new StringBuilder(); + + // check if email attachment format exist + if (emailAttachmentFileFormat != null && Arrays.asList(ScheduledEmailAttachmentFileFormat.validValues()) + .contains(emailAttachmentFileFormat.getId())) { + + final Report stretchyReport = emailCampaign.getStretchyReport(); + + final String reportName = (stretchyReport != null) ? stretchyReport.getReportName() : null; + + final HashMap reportStretchyParams = this + .validateStretchyReportParamMap(emailCampaign.getStretchyReportParamMap()); + + // there is a probability that a client has one or more + // loans or savings therefore we need to send two or + // more attachments + if (reportStretchyParams.containsKey("selectLoan") || reportStretchyParams.containsKey("loanId")) { + // get all ids of the client loans + if (emailMessage.getClient() != null) { + + final List loans = this.loanRepository.findLoanByClientId(emailMessage.getClient().getId()); + + HashMap reportParams = this + .replaceStretchyParamsWithActualClientParams(reportStretchyParams, emailMessage.getClient()); + + for (final Loan loan : loans) { + if (loan.isOpen()) { // only send attachment + // for active loan + + if (reportStretchyParams.containsKey("selectLoan")) { + + reportParams.put("SelectLoan", loan.getId().toString()); + + } else if (reportStretchyParams.containsKey("loanId")) { + + reportParams.put("loanId", loan.getId().toString()); + } + File file = this.generateAttachments(emailCampaign, emailAttachmentFileFormat, reportParams, + reportName, errorLog); + + if (file != null) { + attachmentList.add(file); + } else { + errorLog.append(reportParams.toString()); + } + } + } + + } + } else if (reportStretchyParams.containsKey("savingId")) { + if (emailMessage.getClient() != null) { + + final List savingsAccounts = this.savingsAccountRepository + .findSavingAccountByClientId(emailMessage.getClient().getId()); + + HashMap reportParams = this + .replaceStretchyParamsWithActualClientParams(reportStretchyParams, emailMessage.getClient()); + + for (final SavingsAccount savingsAccount : savingsAccounts) { + + if (savingsAccount.isActive()) { + + reportParams.put("savingId", savingsAccount.getId().toString()); + + File file = this.generateAttachments(emailCampaign, emailAttachmentFileFormat, reportParams, + reportName, errorLog); + + if (file != null) { + attachmentList.add(file); + } else { + errorLog.append(reportParams.toString()); + } + } + } + } + } else { + if (emailMessage.getClient() != null) { + + HashMap reportParams = this + .replaceStretchyParamsWithActualClientParams(reportStretchyParams, emailMessage.getClient()); + + File file = this.generateAttachments(emailCampaign, emailAttachmentFileFormat, reportParams, reportName, + errorLog); + + if (file != null) { + attachmentList.add(file); + } else { + errorLog.append(reportParams.toString()); + } + } + } + + } + + final EmailMessageWithAttachmentData emailMessageWithAttachmentData = EmailMessageWithAttachmentData.createNew( + emailMessage.getEmailAddress(), emailMessage.getMessage(), emailMessage.getEmailSubject(), attachmentList); + + if (!attachmentList.isEmpty() && attachmentList.size() > 0) { // only + // send + // email + // message + // if + // there + // is + // an + // attachment + // to + // it + + this.emailMessageJobEmailService.sendEmailWithAttachment(emailMessageWithAttachmentData); + + emailMessage.setStatusType(EmailMessageStatusType.SENT.getValue()); + + this.emailMessageRepository.save(emailMessage); + } else { + emailMessage.updateErrorMessage(errorLog.toString()); + + emailMessage.setStatusType(EmailMessageStatusType.FAILED.getValue()); + + this.emailMessageRepository.save(emailMessage); + } + } + } + + } + + } + + /** + * This generates the the report and converts it to a file by passing the + * parameters below + * + * @param emailCampaign + * @param emailAttachmentFileFormat + * @param reportParams + * @param reportName + * @param errorLog + * @return + */ + private File generateAttachments(final EmailCampaign emailCampaign, final ScheduledEmailAttachmentFileFormat emailAttachmentFileFormat, + final Map reportParams, final String reportName, final StringBuilder errorLog) { + + try { + final ByteArrayOutputStream byteArrayOutputStream = this.readReportingService.generatePentahoReportAsOutputStream(reportName, + emailAttachmentFileFormat.getValue(), reportParams, null, emailCampaign.getApprovedBy(), errorLog); + + final String fileLocation = FileSystemContentRepository.FINERACT_BASE_DIR + File.separator + ""; + final String fileNameWithoutExtension = fileLocation + File.separator + reportName; + + // check if file directory exists, if not create directory + if (!new File(fileLocation).isDirectory()) { + new File(fileLocation).mkdirs(); + } + + if (byteArrayOutputStream.size() == 0) { + errorLog.append("Pentaho report processing failed, empty output stream created"); + } else if (errorLog.length() == 0 && (byteArrayOutputStream.size() > 0)) { + final String fileName = fileNameWithoutExtension + "." + emailAttachmentFileFormat.getValue(); + + final File file = new File(fileName); + final FileOutputStream outputStream = new FileOutputStream(file); + byteArrayOutputStream.writeTo(outputStream); + + return file; + } + + } catch (IOException | PlatformDataIntegrityException e) { + errorLog.append("The ReportMailingJobWritePlatformServiceImpl.executeReportMailingJobs threw an IOException " + "exception: " + + e.getMessage() + " ---------- "); + } + return null; + } + + /** + * This matches the the actual values to the key in the report stretchy + * parameters map + * + * @param stretchyParams + * @param client + * @return + */ + private HashMap replaceStretchyParamsWithActualClientParams(final HashMap stretchyParams, + final Client client) { + + HashMap actualParams = new HashMap<>(); + + for (Map.Entry entry : stretchyParams.entrySet()) { + if (entry.getKey().equals("selectOffice")) { + // most at times the reports are run by picking the office of + // the staff Id + if (client.getStaff() != null) { + actualParams.put(entry.getKey(), client.getStaff().officeId().toString()); + } else { + actualParams.put(entry.getKey(), client.getOffice().getId().toString()); + } + + } else if (entry.getKey().equals("selectClient")) { + + actualParams.put(entry.getKey(), client.getId().toString()); + + } else if (entry.getKey().equals("selectLoanofficer")) { + + actualParams.put(entry.getKey(), client.getStaff().getId().toString()); + + } else if (entry.getKey().equals("environementUrl")) { + + actualParams.put(entry.getKey(), entry.getKey()); + } + } + return actualParams; + } + + private HashMap validateStretchyReportParamMap(final String stretchyParams) { + + HashMap stretchyReportParamHashMap = new HashMap<>(); + + if (!StringUtils.isEmpty(stretchyParams)) { + try { + stretchyReportParamHashMap = new ObjectMapper().readValue(stretchyParams, new TypeReference>() {}); + } + + catch (Exception e) { + stretchyReportParamHashMap = null; + } + } + + return stretchyReportParamHashMap; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignWritePlatformService.java new file mode 100644 index 000000000..a86262e39 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignWritePlatformService.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.campaigns.email.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.api.JsonQuery; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.infrastructure.campaigns.email.data.PreviewCampaignMessage; + +public interface EmailCampaignWritePlatformService { + + CommandProcessingResult create(JsonCommand command); + + CommandProcessingResult update(Long resourceId, JsonCommand command); + + CommandProcessingResult delete(Long resourceId); + + CommandProcessingResult activateEmailCampaign(Long campaignId, JsonCommand command); + + CommandProcessingResult closeEmailCampaign(Long campaignId, JsonCommand command); + + CommandProcessingResult reactivateEmailCampaign(Long campaignId, JsonCommand command); + + void storeTemplateMessageIntoEmailOutBoundTable() throws JobExecutionException; + + PreviewCampaignMessage previewMessage(JsonQuery query); + + void sendEmailMessage() throws JobExecutionException; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationReadPlatformService.java new file mode 100644 index 000000000..cc704e682 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationReadPlatformService.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.campaigns.email.service; + +import org.apache.fineract.infrastructure.campaigns.email.data.EmailConfigurationData; + +import java.util.Collection; + +public interface EmailConfigurationReadPlatformService { + + Collection retrieveAll(); + + EmailConfigurationData retrieveOne(String name); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationReadPlatformServiceImpl.java new file mode 100644 index 000000000..394e082c0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationReadPlatformServiceImpl.java @@ -0,0 +1,99 @@ +/** + * 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.campaigns.email.service; + +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.RoutingDataSource; +import org.apache.fineract.infrastructure.campaigns.email.exception.EmailConfigurationNotFoundException; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailConfigurationData; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; + +@Service +public class EmailConfigurationReadPlatformServiceImpl implements EmailConfigurationReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final EmailConfigurationRowMapper emailConfigurationRowMapper; + + @Autowired + public EmailConfigurationReadPlatformServiceImpl(final RoutingDataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.emailConfigurationRowMapper = new EmailConfigurationRowMapper(); + + } + + private static final class EmailConfigurationRowMapper implements RowMapper { + + final String schema; + + public EmailConfigurationRowMapper() { + final StringBuilder sql = new StringBuilder(300); + sql.append("cnf.id as id, "); + sql.append("cnf.name as name, "); + sql.append("cnf.value as value "); + sql.append("from scheduled_email_configuration cnf"); + + this.schema = sql.toString(); + } + + public String schema() { + return this.schema; + } + + @Override + public EmailConfigurationData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException { + + final Long id = JdbcSupport.getLong(rs, "id"); + final String name = rs.getString("name"); + final String value = rs.getString("value"); + + return EmailConfigurationData.instance(id, name, value); + } + + } + + @Override + public Collection retrieveAll() { + final String sql = "select " + this.emailConfigurationRowMapper.schema(); + + return this.jdbcTemplate.query(sql, this.emailConfigurationRowMapper, new Object[] {}); + } + + @Override + public EmailConfigurationData retrieveOne(String name) { + try { + final String sql = "select " + this.emailConfigurationRowMapper.schema() + " where cnf.name = ?"; + + return this.jdbcTemplate.queryForObject(sql, this.emailConfigurationRowMapper, name); + } + + catch(final EmptyResultDataAccessException e) { + + throw new EmailConfigurationNotFoundException(name); + } + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationWritePlatformService.java new file mode 100644 index 000000000..3afa8d3e5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationWritePlatformService.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.campaigns.email.service; + + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface EmailConfigurationWritePlatformService { + + CommandProcessingResult update(JsonCommand command); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationWritePlatformServiceImpl.java new file mode 100644 index 000000000..508e2832d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailConfigurationWritePlatformServiceImpl.java @@ -0,0 +1,89 @@ +/** + * 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.campaigns.email.service; + +import com.google.gson.JsonElement; +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.campaigns.email.data.EmailConfigurationData; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailConfigurationValidator; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailConfiguration; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailConfigurationRepository; +import org.apache.fineract.infrastructure.campaigns.email.exception.EmailConfigurationSMTPUsernameNotValid; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +@Service +public class EmailConfigurationWritePlatformServiceImpl implements EmailConfigurationWritePlatformService { + + private final PlatformSecurityContext context; + private final EmailConfigurationRepository repository; + private final EmailConfigurationValidator emailConfigurationValidator; + + @Autowired + public EmailConfigurationWritePlatformServiceImpl(final PlatformSecurityContext context, final EmailConfigurationRepository repository, + final EmailConfigurationValidator emailConfigurationValidator) { + this.context = context; + this.repository = repository; + this.emailConfigurationValidator = emailConfigurationValidator; + } + + @Override + public CommandProcessingResult update(final JsonCommand command) { + + final AppUser currentUser = this.context.authenticatedUser(); + + this.emailConfigurationValidator.validateUpdateConfiguration(command.json()); + final String smtpUsername = command.stringValueOfParameterNamed("SMTP_USERNAME"); + + if(!this.emailConfigurationValidator.isValidEmail(smtpUsername)){ + throw new EmailConfigurationSMTPUsernameNotValid(smtpUsername); + } + + + final Map changes = new HashMap<>(4); + + Collection configurations = this.repository.findAll(); + /** + *Default SMTP configuration added to flyway + */ + for (EmailConfiguration config : configurations) { + if(config.getName() !=null){ + String value = command.stringValueOfParameterNamed(config.getName()); + config.setValue(value); changes.put(config.getName(),value); + this.repository.saveAndFlush(config); + } + } + + return new CommandProcessingResultBuilder() // + .with(changes) + .build(); + + } + + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailMessageJobEmailService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailMessageJobEmailService.java new file mode 100644 index 000000000..37b193b2a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailMessageJobEmailService.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.campaigns.email.service; + + +import org.apache.fineract.infrastructure.campaigns.email.data.EmailMessageWithAttachmentData; + +public interface EmailMessageJobEmailService { + + void sendEmailWithAttachment(EmailMessageWithAttachmentData emailMessageWithAttachmentData); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailMessageJobEmailServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailMessageJobEmailServiceImpl.java new file mode 100644 index 000000000..c8854a270 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailMessageJobEmailServiceImpl.java @@ -0,0 +1,110 @@ +/** + * 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.campaigns.email.service; + + +import org.apache.fineract.infrastructure.campaigns.email.EmailApiConstants; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailMessageWithAttachmentData; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailConfiguration; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailConfigurationRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.io.File; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +@Service +public class EmailMessageJobEmailServiceImpl implements EmailMessageJobEmailService { + + private EmailConfigurationRepository emailConfigurationRepository; + + @Autowired + private EmailMessageJobEmailServiceImpl(final EmailConfigurationRepository emailConfigurationRepository) { + this.emailConfigurationRepository = emailConfigurationRepository; + } + + @Override + public void sendEmailWithAttachment(EmailMessageWithAttachmentData emailMessageWithAttachmentData) { + try{ + JavaMailSenderImpl javaMailSenderImpl = new JavaMailSenderImpl(); + javaMailSenderImpl.setHost(this.getGmailSmtpServer()); + javaMailSenderImpl.setPort(this.getGmailSmtpPort()); + javaMailSenderImpl.setUsername(this.getGmailSmtpUsername()); + javaMailSenderImpl.setPassword(this.getGmailSmtpPassword()); + javaMailSenderImpl.setJavaMailProperties(this.getJavaMailProperties()); + + MimeMessage mimeMessage = javaMailSenderImpl.createMimeMessage(); + + // use the true flag to indicate you need a multipart message + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); + + mimeMessageHelper.setTo(emailMessageWithAttachmentData.getTo()); + mimeMessageHelper.setText(emailMessageWithAttachmentData.getText()); + mimeMessageHelper.setSubject(emailMessageWithAttachmentData.getSubject()); + final List attachments = emailMessageWithAttachmentData.getAttachments(); + if(attachments !=null && attachments.size() > 0){ + for(final File attachment : attachments){ + if(attachment !=null){ + mimeMessageHelper.addAttachment(attachment.getName(),attachment); + } + } + } + + javaMailSenderImpl.send(mimeMessage); + + }catch(MessagingException e){ + + } + + } + + + private String getGmailSmtpServer(){ + final EmailConfiguration gmailSmtpServer = this.emailConfigurationRepository.findByName(EmailApiConstants.SMTP_SERVER); + return (gmailSmtpServer !=null) ? gmailSmtpServer.getValue() : null; + } + private Integer getGmailSmtpPort(){ + final EmailConfiguration gmailSmtpPort = this.emailConfigurationRepository.findByName(EmailApiConstants.SMTP_PORT); + return (gmailSmtpPort !=null) ? Integer.parseInt(gmailSmtpPort.getValue()) : null; + } + private String getGmailSmtpUsername(){ + final EmailConfiguration gmailSmtpUsername = this.emailConfigurationRepository.findByName(EmailApiConstants.SMTP_USERNAME); + return (gmailSmtpUsername !=null) ? gmailSmtpUsername.getValue() : null; + } + + private String getGmailSmtpPassword(){ + final EmailConfiguration gmailSmtpPassword = this.emailConfigurationRepository.findByName(EmailApiConstants.SMTP_PASSWORD); + return (gmailSmtpPassword !=null) ? gmailSmtpPassword.getValue() : null; + } + + private Properties getJavaMailProperties() { + Properties properties = new Properties(); + properties.setProperty("mail.smtp.starttls.enable", "true"); + properties.setProperty("mail.smtp.auth", "true"); + properties.setProperty("mail.smtp.ssl.trust", this.getGmailSmtpServer()); + + return properties; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailReadPlatformService.java new file mode 100644 index 000000000..961f9691f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailReadPlatformService.java @@ -0,0 +1,46 @@ +/** + * 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.campaigns.email.service; + +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.core.service.SearchParameters; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailData; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +public interface EmailReadPlatformService { + + Collection retrieveAll(); + + EmailData retrieveOne(Long resourceId); + + Collection retrieveAllPending(final SearchParameters searchParameters); + + Collection retrieveAllSent(final SearchParameters searchParameters); + + Collection retrieveAllDelivered(Integer limit); + + Collection retrieveAllFailed(final SearchParameters searchParameters); + + Page retrieveEmailByStatus(Integer limit, Integer status, Date dateFrom, Date dateTo); + + List retrieveExternalIdsOfAllSent(Integer limit); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailReadPlatformServiceImpl.java new file mode 100644 index 000000000..870251b7b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailReadPlatformServiceImpl.java @@ -0,0 +1,200 @@ +/** + * 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.campaigns.email.service; + +import org.joda.time.LocalDate; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.core.service.PaginationHelper; +import org.apache.fineract.infrastructure.core.service.RoutingDataSource; +import org.apache.fineract.infrastructure.core.service.SearchParameters; +import org.apache.fineract.infrastructure.campaigns.email.exception.EmailNotFoundException; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailData; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageEnumerations; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageStatusType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +@Service +public class EmailReadPlatformServiceImpl implements EmailReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final EmailMapper emailRowMapper = new EmailMapper(); + private final PaginationHelper paginationHelper = new PaginationHelper<>(); + + + @Autowired + public EmailReadPlatformServiceImpl(final RoutingDataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + private static final class EmailMapper implements RowMapper { + + final String schema; + + public EmailMapper() { + final StringBuilder sql = new StringBuilder(300); + sql.append(" emo.id as id, "); + sql.append("emo.group_id as groupId, "); + sql.append("emo.client_id as clientId, "); + sql.append("emo.staff_id as staffId, "); + sql.append("emo.campaign_name as campaignName, "); + sql.append("emo.status_enum as statusId, "); + sql.append("emo.email_address as emailAddress, "); + sql.append("emo.submittedon_date as sentDate, "); + sql.append("emo.email_subject as emailSubject, "); + sql.append("emo.message as message, "); + sql.append("emo.error_message as errorMessage "); + sql.append("from " + tableName() + " emo"); + + this.schema = sql.toString(); + } + + public String schema() { + return this.schema; + } + + public String tableName() { + return "scheduled_email_messages_outbound"; + } + + @Override + public EmailData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = JdbcSupport.getLong(rs, "id"); + final Long groupId = JdbcSupport.getLong(rs, "groupId"); + final Long clientId = JdbcSupport.getLong(rs, "clientId"); + final Long staffId = JdbcSupport.getLong(rs, "staffId"); + + final String emailAddress = rs.getString("emailAddress"); + final String emailSubject = rs.getString("emailSubject"); + final String message = rs.getString("message"); + final String campaignName = rs.getString("campaignName"); + + final Integer statusId = JdbcSupport.getInteger(rs, "statusId"); + final LocalDate sentDate = JdbcSupport.getLocalDate(rs, "sentDate"); + final String errorMessage = rs.getString("errorMessage"); + + final EnumOptionData status = EmailMessageEnumerations.status(statusId); + + return EmailData.instance(id,groupId, clientId, staffId, status, emailAddress, emailSubject, + message,null,null,null,null,null,campaignName,sentDate,errorMessage); + } + } + + @Override + public Collection retrieveAll() { + + final String sql = "select " + this.emailRowMapper.schema(); + + return this.jdbcTemplate.query(sql, this.emailRowMapper, new Object[] {}); + } + + @Override + public EmailData retrieveOne(final Long resourceId) { + try { + final String sql = "select " + this.emailRowMapper.schema() + " where emo.id = ?"; + + return this.jdbcTemplate.queryForObject(sql, this.emailRowMapper, resourceId); + } catch (final EmptyResultDataAccessException e) { + throw new EmailNotFoundException(resourceId); + } + } + + @Override + public Collection retrieveAllPending(final SearchParameters searchParameters) { + final String sqlPlusLimit = (searchParameters.getLimit() > 0) ? " limit 0, " + searchParameters.getLimit() : ""; + final String sql = "select " + this.emailRowMapper.schema() + " where emo.status_enum = " + + EmailMessageStatusType.PENDING.getValue() + sqlPlusLimit; + + return this.jdbcTemplate.query(sql, this.emailRowMapper, new Object[] {}); + } + + @Override + public Collection retrieveAllSent(final SearchParameters searchParameters) { + final String sqlPlusLimit = (searchParameters.getLimit() > 0) ? " limit 0, " + searchParameters.getLimit() : ""; + final String sql = "select " + this.emailRowMapper.schema() + " where emo.status_enum = " + + EmailMessageStatusType.SENT.getValue() + sqlPlusLimit; + + return this.jdbcTemplate.query(sql, this.emailRowMapper, new Object[] {}); + } + + @Override + public List retrieveExternalIdsOfAllSent(final Integer limit) { + final String sqlPlusLimit = (limit > 0) ? " limit 0, " + limit : ""; + final String sql = "select external_id from " + this.emailRowMapper.tableName() + " where status_enum = " + + EmailMessageStatusType.SENT.getValue() + sqlPlusLimit; + + return this.jdbcTemplate.queryForList(sql, Long.class); + } + + @Override + public Collection retrieveAllDelivered(final Integer limit) { + final String sqlPlusLimit = (limit > 0) ? " limit 0, " + limit : ""; + final String sql = "select " + this.emailRowMapper.schema() + " where emo.status_enum = " + + EmailMessageStatusType.DELIVERED.getValue() + sqlPlusLimit; + + return this.jdbcTemplate.query(sql, this.emailRowMapper, new Object[] {}); + } + + @Override + public Collection retrieveAllFailed(final SearchParameters searchParameters) { + final String sqlPlusLimit = (searchParameters.getLimit() > 0) ? " limit 0, " + searchParameters.getLimit() : ""; + final String sql = "select " + this.emailRowMapper.schema() + " where emo.status_enum = " + + EmailMessageStatusType.FAILED.getValue() + sqlPlusLimit; + + return this.jdbcTemplate.query(sql, this.emailRowMapper, new Object[] {}); + } + + @Override + public Page retrieveEmailByStatus(final Integer limit, final Integer status,final Date dateFrom, final Date dateTo) { + final StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder.append("select SQL_CALC_FOUND_ROWS "); + sqlBuilder.append(this.emailRowMapper.schema()); + if(status !=null){ + sqlBuilder.append(" where emo.status_enum= ? "); + } + String fromDateString = null; + String toDateString = null; + if(dateFrom !=null && dateTo !=null){ + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + fromDateString = df.format(dateFrom); + toDateString = df.format(dateTo); + sqlBuilder.append(" and emo.submittedon_date >= ? and emo.submittedon_date <= ? "); + } + final String sqlPlusLimit = (limit > 0) ? " limit 0, " + limit : ""; + if(!sqlPlusLimit.isEmpty()){ + sqlBuilder.append(sqlPlusLimit); + } + final String sqlCountRows = "SELECT FOUND_ROWS()"; + return this.paginationHelper.fetchPage(this.jdbcTemplate,sqlCountRows,sqlBuilder.toString(),new Object[]{status,fromDateString,toDateString},this.emailRowMapper); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailWritePlatformService.java new file mode 100644 index 000000000..233427c23 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailWritePlatformService.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.campaigns.email.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface EmailWritePlatformService { + + CommandProcessingResult create(JsonCommand command); + + CommandProcessingResult update(Long resourceId, JsonCommand command); + + CommandProcessingResult delete(Long resourceId); +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailWritePlatformServiceJpaRepositoryImpl.java new file mode 100644 index 000000000..9e23bc608 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailWritePlatformServiceJpaRepositoryImpl.java @@ -0,0 +1,134 @@ +/** + * 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.campaigns.email.service; + +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.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailDataValidator; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessage; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageAssembler; +import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; + +@Service +public class EmailWritePlatformServiceJpaRepositoryImpl implements EmailWritePlatformService { + + private final static Logger logger = LoggerFactory.getLogger(EmailWritePlatformServiceJpaRepositoryImpl.class); + + private final EmailMessageAssembler assembler; + private final EmailMessageRepository repository; + private final EmailDataValidator validator; + + @Autowired + public EmailWritePlatformServiceJpaRepositoryImpl(final EmailMessageAssembler assembler, final EmailMessageRepository repository, + final EmailDataValidator validator) { + this.assembler = assembler; + this.repository = repository; + this.validator = validator; + } + + @Transactional + @Override + public CommandProcessingResult create(final JsonCommand command) { + + try { + this.validator.validateCreateRequest(command); + + final EmailMessage message = this.assembler.assembleFromJson(command); + + // TODO: at this point we also want to fire off request using third + // party service to send Email. + // TODO: decision to be made on wheter we 'wait' for response or use + // 'future/promise' to capture response and update the EmailMessage + // table + this.repository.save(message); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(message.getId()) // + .build(); + } catch (final DataIntegrityViolationException dve) { + handleDataIntegrityIssues(command, dve); + return CommandProcessingResult.empty(); + } + } + + @Transactional + @Override + public CommandProcessingResult update(final Long resourceId, final JsonCommand command) { + + try { + this.validator.validateUpdateRequest(command); + + final EmailMessage message = this.assembler.assembleFromResourceId(resourceId); + final Map changes = message.update(command); + if (!changes.isEmpty()) { + this.repository.save(message); + } + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(resourceId) // + .with(changes) // + .build(); + } catch (final DataIntegrityViolationException dve) { + handleDataIntegrityIssues(command, dve); + return CommandProcessingResult.empty(); + } + } + + @Transactional + @Override + public CommandProcessingResult delete(final Long resourceId) { + + try { + final EmailMessage message = this.assembler.assembleFromResourceId(resourceId); + this.repository.delete(message); + this.repository.flush(); + } catch (final DataIntegrityViolationException dve) { + handleDataIntegrityIssues(null, dve); + return CommandProcessingResult.empty(); + } + return new CommandProcessingResultBuilder().withEntityId(resourceId).build(); + } + + /* + * Guaranteed to throw an exception no matter what the data integrity issue + * is. + */ + private void handleDataIntegrityIssues(@SuppressWarnings("unused") final JsonCommand command, final DataIntegrityViolationException dve) { + final Throwable realCause = dve.getMostSpecificCause(); + + if (realCause.getMessage().contains("email_address")) { throw new PlatformDataIntegrityException("error.msg.email.no.email.address.exists", + "The group, client or staff provided has no email address.", "id"); } + + logger.error(dve.getMessage(), dve); + throw new PlatformDataIntegrityException("error.msg.email.unknown.data.integrity.issue", + "Unknown data integrity issue with resource: " + realCause.getMessage()); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/api/SmsCampaignApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/api/SmsCampaignApiResource.java index 1a49ce245..829323b6f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/api/SmsCampaignApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/api/SmsCampaignApiResource.java @@ -137,7 +137,7 @@ public class SmsCampaignApiResource { @Produces({ MediaType.APPLICATION_JSON }) @ApiOperation(value = "List SMS Campaigns", notes = "Example Requests:\n" + "\n" + "smscampaigns") @ApiResponse(code = 200, message = "", response = SmsCampaignData.class) - public String retrieveAllCampaigns(@QueryParam("sqlSearch") final String sqlSearch, + public String retrieveAllEmails(@QueryParam("sqlSearch") final String sqlSearch, @QueryParam("offset") final Integer offset, @QueryParam("limit") final Integer limit, @QueryParam("orderBy") final String orderBy, @QueryParam("sortOrder") final String sortOrder, @Context final UriInfo uriInfo) { this.platformSecurityContext.authenticatedUser().validateHasReadPermission(SmsCampaignConstants.RESOURCE_NAME); 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..c920613a0 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<>(); @@ -514,7 +535,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP final String reportType = "report"; List> resultList = new ArrayList<>(); - final GenericResultsetData results = this.readReportingService.retrieveGenericResultSetForSmsCampaign(reportName, reportType, + final GenericResultsetData results = this.readReportingService.retrieveGenericResultSetForSmsEmailCampaign(reportName, reportType, queryParams); try { @@ -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/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/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/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/core/service/SearchParameters.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java index 34d397f11..a3b14fb89 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java @@ -225,6 +225,21 @@ public final class SearchParameters { staffId, accountNo, loanId, savingsId, orphansOnly, isSelfUser); } + public static SearchParameters forEmailCampaign(final String sqlSearch, final Integer offset, final Integer limit, final String orderBy, final String sortOrder) { + + final String externalId = null; + final Integer maxLimitAllowed = getCheckedLimit(limit); + final Long staffId = null; + final String accountNo = null; + final Long loanId = null; + final Long savingsId = null; + final Boolean orphansOnly = false; + final boolean isSelfUser = false; + + return new SearchParameters(sqlSearch, null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, + staffId, accountNo, loanId, savingsId, orphansOnly, isSelfUser); + } + private SearchParameters(final String sqlSearch, final Long officeId, final String externalId, final String name, final String hierarchy, final String firstname, final String lastname, final Integer offset, final Integer limit, final String orderBy, final String sortOrder, final Long staffId, final String accountNo, final Long loanId, 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>() { + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.supportedParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource("CreditBureau"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + + final String creditBureauIdParameter = "creditBureauId"; + if (this.fromApiJsonHelper.parameterExists(creditBureauIdParameter, element)) { + final Long creditBureauId = this.fromApiJsonHelper.extractLongNamed("creditBureauId", element); + baseDataValidator.reset().parameter("creditBureauId").value(creditBureauId).notNull().notBlank().longGreaterThanZero(); + } + + final String is_activeParameter = "is_active"; + if (this.fromApiJsonHelper.parameterExists(is_activeParameter, element)) { + final boolean is_active = this.fromApiJsonHelper.extractBooleanNamed("is_active", element); + baseDataValidator.reset().parameter("is_active").value(is_active).notNull().notBlank().trueOrFalseRequired(is_active); + } + + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + + } private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/serialization/CreditBureauLoanProductCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/serialization/CreditBureauLoanProductCommandFromApiJsonDeserializer.java index 4aa2fb69f..76e7fa5a0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/serialization/CreditBureauLoanProductCommandFromApiJsonDeserializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/serialization/CreditBureauLoanProductCommandFromApiJsonDeserializer.java @@ -40,8 +40,8 @@ import com.google.gson.reflect.TypeToken; @Component public class CreditBureauLoanProductCommandFromApiJsonDeserializer { - private final Set supportedParameters = new HashSet<>(Arrays.asList("loan_product_id", - "is_creditcheck_mandatory", "skip_creditcheck_in_failure", "stale_period", "is_active", "locale")); + private final Set supportedParameters = new HashSet<>(Arrays.asList("loanProductId", + "isCreditcheckMandatory", "skipCreditcheckInFailure", "stalePeriod", "is_active", "locale","creditbureauLoanProductMappingId")); private final FromJsonHelper fromApiJsonHelper; @@ -67,47 +67,47 @@ public class CreditBureauLoanProductCommandFromApiJsonDeserializer { baseDataValidator.reset().value(cb_id).notBlank().integerGreaterThanZero(); - final long loan_product_id = this.fromApiJsonHelper.extractLongNamed("loan_product_id", element); - baseDataValidator.reset().parameter("loan_product_id").value(loan_product_id).notBlank() + final long loanProductId = this.fromApiJsonHelper.extractLongNamed("loanProductId", element); + baseDataValidator.reset().parameter("loanProductId").value(loanProductId).notBlank() .integerGreaterThanZero(); - System.out.println("loan product id " + loan_product_id); + - if(this.fromApiJsonHelper.extractBooleanNamed("is_creditcheck_mandatory",element)!= null) + if(this.fromApiJsonHelper.extractBooleanNamed("isCreditcheckMandatory",element)!= null) { - final boolean is_creditcheck_mandatory = this.fromApiJsonHelper.extractBooleanNamed("is_creditcheck_mandatory", + final boolean isCreditcheckMandatory = this.fromApiJsonHelper.extractBooleanNamed("isCreditcheckMandatory", element); - baseDataValidator.reset().parameter("is_creditcheck_mandatory").value(is_creditcheck_mandatory).notBlank() - .trueOrFalseRequired(is_creditcheck_mandatory); + baseDataValidator.reset().parameter("isCreditcheckMandatory").value(isCreditcheckMandatory).notBlank() + .trueOrFalseRequired(isCreditcheckMandatory); } else { - baseDataValidator.reset().parameter("is_creditcheck_mandatory").value(this.fromApiJsonHelper.extractBooleanNamed("is_creditcheck_mandatory", + baseDataValidator.reset().parameter("isCreditcheckMandatory").value(this.fromApiJsonHelper.extractBooleanNamed("isCreditcheckMandatory", element)).notBlank() - .trueOrFalseRequired(this.fromApiJsonHelper.extractBooleanNamed("is_creditcheck_mandatory", + .trueOrFalseRequired(this.fromApiJsonHelper.extractBooleanNamed("isCreditcheckMandatory", element)); } - if( this.fromApiJsonHelper.extractBooleanNamed("skip_creditcheck_in_failure", element)!=null) + if( this.fromApiJsonHelper.extractBooleanNamed("skipCreditcheckInFailure", element)!=null) { - final boolean skip_creditcheck_in_failure = this.fromApiJsonHelper.extractBooleanNamed("skip_creditcheck_in_failure", element); - baseDataValidator.reset().parameter("skip_creditcheck_in_failure").value(skip_creditcheck_in_failure).notBlank().trueOrFalseRequired(skip_creditcheck_in_failure); + final boolean skipCreditcheckInFailure = this.fromApiJsonHelper.extractBooleanNamed("skipCreditcheckInFailure", element); + baseDataValidator.reset().parameter("skipCreditcheckInFailure").value(skipCreditcheckInFailure).notBlank().trueOrFalseRequired(skipCreditcheckInFailure); } else { - baseDataValidator.reset().parameter("skip_creditcheck_in_failure").value(this.fromApiJsonHelper.extractBooleanNamed("skip_creditcheck_in_failure", element)).notBlank().trueOrFalseRequired(this.fromApiJsonHelper.extractBooleanNamed("skip_creditcheck_in_failure", element)); + baseDataValidator.reset().parameter("skipCreditcheckInFailure").value(this.fromApiJsonHelper.extractBooleanNamed("skipCreditcheckInFailure", element)).notBlank().trueOrFalseRequired(this.fromApiJsonHelper.extractBooleanNamed("skipCreditcheckInFailure", element)); } - if(this.fromApiJsonHelper.extractLongNamed("stale_period", element)!=null) + if(this.fromApiJsonHelper.extractLongNamed("stalePeriod", element)!=null) { - final long stale_period = this.fromApiJsonHelper.extractLongNamed("stale_period", element); - baseDataValidator.reset().parameter("stale_period").value(stale_period).notBlank().integerGreaterThanZero(); + final long stalePeriod = this.fromApiJsonHelper.extractLongNamed("stalePeriod", element); + baseDataValidator.reset().parameter("stalePeriod").value(stalePeriod).notBlank().integerGreaterThanZero(); } else { - baseDataValidator.reset().parameter("stale_period").value(this.fromApiJsonHelper.extractLongNamed("stale_period", element)).notBlank().integerGreaterThanZero(); + baseDataValidator.reset().parameter("stalePeriod").value(this.fromApiJsonHelper.extractLongNamed("stalePeriod", element)).notBlank().integerGreaterThanZero(); } @@ -127,6 +127,42 @@ public class CreditBureauLoanProductCommandFromApiJsonDeserializer { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + + + + public void validateForUpdate(final String json) + { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() { + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.supportedParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource("CREDITBUREAU_LOANPRODUCT_MAPPING"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + + final String creditbureauLoanProductMappingIdParameter = "creditbureauLoanProductMappingId"; + if (this.fromApiJsonHelper.parameterExists(creditbureauLoanProductMappingIdParameter, element)) { + final Long creditbureauLoanProductMappingId = this.fromApiJsonHelper.extractLongNamed("creditbureauLoanProductMappingId", element); + baseDataValidator.reset().parameter("creditbureauLoanProductMappingId").value(creditbureauLoanProductMappingId).notNull().notBlank().longGreaterThanZero(); + } + + final String is_activeParameter = "is_active"; + if (this.fromApiJsonHelper.parameterExists(is_activeParameter, element)) { + final boolean is_active = this.fromApiJsonHelper.extractBooleanNamed("is_active", element); + baseDataValidator.reset().parameter("is_active").value(is_active).notNull().notBlank().trueOrFalseRequired(is_active); + } + + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + + } private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/CreditBureauLoanProductMappingWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/CreditBureauLoanProductMappingWritePlatformServiceImpl.java index b6a04a7f1..2440699ba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/CreditBureauLoanProductMappingWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/CreditBureauLoanProductMappingWritePlatformServiceImpl.java @@ -64,10 +64,9 @@ public class CreditBureauLoanProductMappingWritePlatformServiceImpl implements C public CommandProcessingResult addCreditBureauLoanProductMapping(Long creditBureau_id, JsonCommand command) { this.context.authenticatedUser(); - System.out.println("mapping called for creditBureau_id "+creditBureau_id); this.fromApiJsonDeserializer.validateForCreate(command.json(), creditBureau_id); - final long lpid = command.longValueOfParameterNamed("loan_product_id"); + final long lpid = command.longValueOfParameterNamed("loanProductId"); final OrganisationCreditBureau orgcb = this.organisationCreditBureauRepository.getOne(creditBureau_id); @@ -84,6 +83,9 @@ public class CreditBureauLoanProductMappingWritePlatformServiceImpl implements C @Override public CommandProcessingResult updateCreditBureauLoanProductMapping(JsonCommand command) { + + this.context.authenticatedUser(); + this.fromApiJsonDeserializer.validateForUpdate(command.json()); final Long mappingid = command.longValueOfParameterNamed("creditbureauLoanProductMappingId"); final boolean is_active = command.booleanPrimitiveValueOfParameterNamed("is_active"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/OrganisationCreditBureauWritePlatflormServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/OrganisationCreditBureauWritePlatflormServiceImpl.java index 1131d586b..f6bceb7a3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/OrganisationCreditBureauWritePlatflormServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/OrganisationCreditBureauWritePlatflormServiceImpl.java @@ -72,12 +72,11 @@ public class OrganisationCreditBureauWritePlatflormServiceImpl @Transactional @Override public CommandProcessingResult updateCreditBureau(JsonCommand command) { - // this.context.authenticatedUser(); - // this.fromApiJsonDeserializer.validateForCreate(command.json()); + this.context.authenticatedUser(); + this.fromApiJsonDeserializer.validateForUpdate(command.json()); final long creditbureauID = command.longValueOfParameterNamed("creditBureauId"); - //System.out.println("creditbureauID is " + creditbureauID); - + final boolean is_active = command.booleanPrimitiveValueOfParameterNamed("is_active"); final OrganisationCreditBureau orgcb = organisationCreditBureauRepository.getOne(creditbureauID); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/Report.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/Report.java index e39a59169..f909c33b6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/Report.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/Report.java @@ -276,4 +276,8 @@ public final class Report extends AbstractPersistableCustom { return false; } + + public Set getReportParameterUsages() { + return this.reportParameterUsages; + } } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/ReportParameterUsage.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/ReportParameterUsage.java index 4d10fefdd..7228e9c7a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/ReportParameterUsage.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/ReportParameterUsage.java @@ -86,4 +86,8 @@ public final class ReportParameterUsage extends AbstractPersistableCustom public void updateParameterName(final String parameterName) { this.reportParameterName = parameterName; } + + public String getReportParameterName() { + return this.reportParameterName; + } } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/ReportParameterUsageRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/ReportParameterUsageRepository.java index 8a49331fa..2d47db2fb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/ReportParameterUsageRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/domain/ReportParameterUsageRepository.java @@ -18,10 +18,14 @@ */ package org.apache.fineract.infrastructure.dataqueries.domain; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface ReportParameterUsageRepository extends JpaRepository, JpaSpecificationExecutor { // no added behaviour + + List findByReport(Report report); } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingService.java index a21f645b8..952a36a42 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingService.java @@ -18,7 +18,9 @@ */ package org.apache.fineract.infrastructure.dataqueries.service; +import java.io.ByteArrayOutputStream; import java.util.Collection; +import java.util.Locale; import java.util.Map; import javax.ws.rs.core.StreamingOutput; @@ -26,6 +28,7 @@ import javax.ws.rs.core.StreamingOutput; import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; import org.apache.fineract.infrastructure.dataqueries.data.ReportData; import org.apache.fineract.infrastructure.dataqueries.data.ReportParameterData; +import org.apache.fineract.useradministration.domain.AppUser; public interface ReadReportingService { @@ -45,8 +48,11 @@ public interface ReadReportingService { Collection getAllowedReportTypes(); - //needed for smsCampaign jobs where securityContext is null - GenericResultsetData retrieveGenericResultSetForSmsCampaign(String name, String type, Map extractedQueryParams); - - String sqlToRunForSmsCampaign(String name, String type, Map queryParams); + //needed for smsCampaign and emailCampaign jobs where securityContext is null + GenericResultsetData retrieveGenericResultSetForSmsEmailCampaign(String name, String type, Map extractedQueryParams); + + String sqlToRunForSmsEmailCampaign(String name, String type, Map queryParams); + + ByteArrayOutputStream generatePentahoReportAsOutputStream(String reportName, String outputTypeParam, + Map queryParams, Locale locale, AppUser runReportAsUser, StringBuilder errorLog); } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java index f25c371f8..b7cd352e1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java @@ -19,6 +19,7 @@ package org.apache.fineract.infrastructure.dataqueries.service; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; @@ -28,6 +29,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -486,11 +488,11 @@ public class ReadReportingServiceImpl implements ReadReportingService { } @Override - public GenericResultsetData retrieveGenericResultSetForSmsCampaign(String name, String type, Map queryParams) { + public GenericResultsetData retrieveGenericResultSetForSmsEmailCampaign(String name, String type, Map queryParams) { final long startTime = System.currentTimeMillis(); logger.info("STARTING REPORT: " + name + " Type: " + type); - final String sql = sqlToRunForSmsCampaign(name, type, queryParams); + final String sql = sqlToRunForSmsEmailCampaign(name, type, queryParams); final GenericResultsetData result = this.genericDataService.fillGenericResultSet(sql); @@ -500,7 +502,7 @@ public class ReadReportingServiceImpl implements ReadReportingService { } @Override - public String sqlToRunForSmsCampaign(final String name, final String type, final Map queryParams) { + public String sqlToRunForSmsEmailCampaign(final String name, final String type, final Map queryParams) { String sql = getSql(name, type); final Set keys = queryParams.keySet(); @@ -515,4 +517,86 @@ public class ReadReportingServiceImpl implements ReadReportingService { return sql; } + + @Override + public ByteArrayOutputStream generatePentahoReportAsOutputStream(final String reportName, final String outputTypeParam, final Map queryParams, + final Locale locale, final AppUser runReportAsUser, final StringBuilder errorLog) { + //This complete implementation should be moved to Pentaho Report Service + /* + String outputType = "HTML"; + if (StringUtils.isNotBlank(outputTypeParam)) { + outputType = outputTypeParam; + } + + if (!(outputType.equalsIgnoreCase("HTML") || outputType.equalsIgnoreCase("PDF") || outputType.equalsIgnoreCase("XLS") || outputType + .equalsIgnoreCase("CSV"))) { throw new PlatformDataIntegrityException("error.msg.invalid.outputType", + "No matching Output Type: " + outputType); } + + if (this.noPentaho) { throw new PlatformDataIntegrityException("error.msg.no.pentaho", "Pentaho is not enabled", + "Pentaho is not enabled"); } + + final String reportPath = FileSystemContentRepository.FINERACT_BASE_DIR + File.separator + "pentahoReports" + File.separator + + reportName + ".prpt"; + logger.info("Report path: " + reportPath); + + // load report definition + final ResourceManager manager = new ResourceManager(); + manager.registerDefaults(); + Resource res; + + try { + res = manager.createDirectly(reportPath, MasterReport.class); + final MasterReport masterReport = (MasterReport) res.getResource(); + final DefaultReportEnvironment reportEnvironment = (DefaultReportEnvironment) masterReport.getReportEnvironment(); + + if (locale != null) { + reportEnvironment.setLocale(locale); + } + addParametersToReport(masterReport, queryParams, runReportAsUser, errorLog); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + if ("PDF".equalsIgnoreCase(outputType)) { + PdfReportUtil.createPDF(masterReport, baos); + return baos; + } + + if ("XLS".equalsIgnoreCase(outputType)) { + ExcelReportUtil.createXLS(masterReport, baos); + return baos; + } + + if ("CSV".equalsIgnoreCase(outputType)) { + CSVReportUtil.createCSV(masterReport, baos, "UTF-8"); + return baos; + } + + if ("HTML".equalsIgnoreCase(outputType)) { + HtmlReportUtil.createStreamHTML(masterReport, baos); + return baos; + } + + } catch (final ResourceException e) { + errorLog.append("ReadReportingServiceImpl.generatePentahoReportAsOutputStream method threw a Pentaho ResourceException " + + "exception: " + e.getMessage() + " ---------- "); + throw new PlatformDataIntegrityException("error.msg.reporting.error", e.getMessage()); + } catch (final ReportProcessingException e) { + errorLog.append("ReadReportingServiceImpl.generatePentahoReportAsOutputStream method threw a Pentaho ReportProcessingException " + + "exception: " + e.getMessage() + " ---------- "); + throw new PlatformDataIntegrityException("error.msg.reporting.error", e.getMessage()); + } catch (final IOException e) { + errorLog.append("ReadReportingServiceImpl.generatePentahoReportAsOutputStream method threw an IOException " + + "exception: " + e.getMessage() + " ---------- "); + throw new PlatformDataIntegrityException("error.msg.reporting.error", e.getMessage()); + } + + errorLog.append("ReadReportingServiceImpl.generatePentahoReportAsOutputStream method threw a PlatformDataIntegrityException " + + "exception: No matching Output Type: " + outputType + " ---------- "); + throw new PlatformDataIntegrityException("error.msg.invalid.outputType", "No matching Output Type: " + outputType); + + */ + return null ; + } } + + 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: + * + *

+ * 
+ * Message message = new Message.Builder().build();
+ * 
+ * + * + * + * Message with optional attributes: + * + *
+ * 
+ * Message message = new Message.Builder()
+ *    .collapseKey(collapseKey)
+ *    .timeToLive(3)
+ *    .delayWhileIdle(true)
+ *    .dryRun(true)
+ *    .restrictedPackageName(restrictedPackageName)
+ *    .build();
+ * 
+ * + * + * + * Message with optional attributes and payload data: + * + *
+ * 
+ * Message message = new Message.Builder()
+ *    .priority("normal")
+ *    .collapseKey(collapseKey)
+ *    .timeToLive(3)
+ *    .delayWhileIdle(true)
+ *    .dryRun(true)
+ *    .restrictedPackageName(restrictedPackageName)
+ *    .addData("key1", "value1")
+ *    .addData("key2", "value2")
+ *    .build();
+ * 
+ * + * + */ +public final class Message implements Serializable { + + private final String collapseKey; + private final Boolean delayWhileIdle; + private final Integer timeToLive; + private final Map data; + private final Boolean dryRun; + private final String restrictedPackageName; + private final String priority; + private final Boolean contentAvailable; + private final Notification notification; + + public enum Priority { + NORMAL, HIGH + } + + public static final class Builder { + + private final Map data; + + // optional parameters + private String collapseKey; + private Boolean delayWhileIdle; + private Integer timeToLive; + private Boolean dryRun; + private String restrictedPackageName; + private String priority; + private Boolean contentAvailable; + private Notification notification; + + public Builder() { + this.data = new LinkedHashMap<>(); + } + + /** + * Sets the collapseKey property. + */ + public Builder collapseKey(String value) { + collapseKey = value; + return this; + } + + /** + * Sets the delayWhileIdle property (default value is {@literal false}). + */ + public Builder delayWhileIdle(boolean value) { + delayWhileIdle = value; + return this; + } + + /** + * Sets the time to live, in seconds. + */ + public Builder timeToLive(int value) { + timeToLive = value; + return this; + } + + /** + * Adds a key/value pair to the payload data. + */ + public Builder addData(String key, String value) { + data.put(key, value); + return this; + } + + /** + * Sets the dryRun property (default value is {@literal false}). + */ + public Builder dryRun(boolean value) { + dryRun = value; + return this; + } + + /** + * Sets the restrictedPackageName property. + */ + public Builder restrictedPackageName(String value) { + restrictedPackageName = value; + return this; + } + + /** + * Sets the priority property. + */ + public Builder priority(Priority value) { + switch (value) { + case NORMAL: + priority = GcmConstants.MESSAGE_PRIORITY_NORMAL; + break; + case HIGH: + priority = GcmConstants.MESSAGE_PRIORITY_HIGH; + break; + } + return this; + } + + /** + * Sets the notification property. + */ + public Builder notification(Notification value) { + notification = value; + return this; + } + + /** + * Sets the contentAvailable property + */ + public Builder contentAvailable(Boolean value) { + contentAvailable = value; + return this; + } + + public Message build() { + return new Message(this); + } + + } + + private Message(Builder builder) { + collapseKey = builder.collapseKey; + delayWhileIdle = builder.delayWhileIdle; + data = Collections.unmodifiableMap(builder.data); + timeToLive = builder.timeToLive; + dryRun = builder.dryRun; + restrictedPackageName = builder.restrictedPackageName; + priority = builder.priority; + contentAvailable = builder.contentAvailable; + notification = builder.notification; + } + + /** + * Gets the collapse key. + */ + public String getCollapseKey() { + return collapseKey; + } + + /** + * Gets the delayWhileIdle flag. + */ + public Boolean isDelayWhileIdle() { + return delayWhileIdle; + } + + /** + * Gets the time to live (in seconds). + */ + public Integer getTimeToLive() { + return timeToLive; + } + + /** + * Gets the dryRun flag. + */ + public Boolean isDryRun() { + return dryRun; + } + + /** + * Gets the restricted package name. + */ + public String getRestrictedPackageName() { + return restrictedPackageName; + } + + /** + * Gets the message priority value. + */ + public String getPriority() { + return priority; + } + + /** + * Gets the contentAvailable value + */ + public Boolean getContentAvailable() { + return contentAvailable; + } + + /** + * Gets the payload data, which is immutable. + */ + public Map getData() { + return data; + } + + /** + * Gets notification payload, which is immutable. + */ + public Notification getNotification() { + return notification; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("Message("); + if (priority != null) { + builder.append("priority=").append(priority).append(", "); + } + if (contentAvailable != null) { + builder.append("contentAvailable=").append(contentAvailable) + .append(", "); + } + if (collapseKey != null) { + builder.append("collapseKey=").append(collapseKey).append(", "); + } + if (timeToLive != null) { + builder.append("timeToLive=").append(timeToLive).append(", "); + } + if (delayWhileIdle != null) { + builder.append("delayWhileIdle=").append(delayWhileIdle) + .append(", "); + } + if (dryRun != null) { + builder.append("dryRun=").append(dryRun).append(", "); + } + if (restrictedPackageName != null) { + builder.append("restrictedPackageName=") + .append(restrictedPackageName).append(", "); + } + if (notification != null) { + builder.append("notification: ").append(notification).append(", "); + } + if (!data.isEmpty()) { + builder.append("data: {"); + for (Map.Entry entry : data.entrySet()) { + builder.append(entry.getKey()).append("=") + .append(entry.getValue()).append(","); + } + builder.delete(builder.length() - 1, builder.length()); + builder.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/MulticastResult.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/MulticastResult.java new file mode 100644 index 000000000..c58de9fe0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/MulticastResult.java @@ -0,0 +1,151 @@ +/** + * 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.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Result of a GCM multicast message request . + */ +public final class MulticastResult implements Serializable { + + private final int success; + private final int failure; + private final int canonicalIds; + private final long multicastId; + private final List results; + private final List retryMulticastIds; + + public static final class Builder { + + private final List results = new ArrayList<>(); + + // required parameters + private final int success; + private final int failure; + private final int canonicalIds; + private final long multicastId; + + // optional parameters + private List retryMulticastIds; + + public Builder(int success, int failure, int canonicalIds, + long multicastId) { + this.success = success; + this.failure = failure; + this.canonicalIds = canonicalIds; + this.multicastId = multicastId; + } + + public Builder addResult(Result result) { + results.add(result); + return this; + } + + public Builder retryMulticastIds(List retryMulticastIds) { + this.retryMulticastIds = retryMulticastIds; + return this; + } + + public MulticastResult build() { + return new MulticastResult(this); + } + } + + private MulticastResult(Builder builder) { + success = builder.success; + failure = builder.failure; + canonicalIds = builder.canonicalIds; + multicastId = builder.multicastId; + results = Collections.unmodifiableList(builder.results); + List tmpList = builder.retryMulticastIds; + if (tmpList == null) { + tmpList = Collections.emptyList(); + } + retryMulticastIds = Collections.unmodifiableList(tmpList); + } + + /** + * Gets the multicast id. + */ + public long getMulticastId() { + return multicastId; + } + + /** + * Gets the number of successful messages. + */ + public int getSuccess() { + return success; + } + + /** + * Gets the total number of messages sent, regardless of the status. + */ + public int getTotal() { + return success + failure; + } + + /** + * Gets the number of failed messages. + */ + public int getFailure() { + return failure; + } + + /** + * Gets the number of successful messages that also returned a canonical + * registration id. + */ + public int getCanonicalIds() { + return canonicalIds; + } + + /** + * Gets the results of each individual message, which is immutable. + */ + public List getResults() { + return results; + } + + /** + * Gets additional ids if more than one multicast message was sent. + */ + public List getRetryMulticastIds() { + return retryMulticastIds; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("MulticastResult(") + .append("multicast_id=").append(multicastId).append(",") + .append("total=").append(getTotal()).append(",") + .append("success=").append(success).append(",") + .append("failure=").append(failure).append(",") + .append("canonical_ids=").append(canonicalIds).append(","); + if (!results.isEmpty()) { + builder.append("results: " + results); + } + return builder.toString(); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Notification.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Notification.java new file mode 100644 index 000000000..589e7725d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Notification.java @@ -0,0 +1,330 @@ +/** + * 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.List; + +/** + * GCM message notification part. + * + *

+ * Instances of this class are immutable and should be created using a + * {@link Builder}. Examples: + * + * Simplest notification: + * + *

+ * 
+ * Notification notification = new Notification.Builder("myicon").build();
+ * 
+ * + * + * + * Notification with optional attributes: + * + *
+ * 
+ * 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 jsonRequest = new HashMap<>(); + messageToMap(message, jsonRequest); + jsonRequest.put(JSON_TO, to); + Map responseMap = makeGcmHttpRequest(jsonRequest); + String responseBody = null; + if (responseMap.get("responseBody") != null) { + responseBody = (String) responseMap.get("responseBody"); + } + int status = (int) responseMap.get("status"); + //responseBody + if (responseBody == null) { + return null; + } + JsonParser jsonParser = new JsonParser(); + JsonObject jsonResponse; + try { + jsonResponse = (JsonObject) jsonParser.parse(responseBody); + Result.Builder resultBuilder = new Result.Builder(); + if (jsonResponse.has("results")) { + // Handle response from message sent to specific device. + JsonArray jsonResults = (JsonArray) jsonResponse.get("results"); + if (jsonResults.size() == 1) { + JsonObject jsonResult = (JsonObject) jsonResults.get(0); + String messageId = null; + String canonicalRegId = null; + String error = null; + if(jsonResult.has(JSON_MESSAGE_ID)){ + messageId = jsonResult.get(JSON_MESSAGE_ID).getAsString(); + } + if(jsonResult.has(TOKEN_CANONICAL_REG_ID)){ + canonicalRegId = jsonResult + .get(TOKEN_CANONICAL_REG_ID).getAsString(); + } + if(jsonResult.has(JSON_ERROR)){ + error = (String) jsonResult.get(JSON_ERROR).getAsString(); + } + int success = 0; + int failure = 0; + if(jsonResponse.get("success") != null){ + success = Integer.parseInt(jsonResponse.get("success").toString()); + } + if(jsonResponse.get("failure") != null){ + failure = Integer.parseInt(jsonResponse.get("failure").toString()); + } + resultBuilder.messageId(messageId) + .canonicalRegistrationId(canonicalRegId) + .success(success) + .failure(failure) + .status(status) + .errorCode(error); + } else { + logger.log(Level.WARNING, + "Found null or " + jsonResults.size() + + " results, expected one"); + return null; + } + } else if (to.startsWith(TOPIC_PREFIX)) { + if (jsonResponse.has(JSON_MESSAGE_ID)) { + // message_id is expected when this is the response from a + // topic message. + Long messageId = jsonResponse.get(JSON_MESSAGE_ID).getAsLong(); + resultBuilder.messageId(messageId.toString()); + } else if (jsonResponse.has(JSON_ERROR)) { + String error = jsonResponse.get(JSON_ERROR).getAsString(); + resultBuilder.errorCode(error); + } else { + logger.log(Level.WARNING, "Expected " + JSON_MESSAGE_ID + + " or " + JSON_ERROR + " found: " + responseBody); + return null; + } + } else if (jsonResponse.has(JSON_SUCCESS) + && jsonResponse.has(JSON_FAILURE)) { + // success and failure are expected when response is from group + // message. + int success = getNumber(responseMap, JSON_SUCCESS).intValue(); + int failure = getNumber(responseMap, JSON_FAILURE).intValue(); + List failedIds = null; + if (jsonResponse.has("failed_registration_ids")) { + JsonArray jFailedIds = (JsonArray) jsonResponse + .get("failed_registration_ids").getAsJsonArray(); + failedIds = new ArrayList<>(); + for (int i = 0; i < jFailedIds.size(); i++) { + failedIds.add(jFailedIds.get(i).getAsString()); + } + } + resultBuilder.success(success).failure(failure) + .failedRegistrationIds(failedIds); + } else { + logger.warning("Unrecognized response: " + responseBody); + throw newIoException(responseBody, new Exception( + "Unrecognized response.")); + } + return resultBuilder.build(); + } catch (CustomParserException e) { + throw newIoException(responseBody, e); + } + } + + /** + * Sends a message to many devices, 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. + * @param regIds + * registration id of the devices that will receive the message. + * @param retries + * number of retries in case of service unavailability errors. + * + * @return combined result of all requests made. + * + * @throws IllegalArgumentException + * if registrationIds is {@literal null} or empty. + * @throws InvalidRequestException + * if GCM didn't returned a 200 or 503 status. + * @throws IOException + * if message could not be sent. + */ + public MulticastResult send(Message message, List regIds, + int retries) throws IOException { + int attempt = 0; + MulticastResult multicastResult; + int backoff = BACKOFF_INITIAL_DELAY; + // Map of results by registration id, it will be updated after each + // attempt + // to send the messages + Map results = new HashMap<>(); + List unsentRegIds = new ArrayList<>(regIds); + boolean tryAgain; + List multicastIds = new ArrayList<>(); + do { + multicastResult = null; + attempt++; + if (logger.isLoggable(Level.FINE)) { + logger.fine("Attempt #" + attempt + " to send message " + + message + " to regIds " + unsentRegIds); + } + try { + multicastResult = sendNoRetry(message, unsentRegIds); + } catch (IOException e) { + // no need for WARNING since exception might be already logged + logger.log(Level.FINEST, "IOException on attempt " + attempt, e); + } + if (multicastResult != null) { + long multicastId = multicastResult.getMulticastId(); + logger.fine("multicast_id on attempt # " + attempt + ": " + + multicastId); + multicastIds.add(multicastId); + unsentRegIds = updateStatus(unsentRegIds, results, + multicastResult); + tryAgain = !unsentRegIds.isEmpty() && attempt <= retries; + } else { + tryAgain = attempt <= retries; + } + if (tryAgain) { + int sleepTime = backoff / 2 + random.nextInt(backoff); + sleep(sleepTime); + if (2 * backoff < MAX_BACKOFF_DELAY) { + backoff *= 2; + } + } + } while (tryAgain); + if (multicastIds.isEmpty()) { + // all JSON posts failed due to GCM unavailability + throw new IOException("Could not post JSON requests to GCM after " + + attempt + " attempts"); + } + // calculate summary + int success = 0, failure = 0, canonicalIds = 0; + for (Result result : results.values()) { + if (result.getMessageId() != null) { + success++; + if (result.getCanonicalRegistrationId() != null) { + canonicalIds++; + } + } else { + failure++; + } + } + // build a new object with the overall result + long multicastId = multicastIds.remove(0); + MulticastResult.Builder builder = new MulticastResult.Builder(success, + failure, canonicalIds, multicastId) + .retryMulticastIds(multicastIds); + // add results, in the same order as the input + for (String regId : regIds) { + Result result = results.get(regId); + builder.addResult(result); + } + return builder.build(); + } + + /** + * Updates the status of the messages sent to devices and the list of + * devices that should be retried. + * + * @param unsentRegIds + * list of devices that are still pending an update. + * @param allResults + * map of status that will be updated. + * @param multicastResult + * result of the last multicast sent. + * + * @return updated version of devices that should be retried. + */ + private List updateStatus(List unsentRegIds, + Map allResults, MulticastResult multicastResult) { + List results = multicastResult.getResults(); + if (results.size() != unsentRegIds.size()) { + // should never happen, unless there is a flaw in the algorithm + throw new RuntimeException("Internal error: sizes do not match. " + + "currentResults: " + results + "; unsentRegIds: " + + unsentRegIds); + } + List newUnsentRegIds = new ArrayList<>(); + for (int i = 0; i < unsentRegIds.size(); i++) { + String regId = unsentRegIds.get(i); + Result result = results.get(i); + allResults.put(regId, result); + String error = result.getErrorCodeName(); + if (error != null + && (error.equals(GcmConstants.ERROR_UNAVAILABLE) || error + .equals(GcmConstants.ERROR_INTERNAL_SERVER_ERROR))) { + newUnsentRegIds.add(regId); + } + } + return newUnsentRegIds; + } + + /** + * Sends a message without retrying in case of service unavailability. See + * {@link #send(Message, List, int)} for more info. + * + * @return multicast results if the message was sent successfully, + * {@literal null} if it failed but could be retried. + * + * @throws IllegalArgumentException + * if registrationIds is {@literal null} or empty. + * @throws InvalidRequestException + * if GCM didn't returned a 200 status. + * @throws IOException + * if there was a JSON parsing error + */ + public MulticastResult sendNoRetry(Message message, + List registrationIds) throws IOException { + if (nonNull(registrationIds).isEmpty()) { + throw new IllegalArgumentException( + "registrationIds cannot be empty"); + } + Map jsonRequest = new HashMap<>(); + messageToMap(message, jsonRequest); + jsonRequest.put(JSON_REGISTRATION_IDS, registrationIds); + Map responseMap = makeGcmHttpRequest(jsonRequest); + String responseBody = null; + if (responseMap.get("responseBody") != null) { + responseBody = (String) responseMap.get("responseBody"); + } + if (responseBody == null) { + return null; + } + + JsonParser parser = new JsonParser(); + JsonObject jsonResponse; + try { + jsonResponse = (JsonObject) parser.parse(responseBody); + int success = getNumber(responseMap, JSON_SUCCESS).intValue(); + int failure = getNumber(responseMap, JSON_FAILURE).intValue(); + int canonicalIds = getNumber(responseMap, JSON_CANONICAL_IDS) + .intValue(); + long multicastId = getNumber(responseMap, JSON_MULTICAST_ID) + .longValue(); + MulticastResult.Builder builder = new MulticastResult.Builder( + success, failure, canonicalIds, multicastId); + @SuppressWarnings("unchecked") + List> results = (List>) jsonResponse + .get(JSON_RESULTS); + if (results != null) { + for (Map jsonResult : results) { + String messageId = (String) jsonResult.get(JSON_MESSAGE_ID); + String canonicalRegId = (String) jsonResult + .get(TOKEN_CANONICAL_REG_ID); + String error = (String) jsonResult.get(JSON_ERROR); + Result result = new Result.Builder().messageId(messageId) + .canonicalRegistrationId(canonicalRegId) + .errorCode(error).build(); + builder.addResult(result); + } + } + return builder.build(); + } catch (CustomParserException e) { + throw newIoException(responseBody, e); + } + } + + private Map makeGcmHttpRequest(Map jsonRequest) + throws InvalidRequestException { + String requestBody = new Gson().toJson(jsonRequest); + logger.finest("JSON request: " + requestBody); + HttpURLConnection conn; + int status; + try { + conn = post(getEndpoint(), "application/json", requestBody); + status = conn.getResponseCode(); + } catch (IOException e) { + logger.log(Level.FINE, "IOException posting to GCM", e); + return null; + } + String responseBody; + if (status != 200) { + try { + responseBody = getAndClose(conn.getErrorStream()); + logger.finest("JSON error response: " + responseBody); + } catch (IOException e) { + // ignore the exception since it will thrown an + // InvalidRequestException + // anyways + responseBody = "N/A"; + logger.log(Level.FINE, "Exception reading response: ", e); + } + throw new InvalidRequestException(status, responseBody); + } + try { + responseBody = getAndClose(conn.getInputStream()); + } catch (IOException e) { + logger.log(Level.WARNING, "IOException reading response", e); + return null; + } + logger.finest("JSON response: " + responseBody); + Map map = new HashMap<>(); + map.put("responseBody", responseBody); + map.put("status", status); + + return map; + } + + /** + * Populate Map with message. + * + * @param message + * Message used to populate Map. + * @param mapRequest + * Map populated by Message. + */ + private void messageToMap(Message message, Map mapRequest) { + if (message == null || mapRequest == null) { + return; + } + setJsonField(mapRequest, PARAM_PRIORITY, message.getPriority()); + setJsonField(mapRequest, PARAM_CONTENT_AVAILABLE, + message.getContentAvailable()); + setJsonField(mapRequest, PARAM_TIME_TO_LIVE, message.getTimeToLive()); + setJsonField(mapRequest, PARAM_COLLAPSE_KEY, message.getCollapseKey()); + setJsonField(mapRequest, PARAM_RESTRICTED_PACKAGE_NAME, + message.getRestrictedPackageName()); + setJsonField(mapRequest, PARAM_DELAY_WHILE_IDLE, + message.isDelayWhileIdle()); + setJsonField(mapRequest, PARAM_DRY_RUN, message.isDryRun()); + Map payload = message.getData(); + if (!payload.isEmpty()) { + mapRequest.put(JSON_PAYLOAD, payload); + } + if (message.getNotification() != null) { + Notification notification = message.getNotification(); + Map nMap = new HashMap<>(); + if (notification.getBadge() != null) { + setJsonField(nMap, JSON_NOTIFICATION_BADGE, notification + .getBadge().toString()); + } + setJsonField(nMap, JSON_NOTIFICATION_BODY, notification.getBody()); + setJsonField(nMap, JSON_NOTIFICATION_BODY_LOC_ARGS, + notification.getBodyLocArgs()); + setJsonField(nMap, JSON_NOTIFICATION_BODY_LOC_KEY, + notification.getBodyLocKey()); + setJsonField(nMap, JSON_NOTIFICATION_CLICK_ACTION, + notification.getClickAction()); + setJsonField(nMap, JSON_NOTIFICATION_COLOR, notification.getColor()); + setJsonField(nMap, JSON_NOTIFICATION_ICON, notification.getIcon()); + setJsonField(nMap, JSON_NOTIFICATION_SOUND, notification.getSound()); + setJsonField(nMap, JSON_NOTIFICATION_TAG, notification.getTag()); + setJsonField(nMap, JSON_NOTIFICATION_TITLE, notification.getTitle()); + setJsonField(nMap, JSON_NOTIFICATION_TITLE_LOC_ARGS, + notification.getTitleLocArgs()); + setJsonField(nMap, JSON_NOTIFICATION_TITLE_LOC_KEY, + notification.getTitleLocKey()); + mapRequest.put(JSON_NOTIFICATION, nMap); + } + } + + private IOException newIoException(String responseBody, Exception e) { + // log exception, as IOException constructor that takes a message and + // cause + // is only available on Java 6 + String msg = "Error parsing JSON response (" + responseBody + ")"; + logger.log(Level.WARNING, msg, e); + return new IOException(msg + ":" + e); + } + + private static void close(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + // ignore error + logger.log(Level.FINEST, "IOException closing stream", e); + } + } + } + + /** + * Sets a JSON field, but only if the value is not {@literal null}. + */ + private void setJsonField(Map json, String field, + Object value) { + if (value != null) { + json.put(field, value); + } + } + + private Number getNumber(Map json, String field) { + Object value = json.get(field); + if (value == null) { + throw new CustomParserException("Missing field: " + field); + } + if (!(value instanceof Number)) { + throw new CustomParserException("Field " + field + + " does not contain a number: " + value); + } + return (Number) value; + } + + class CustomParserException extends RuntimeException { + CustomParserException(String message) { + super(message); + } + } + + /** + * Make an HTTP post to a given URL. + * + * @return HTTP response. + */ + protected HttpURLConnection post(String url, String body) + throws IOException { + return post(url, "application/x-www-form-urlencoded;charset=UTF-8", + body); + } + + /** + * Makes an HTTP POST request to a given endpoint. + * + *

+ * Note: the returned connected should not be + * disconnected, otherwise it would kill persistent connections made using + * Keep-Alive. + * + * @param url + * endpoint to post the request. + * @param contentType + * type of request. + * @param body + * body of the request. + * + * @return the underlying connection. + * + * @throws IOException + * propagated from underlying methods. + */ + protected HttpURLConnection post(String url, String contentType, String body) + throws IOException { + if (url == null || contentType == null || body == null) { + throw new IllegalArgumentException("arguments cannot be null"); + } + if (!url.startsWith("https://")) { + logger.warning("URL does not use https: " + url); + } + logger.fine("Sending POST to " + url); + logger.finest("POST body: " + body); + byte[] bytes = body.getBytes(UTF8); + HttpURLConnection conn = getConnection(url); + conn.setDoOutput(true); + conn.setUseCaches(false); + conn.setFixedLengthStreamingMode(bytes.length); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", contentType); + conn.setRequestProperty("Authorization", "key=" + key); + OutputStream out = conn.getOutputStream(); + try { + out.write(bytes); + } finally { + close(out); + } + return conn; + } + + /** + * Creates a map with just one key-value pair. + */ + protected static final Map newKeyValues(String key, + String value) { + Map keyValues = new HashMap<>(1); + keyValues.put(nonNull(key), nonNull(value)); + return keyValues; + } + + /** + * Creates a {@link StringBuilder} to be used as the body of an HTTP POST. + * + * @param name + * initial parameter for the POST. + * @param value + * initial value for that parameter. + * @return StringBuilder to be used an HTTP POST body. + */ + protected static StringBuilder newBody(String name, String value) { + return new StringBuilder(nonNull(name)).append('=').append( + nonNull(value)); + } + + /** + * Adds a new parameter to the HTTP POST body. + * + * @param body + * HTTP POST body. + * @param name + * parameter's name. + * @param value + * parameter's value. + */ + protected static void addParameter(StringBuilder body, String name, + String value) { + nonNull(body).append('&').append(nonNull(name)).append('=') + .append(nonNull(value)); + } + + /** + * Gets an {@link HttpURLConnection} given an URL. + */ + protected HttpURLConnection getConnection(String url) throws IOException { + HttpURLConnection conn = (HttpURLConnection) new URL(url) + .openConnection(); + conn.setConnectTimeout(connectTimeout); + conn.setReadTimeout(readTimeout); + return conn; + } + + /** + * Convenience method to convert an InputStream to a String. + *

+ * If the stream ends in a newline character, it will be stripped. + *

+ * If the stream is {@literal null}, returns an empty string. + */ + protected static String getString(InputStream stream) throws IOException { + if (stream == null) { + return ""; + } + BufferedReader reader = new BufferedReader( + new InputStreamReader(stream)); + StringBuilder content = new StringBuilder(); + String newLine; + do { + newLine = reader.readLine(); + if (newLine != null) { + content.append(newLine).append('\n'); + } + } while (newLine != null); + if (content.length() > 0) { + // strip last newline + content.setLength(content.length() - 1); + } + return content.toString(); + } + + private static String getAndClose(InputStream stream) throws IOException { + try { + return getString(stream); + } finally { + if (stream != null) { + close(stream); + } + } + } + + static T nonNull(T argument) { + if (argument == null) { + throw new IllegalArgumentException("argument cannot be null"); + } + return argument; + } + + void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/DeviceRegistrationNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/DeviceRegistrationNotFoundException.java new file mode 100644 index 000000000..cc730a0ec --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/DeviceRegistrationNotFoundException.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.gcm.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class DeviceRegistrationNotFoundException extends + AbstractPlatformResourceNotFoundException { + + public DeviceRegistrationNotFoundException(final Long id) { + super("error.msg.device.registration.id.invalid", + "Device registration with identifier " + id + " does not exist", + id); + } + + public DeviceRegistrationNotFoundException(final Long clientId, String value) { + super("error.msg.device.registration." + value + ".invalid", + "Device registration with " + value + " identifier " + clientId + + " does not exist", clientId); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/InvalidRequestException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/InvalidRequestException.java new file mode 100644 index 000000000..2194b43d8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/InvalidRequestException.java @@ -0,0 +1,66 @@ +/** + * 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.exception; + +import java.io.IOException; + +/** + * Exception thrown when GCM returned an error due to an invalid request. + *

+ * This is equivalent to GCM posts that return an HTTP error different of 200. + */ +public final class InvalidRequestException extends IOException { + + private final int status; + private final String description; + + public InvalidRequestException(int status) { + this(status, null); + } + + public InvalidRequestException(int status, String description) { + super(getMessage(status, description)); + this.status = status; + this.description = description; + } + + private static String getMessage(int status, String description) { + StringBuilder base = new StringBuilder("HTTP Status Code: ") + .append(status); + if (description != null) { + base.append("(").append(description).append(")"); + } + return base.toString(); + } + + /** + * Gets the HTTP Status Code. + */ + public int getHttpStatusCode() { + return status; + } + + /** + * Gets the error description. + */ + public String getDescription() { + return description; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformService.java new file mode 100644 index 000000000..1b4ec42c8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformService.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.gcm.service; + +import java.util.Collection; + +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationData; + +public interface DeviceRegistrationReadPlatformService { + + Collection retrieveAllDeviceRegiistrations(); + + DeviceRegistrationData retrieveDeviceRegiistration(Long id); + + DeviceRegistrationData retrieveDeviceRegiistrationByClientId(Long clientId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformServiceImpl.java new file mode 100644 index 000000000..8c64fe3b3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformServiceImpl.java @@ -0,0 +1,125 @@ +/** + * 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.service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; + +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.RoutingDataSource; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationData; +import org.apache.fineract.infrastructure.gcm.exception.DeviceRegistrationNotFoundException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.client.data.ClientData; +import org.joda.time.LocalDate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +public class DeviceRegistrationReadPlatformServiceImpl implements + DeviceRegistrationReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final PlatformSecurityContext context; + + @Autowired + public DeviceRegistrationReadPlatformServiceImpl( + final PlatformSecurityContext context, + final RoutingDataSource dataSource) { + this.context = context; + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + private static final class DeviceRegistrationDataMapper implements + RowMapper { + + private final String schema; + + public DeviceRegistrationDataMapper() { + final StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder + .append(" cdr.id as id, cdr.registration_id as registrationId, cdr.updatedon_date as updatedOnDate, "); + sqlBuilder + .append(" c.id as clientId, c.display_name as clientName "); + sqlBuilder.append(" from client_device_registration cdr "); + sqlBuilder.append(" left join m_client c on c.id = cdr.client_id "); + this.schema = sqlBuilder.toString(); + } + + public String schema() { + return this.schema; + } + + @Override + public DeviceRegistrationData mapRow(final ResultSet rs, + @SuppressWarnings("unused") final int rowNum) + throws SQLException { + + final Long id = JdbcSupport.getLong(rs, "id"); + final LocalDate updatedOnDate = JdbcSupport.getLocalDate(rs, + "updatedOnDate"); + final String registrationId = rs.getString("registrationId"); + final Long clientId = rs.getLong("clientId"); + final String clientName = rs.getString("clientName"); + ClientData clientData = ClientData.instance(clientId, clientName); + return DeviceRegistrationData.instance(id, clientData, + registrationId, updatedOnDate.toDate()); + } + } + + @Override + public Collection retrieveAllDeviceRegiistrations() { + this.context.authenticatedUser(); + DeviceRegistrationDataMapper drm = new DeviceRegistrationDataMapper(); + String sql = "select " + drm.schema(); + return this.jdbcTemplate.query(sql, drm, new Object[] {}); + } + + @Override + public DeviceRegistrationData retrieveDeviceRegiistration(Long id) { + try { + this.context.authenticatedUser(); + DeviceRegistrationDataMapper drm = new DeviceRegistrationDataMapper(); + String sql = "select " + drm.schema() + " where cdr.id = ? "; + return this.jdbcTemplate.queryForObject(sql, drm, + new Object[] { id }); + } catch (final EmptyResultDataAccessException e) { + throw new DeviceRegistrationNotFoundException(id); + } + } + + @Override + public DeviceRegistrationData retrieveDeviceRegiistrationByClientId( + Long clientId) { + try { + this.context.authenticatedUser(); + DeviceRegistrationDataMapper drm = new DeviceRegistrationDataMapper(); + String sql = "select " + drm.schema() + " where c.id = ? "; + return this.jdbcTemplate.queryForObject(sql, drm, + new Object[] { clientId }); + } catch (final EmptyResultDataAccessException e) { + throw new DeviceRegistrationNotFoundException(clientId, "client"); + } + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformService.java new file mode 100644 index 000000000..2391fdd58 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformService.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.gcm.service; + +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration; + +public interface DeviceRegistrationWritePlatformService { + + public DeviceRegistration registerDevice(final Long clientId, final String registrationId); + + public DeviceRegistration updateDeviceRegistration(final Long id, final Long clientId, final String registrationId); + + public void deleteDeviceRegistration(final Long id); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformServiceImpl.java new file mode 100644 index 000000000..52653eaa2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformServiceImpl.java @@ -0,0 +1,123 @@ +/** + * 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.service; + +import javax.persistence.PersistenceException; + +import org.apache.commons.lang.exception.ExceptionUtils; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationRepositoryWrapper; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.openjpa.persistence.EntityExistsException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class DeviceRegistrationWritePlatformServiceImpl implements + DeviceRegistrationWritePlatformService { + + private final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository; + private final ClientRepositoryWrapper clientRepositoryWrapper; + private final PlatformSecurityContext context; + + @Autowired + public DeviceRegistrationWritePlatformServiceImpl( + final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository, + final ClientRepositoryWrapper clientRepositoryWrapper, + final PlatformSecurityContext context) { + this.deviceRegistrationRepository = deviceRegistrationRepository; + this.clientRepositoryWrapper = clientRepositoryWrapper; + this.context = context; + } + + @Transactional + @Override + public DeviceRegistration registerDevice(Long clientId, + String registrationId) { + this.context.authenticatedUser(); + Client client = this.clientRepositoryWrapper + .findOneWithNotFoundDetection(clientId); + try { + DeviceRegistration deviceRegistration = DeviceRegistration + .instance(client, registrationId); + this.deviceRegistrationRepository.save(deviceRegistration); + return deviceRegistration; + } catch (final EntityExistsException dve) { + handleDataIntegrityIssues(registrationId, dve, dve); + return null; + } catch (final DataIntegrityViolationException dve) { + handleDataIntegrityIssues(registrationId, + dve.getMostSpecificCause(), dve); + return null; + } catch (final PersistenceException dve) { + Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); + handleDataIntegrityIssues(registrationId, throwable, dve); + return null; + } catch (final Exception dve) { + Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); + handleDataIntegrityIssues(registrationId, throwable, dve); + return null; + } + + } + + private void handleDataIntegrityIssues(final String registrationId, + final Throwable realCause, + @SuppressWarnings("unused") final Exception dve) { + + if (realCause.getMessage().contains("registration_key")) { + throw new PlatformDataIntegrityException( + "error.msg.duplicate.device.registration.id", + "Registration id : " + registrationId + " already exist.", + "name", registrationId); + } + + throw new PlatformDataIntegrityException( + "error.msg.charge.unknown.data.integrity.issue", + "Unknown data integrity issue with resource: " + + realCause.getMessage()); + } + + @Override + public DeviceRegistration updateDeviceRegistration(Long id, Long clientId, + String registrationId) { + DeviceRegistration deviceRegistration = this.deviceRegistrationRepository + .findOneWithNotFoundDetection(id); + Client client = this.clientRepositoryWrapper + .findOneWithNotFoundDetection(clientId); + deviceRegistration.setClient(client); + deviceRegistration.setRegistrationId(registrationId); + deviceRegistration.setUpdatedOnDate(DateUtils + .getLocalDateTimeOfTenant().toDate()); + return deviceRegistration; + } + + @Override + public void deleteDeviceRegistration(Long id) { + DeviceRegistration deviceRegistration = this.deviceRegistrationRepository.findOneWithNotFoundDetection(id); + this.deviceRegistrationRepository.delete(deviceRegistration); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/NotificationSenderService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/NotificationSenderService.java new file mode 100644 index 000000000..67fb07237 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/NotificationSenderService.java @@ -0,0 +1,133 @@ +/** + * 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.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.fineract.infrastructure.configuration.service.ExternalServicesPropertiesReadPlatformService; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.gcm.GcmConstants; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationRepositoryWrapper; +import org.apache.fineract.infrastructure.gcm.domain.Message; +import org.apache.fineract.infrastructure.gcm.domain.Message.Builder; +import org.apache.fineract.infrastructure.gcm.domain.Message.Priority; +import org.apache.fineract.infrastructure.gcm.domain.Notification; +import org.apache.fineract.infrastructure.gcm.domain.NotificationConfigurationData; +import org.apache.fineract.infrastructure.gcm.domain.Result; +import org.apache.fineract.infrastructure.gcm.domain.Sender; +import org.apache.fineract.infrastructure.sms.domain.SmsMessage; +import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository; +import org.apache.fineract.infrastructure.sms.domain.SmsMessageStatusType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class NotificationSenderService { + + private final DeviceRegistrationRepositoryWrapper deviceRegistrationRepositoryWrapper; + private final SmsMessageRepository smsMessageRepository; + private ExternalServicesPropertiesReadPlatformService propertiesReadPlatformService; + + @Autowired + public NotificationSenderService( + final DeviceRegistrationRepositoryWrapper deviceRegistrationRepositoryWrapper, + final SmsMessageRepository smsMessageRepository, final ExternalServicesPropertiesReadPlatformService propertiesReadPlatformService) { + this.deviceRegistrationRepositoryWrapper = deviceRegistrationRepositoryWrapper; + this.smsMessageRepository = smsMessageRepository; + this.propertiesReadPlatformService = propertiesReadPlatformService; + } + + public void sendNotification(List smsMessages) { + Map> notificationByEachClient = getNotificationListByClient(smsMessages); + for (Map.Entry> entry : notificationByEachClient + .entrySet()) { + this.sendNotifiaction(entry.getKey(), entry.getValue()); + } + } + + public Map> getNotificationListByClient( + List smsMessages) { + Map> notificationByEachClient = new HashMap<>(); + for (SmsMessage smsMessage : smsMessages) { + if (smsMessage.getClient() != null) { + Long clientId = smsMessage.getClient().getId(); + if (notificationByEachClient.containsKey(clientId)) { + notificationByEachClient.get(clientId).add(smsMessage); + } else { + List msgList = new ArrayList<>( + Arrays.asList(smsMessage)); + notificationByEachClient.put(clientId, msgList); + } + + } + } + return notificationByEachClient; + } + + public void sendNotifiaction(Long clientId, List smsList) { + + DeviceRegistration deviceRegistration = this.deviceRegistrationRepositoryWrapper + .findDeviceRegistrationByClientId(clientId); + NotificationConfigurationData notificationConfigurationData = this.propertiesReadPlatformService.getNotificationConfiguration(); + String registrationId = null; + if (deviceRegistration != null) { + registrationId = deviceRegistration.getRegistrationId(); + } + for (SmsMessage smsMessage : smsList) { + try { + Notification notification = new Notification.Builder( + GcmConstants.defaultIcon).title(GcmConstants.title) + .body(smsMessage.getMessage()).build(); + Builder b = new Builder(); + b.notification(notification); + b.dryRun(false); + b.contentAvailable(true); + b.timeToLive(GcmConstants.TIME_TO_LIVE); + b.priority(Priority.HIGH); + b.delayWhileIdle(true); + Message msg = b.build(); + Sender s = new Sender(notificationConfigurationData.getServerKey(),notificationConfigurationData.getFcmEndPoint()); + Result res; + + res = s.send(msg, registrationId, 3); + if (res.getSuccess() != null && res.getSuccess()>0) { + smsMessage.setStatusType(SmsMessageStatusType.SENT + .getValue()); + smsMessage.setDeliveredOnDate(DateUtils.getLocalDateOfTenant().toDate()); + } else if (res.getFailure() != null && res.getFailure()>0) { + smsMessage.setStatusType(SmsMessageStatusType.FAILED + .getValue()); + } + } catch (IOException e) { + smsMessage + .setStatusType(SmsMessageStatusType.FAILED.getValue()); + } + } + + this.smsMessageRepository.save(smsList); + + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java index f3a160f96..19ad8e14c 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java @@ -46,7 +46,11 @@ public enum JobName { UPDATE_SMS_OUTBOUND_WITH_CAMPAIGN_MESSAGE("Update SMS Outbound with Campaign Message"), SEND_MESSAGES_TO_SMS_GATEWAY("Send Messages to SMS Gateway"), GET_DELIVERY_REPORTS_FROM_SMS_GATEWAY("Get Delivery Reports from SMS Gateway"), - GENERATE_ADHOCCLIENT_SCEHDULE("Generate AdhocClient Schedule"); + GENERATE_ADHOCCLIENT_SCEHDULE("Generate AdhocClient Schedule"), + SEND_MESSAGES_TO_EMAIL_GATEWAY("Send messages to Email gateway"), + UPDATE_EMAIL_OUTBOUND_WITH_CAMPAIGN_MESSAGE("Update Email Outbound with campaign message"), + EXECUTE_EMAIL("Execute Email"); + private final String name; private JobName(final String name) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/domain/ReportMailingJobEmailAttachmentFileFormat.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/domain/ReportMailingJobEmailAttachmentFileFormat.java new file mode 100644 index 000000000..ef24a20d2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/domain/ReportMailingJobEmailAttachmentFileFormat.java @@ -0,0 +1,110 @@ +/** + * 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.reportmailingjob.domain; + +public enum ReportMailingJobEmailAttachmentFileFormat { + INVALID(0, "ReportMailingJobEmailAttachmentFileFormat.invalid", "invalid"), + XLS(1, "ReportMailingJobEmailAttachmentFileFormat.xls", "xls"), + PDF(2, "ReportMailingJobEmailAttachmentFileFormat.pdf", "pdf"), + CSV(3, "ReportMailingJobEmailAttachmentFileFormat.csv", "csv"); + + private String code; + private String value; + private Integer id; + + private ReportMailingJobEmailAttachmentFileFormat(final Integer id, final String code, final String value) { + this.value = value; + this.code = code; + this.id = id; + } + + public static ReportMailingJobEmailAttachmentFileFormat instance(final String value) { + ReportMailingJobEmailAttachmentFileFormat emailAttachmentFileFormat = INVALID; + + switch (value) { + case "xls": + emailAttachmentFileFormat = XLS; + break; + + case "pdf": + emailAttachmentFileFormat = PDF; + break; + + case "csv": + emailAttachmentFileFormat = CSV; + break; + + default: + break; + } + + return emailAttachmentFileFormat; + } + + public static ReportMailingJobEmailAttachmentFileFormat instance(final Integer id) { + ReportMailingJobEmailAttachmentFileFormat emailAttachmentFileFormat = INVALID; + + switch (id) { + case 1: + emailAttachmentFileFormat = XLS; + break; + + case 2: + emailAttachmentFileFormat = PDF; + break; + + case 3: + emailAttachmentFileFormat = CSV; + break; + + default: + break; + } + + return emailAttachmentFileFormat; + } + + /** + * @return the code + */ + public String getCode() { + return code; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @return the id + */ + public Integer getId() { + return id; + } + + /** + * @return list of valid ReportMailingJobEmailAttachmentFileFormat ids + **/ + public static Object[] validValues() { + return new Object[] { XLS.getId(), PDF.getId(), CSV.getId() }; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/helper/IPv4Helper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/helper/IPv4Helper.java new file mode 100644 index 000000000..2281416e1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/helper/IPv4Helper.java @@ -0,0 +1,143 @@ +/** + * 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.reportmailingjob.helper; + +import java.net.InetAddress; + +import org.apache.commons.lang.StringUtils; + +/** + * This utility provides methods to either convert an IPv4 address to its long format or a 32bit dotted format. + * + * @see http://hawkee.com/snippet/9731/ + */ +public class IPv4Helper { + /** + * Returns the long format of the provided IP address. + * + * @param ipAddress the IP address + * @return the long format of ipAddress + * @throws IllegalArgumentException if ipAddress is invalid + */ + public static long ipAddressToLong(String ipAddress) { + if (ipAddress == null || ipAddress.isEmpty()) { + throw new IllegalArgumentException("ip address cannot be null or empty"); + } + + String[] octets = ipAddress.split(java.util.regex.Pattern.quote(".")); + + if (octets.length != 4) { + throw new IllegalArgumentException("invalid ip address"); + } + + long ip = 0; + + for (int i = 3; i >= 0; i--) { + long octet = Long.parseLong(octets[3 - i]); + + if (octet > 255 || octet < 0) { + throw new IllegalArgumentException("invalid ip address"); + } + + ip |= octet << (i * 8); + } + + return ip; + } + + /** + * Returns the 32bit dotted format of the provided long ip. + * + * @param ip the long ip + * @return the 32bit dotted format of ip + * @throws IllegalArgumentException if ip is invalid + */ + public static String longToIpAddress(long ip) { + // if ip is bigger than 255.255.255.255 or smaller than 0.0.0.0 + if (ip > 4294967295l || ip < 0) { + throw new IllegalArgumentException("invalid ip"); + } + + StringBuilder ipAddress = new StringBuilder(); + + for (int i = 3; i >= 0; i--) { + int shift = i * 8; + ipAddress.append((ip & (0xff << shift)) >> shift); + if (i > 0) { + ipAddress.append("."); + } + } + + return ipAddress.toString(); + } + + /** + * check if an IP Address is within a given range of IP Addresses + * + * @param ipAddress -- the IP Address to be checked + * @param startOfRange -- the first IP address in the range + * @param endOfRange -- the last IP address in the range + * @return boolean true if IP address is in the range of IP addresses + **/ + public static boolean ipAddressIsInRange(final String ipAddress, final String startOfRange, final String endOfRange) { + final long ipAddressToLong = ipAddressToLong(ipAddress); + final long startOfRangeToLong = ipAddressToLong(startOfRange); + final long endOfRangeToLong = ipAddressToLong(endOfRange); + + long diff = ipAddressToLong - startOfRangeToLong; + + return (diff >= 0 && (diff <= (endOfRangeToLong - startOfRangeToLong))); + } + + /** + * check if the java application is running on a local machine + * + * @return true if the application is running on a local machine else false + **/ + public static boolean applicationIsRunningOnLocalMachine() { + boolean isRunningOnLocalMachine = false; + + try { + final InetAddress localHost = InetAddress.getLocalHost(); + + if (localHost != null) { + final String hostAddress = localHost.getHostAddress(); + final String startOfIpAddressRange = "127.0.0.0"; + final String endOfIpAddressRange = "127.255.255.255"; + + if (StringUtils.isNotEmpty(hostAddress)) { + isRunningOnLocalMachine = ipAddressIsInRange(hostAddress, startOfIpAddressRange, endOfIpAddressRange); + } + } + } + + catch (Exception exception) { } + + return isRunningOnLocalMachine; + } + + /** + * check if the java application is not running on a local machine + * + * @return true if the application is not running on a local machine else false + **/ + public static boolean applicationIsNotRunningOnLocalMachine() { + return !applicationIsRunningOnLocalMachine(); + } +} 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 80a6a7e36..c99a4fd47 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 @@ -31,8 +31,10 @@ import javax.ws.rs.core.MediaType; import io.swagger.annotations.*; 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; @@ -58,15 +60,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 @@ -104,12 +108,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 04981f919..91aaea0b3 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 @@ -31,8 +31,10 @@ import javax.ws.rs.core.MediaType; import io.swagger.annotations.*; 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; @@ -58,14 +60,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 @@ -100,12 +105,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..63183dd9a --- /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, false); + 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/infrastructure/sms/domain/SmsMessage.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessage.java index e589f00b5..c4966040f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessage.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessage.java @@ -66,7 +66,7 @@ public class SmsMessage extends AbstractPersistableCustom { @Column(name = "status_enum", nullable = false) private Integer statusType; - @Column(name = "mobile_no", nullable = false, length = 50) + @Column(name = "mobile_no", nullable = true, length = 50) private String mobileNo; @Column(name = "message", nullable = false) @@ -84,22 +84,25 @@ public class SmsMessage extends AbstractPersistableCustom { @Column(name = "delivered_on_date", nullable = true) @Temporal(TemporalType.TIMESTAMP) - private Date deliveredOnDate; + private Date deliveredOnDate; + + @Column(name = "is_notification", nullable = true) + private boolean isNotification; public static SmsMessage pendingSms(final String externalId, final Group group, final Client client, final Staff staff, - final String message, final String mobileNo, final SmsCampaign smsCampaign) { - return new SmsMessage(externalId, group, client, staff, SmsMessageStatusType.PENDING, message, mobileNo, smsCampaign); + final String message, final String mobileNo, final SmsCampaign smsCampaign, final boolean isNotification) { + return new SmsMessage(externalId, group, client, staff, SmsMessageStatusType.PENDING, message, mobileNo, smsCampaign, isNotification); } public static SmsMessage sentSms(final String externalId, final Group group, final Client client, final Staff staff, - final String message, final String mobileNo, final SmsCampaign smsCampaign) { - return new SmsMessage(externalId, group, client, staff, SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT, message, mobileNo, smsCampaign); + final String message, final String mobileNo, final SmsCampaign smsCampaign, final boolean isNotification) { + return new SmsMessage(externalId, group, client, staff, SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT, message, mobileNo, smsCampaign, isNotification); } public static SmsMessage instance(String externalId, final Group group, final Client client, final Staff staff, - final SmsMessageStatusType statusType, final String message, final String mobileNo, final SmsCampaign smsCampaign) { + final SmsMessageStatusType statusType, final String message, final String mobileNo, final SmsCampaign smsCampaign, final boolean isNotification) { - return new SmsMessage(externalId, group, client, staff, statusType, message, mobileNo, smsCampaign); + return new SmsMessage(externalId, group, client, staff, statusType, message, mobileNo, smsCampaign, isNotification); } protected SmsMessage() { @@ -107,7 +110,7 @@ public class SmsMessage extends AbstractPersistableCustom { } private SmsMessage(String externalId, final Group group, final Client client, final Staff staff, final SmsMessageStatusType statusType, - final String message, final String mobileNo, final SmsCampaign smsCampaign) { + final String message, final String mobileNo, final SmsCampaign smsCampaign, final boolean isNotification) { this.externalId = externalId; this.group = group; this.client = client; @@ -117,6 +120,7 @@ public class SmsMessage extends AbstractPersistableCustom { this.message = message; this.smsCampaign = smsCampaign; this.submittedOnDate = LocalDate.now().toDate(); + this.isNotification = isNotification; } public Map update(final JsonCommand command) { @@ -183,4 +187,14 @@ public class SmsMessage extends AbstractPersistableCustom { public void setDeliveredOnDate(final Date deliveredOnDate) { this.deliveredOnDate = deliveredOnDate; } + + public boolean isNotification() { + return this.isNotification; + } + + public void setNotification(boolean isNotification) { + this.isNotification = isNotification; + } + + } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessageAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessageAssembler.java index c879ffb6c..32591338b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessageAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessageAssembler.java @@ -71,10 +71,12 @@ public class SmsMessageAssembler { } SmsCampaign smsCampaign = null; + boolean isNotification = false; if (this.fromApiJsonHelper.parameterExists(SmsApiConstants.campaignIdParamName, element)) { final Long campaignId = this.fromApiJsonHelper.extractLongNamed(SmsApiConstants.campaignIdParamName, element); smsCampaign = this.smsCampaignRepository.findOne(campaignId); if (smsCampaign == null) { throw new SmsCampaignNotFound(campaignId); } + isNotification = smsCampaign.isNotification(); } Client client = null; @@ -93,7 +95,7 @@ public class SmsMessageAssembler { final String message = this.fromApiJsonHelper.extractStringNamed(SmsApiConstants.messageParamName, element); - return SmsMessage.pendingSms(externalId, group, client, staff, message, mobileNo, smsCampaign); + return SmsMessage.pendingSms(externalId, group, client, staff, message, mobileNo, smsCampaign, isNotification); } public SmsMessage assembleFromResourceId(final Long resourceId) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/scheduler/SmsMessageScheduledJobServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/scheduler/SmsMessageScheduledJobServiceImpl.java index 373af1d08..e2e998a4f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/scheduler/SmsMessageScheduledJobServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/scheduler/SmsMessageScheduledJobServiceImpl.java @@ -37,6 +37,7 @@ import org.apache.fineract.infrastructure.campaigns.sms.exception.ConnectionFail import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.gcm.service.NotificationSenderService; import org.apache.fineract.infrastructure.jobs.annotation.CronTarget; import org.apache.fineract.infrastructure.jobs.service.JobName; import org.apache.fineract.infrastructure.sms.data.SmsMessageApiQueueResourceData; @@ -76,6 +77,7 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob private ExecutorService genericExecutorService ; private ExecutorService triggeredExecutorService ; private final SmsConfigUtils smsConfigUtils ; + private final NotificationSenderService notificationSenderService; /** @@ -83,10 +85,11 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob **/ @Autowired public SmsMessageScheduledJobServiceImpl(SmsMessageRepository smsMessageRepository, SmsReadPlatformService smsReadPlatformService, - final SmsConfigUtils smsConfigUtils) { + final SmsConfigUtils smsConfigUtils, final NotificationSenderService notificationSenderService) { this.smsMessageRepository = smsMessageRepository; this.smsReadPlatformService = smsReadPlatformService; this.smsConfigUtils = smsConfigUtils ; + this.notificationSenderService = notificationSenderService; } @PostConstruct @@ -110,6 +113,7 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob org.springframework.data.domain.Page pendingMessages = this.smsMessageRepository.findByStatusType( SmsMessageStatusType.PENDING.getValue(), pageRequest); List toSaveMessages = new ArrayList<>() ; + List toSendNotificationMessages = new ArrayList<>() ; try { if (pendingMessages.getContent().size() > 0) { @@ -118,18 +122,26 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob Collection apiQueueResourceDatas = new ArrayList<>(); while (pendingMessageIterator.hasNext()) { SmsMessage smsData = pendingMessageIterator.next(); - - SmsMessageApiQueueResourceData apiQueueResourceData = SmsMessageApiQueueResourceData.instance(smsData.getId(), - tenantIdentifier, null, null, smsData.getMobileNo(), smsData.getMessage(), smsData.getSmsCampaign() - .getProviderId()); - apiQueueResourceDatas.add(apiQueueResourceData); - smsData.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue()); - toSaveMessages.add(smsData) ; + if(smsData.isNotification()){ + smsData.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue()); + toSendNotificationMessages.add(smsData); + }else{ + SmsMessageApiQueueResourceData apiQueueResourceData = SmsMessageApiQueueResourceData.instance(smsData.getId(), + tenantIdentifier, null, null, smsData.getMobileNo(), smsData.getMessage(), smsData.getSmsCampaign() + .getProviderId()); + apiQueueResourceDatas.add(apiQueueResourceData); + smsData.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue()); + toSaveMessages.add(smsData) ; + } + } + if(toSaveMessages.size()>0){ + this.smsMessageRepository.save(toSaveMessages); + this.smsMessageRepository.flush(); + this.genericExecutorService.execute(new SmsTask(ThreadLocalContextUtil.getTenant(), apiQueueResourceDatas)); + } + if(!toSendNotificationMessages.isEmpty()){ + this.notificationSenderService.sendNotification(toSendNotificationMessages); } - this.smsMessageRepository.save(toSaveMessages); - this.smsMessageRepository.flush(); - this.genericExecutorService.execute(new SmsTask(ThreadLocalContextUtil.getTenant(), apiQueueResourceDatas)); - // new MyThread(ThreadLocalContextUtil.getTenant(), apiQueueResourceDatas).start(); } } catch (Exception e) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/Staff.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/Staff.java index 64f6f90d0..175fcb797 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/Staff.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/Staff.java @@ -61,6 +61,9 @@ public class Staff extends AbstractPersistableCustom { @Column(name = "external_id", length = 100, nullable = true, unique = true) private String externalId; + @Column(name = "email_address", length = 50, unique = true) + private String emailAddress; + @ManyToOne @JoinColumn(name = "office_id", nullable = false) private Office office; @@ -246,6 +249,10 @@ public class Staff extends AbstractPersistableCustom { return getId().equals(staff.getId()); } + public String emailAddress() { + return emailAddress; + } + public Long officeId() { return this.office.getId(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java index bc1bbcdd1..76b03711a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java @@ -440,8 +440,8 @@ public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformSer final JournalEntry debitJournalEntry = JournalEntry.createNew(cashierOffice, null, // payment // detail - debitAccount, "USD", // FIXME: Take currency code from the - // transaction + debitAccount, cashierTxn.getCurrencyCode(), + transactionId, false, // manual entry cashierTxn.getTxnDate(), JournalEntryType.DEBIT, cashierTxn.getTxnAmount(), cashierTxn.getTxnNote(), // Description null, null, null, // entity Type, entityId, reference number @@ -449,8 +449,8 @@ public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformSer final JournalEntry creditJournalEntry = JournalEntry.createNew(cashierOffice, null, // payment // detail - creditAccount, "USD", // FIXME: Take currency code from the - // transaction + creditAccount, cashierTxn.getCurrencyCode(), + transactionId, false, // manual entry cashierTxn.getTxnDate(), JournalEntryType.CREDIT, cashierTxn.getTxnAmount(), cashierTxn.getTxnNote(), // Description null, null, null, // entity Type, entityId, reference number diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java index 090cc0a9e..28ed583ae 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java @@ -83,6 +83,7 @@ public class ClientApiConstants { public static final String accountNoParamName = "accountNo"; public static final String externalIdParamName = "externalId"; public static final String mobileNoParamName = "mobileNo"; + public static final String emailAddressParamName = "emailAddress"; public static final String firstnameParamName = "firstname"; public static final String middlenameParamName = "middlename"; public static final String lastnameParamName = "lastname"; @@ -194,7 +195,7 @@ public class ClientApiConstants { protected static final Set CLIENT_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(idParamName, accountNoParamName, externalIdParamName, statusParamName, activeParamName, activationDateParamName, firstnameParamName, middlenameParamName, - lastnameParamName, fullnameParamName, displayNameParamName, mobileNoParamName, officeIdParamName, officeNameParamName, + lastnameParamName, fullnameParamName, displayNameParamName, mobileNoParamName, emailAddressParamName, officeIdParamName, officeNameParamName, transferToOfficeIdParamName, transferToOfficeNameParamName, hierarchyParamName, imageIdParamName, imagePresentParamName, staffIdParamName, staffNameParamName, timelineParamName, groupsParamName, officeOptionsParamName, staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName, legalFormParamName, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java index 04b5fe4de..acf0dc79b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java @@ -29,7 +29,7 @@ public class ClientApiCollectionConstants extends ClientApiConstants{ protected static final Set CLIENT_CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>( Arrays.asList(familyMembers,address,localeParamName, dateFormatParamName, groupIdParamName, accountNoParamName, externalIdParamName, - mobileNoParamName, firstnameParamName, middlenameParamName, lastnameParamName, fullnameParamName, officeIdParamName, + mobileNoParamName, emailAddressParamName, firstnameParamName, middlenameParamName, lastnameParamName, fullnameParamName, officeIdParamName, activeParamName, activationDateParamName, staffIdParamName, submittedOnDateParamName, savingsProductIdParamName, dateOfBirthParamName, genderIdParamName, clientTypeIdParamName, clientClassificationIdParamName, clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, datatables, isStaffParamName)); @@ -39,7 +39,7 @@ public class ClientApiCollectionConstants extends ClientApiConstants{ constitutionIdParamName, mainBusinessLineIdParamName, datatables)); protected static final Set CLIENT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, - dateFormatParamName, accountNoParamName, externalIdParamName, mobileNoParamName, firstnameParamName, middlenameParamName, + dateFormatParamName, accountNoParamName, externalIdParamName, mobileNoParamName, emailAddressParamName, firstnameParamName, middlenameParamName, lastnameParamName, fullnameParamName, activeParamName, activationDateParamName, staffIdParamName, savingsProductIdParamName, dateOfBirthParamName, genderIdParamName, clientTypeIdParamName, clientClassificationIdParamName, submittedOnDateParamName, clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, isStaffParamName)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java index a31a62227..47720eb8a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java @@ -60,6 +60,7 @@ final public class ClientData implements Comparable { private final String fullname; private final String displayName; private final String mobileNo; + private final String emailAddress; private final LocalDate dateOfBirth; private final CodeValueData gender; private final CodeValueData clientType; @@ -133,6 +134,7 @@ final public class ClientData implements Comparable { final String displayName = null; final String externalId = null; final String mobileNo = null; + final String emailAddress = null; final LocalDate dateOfBirth = null; final CodeValueData gender = null; final Long imageId = null; @@ -150,7 +152,7 @@ final public class ClientData implements Comparable { final Boolean isStaff = false; final ClientNonPersonData clientNonPersonDetails = null; return new ClientData(accountNo, status, subStatus, officeId, officeName, transferToOfficeId, transferToOfficeName, id, firstname, - middlename, lastname, fullname, displayName, externalId, mobileNo, dateOfBirth, gender, joinedDate, imageId, staffId, + middlename, lastname, fullname, displayName, externalId, mobileNo, emailAddress, dateOfBirth, gender, joinedDate, imageId, staffId, staffName, officeOptions, groups, staffOptions, narrations, genderOptions, timeline, savingProductOptions, savingsProductId, savingsProductName, savingsAccountId, savingAccountOptions, clientType, clientClassification, clientTypeOptions, clientClassificationOptions, clientNonPersonConstitutionOptions, clientNonPersonMainBusinessLineOptions, @@ -162,7 +164,7 @@ final public class ClientData implements Comparable { return new ClientData(clientData.accountNo, clientData.status, clientData.subStatus, clientData.officeId, clientData.officeName, clientData.transferToOfficeId, clientData.transferToOfficeName, clientData.id, clientData.firstname, clientData.middlename, - clientData.lastname, clientData.fullname, clientData.displayName, clientData.externalId, clientData.mobileNo, + clientData.lastname, clientData.fullname, clientData.displayName, clientData.externalId, clientData.mobileNo, clientData.emailAddress, clientData.dateOfBirth, clientData.gender, clientData.activationDate, clientData.imageId, clientData.staffId, clientData.staffName, templateData.officeOptions, clientData.groups, templateData.staffOptions, templateData.narrations, templateData.genderOptions, clientData.timeline, templateData.savingProductOptions, clientData.savingsProductId, @@ -178,7 +180,7 @@ final public class ClientData implements Comparable { return new ClientData(clientData.accountNo, clientData.status, clientData.subStatus, clientData.officeId, clientData.officeName, clientData.transferToOfficeId, clientData.transferToOfficeName, clientData.id, clientData.firstname, clientData.middlename, - clientData.lastname, clientData.fullname, clientData.displayName, clientData.externalId, clientData.mobileNo, + clientData.lastname, clientData.fullname, clientData.displayName, clientData.externalId, clientData.mobileNo, clientData.emailAddress, clientData.dateOfBirth, clientData.gender, clientData.activationDate, clientData.imageId, clientData.staffId, clientData.staffName, clientData.officeOptions, clientData.groups, clientData.staffOptions, clientData.narrations, clientData.genderOptions, clientData.timeline, clientData.savingProductOptions, clientData.savingsProductId, @@ -192,7 +194,7 @@ final public class ClientData implements Comparable { public static ClientData setParentGroups(final ClientData clientData, final Collection parentGroups) { return new ClientData(clientData.accountNo, clientData.status, clientData.subStatus, clientData.officeId, clientData.officeName, clientData.transferToOfficeId, clientData.transferToOfficeName, clientData.id, clientData.firstname, clientData.middlename, - clientData.lastname, clientData.fullname, clientData.displayName, clientData.externalId, clientData.mobileNo, + clientData.lastname, clientData.fullname, clientData.displayName, clientData.externalId, clientData.mobileNo, clientData.emailAddress, clientData.dateOfBirth, clientData.gender, clientData.activationDate, clientData.imageId, clientData.staffId, clientData.staffName, clientData.officeOptions, parentGroups, clientData.staffOptions, null, null, clientData.timeline, clientData.savingProductOptions, clientData.savingsProductId, clientData.savingsProductName, clientData.savingsAccountId, @@ -210,6 +212,7 @@ final public class ClientData implements Comparable { final String transferToOfficeName = null; final String externalId = null; final String mobileNo = null; + final String emailAddress = null; final LocalDate dateOfBirth = null; final CodeValueData gender = null; final LocalDate activationDate = null; @@ -241,7 +244,7 @@ final public class ClientData implements Comparable { final Boolean isStaff = false; final ClientNonPersonData clientNonPerson = null; return new ClientData(accountNo, status, subStatus, officeId, officeName, transferToOfficeId, transferToOfficeName, id, firstname, - middlename, lastname, fullname, displayName, externalId, mobileNo, dateOfBirth, gender, activationDate, imageId, staffId, + middlename, lastname, fullname, displayName, externalId, mobileNo, emailAddress, dateOfBirth, gender, activationDate, imageId, staffId, staffName, allowedOffices, groups, staffOptions, closureReasons, genderOptions, timeline, savingProductOptions, savingsProductId, savingsProductName, savingsAccountId, savingAccountOptions, clientType, clientClassification, clientTypeOptions, clientClassificationOptions, clientNonPersonConstitutionOptions, clientNonPersonMainBusinessLineOptions, @@ -260,6 +263,7 @@ final public class ClientData implements Comparable { final String fullname = null; final String externalId = null; final String mobileNo = null; + final String emailAddress = null; final LocalDate dateOfBirth = null; final CodeValueData gender = null; final LocalDate activationDate = null; @@ -289,18 +293,24 @@ final public class ClientData implements Comparable { final Boolean isStaff = false; final ClientNonPersonData clientNonPerson = null; return new ClientData(accountNo, status, subStatus, officeId, officeName, transferToOfficeId, transferToOfficeName, id, firstname, - middlename, lastname, fullname, displayName, externalId, mobileNo, dateOfBirth, gender, activationDate, imageId, staffId, + middlename, lastname, fullname, displayName, externalId, mobileNo, emailAddress, dateOfBirth, gender, activationDate, imageId, staffId, staffName, allowedOffices, groups, staffOptions, closureReasons, genderOptions, timeline, savingProductOptions, savingsProductId, savingsProductName, savingsAccountId, savingAccountOptions, clientType, clientClassification, clientTypeOptions, clientClassificationOptions, clientNonPersonConstitutionOptions, clientNonPersonMainBusinessLineOptions, clientNonPerson, clientLegalFormOptions,familyMemberOptions, legalForm,null,null, null, isStaff); } + + public static ClientData instance(final Long id, final String displayName){ + final Long officeId = null; + final String officeName = null; + return lookup(id, displayName, officeId, officeName); + } public static ClientData instance(final String accountNo, final EnumOptionData status, final CodeValueData subStatus, final Long officeId, final String officeName, final Long transferToOfficeId, final String transferToOfficeName, final Long id, final String firstname, final String middlename, final String lastname, final String fullname, final String displayName, - final String externalId, final String mobileNo, final LocalDate dateOfBirth, final CodeValueData gender, + final String externalId, final String mobileNo, final String emailAddress, final LocalDate dateOfBirth, final CodeValueData gender, final LocalDate activationDate, final Long imageId, final Long staffId, final String staffName, final ClientTimelineData timeline, final Long savingsProductId, final String savingsProductName, final Long savingsAccountId, final CodeValueData clientType, final CodeValueData clientClassification, final EnumOptionData legalForm, final ClientNonPersonData clientNonPerson, final Boolean isStaff) { @@ -318,7 +328,7 @@ final public class ClientData implements Comparable { final List clientLegalFormOptions = null; final ClientFamilyMembersData familyMemberOptions=null; return new ClientData(accountNo, status, subStatus, officeId, officeName, transferToOfficeId, transferToOfficeName, id, firstname, - middlename, lastname, fullname, displayName, externalId, mobileNo, dateOfBirth, gender, activationDate, imageId, staffId, + middlename, lastname, fullname, displayName, externalId, mobileNo, emailAddress, dateOfBirth, gender, activationDate, imageId, staffId, staffName, allowedOffices, groups, staffOptions, closureReasons, genderOptions, timeline, savingProductOptions, savingsProductId, savingsProductName, savingsAccountId, null, clientType, clientClassification, clientTypeOptions, clientClassificationOptions, clientNonPersonConstitutionOptions, clientNonPersonMainBusinessLineOptions, clientNonPerson, @@ -329,7 +339,7 @@ final public class ClientData implements Comparable { private ClientData(final String accountNo, final EnumOptionData status, final CodeValueData subStatus, final Long officeId, final String officeName, final Long transferToOfficeId, final String transferToOfficeName, final Long id, final String firstname, final String middlename, final String lastname, final String fullname, final String displayName, - final String externalId, final String mobileNo, final LocalDate dateOfBirth, final CodeValueData gender, + final String externalId, final String mobileNo, final String emailAddress, final LocalDate dateOfBirth, final CodeValueData gender, final LocalDate activationDate, final Long imageId, final Long staffId, final String staffName, final Collection allowedOffices, final Collection groups, final Collection staffOptions, final Collection narrations, @@ -361,6 +371,7 @@ final public class ClientData implements Comparable { this.displayName = StringUtils.defaultIfEmpty(displayName, null); this.externalId = StringUtils.defaultIfEmpty(externalId, null); this.mobileNo = StringUtils.defaultIfEmpty(mobileNo, null); + this.emailAddress = StringUtils.defaultIfEmpty(emailAddress, null); this.activationDate = activationDate; this.dateOfBirth = dateOfBirth; this.gender = gender; @@ -450,6 +461,7 @@ final public class ClientData implements Comparable { .append(this.id, obj.id) // .append(this.displayName, obj.displayName) // .append(this.mobileNo, obj.mobileNo) // + .append(this.emailAddress, obj.emailAddress) // .toComparison(); } @@ -463,6 +475,7 @@ final public class ClientData implements Comparable { .append(this.id, rhs.id) // .append(this.displayName, rhs.displayName) // .append(this.mobileNo, rhs.mobileNo) // + .append(this.emailAddress, rhs.emailAddress) // .isEquals(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java index 08425f7f5..1b1b3938b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java @@ -114,6 +114,9 @@ public final class Client extends AbstractPersistableCustom { @Column(name = "mobile_no", length = 50, nullable = false, unique = true) private String mobileNo; + + @Column(name = "email_address", length = 50, unique = true) + private String emailAddress; @Column(name = "is_staff", nullable = false) private boolean isStaff; @@ -236,6 +239,7 @@ public final class Client extends AbstractPersistableCustom { final String accountNo = command.stringValueOfParameterNamed(ClientApiConstants.accountNoParamName); final String externalId = command.stringValueOfParameterNamed(ClientApiConstants.externalIdParamName); final String mobileNo = command.stringValueOfParameterNamed(ClientApiConstants.mobileNoParamName); + final String emailAddress = command.stringValueOfParameterNamed(ClientApiConstants.emailAddressParamName); final String firstname = command.stringValueOfParameterNamed(ClientApiConstants.firstnameParamName); final String middlename = command.stringValueOfParameterNamed(ClientApiConstants.middlenameParamName); @@ -269,7 +273,7 @@ public final class Client extends AbstractPersistableCustom { } final Long savingsAccountId = null; return new Client(currentUser, status, clientOffice, clientParentGroup, accountNo, firstname, middlename, lastname, fullname, - activationDate, officeJoiningDate, externalId, mobileNo, staff, submittedOnDate, savingsProductId, savingsAccountId, dataOfBirth, + activationDate, officeJoiningDate, externalId, mobileNo, emailAddress, staff, submittedOnDate, savingsProductId, savingsAccountId, dataOfBirth, gender, clientType, clientClassification, legalForm, isStaff); } @@ -279,7 +283,7 @@ public final class Client extends AbstractPersistableCustom { private Client(final AppUser currentUser, final ClientStatus status, final Office office, final Group clientParentGroup, final String accountNo, final String firstname, final String middlename, final String lastname, final String fullname, - final LocalDate activationDate, final LocalDate officeJoiningDate, final String externalId, final String mobileNo, + final LocalDate activationDate, final LocalDate officeJoiningDate, final String externalId, final String mobileNo, final String emailAddress, final Staff staff, final LocalDate submittedOnDate, final Long savingsProductId, final Long savingsAccountId, final LocalDate dateOfBirth, final CodeValue gender, final CodeValue clientType, final CodeValue clientClassification, final Integer legalForm, final Boolean isStaff) { @@ -307,6 +311,12 @@ public final class Client extends AbstractPersistableCustom { this.mobileNo = null; } + if (StringUtils.isNotBlank(emailAddress)) { + this.emailAddress = emailAddress.trim(); + } else { + this.emailAddress = null; + } + if (activationDate != null) { this.activationDate = activationDate.toDateTimeAtStartOfDay().toDate(); this.activatedBy = currentUser; @@ -498,6 +508,12 @@ public final class Client extends AbstractPersistableCustom { actualChanges.put(ClientApiConstants.mobileNoParamName, newValue); this.mobileNo = StringUtils.defaultIfEmpty(newValue, null); } + + if (command.isChangeInStringParameterNamed(ClientApiConstants.emailAddressParamName, this.emailAddress)) { + final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.emailAddressParamName); + actualChanges.put(ClientApiConstants.emailAddressParamName, newValue); + this.emailAddress = StringUtils.defaultIfEmpty(newValue, null); + } if (command.isChangeInStringParameterNamed(ClientApiConstants.firstnameParamName, this.firstname)) { final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.firstnameParamName); @@ -771,6 +787,10 @@ public final class Client extends AbstractPersistableCustom { return this.mobileNo; } + public String emailAddress() { + return this.emailAddress; + } + public void setMobileNo(final String mobileNo) { this.mobileNo = mobileNo; } @@ -787,6 +807,10 @@ public final class Client extends AbstractPersistableCustom { return this.externalId; } + public void setEmailAddress(final String emailAddress) { + this.emailAddress = emailAddress; + } + public String getDisplayName() { return this.displayName; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java index b5e88259b..4d54c1e96 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java @@ -363,6 +363,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService sqlBuilder.append("c.fullname as fullname, c.display_name as displayName, "); sqlBuilder.append("c.mobile_no as mobileNo, "); sqlBuilder.append("c.is_staff as isStaff, "); + sqlBuilder.append("c.email_address as emailAddress, "); sqlBuilder.append("c.date_of_birth as dateOfBirth, "); sqlBuilder.append("c.gender_cv_id as genderId, "); sqlBuilder.append("cv.code_value as genderValue, "); @@ -452,6 +453,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String externalId = rs.getString("externalId"); final String mobileNo = rs.getString("mobileNo"); final boolean isStaff = rs.getBoolean("isStaff"); + final String emailAddress = rs.getString("emailAddress"); final LocalDate dateOfBirth = JdbcSupport.getLocalDate(rs, "dateOfBirth"); final Long genderId = JdbcSupport.getLong(rs, "genderId"); final String genderValue = rs.getString("genderValue"); @@ -511,7 +513,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService closedByUsername, closedByFirstname, closedByLastname); return ClientData.instance(accountNo, status, subStatus, officeId, officeName, transferToOfficeId, transferToOfficeName, id, - firstname, middlename, lastname, fullname, displayName, externalId, mobileNo, dateOfBirth, gender, activationDate, + firstname, middlename, lastname, fullname, displayName, externalId, mobileNo, emailAddress, dateOfBirth, gender, activationDate, imageId, staffId, staffName, timeline, savingsProductId, savingsProductName, savingsAccountId, clienttype, classification, legalForm, clientNonPerson, isStaff); @@ -547,6 +549,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService builder.append("c.fullname as fullname, c.display_name as displayName, "); builder.append("c.mobile_no as mobileNo, "); builder.append("c.is_staff as isStaff, "); + builder.append("c.email_address as emailAddress, "); builder.append("c.date_of_birth as dateOfBirth, "); builder.append("c.gender_cv_id as genderId, "); builder.append("cv.code_value as genderValue, "); @@ -635,6 +638,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String externalId = rs.getString("externalId"); final String mobileNo = rs.getString("mobileNo"); final boolean isStaff = rs.getBoolean("isStaff"); + final String emailAddress = rs.getString("emailAddress"); final LocalDate dateOfBirth = JdbcSupport.getLocalDate(rs, "dateOfBirth"); final Long genderId = JdbcSupport.getLong(rs, "genderId"); final String genderValue = rs.getString("genderValue"); @@ -693,7 +697,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService closedByUsername, closedByFirstname, closedByLastname); return ClientData.instance(accountNo, status, subStatus, officeId, officeName, transferToOfficeId, transferToOfficeName, id, - firstname, middlename, lastname, fullname, displayName, externalId, mobileNo, dateOfBirth, gender, activationDate, + firstname, middlename, lastname, fullname, displayName, externalId, mobileNo, emailAddress, dateOfBirth, gender, activationDate, imageId, staffId, staffName, timeline, savingsProductId, savingsProductName, savingsAccountId, clienttype, classification, legalForm, clientNonPerson, isStaff); 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/domain/LoanRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index fe17368b1..2db0f7607 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -29,10 +29,10 @@ import org.springframework.data.repository.query.Param; public interface LoanRepository extends JpaRepository, JpaSpecificationExecutor { - public static final String FIND_GROUP_LOANS_DISBURSED_AFTER = "select l from Loan l where l.actualDisbursementDate > :disbursementDate and " + public static final String FIND_GROUP_LOANS_DISBURSED_AFTER = "select l from Loan l where ( l.actualDisbursementDate IS NOT NULL and l.actualDisbursementDate > :disbursementDate) and " + "l.group.id = :groupId and l.loanType = :loanType order by l.actualDisbursementDate"; - public static final String FIND_CLIENT_OR_JLG_LOANS_DISBURSED_AFTER = "select l from Loan l where l.actualDisbursementDate > :disbursementDate and " + public static final String FIND_CLIENT_OR_JLG_LOANS_DISBURSED_AFTER = "select l from Loan l where (l.actualDisbursementDate IS NOT NULL and l.actualDisbursementDate > :disbursementDate) and " + "l.client.id = :clientId order by l.actualDisbursementDate"; public static final String FIND_MAX_GROUP_LOAN_COUNTER_QUERY = "Select MAX(l.loanCounter) from Loan l where l.group.id = :groupId " 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/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index cde266c61..5f09e6835 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -320,7 +320,7 @@ public class FixedDepositAccount extends SavingsAccount { final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, transactions, this.currency, compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), maturityDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, - isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting); + isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting, financialYearBeginningMonth); periodStartingBalance = postingPeriod.closingBalance(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java index 8e276a933..4ad538e3e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java @@ -352,7 +352,7 @@ public class RecurringDepositAccount extends SavingsAccount { final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, transactions, this.currency, compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), maturityDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, - isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting); + isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting, financialYearBeginningMonth); periodStartingBalance = postingPeriod.closingBalance(); 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..3def00ae5 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 @@ -749,7 +749,7 @@ public class SavingsAccount extends AbstractPersistableCustom { retreiveOrderedNonInterestPostingTransactions(), this.currency, compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, - overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting); + overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); periodStartingBalance = postingPeriod.closingBalance(); @@ -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) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java index 2341288cf..6e4070c23 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java @@ -22,8 +22,10 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.TreeSet; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; @@ -33,8 +35,7 @@ import org.joda.time.DateTime; import org.joda.time.LocalDate; public class PostingPeriod { - - @SuppressWarnings("unused") + private final LocalDateInterval periodInterval; private final MonetaryCurrency currency; private final SavingsCompoundingInterestPeriodType interestCompoundingType; @@ -60,13 +61,15 @@ public class PostingPeriod { private final Money minBalanceForInterestCalculation; private BigDecimal overdraftInterestRateAsFraction; private Money minOverdraftForInterestCalculation; + + private Integer financialYearBeginningMonth; public static PostingPeriod createFrom(final LocalDateInterval periodInterval, final Money periodStartingBalance, final List orderedListOfTransactions, final MonetaryCurrency currency, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestRateAsFraction, final long daysInYear, final LocalDate upToInterestCalculationDate, Collection interestPostTransactions, boolean isInterestTransfer, - final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,final boolean isUserPosting) { + final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,final boolean isUserPosting, Integer financialYearBeginningMonth) { final BigDecimal overdraftInterestRateAsFraction = BigDecimal.ZERO; final Money minOverdraftForInterestCalculation = Money.zero(currency); @@ -75,7 +78,7 @@ public class PostingPeriod { interestCompoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYear, upToInterestCalculationDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, - overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting); + overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); } // isInterestTransfer boolean is to identify newly created transaction is @@ -86,7 +89,7 @@ public class PostingPeriod { final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestRateAsFraction, final long daysInYear, final LocalDate upToInterestCalculationDate, Collection interestPostTransactions, boolean isInterestTransfer, final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, - final BigDecimal overdraftInterestRateAsFraction, final Money minOverdraftForInterestCalculation, boolean isUserPosting) { + final BigDecimal overdraftInterestRateAsFraction, final Money minOverdraftForInterestCalculation, boolean isUserPosting, int financialYearBeginningMonth) { final List accountEndOfDayBalances = new ArrayList<>(); boolean interestTransfered = false; @@ -143,12 +146,12 @@ public class PostingPeriod { } final List compoundingPeriods = compoundingPeriodsInPostingPeriod(periodInterval, interestCompoundingPeriodType, - accountEndOfDayBalances, upToInterestCalculationDate); + accountEndOfDayBalances, upToInterestCalculationDate, financialYearBeginningMonth); return new PostingPeriod(periodInterval, currency, periodStartingBalance, openingDayBalance, interestCompoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYear, compoundingPeriods, interestTransfered, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, - overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting); + overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); } private PostingPeriod(final LocalDateInterval periodInterval, final MonetaryCurrency currency, final Money openingBalance, @@ -156,7 +159,7 @@ public class PostingPeriod { final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestRateAsFraction, final long daysInYear, final List compoundingPeriods, boolean interestTransfered, final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final BigDecimal overdraftInterestRateAsFraction, - final Money minOverdraftForInterestCalculation, boolean isUserPosting) { + final Money minOverdraftForInterestCalculation, boolean isUserPosting, Integer financialYearBeginningMonth) { this.periodInterval = periodInterval; this.currency = currency; this.openingBalance = openingBalance; @@ -176,6 +179,7 @@ public class PostingPeriod { this.overdraftInterestRateAsFraction = overdraftInterestRateAsFraction; this.minOverdraftForInterestCalculation = minOverdraftForInterestCalculation; this.isUserPosting = isUserPosting; + this.financialYearBeginningMonth = financialYearBeginningMonth; } public Money interest() { @@ -211,7 +215,7 @@ public class PostingPeriod { if (!SavingsCompoundingInterestPeriodType.DAILY.equals(this.interestCompoundingType)) { compoundingPeriodEndDate = determineInterestPeriodEndDateFrom( compoundingPeriod.getPeriodInterval().startDate(), this.interestCompoundingType, - compoundingPeriod.getPeriodInterval().endDate()); + compoundingPeriod.getPeriodInterval().endDate(), this.getFinancialYearBeginningMonth()); } if (compoundingPeriodEndDate.equals(compoundingPeriod.getPeriodInterval().endDate())) { @@ -235,7 +239,7 @@ public class PostingPeriod { private static List compoundingPeriodsInPostingPeriod(final LocalDateInterval postingPeriodInterval, final SavingsCompoundingInterestPeriodType interestPeriodType, final List allEndOfDayBalances, - final LocalDate upToInterestCalculationDate) { + final LocalDate upToInterestCalculationDate, int financialYearBeginningMonth) { final List compoundingPeriods = new ArrayList<>(); @@ -256,7 +260,7 @@ public class PostingPeriod { while (!periodStartDate.isAfter(postingPeriodEndDate) && !periodEndDate.isAfter(postingPeriodEndDate)) { - periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate); + periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate, financialYearBeginningMonth); if (periodEndDate.isAfter(postingPeriodEndDate)) { periodEndDate = postingPeriodEndDate; } @@ -285,7 +289,7 @@ public class PostingPeriod { while (!periodStartDate.isAfter(qPostingPeriodEndDate) && !periodEndDate.isAfter(qPostingPeriodEndDate)) { - periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate); + periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate, financialYearBeginningMonth); if (periodEndDate.isAfter(qPostingPeriodEndDate)) { periodEndDate = qPostingPeriodEndDate; } @@ -310,7 +314,7 @@ public class PostingPeriod { while (!periodStartDate.isAfter(bPostingPeriodEndDate) && !periodEndDate.isAfter(bPostingPeriodEndDate)) { - periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate); + periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate, financialYearBeginningMonth); if (periodEndDate.isAfter(bPostingPeriodEndDate)) { periodEndDate = bPostingPeriodEndDate; } @@ -335,7 +339,7 @@ public class PostingPeriod { while (!periodStartDate.isAfter(aPostingPeriodEndDate) && !periodEndDate.isAfter(aPostingPeriodEndDate)) { - periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate); + periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate, financialYearBeginningMonth); if (periodEndDate.isAfter(aPostingPeriodEndDate)) { periodEndDate = aPostingPeriodEndDate; } @@ -360,10 +364,14 @@ public class PostingPeriod { } private static LocalDate determineInterestPeriodEndDateFrom(final LocalDate periodStartDate, - final SavingsCompoundingInterestPeriodType interestPeriodType, final LocalDate upToInterestCalculationDate) { + final SavingsCompoundingInterestPeriodType interestPeriodType, final LocalDate upToInterestCalculationDate, int financialYearBeginningMonth) { LocalDate periodEndDate = upToInterestCalculationDate; - + int previousMonth = financialYearBeginningMonth-1; + if(previousMonth==0){ + previousMonth = 12; + } + int periodsInMonth = 1; switch (interestPeriodType) { case INVALID: break; @@ -383,34 +391,20 @@ public class PostingPeriod { periodEndDate = periodStartDate.dayOfMonth().withMaximumValue(); break; case QUATERLY: - // // jan 1st to mar 31st, 1st apr to jun 30, jul 1st to sept - // 30, - // // oct 1st to dec 31 - int year = periodStartDate.getYearOfEra(); - int monthofYear = periodStartDate.getMonthOfYear(); - if (monthofYear <= 3) { - periodEndDate = new DateTime().withDate(year, 3, 31).toLocalDate(); - } else if (monthofYear <= 6) { - periodEndDate = new DateTime().withDate(year, 6, 30).toLocalDate(); - } else if (monthofYear <= 9) { - periodEndDate = new DateTime().withDate(year, 9, 30).toLocalDate(); - } else if (monthofYear <= 12) { - periodEndDate = new DateTime().withDate(year, 12, 31).toLocalDate(); - } + periodsInMonth = 4; + periodEndDate = getPeriodEndDate(periodEndDate, previousMonth, periodsInMonth, periodStartDate); break; - case BI_ANNUAL: - // // jan 1st to 30, jul 1st to dec 31 - year = periodStartDate.getYearOfEra(); - monthofYear = periodStartDate.getMonthOfYear(); - if (monthofYear <= 6) { - periodEndDate = new DateTime().withDate(year, 6, 30).toLocalDate(); - } else if (monthofYear <= 12) { - periodEndDate = new DateTime().withDate(year, 12, 31).toLocalDate(); - } + case BI_ANNUAL: + periodsInMonth = 2; + periodEndDate = getPeriodEndDate(periodEndDate, previousMonth, periodsInMonth, periodStartDate); + break; case ANNUAL: - periodEndDate = periodStartDate.monthOfYear().withMaximumValue(); + periodEndDate = periodStartDate.withMonthOfYear(previousMonth); periodEndDate = periodEndDate.dayOfMonth().withMaximumValue(); + if(periodEndDate.isBefore(periodStartDate)){ + periodEndDate = periodEndDate.plusYears(1); + } break; // case NO_COMPOUNDING_SIMPLE_INTEREST: @@ -422,6 +416,40 @@ public class PostingPeriod { return periodEndDate; } + private static LocalDate getPeriodEndDate(LocalDate periodEndDate, int previousMonth, int periodsInMonth, LocalDate periodStartDate) { + int year = periodStartDate.getYearOfEra(); + int monthofYear = periodStartDate.getMonthOfYear(); + LocalDate date = DateUtils.getLocalDateOfTenant(); + TreeSet monthSet = new TreeSet<>(); + date = date.withMonthOfYear(previousMonth); + monthSet.add(date.getMonthOfYear()); + int count =0; + while(count<(periodsInMonth-1)){ + date = date.plusMonths((12/periodsInMonth)); + monthSet.add(date.getMonthOfYear()); + count++; + } + boolean notInRange = true; + /* + * if strt date is 2016-10-05 + * if financial year set to 4 then applicable month will be march and september that is (3,9) + * if start date fall in month of oct,nov or dec then month will be 10, 11 or 12 + * so period end date should be taken from next year for march month + */ + + for (Integer month : monthSet) { + if(monthofYear<=month.intValue()){ + periodEndDate = new DateTime().withDate(year, month, DateUtils.getLocalDateOfTenant().withMonthOfYear(month).dayOfMonth().withMaximumValue().getDayOfMonth()).toLocalDate(); + notInRange = false; + break; + } + } + if(notInRange){ + periodEndDate = new DateTime().withDate(year+1, monthSet.first(), DateUtils.getLocalDateOfTenant().withMonthOfYear(monthSet.first()).dayOfMonth().withMaximumValue().getDayOfMonth()).toLocalDate(); + } + return periodEndDate; + } + public boolean isInterestTransfered() { return this.interestTransfered; } @@ -433,4 +461,11 @@ public class PostingPeriod { public boolean isUserPosting() { return this.isUserPosting; } + + + public Integer getFinancialYearBeginningMonth() { + return this.financialYearBeginningMonth; + } + + } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java index 9952dcf58..8b0b6a48c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java @@ -203,8 +203,9 @@ public class SelfServiceRegistrationWritePlatformServiceImpl implements SelfServ Group group = null; Staff staff = null; SmsCampaign smsCampaign = null; + boolean isNotification = false; SmsMessage smsMessage = SmsMessage.instance(externalId, group, selfServiceRegistration.getClient(), staff, - SmsMessageStatusType.PENDING, message, selfServiceRegistration.getMobileNumber(), smsCampaign); + SmsMessageStatusType.PENDING, message, selfServiceRegistration.getMobileNumber(), smsCampaign, isNotification); this.smsMessageRepository.save(smsMessage); this.smsMessageScheduledJobService.sendTriggeredMessage(new ArrayList<>(Arrays.asList(smsMessage)), providerId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/spm/api/SpmApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/spm/api/SpmApiResource.java index cc97c1d55..8b8244f6b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/spm/api/SpmApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/spm/api/SpmApiResource.java @@ -32,8 +32,10 @@ import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; +import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.spm.data.SurveyData; import org.apache.fineract.spm.domain.Survey; @@ -67,10 +69,15 @@ public class SpmApiResource { @Transactional @ApiOperation(value = "List all Surveys", notes = "") @ApiResponses({@ApiResponse(code = 200, message = "", response = SurveyData.class, responseContainer = "list")}) - public List fetchActiveSurveys() { + public List fetchAllSurveys(@QueryParam("isActive") final Boolean isActive) { this.securityContext.authenticatedUser(); final List result = new ArrayList<>(); - final List surveys = this.spmService.fetchValidSurveys(); + List surveys = null; + if(isActive != null && isActive){ + surveys = this.spmService.fetchValidSurveys(); + }else{ + surveys = this.spmService.fetchAllSurveys(); + } if (surveys != null) { for (final Survey survey : surveys) { result.add(SurveyMapper.map(survey)); @@ -119,16 +126,23 @@ public class SpmApiResource { return getResponse(survey.getId()); } - @DELETE + @POST @Path("/{id}") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Transactional @ApiOperation(value = "Deactivate Survey", notes = "") @ApiResponses({@ApiResponse(code = 200, message = "OK")}) - public void deactivateSurvey(@PathParam("id") @ApiParam(value = "Enter id") final Long id) { + public void activateOrDeactivateSurvey(@PathParam("id") final Long id, @QueryParam("command") final String command) { this.securityContext.authenticatedUser(); - this.spmService.deactivateSurvey(id); + if(command != null && command.equalsIgnoreCase("activate")){ + this.spmService.activateSurvey(id);; + }else if(command != null && command.equalsIgnoreCase("deactivate")){ + this.spmService.deactivateSurvey(id); + }else{ + throw new UnrecognizedQueryParamException("command", command); + } + } private String getResponse(Long id) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/spm/exception/SurveyResponseNotAvailableException.java b/fineract-provider/src/main/java/org/apache/fineract/spm/exception/SurveyResponseNotAvailableException.java new file mode 100644 index 000000000..a194b6cf3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/spm/exception/SurveyResponseNotAvailableException.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.spm.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + + +public class SurveyResponseNotAvailableException extends AbstractPlatformDomainRuleException { + + public SurveyResponseNotAvailableException() { + super("error.msg.no.survey.response","No response available for survey."); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/spm/repository/SurveyRepository.java b/fineract-provider/src/main/java/org/apache/fineract/spm/repository/SurveyRepository.java index 6c6344ac9..4e60376ae 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/spm/repository/SurveyRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/spm/repository/SurveyRepository.java @@ -30,6 +30,9 @@ public interface SurveyRepository extends JpaRepository { @Query("select s from Survey s where :pointInTime between s.validFrom and s.validTo") List fetchActiveSurveys(@Param("pointInTime") final Date pointInTime); + + @Query("select s from Survey s ") + List fetchAllSurveys(); @Query("select s from Survey s where s.key = :key and :pointInTime between s.validFrom and s.validTo") Survey findByKey(@Param("key") final String key, @Param("pointInTime") final Date pointInTime); diff --git a/fineract-provider/src/main/java/org/apache/fineract/spm/service/SpmService.java b/fineract-provider/src/main/java/org/apache/fineract/spm/service/SpmService.java index 8d6f0fa24..a1e837a0b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/spm/service/SpmService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/spm/service/SpmService.java @@ -61,6 +61,12 @@ public class SpmService { return this.surveyRepository.fetchActiveSurveys(new Date()); } + + public List fetchAllSurveys() { + this.securityContext.authenticatedUser(); + + return this.surveyRepository.fetchAllSurveys(); + } public Survey findById(final Long id) { this.securityContext.authenticatedUser(); @@ -127,6 +133,21 @@ public class SpmService { this.surveyRepository.save(survey); } + public void activateSurvey(final Long id) { + this.securityContext.authenticatedUser(); + + final Survey survey = findById(id); + LocalDate validFrom = DateUtils.getLocalDateOfTenant() ; + Calendar cal = Calendar.getInstance() ; + cal.setTime(validFrom.toDate()); + cal.add(Calendar.YEAR, 100); + survey.setValidFrom(validFrom.toDate()); + survey.setValidTo(cal.getTime()); + + this.surveyRepository.save(survey); + } + + public static DateTime getStartOfToday() { return DateTime.now().withHourOfDay(0).withMinuteOfHour(0).withSecondOfMinute(0).withMillisOfSecond(0); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/spm/util/ScorecardMapper.java b/fineract-provider/src/main/java/org/apache/fineract/spm/util/ScorecardMapper.java index 60588d8ea..573c52d51 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/spm/util/ScorecardMapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/spm/util/ScorecardMapper.java @@ -29,6 +29,7 @@ import org.apache.fineract.spm.domain.Question; import org.apache.fineract.spm.domain.Response; import org.apache.fineract.spm.domain.Scorecard; import org.apache.fineract.spm.domain.Survey; +import org.apache.fineract.spm.exception.SurveyResponseNotAvailableException; import org.apache.fineract.useradministration.domain.AppUser; public class ScorecardMapper { @@ -43,7 +44,7 @@ public class ScorecardMapper { final List scorecardValues = scorecardData.getScorecardValues(); - if (scorecardValues != null) { + if (scorecardValues != null && !scorecardValues.isEmpty()) { for (ScorecardValue scorecardValue : scorecardValues) { final Scorecard scorecard = new Scorecard(); scorecards.add(scorecard); @@ -54,6 +55,8 @@ public class ScorecardMapper { scorecard.setCreatedOn(DateUtils.getLocalDateOfTenant().toDate()); scorecard.setValue(scorecardValue.getValue()); } + }else{ + throw new SurveyResponseNotAvailableException(); } return scorecards; } 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/appContext.xml b/fineract-provider/src/main/resources/META-INF/spring/appContext.xml index ce8364c8f..9326183b5 100644 --- a/fineract-provider/src/main/resources/META-INF/spring/appContext.xml +++ b/fineract-provider/src/main/resources/META-INF/spring/appContext.xml @@ -51,13 +51,14 @@ org.apache.fineract.portfolio.savingsaccount.*, org.apache.fineract.portfolio.*, org.apache.fineract.useradministration.*, - org.apache.fineract.mix.*, + org.apache.fineract.mix.*, org.apache.fineract.notification.*, org.apache.fineract.template.*, org.apache.fineract.template.service.*, org.apache.fineract.useradministration.*, org.apache.fineract.batch, - org.apache.fineract.adhocquery.*"> + org.apache.fineract.adhocquery.*, + org.apache.fineract.infrastructure.campaigns.**"> @@ -84,6 +85,8 @@ + + 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/V322_1__scheduled_email_campaign.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V322_1__scheduled_email_campaign.sql new file mode 100644 index 000000000..f581a0c5c --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V322_1__scheduled_email_campaign.sql @@ -0,0 +1,169 @@ +-- +-- 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. +-- +create table if not exists scheduled_email_campaign ( +id bigint(20) NOT NULL AUTO_INCREMENT, +campaign_name varchar(100) NOT NULL, +campaign_type int NOT NULL, +businessRule_id int NOT NULL, +param_value text, +status_enum int NOT NULL, +closedon_date date, +closedon_userid bigint(20), +submittedon_date date, +submittedon_userid bigint(20), +approvedon_date date, +approvedon_userid bigint(20), +recurrence varchar(100), +next_trigger_date datetime, +last_trigger_date datetime, +recurrence_start_date datetime, +email_subject varchar(100) not null, +email_message text not null, +email_attachment_file_format varchar(10) not null, +stretchy_report_id int not null, +stretchy_report_param_map text null, +previous_run_status varchar(10) null, +previous_run_error_log text null, +previous_run_error_message text null, +is_visible tinyint(1) null, +foreign key (submittedon_userid) references m_appuser(id), +foreign key (stretchy_report_id) references stretchy_report(id), + PRIMARY KEY (id) +)ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS scheduled_email_messages_outbound ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `group_id` bigint(20) DEFAULT NULL, + `client_id` bigint(20) DEFAULT NULL, + `staff_id` bigint(20) DEFAULT NULL, + `email_campaign_id` bigint(20) DEFAULT NULL, + `status_enum` int(5) NOT NULL DEFAULT '100', + `email_address` varchar(50) NOT NULL, + `email_subject` varchar(50) NOT NULL, + `message` text NOT NULL, + `campaign_name` varchar(200) DEFAULT NULL, + `submittedon_date` date, + `error_message` text, + PRIMARY KEY (`id`), + KEY `SEFKGROUP000000001` (`group_id`), + KEY `SEFKCLIENT00000001` (`client_id`), + key `SEFKSTAFF000000001` (`staff_id`), + CONSTRAINT `SEFKGROUP000000001` FOREIGN KEY (`group_id`) REFERENCES `m_group` (`id`), + CONSTRAINT `SEFKCLIENT00000001` FOREIGN KEY (`client_id`) REFERENCES `m_client` (`id`), + CONSTRAINT `SEFKSTAFF000000001` FOREIGN KEY (`staff_id`) REFERENCES `m_staff` (`id`), + CONSTRAINT `fk_schedule_email_campign` FOREIGN KEY (`email_campaign_id`) REFERENCES `scheduled_email_campaign` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +create table if not exists scheduled_email_configuration ( +id int primary key auto_increment, +name varchar(50) not null, +`value` varchar(200) null, +constraint unique_name unique (name) +); + +DELETE FROM `m_permission` WHERE `code`='READ_EMAIL'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'READ_EMAIL', 'EMAIL', 'READ', 0); + +DELETE FROM `m_permission` WHERE `code`='CREATE_EMAIL'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'CREATE_EMAIL', 'EMAIL', 'CREATE', 0); + +DELETE FROM `m_permission` WHERE `code`='CREATE_EMAIL_CHECKER'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'CREATE_EMAIL_CHECKER', 'EMAIL', 'CREATE_CHECKER', 0); + +DELETE FROM `m_permission` WHERE `code`='UPDATE_EMAIL'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'UPDATE_EMAIL', 'EMAIL', 'UPDATE', 0); + +DELETE FROM `m_permission` WHERE `code`='UPDATE_EMAIL_CHECKER'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'UPDATE_EMAIL_CHECKER', 'EMAIL', 'UPDATE_CHECKER', 0); + +DELETE FROM `m_permission` WHERE `code`='DELETE_EMAIL'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'DELETE_EMAIL', 'EMAIL', 'DELETE', 0); + +DELETE FROM `m_permission` WHERE `code`='DELETE_EMAIL_CHECKER'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'DELETE_EMAIL_CHECKER', 'EMAIL', 'DELETE_CHECKER', 0); + +DELETE FROM `m_permission` WHERE `code`='READ_EMAIL_CAMPAIGN'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'READ_EMAIL_CAMPAIGN', 'EMAIL_CAMPAIGN', 'READ', 0); + +DELETE FROM `m_permission` WHERE `code`='CREATE_EMAIL_CAMPAIGN'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'CREATE_EMAIL_CAMPAIGN', 'EMAIL_CAMPAIGN', 'CREATE', 0); + +DELETE FROM `m_permission` WHERE `code`='CREATE_EMAIL_CAMPAIGN_CHECKER'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'CREATE_EMAIL_CAMPAIGN_CHECKER', 'EMAIL_CAMPAIGN', 'CREATE_CHECKER', 0); + +DELETE FROM `m_permission` WHERE `code`='UPDATE_EMAIL_CAMPAIGN'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'UPDATE_EMAIL_CAMPAIGN', 'EMAIL_CAMPAIGN', 'UPDATE', 0); + +DELETE FROM `m_permission` WHERE `code`='UPDATE_EMAIL_CAMPAIGN_CHECKER'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'UPDATE_EMAIL_CAMPAIGN_CHECKER', 'EMAIL_CAMPAIGN', 'UPDATE_CHECKER', 0); + +DELETE FROM `m_permission` WHERE `code`='DELETE_EMAIL_CAMPAIGN'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'DELETE_EMAIL_CAMPAIGN', 'EMAIL_CAMPAIGN', 'DELETE', 0); + +DELETE FROM `m_permission` WHERE `code`='DELETE_EMAIL_CAMPAIGN_CHECKER'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'DELETE_EMAIL_CAMPAIGN_CHECKER', 'EMAIL_CAMPAIGN', 'DELETE_CHECKER', 0); + +DELETE FROM `m_permission` WHERE `code`='CLOSE_EMAIL_CAMPAIGN'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'CLOSE_EMAIL_CAMPAIGN', 'EMAIL_CAMPAIGN', 'CLOSE', 0); + +DELETE FROM `m_permission` WHERE `code`='ACTIVATE_EMAIL_CAMPAIGN'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'ACTIVATE_EMAIL_CAMPAIGN', 'EMAIL_CAMPAIGN', 'ACTIVATE', 0); + +DELETE FROM `m_permission` WHERE `code`='REACTIVATE_EMAIL_CAMPAIGN'; +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'REACTIVATE_EMAIL_CAMPAIGN', 'EMAIL_CAMPAIGN', 'REACTIVATE', 0); + + +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('organisation', 'READ_EMAIL_CONFIGURATION', 'EMAIL_CONFIGURATION', 'READ', 0), +('organisation', 'UPDATE_EMAIL_CONFIGURATION', 'EMAIL_CONFIGURATION', 'UPDATE', 0); + +Alter table m_client +ADD Column email_address varchar(150); + +Alter table m_staff +ADD Column email_address varchar(150); + + +insert into job (name, display_name, cron_expression, create_time) +values ('Execute Email', 'Execute Email', '0 0/10 * * * ?', NOW()); + +insert into job (name, display_name, cron_expression, create_time) +values ('Update Email Outbound with campaign message', 'Update Email Outbound with campaign message', '0 0/15 * * * ?', NOW()); + +INSERT INTO `scheduled_email_configuration` (`name`) +VALUES ('SMTP_SERVER'), +('SMTP_PORT'),('SMTP_USERNAME'), ('SMTP_PASSWORD'); + diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V322_2__email_business_rules.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V322_2__email_business_rules.sql new file mode 100644 index 000000000..c17cb5128 --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V322_2__email_business_rules.sql @@ -0,0 +1,126 @@ +-- +-- 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. +-- +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Active Clients - Email', 'Email', 'SELECT mc.id, mc.firstname, ifnull(mc.middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, mc.email_address as EmailAddress,\nmo.name as officename, ifnull(od.phoneNumber,\'\') as officenummber\nFROM\nm_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_client mc on mc.office_id = ounder.id\nleft join ml_office_details as od on od.office_id = mo.id\nWHERE mc.status_enum = 300 and mc.email_address is not null\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1)', 'All clients with the status ‘Active’', '0', '0'); +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Prospective Clients - Email', 'Email', 'select mc.id,mo.name as OfficeName, mc.firstname, ifnull(mc.middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, mc.email_address as EmailAddress,ifnull(od.phoneNumber,\'\') as officenummber\nFROM\nm_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_client mc on mc.office_id = ounder.id\nleft join ml_office_details as od on od.office_id = mo.id\nwhere\n(\nSELECT count(ml.id) as loansCount\nFROM m_loan ml\nWhere ml.client_id = mc.id and (ml.writtenoffon_date>=CURDATE() OR ml.writtenoffon_date IS NULL)\nAND disbursedon_date<=CURDATE()\n) = 0\nAND mc.activation_dateCURDATE() OR mc.closedon_date IS NULL)\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1)', 'All clients with the status ‘Active’ who have never had a loan before', '0', '0'); +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Active Loan Clients - Email', 'Email', '(select mc.id, mc.firstname, ifnull(mc.middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, email_address as EmailAddress, round(ml.principal_amount, ml.currency_digits) as LoanAmount, round(ml.`total_outstanding_derived`, ml.currency_digits) as LoanOutstanding, ml.`disbursedon_date` as LoanDisbursed,\nifnull(od.phoneNumber,\'\') as officenummber,ml.`account_no` as LoanAccountId,ifnull(GROUP_CONCAT(DISTINCT t.lastname SEPARATOR \', \') ,\'\') as guarantorLastname,sum(t.totalGuarantors) as numberOfGuarantors,ifnull(mg.display_name,\'individual\') as groupName\nFROM m_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_client mc on mc.office_id = ounder.id\nLEFT join m_group_client mgc on mgc.client_id = mc.id\nLeft join m_group mg on mg.id = mgc.group_id\nleft join ml_office_details as od on od.office_id = mo.id\nright join m_loan as ml on mc.id = ml.client_id\nleft join (\nselect mg.loan_id,mc.lastname,count(mg.id) as totalGuarantors\nfrom m_guarantor mg\nleft join m_client mc on mc.id = mg.entity_id\nright join m_guarantor_funding_details mgf on mgf.guarantor_id = mg.id\nwhere mg.entity_id is not null\ngroup by mg.id\n) as t on t.loan_id = ml.id\nwhere mc.status_enum = 300\nand email_address is not null\nand ml.id is not null\nand ml.`loan_status_id` = 300\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1)\nand ifnull(ml.loan_counter,0) between ${cycleX} and ${cycleY}\ngroup by mc.id,ml.id )\nunion\n(\nselect mc.id, mc.firstname, ifnull(mc.middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, email_address as EmailAddress, round(ml.principal_amount, ml.currency_digits) as LoanAmount, round(ml.`total_outstanding_derived`, ml.currency_digits) as LoanOutstanding, ml.`disbursedon_date` as LoanDisbursed,\nifnull(od.phoneNumber,\'\') as officenummber,ml.`account_no` as LoanAccountId,ifnull(GROUP_CONCAT(DISTINCT t.lastname SEPARATOR \', \') ,\'\') as guarantorLastname,sum(t.totalGuarantors) as numberOfGuarantors,mg.display_name as groupName\nFROM m_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_group mg on mg.office_id = ounder.id\nLEFT join m_group_client mgc on mgc.group_id = mg.id\nleft join m_client as mc on mgc.client_id = mc.id\nleft join ml_office_details as od on od.office_id = mo.id\nright join m_loan as ml on ml.group_id = mg.id and ml.group_id is not null\nleft join (\nselect mg.loan_id,mc.lastname,count(mg.id) as totalGuarantors\nfrom m_guarantor mg\nleft join m_client mc on mc.id = mg.entity_id\nright join m_guarantor_funding_details mgf on mgf.guarantor_id = mg.id\nwhere mg.entity_id is not null\ngroup by mg.id\n) as t on t.loan_id = ml.id\nwhere mc.status_enum = 300 and mg.status_enum = 300\nand email_address is not null\nand ml.id is not null\nand ml.`loan_status_id` = 300\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mg.staff_id = ${staffId} or ${staffId} = -1)\nand ml.group_id is not null\nand ifnull(ml.loan_counter,0) between ${cycleX} and ${cycleY}\ngroup by mc.id,ml.id\n)', 'All clients with an outstanding loan', '0', '0'); +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Loans in arrears - Email', 'Email', '(select mc.id, mc.firstname, ifnull(mc.middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, email_address as EmailAddress, round(ml.principal_amount, ml.currency_digits) as LoanAmount, round(ml.`total_outstanding_derived`, ml.currency_digits) as LoanOutstanding, ml.`disbursedon_date` as LoanDisbursed, mls.`duedate` as PaymentDueDate,ifnull(laa.total_overdue_derived,0) as TotalDue,\nifnull(od.phoneNumber,\'\') as officenummber,ml.`account_no` as LoanAccountId,ifnull(GROUP_CONCAT(DISTINCT t.lastname SEPARATOR \', \') ,\'\') as guarantorLastname,sum(t.totalGuarantors) as numberOfGuarantors,ifnull(mg.display_name,\'individual\') as groupName\nFROM m_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_client mc on mc.office_id = ounder.id\nLEFT join m_group_client mgc on mgc.client_id = mc.id\nLeft join m_group mg on mg.id = mgc.group_id\nleft join ml_office_details as od on od.office_id = mo.id\nright join m_loan as ml on mc.id = ml.client_id\nleft join (\nselect mg.loan_id,mc.lastname,count(mg.id) as totalGuarantors\nfrom m_guarantor mg\nleft join m_client mc on mc.id = mg.entity_id\nright join m_guarantor_funding_details mgf on mgf.guarantor_id = mg.id\nwhere mg.entity_id is not null\ngroup by mg.id\n) as t on t.loan_id = ml.id\nright join m_loan_arrears_aging as laa on laa.loan_id = ml.id\nright join m_loan_repayment_schedule as mls on ml.id = mls.loan_id and (mls.`duedate` BETWEEN DATE_SUB(CURDATE(), Interval ${toY} Day) and DATE_SUB(CURDATE(), Interval ${fromX} Day)) and mls.`completed_derived` = 0\nand mls.installment = (SELECT MIN(installment) from m_loan_repayment_schedule where loan_id = ml.id and duedate <= CURDATE() and completed_derived=0)\nwhere mc.status_enum = 300 and email_address is not null and ml.`loan_status_id` = 300\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1)\ngroup by ml.id )\nunion\n(\nselect mc.id, mc.firstname, ifnull(mc.middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, email_address as EmailAddress, round(ml.principal_amount, ml.currency_digits) as LoanAmount, round(ml.`total_outstanding_derived`, ml.currency_digits) as LoanOutstanding, ml.`disbursedon_date` as LoanDisbursed, mls.`duedate` as PaymentDueDate,ifnull(laa.total_overdue_derived,0) as TotalDue,\nifnull(od.phoneNumber,\'\') as officenummber,ml.`account_no` as LoanAccountId,ifnull(GROUP_CONCAT(DISTINCT t.lastname SEPARATOR \', \') ,\'\') as guarantorLastname,sum(t.totalGuarantors) as numberOfGuarantors,mg.display_name as groupName\nFROM m_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_group mg on mg.office_id = ounder.id\nLEFT join m_group_client mgc on mgc.group_id = mg.id\nleft join m_client as mc on mgc.client_id = mc.id\nleft join ml_office_details as od on od.office_id = mo.id\nright join m_loan as ml on ml.group_id = mg.id and ml.group_id is not null\nleft join (\nselect mg.loan_id,mc.lastname,count(mg.id) as totalGuarantors\nfrom m_guarantor mg\nleft join m_client mc on mc.id = mg.entity_id\nright join m_guarantor_funding_details mgf on mgf.guarantor_id = mg.id\nwhere mg.entity_id is not null\ngroup by mg.id\n) as t on t.loan_id = ml.id\nright join m_loan_arrears_aging as laa on laa.loan_id = ml.id\nright join m_loan_repayment_schedule as mls on ml.id = mls.loan_id and (mls.`duedate` BETWEEN DATE_SUB(CURDATE(), Interval ${toY} Day) and DATE_SUB(CURDATE(), Interval ${fromX} Day)) and mls.`completed_derived` = 0\nand mls.installment = (SELECT MIN(installment) from m_loan_repayment_schedule where loan_id = ml.id and duedate <= CURDATE() and completed_derived=0)\nwhere mc.status_enum = 300 and email_address is not null and ml.`loan_status_id` = 300 and mg.status_enum = 300 and ml.group_id is not null\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1)\ngroup by mc.id,ml.id)', 'All clients with an outstanding loan in arrears between X and Y days', '0', '0'); +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Loans disbursed to clients - Email', 'Email', '(select mc.id, mc.firstname, ifnull(middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, email_address as EmailAddress, round(ml.principal_amount, ml.currency_digits) as LoanAmount, round(ml.`total_outstanding_derived`, ml.currency_digits) as LoanOutstanding, ml.`disbursedon_date` as LoanDisbursed,\nifnull(od.phoneNumber,\'\') as officenummber,ml.`account_no` as LoanAccountId,ifnull(GROUP_CONCAT(DISTINCT t.lastname SEPARATOR \', \') ,\'\') as guarantorLastname,sum(t.totalGuarantors) as numberOfGuarantors,mls.duedate,(ifnull(mls.principal_amount,0) + ifnull(mls.interest_amount,0) + ifnull(mls.fee_charges_amount,0) + ifnull(mls.penalty_charges_amount,0)) - (ifnull(mls.principal_completed_derived,0) + ifnull(mls.interest_completed_derived,0) + ifnull(mls.fee_charges_completed_derived,0) + ifnull(mls.penalty_charges_completed_derived,0)) as TotalDue,ifnull(mg.display_name,\'individual\') as groupName\nFROM m_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_client mc on mc.office_id = ounder.id\nLEFT join m_group_client mgc on mgc.client_id = mc.id\nLeft join m_group mg on mg.id = mgc.group_id\nleft join ml_office_details as od on od.office_id = mo.id\nright join m_loan as ml on mc.id = ml.client_id and (ml.`disbursedon_date` BETWEEN DATE_SUB(CURDATE(), Interval ${toY} Day) and DATE_SUB(CURDATE(), Interval ${fromX} Day))\nright join m_loan_repayment_schedule as mls on ml.id = mls.loan_id\nand mls.installment = (SELECT MIN(installment) from m_loan_repayment_schedule where loan_id = ml.id and duedate >= CURDATE() and obligations_met_on_date is null)\nleft join (\nselect mg.loan_id,mc.lastname,count(mg.id) as totalGuarantors\nfrom m_guarantor mg\nleft join m_client mc on mc.id = mg.entity_id\nright join m_guarantor_funding_details mgf on mgf.guarantor_id = mg.id\nwhere mg.entity_id is not null\ngroup by mg.id\n) as t on t.loan_id = ml.id\nwhere mc.status_enum = 300 and email_address is not null and ml.`loan_status_id` = 300\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1)\nand IFNULL(ml.loan_counter,0) between ${cycleX} and ${cycleY}\ngroup by ml.id )\nunion\n(select mc.id, mc.firstname, ifnull(middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, email_address as EmailAddress, round(ml.principal_amount, ml.currency_digits) as LoanAmount, round(ml.`total_outstanding_derived`, ml.currency_digits) as LoanOutstanding, ml.`disbursedon_date` as LoanDisbursed,\nifnull(od.phoneNumber,\'\') as officenummber,ml.`account_no` as LoanAccountId,ifnull(GROUP_CONCAT(DISTINCT t.lastname SEPARATOR \', \') ,\'\') as guarantorLastname,sum(t.totalGuarantors) as numberOfGuarantors,mls.duedate,(ifnull(mls.principal_amount,0) + ifnull(mls.interest_amount,0) + ifnull(mls.fee_charges_amount,0) + ifnull(mls.penalty_charges_amount,0)) - (ifnull(mls.principal_completed_derived,0) + ifnull(mls.interest_completed_derived,0) + ifnull(mls.fee_charges_completed_derived,0) + ifnull(mls.penalty_charges_completed_derived,0)) as TotalDue,mg.display_name as groupName\nFROM m_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_group mg on mg.office_id = ounder.id\nLEFT join m_group_client mgc on mgc.group_id = mg.id\nleft join m_client as mc on mgc.client_id = mc.id\n\nleft join ml_office_details as od on od.office_id = mo.id\nright join m_loan as ml on ml.group_id = mg.id and (ml.`disbursedon_date` BETWEEN DATE_SUB(CURDATE(), Interval ${toY} Day) and DATE_SUB(CURDATE(), Interval ${fromX} Day))\nright join m_loan_repayment_schedule as mls on ml.id = mls.loan_id\nand mls.installment = (SELECT MIN(installment) from m_loan_repayment_schedule where loan_id = ml.id and duedate >= CURDATE() and obligations_met_on_date is null)\nleft join (\nselect mg.loan_id,mc.lastname,count(mg.id) as totalGuarantors\nfrom m_guarantor mg\nleft join m_client mc on mc.id = mg.entity_id\nright join m_guarantor_funding_details mgf on mgf.guarantor_id = mg.id\nwhere mg.entity_id is not null\ngroup by mg.id\n) as t on t.loan_id = ml.id\nwhere mc.status_enum = 300 and email_address is not null and ml.`loan_status_id` = 300 and mg.status_enum = 300\nand (mo.id = ${officeId} or ${officeId} = -1) and ml.group_id is not null\nand (mc.staff_id = ${staffId} or ${staffId} = -1)\nand IFNULL(ml.loan_counter,0) between ${cycleX} and ${cycleY}\ngroup by ml.id,mc.id)', 'All clients who have had a loan disbursed to them in the last X to Y days', '0', '0'); +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Loan payments due - Email', 'Email', '(select mc.id, mc.firstname, ifnull(middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, email_address as EmailAddress, round(ml.principal_amount, ml.currency_digits) as LoanAmount,ifnull(od.phoneNumber,\'\') as officenummber,\nround(ml.`total_outstanding_derived`, ml.currency_digits) as LoanOutstanding, ml.`disbursedon_date` as LoanDisbursed, mls.`duedate` as PaymentDueDate,\n(ifnull(mls.principal_amount,0) + ifnull(mls.interest_amount,0) + ifnull(mls.fee_charges_amount,0) + ifnull(mls.penalty_charges_amount,0)) - (ifnull(mls.principal_completed_derived,0) + ifnull(mls.interest_completed_derived,0) + ifnull(mls.fee_charges_completed_derived,0) + ifnull(mls.penalty_charges_completed_derived,0)) + ifnull(laa.total_overdue_derived,0) as TotalDue,\nifnull(laa.total_overdue_derived,0) as TotalOverdue,ml.`account_no` as LoanAccountId,ifnull(GROUP_CONCAT(DISTINCT t.lastname SEPARATOR \', \') ,\'\') as guarantorLastname,sum(t.totalGuarantors) as numberOfGuarantors,ifnull(mg.display_name,\'individual\') as groupName\nFROM m_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_client mc on mc.office_id = ounder.id\nLEFT join m_group_client mgc on mgc.client_id = mc.id\nLeft join m_group mg on mg.id = mgc.group_id\nleft join ml_office_details as od on od.office_id = mo.id\nright join m_loan as ml on mc.id = ml.client_id\nleft join (\nselect mg.loan_id,mc.lastname,count(mg.id) as totalGuarantors\nfrom m_guarantor mg\nleft join m_client mc on mc.id = mg.entity_id\nright join m_guarantor_funding_details mgf on mgf.guarantor_id = mg.id\nwhere mg.entity_id is not null\ngroup by mg.id\n) as t on t.loan_id = ml.id\nleft join m_loan_arrears_aging as laa on laa.loan_id = ml.id\nright join m_loan_repayment_schedule as mls on ml.id = mls.loan_id and (mls.`duedate` BETWEEN DATE_ADD(CURDATE(), Interval ${fromX} Day) and DATE_ADD(CURDATE(), Interval ${toY} Day)) and mls.`completed_derived` = 0\nwhere mc.status_enum = 300 and email_address is not null and ml.`loan_status_id` = 300\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1) group by ml.id )\nunion\n(\nselect mc.id, mc.firstname, ifnull(middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, email_address as EmailAddress, round(ml.principal_amount, ml.currency_digits) as LoanAmount,ifnull(od.phoneNumber,\'\') as officenummber,\nround(ml.`total_outstanding_derived`, ml.currency_digits) as LoanOutstanding, ml.`disbursedon_date` as LoanDisbursed, mls.`duedate` as PaymentDueDate,\n(ifnull(mls.principal_amount,0) + ifnull(mls.interest_amount,0) + ifnull(mls.fee_charges_amount,0) + ifnull(mls.penalty_charges_amount,0)) - (ifnull(mls.principal_completed_derived,0) + ifnull(mls.interest_completed_derived,0) + ifnull(mls.fee_charges_completed_derived,0) + ifnull(mls.penalty_charges_completed_derived,0)) + ifnull(laa.total_overdue_derived,0) as TotalDue,\nifnull(laa.total_overdue_derived,0) as TotalOverdue,ml.`account_no` as LoanAccountId,ifnull(GROUP_CONCAT(DISTINCT t.lastname SEPARATOR \', \') ,\'\') as guarantorLastname,sum(t.totalGuarantors) as numberOfGuarantors,mg.display_name as groupName\nFROM m_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_group mg on mg.office_id = ounder.id\nLEFT join m_group_client mgc on mgc.group_id = mg.id\nleft join m_client as mc on mgc.client_id = mc.id\nleft join ml_office_details as od on od.office_id = mo.id\nright join m_loan as ml on ml.group_id = mg.id and ml.group_id is not null\nleft join (\nselect mg.loan_id,mc.lastname,count(mg.id) as totalGuarantors\nfrom m_guarantor mg\nleft join m_client mc on mc.id = mg.entity_id\nright join m_guarantor_funding_details mgf on mgf.guarantor_id = mg.id\nwhere mg.entity_id is not null\ngroup by mg.id\n) as t on t.loan_id = ml.id\nleft join m_loan_arrears_aging as laa on laa.loan_id = ml.id\nright join m_loan_repayment_schedule as mls on ml.id = mls.loan_id and (mls.`duedate` BETWEEN DATE_ADD(CURDATE(), Interval ${fromX} Day) and DATE_ADD(CURDATE(), Interval ${toY} Day)) and mls.`completed_derived` = 0\nwhere mc.status_enum = 300 and email_address is not null and ml.`loan_status_id` = 300 and mg.status_enum = 300 and ml.group_id is not null\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1) group by ml.id,mc.id)', 'All clients with an unpaid installment due on their loan between X and Y days', '0', '0'); +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Dormant Prospects - Email', 'Email', 'select mo.name as OfficeName, mc.firstname, ifnull(mc.middlename,\"\") as middlename, mc.lastname, mc.display_name as FullName, mc.email_address as EmailAddress,ifnull(od.phoneNumber,\"\") as officenummber\nFROM\nm_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \"%\")\nAND ounder.hierarchy like CONCAT(\".\", \"%\")\nLEFT JOIN m_client mc on mc.office_id = ounder.id\nleft join ml_office_details as od on od.office_id = mo.id\nwhere\n(\nSELECT count(ml.id) as loansCount\nFROM m_loan ml\nWHERE ml.client_id = mc.id\nand (ml.writtenoffon_date>=CURDATE()\nOR ml.writtenoffon_date IS NULL )\nAND disbursedon_date<=CURDATE()\n) = 0\nAND IFNULL(DATEDIFF(CURDATE(), mc.`activation_date`),0) >90\nAND (mc.closedon_date >CURDATE() OR mc.closedon_date IS NULL)\nAND mc.activation_date ml.expected_maturedon_date and (ml.expected_maturedon_date BETWEEN DATE_SUB(CURDATE(), Interval ${toY} Day) and DATE_SUB(CURDATE(), Interval ${fromX} Day))\nleft join (\nselect mg.loan_id,mc.lastname,count(mg.id) as totalGuarantors\nfrom m_guarantor mg\nleft join m_client mc on mc.id = mg.entity_id\nright join m_guarantor_funding_details mgf on mgf.guarantor_id = mg.id\nwhere mg.entity_id is not null\ngroup by mg.id\n) as t on t.loan_id = ml.id\nright join m_loan_arrears_aging as laa on laa.loan_id = ml.id\nright join m_loan_repayment_schedule as mls on ml.id = mls.loan_id and (mls.`duedate` BETWEEN DATE_SUB(CURDATE(), Interval ${toY} Day) and DATE_SUB(CURDATE(), Interval ${fromX} Day)) and mls.`completed_derived` = 0\nwhere mc.status_enum = 300 and email_address is not null\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1)\ngroup by ml.id )\nunion\n(\nselect mc.id, mc.firstname, ifnull(mc.middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, email_address as EmailAddress, ml.principal_amount as LoanAmount, ml.`total_outstanding_derived` as LoanOutstanding, ml.`disbursedon_date` as LoanDisbursed, mls.`duedate` as PaymentDueDate,ifnull(laa.total_overdue_derived,0) as TotalDue,\nifnull(od.phoneNumber,\'\') as officenummber,ml.`account_no` as LoanAccountId,ifnull(GROUP_CONCAT(DISTINCT t.lastname SEPARATOR \", \") ,\'\') as guarantorLastname,sum(t.totalGuarantors) as numberOfGuarantors,mg.display_name as groupName\nFROM m_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_group mg on mg.office_id = ounder.id\nLEFT join m_group_client mgc on mgc.group_id = mg.id\nleft join m_client as mc on mgc.client_id = mc.id\nleft join ml_office_details as od on od.office_id = mo.id\nright join m_loan as ml on ml.group_id = mg.id and ml.group_id is not null and curdate() > ml.expected_maturedon_date and (ml.expected_maturedon_date BETWEEN DATE_SUB(CURDATE(), Interval ${toY} Day) and DATE_SUB(CURDATE(), Interval ${fromX} Day))\nleft join (\nselect mg.loan_id,mc.lastname,count(mg.id) as totalGuarantors\nfrom m_guarantor mg\nleft join m_client mc on mc.id = mg.entity_id\nright join m_guarantor_funding_details mgf on mgf.guarantor_id = mg.id\nwhere mg.entity_id is not null\ngroup by mg.id\n) as t on t.loan_id = ml.id\nright join m_loan_arrears_aging as laa on laa.loan_id = ml.id\nright join m_loan_repayment_schedule as mls on ml.id = mls.loan_id and (mls.`duedate` BETWEEN DATE_SUB(CURDATE(), Interval ${toY} Day) and DATE_SUB(CURDATE(), Interval ${fromX} Day)) and mls.`completed_derived` = 0\nwhere mc.status_enum = 300 and email_address is not null and ml.`loan_status_id` = 300 and mg.status_enum = 300 and ml.group_id is not null\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1)\ngroup by mc.id,ml.id)', 'All active loans (with an outstanding balance) between X to Y days after the final instalment date on their loan schedule', '0', '0'); +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Happy Birthday - Email', 'Email', 'SELECT mc.id, mc.firstname, ifnull(mc.middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, mc.email_address as EmailAddress,\nmo.name as officename, ifnull(od.phoneNumber,\'\') as officenummber, mc.date_of_birth as dateOfBirth,TIMESTAMPDIFF(YEAR,mc.date_of_birth,CURDATE()) AS age\nFROM\nm_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN m_client mc on mc.office_id = ounder.id\nleft join ml_office_details as od on od.office_id = mo.id\nWHERE mc.status_enum = 300 and mc.email_address is not null\nand (mo.id = 1 or 1 = -1)\nand (mc.staff_id = -1 or -1 = -1)\n AND (\n MONTH(mc.date_of_birth) = MONTH(NOW())\n AND DAY(mc.date_of_birth) = DAY(NOW())\n ) OR (\n MONTH(mc.date_of_birth) = 2 AND DAY(mc.date_of_birth) = 29\n AND MONTH(NOW()) = 3 AND DAY(NOW()) = 1\n AND (YEAR(NOW()) % 4 = 0)\n AND ((YEAR(NOW()) % 100 != 0) OR (YEAR(NOW()) % 400 = 0))\n )\n group by mc.id', 'This sends a message to all clients with the status Active on their Birthday', '0', '0'); +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_subtype`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Loan Rejected - Email', 'Email', 'Triggered', 'SELECT mc.id, mc.firstname, mc.middlename as middlename, mc.lastname, mc.display_name as FullName, mc.email_address as EmailAddress, mc.group_name as GroupName,\n mo.name as officename, ifnull(od.phoneNumber,\'\') as officenummber, ml.id as loanId, ml.account_no as accountnumber, ml.principal_amount_proposed as loanamount, ml.annual_nominal_interest_rate as annualinterestrate\n FROM\n m_office mo\n JOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\n AND ounder.hierarchy like CONCAT(\'.\', \'%\')\n LEFT JOIN (\n select \n ml.id as loanId, \n ifnull(mc.id,mc2.id) as id, \n ifnull(mc.firstname,mc2.firstname) as firstname, \n ifnull(mc.middlename,ifnull(mc2.middlename,(\'\'))) as middlename, \n ifnull(mc.lastname,mc2.lastname) as lastname, \n ifnull(mc.display_name,mc2.display_name) as display_name, \n ifnull(mc.status_enum,mc2.status_enum) as status_enum,\n ifnull(mc.email_address,mc2.email_address) as email_address,\n ifnull(mg.office_id,mc2.office_id) as office_id,\n ifnull(mg.staff_id,mc2.staff_id) as staff_id,\n mg.id as group_id, \nmg.display_name as group_name\n from\n m_loan ml\n left join m_group mg on mg.id = ml.group_id\n left join m_group_client mgc on mgc.group_id = mg.id\n left join m_client mc on mc.id = mgc.client_id\n left join m_client mc2 on mc2.id = ml.client_id\n order by loanId\n ) mc on mc.office_id = ounder.id\n left join ml_office_details as od on od.office_id = mo.id\n left join m_loan ml on ml.id = mc.loanId\n WHERE mc.status_enum = 300 and mc.email_address is not null\n and (mo.id = ${officeId} or ${officeId} = -1)\n and (mc.staff_id = ${staffId} or ${staffId} = -1)\nand (ml.id = ${loanId} or ${loanId} = -1)\nand (mc.id = ${clientId} or ${clientId} = -1)\nand (mc.group_id = ${groupId} or ${groupId} = -1) \nand (ml.loan_type_enum = ${loanType} or ${loanType} = -1)', 'Loan and client data of rejected loan', '0', '0'); +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_subtype`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Loan Approved - Email', 'Email', 'Triggered', 'SELECT mc.id, mc.firstname, mc.middlename as middlename, mc.lastname, mc.display_name as FullName, mc.email_address as EmailAddress, mc.group_name as GroupName,\n mo.name as officename, ifnull(od.phoneNumber,\'\') as officenummber, ml.id as loanId, ml.account_no as accountnumber, ml.principal_amount_proposed as loanamount, ml.annual_nominal_interest_rate as annualinterestrate\n FROM\n m_office mo\n JOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\n AND ounder.hierarchy like CONCAT(\'.\', \'%\')\n LEFT JOIN (\n select \n ml.id as loanId, \n ifnull(mc.id,mc2.id) as id, \n ifnull(mc.firstname,mc2.firstname) as firstname, \n ifnull(mc.middlename,ifnull(mc2.middlename,(\'\'))) as middlename, \n ifnull(mc.lastname,mc2.lastname) as lastname, \n ifnull(mc.display_name,mc2.display_name) as display_name, \n ifnull(mc.status_enum,mc2.status_enum) as status_enum,\n ifnull(mc.email_address,mc2.email_address) as email_address,\n ifnull(mg.office_id,mc2.office_id) as office_id,\n ifnull(mg.staff_id,mc2.staff_id) as staff_id,\n mg.id as group_id, \nmg.display_name as group_name\n from\n m_loan ml\n left join m_group mg on mg.id = ml.group_id\n left join m_group_client mgc on mgc.group_id = mg.id\n left join m_client mc on mc.id = mgc.client_id\n left join m_client mc2 on mc2.id = ml.client_id\n order by loanId\n ) mc on mc.office_id = ounder.id\n left join ml_office_details as od on od.office_id = mo.id\n left join m_loan ml on ml.id = mc.loanId\n WHERE mc.status_enum = 300 and mc.email_address is not null\n and (mo.id = ${officeId} or ${officeId} = -1)\n and (mc.staff_id = ${staffId} or ${staffId} = -1)\nand (ml.id = ${loanId} or ${loanId} = -1)\nand (mc.id = ${clientId} or ${clientId} = -1)\nand (mc.group_id = ${groupId} or ${groupId} = -1)\nand (ml.loan_type_enum = ${loanType} or ${loanType} = -1)', 'Loan and client data of approved loan', '0', '0'); +INSERT INTO `stretchy_report` (`report_name`, `report_type`, `report_subtype`, `report_sql`, `description`, `core_report`, `use_report`) VALUES ('Loan Repayment - Email', 'Email', 'Triggered', 'select ml.id as loanId,mc.id, mc.firstname, ifnull(mc.middlename,\'\') as middlename, mc.lastname, mc.display_name as FullName, email_address as EmailAddress, mc.group_name as GroupName, round(ml.principal_amount, ml.currency_digits) as LoanAmount, round(ml.`total_outstanding_derived`, ml.currency_digits) as LoanOutstanding,\nifnull(od.phoneNumber,\'\') as officenummber,ml.`account_no` as LoanAccountId, round(mlt.amountPaid, ml.currency_digits) as repaymentAmount\nFROM m_office mo\nJOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, \'%\')\nAND ounder.hierarchy like CONCAT(\'.\', \'%\')\nLEFT JOIN (\n select \n ml.id as loanId, \n ifnull(mc.id,mc2.id) as id, \n ifnull(mc.firstname,mc2.firstname) as firstname, \n ifnull(mc.middlename,ifnull(mc2.middlename,(\'\'))) as middlename, \n ifnull(mc.lastname,mc2.lastname) as lastname, \n ifnull(mc.display_name,mc2.display_name) as display_name, \n ifnull(mc.status_enum,mc2.status_enum) as status_enum,\n ifnull(mc.email_address,mc2.email_address) as email_address,\n ifnull(mg.office_id,mc2.office_id) as office_id,\n ifnull(mg.staff_id,mc2.staff_id) as staff_id,\n mg.id as group_id, \nmg.display_name as group_name\n from\n m_loan ml\n left join m_group mg on mg.id = ml.group_id\n left join m_group_client mgc on mgc.group_id = mg.id\n left join m_client mc on mc.id = mgc.client_id\n left join m_client mc2 on mc2.id = ml.client_id\n order by loanId\n ) mc on mc.office_id = ounder.id\nleft join ml_office_details as od on od.office_id = mo.id\nright join m_loan as ml on mc.loanId = ml.id\nright join(\nselect mlt.amount as amountPaid,mlt.id,mlt.loan_id\nfrom m_loan_transaction mlt\nwhere mlt.is_reversed = 0 \ngroup by mlt.loan_id\n) as mlt on mlt.loan_id = ml.id\nright join m_loan_repayment_schedule as mls1 on ml.id = mls1.loan_id and mls1.`completed_derived` = 0\nand mls1.installment = (SELECT MIN(installment) from m_loan_repayment_schedule where loan_id = ml.id and duedate <= CURDATE() and completed_derived=0)\nwhere mc.status_enum = 300 and email_address is not null and ml.`loan_status_id` = 300\nand (mo.id = ${officeId} or ${officeId} = -1)\nand (mc.staff_id = ${staffId} or ${staffId} = -1)\nand (ml.loan_type_enum = ${loanType} or ${loanType} = -1)\nand ml.id in (select mla.loan_id from m_loan_arrears_aging mla)\ngroup by ml.id', 'Loan Repayment', '0', '0'); + + +INSERT INTO `stretchy_report_parameter` (`report_id`, `parameter_id`, `report_parameter_name`) +VALUES +( (SELECT id from stretchy_report where report_name = 'Active Clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Active Clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Prospective Clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Prospective Clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Active Loan Clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Active Loan Clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Active Loan Clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'cycleXSelect'), 'Cycle X'), +( (SELECT id from stretchy_report where report_name = 'Active Loan Clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'cycleYSelect'), 'Cycle Y'), +( (SELECT id from stretchy_report where report_name = 'Loans in arrears - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loans in arrears - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loans in arrears - Email'), (SELECT id from stretchy_parameter where parameter_name = 'fromXSelect'), 'fromX'), +( (SELECT id from stretchy_report where report_name = 'Loans in arrears - Email'), (SELECT id from stretchy_parameter where parameter_name = 'toYSelect'), 'toY'), +( (SELECT id from stretchy_report where report_name = 'Loans disbursed to clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loans disbursed to clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loans disbursed to clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'fromXSelect'), 'fromX'), +( (SELECT id from stretchy_report where report_name = 'Loans disbursed to clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'toYSelect'), 'toY'), +( (SELECT id from stretchy_report where report_name = 'Loans disbursed to clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'cycleXSelect'), 'Cycle X'), +( (SELECT id from stretchy_report where report_name = 'Loans disbursed to clients - Email'), (SELECT id from stretchy_parameter where parameter_name = 'cycleYSelect'), 'Cycle Y'), +( (SELECT id from stretchy_report where report_name = 'Loan payments due - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loan payments due - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan payments due - Email'), (SELECT id from stretchy_parameter where parameter_name = 'fromXSelect'), 'fromX'), +( (SELECT id from stretchy_report where report_name = 'Loan payments due - Email'), (SELECT id from stretchy_parameter where parameter_name = 'toYSelect'), 'toY'), +( (SELECT id from stretchy_report where report_name = 'Dormant Prospects - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Dormant Prospects - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Active Group Leaders - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Active Group Leaders - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Due (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Due (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Due (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'fromXSelect'), 'fromX'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Due (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'toYSelect'), 'toY'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Due (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'overdueXSelect'), 'overdueX'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Due (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'overdueYSelect'), 'overdueY'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Received (Active Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Received (Active Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Received (Active Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'fromXSelect'), 'fromX'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Received (Active Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'toYSelect'), 'toY'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Received (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Received (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Received (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'fromXSelect'), 'fromX'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Received (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'toYSelect'), 'toY'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Received (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'overdueXSelect'), 'overdueX'), +( (SELECT id from stretchy_report where report_name = 'Loan Payments Received (Overdue Loans) - Email'), (SELECT id from stretchy_parameter where parameter_name = 'overdueYSelect'), 'overdueY'), +( (SELECT id from stretchy_report where report_name = 'Happy Birthday - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Happy Birthday - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan Fully Repaid - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loan Fully Repaid - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan Fully Repaid - Email'), (SELECT id from stretchy_parameter where parameter_name = 'fromXSelect'), 'fromX'), +( (SELECT id from stretchy_report where report_name = 'Loan Fully Repaid - Email'), (SELECT id from stretchy_parameter where parameter_name = 'toYSelect'), 'toY'), +( (SELECT id from stretchy_report where report_name = 'Loans Outstanding after final instalment date - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loans Outstanding after final instalment date - Email'), (SELECT id from stretchy_parameter where parameter_name = 'toYSelect'), 'toY'), +( (SELECT id from stretchy_report where report_name = 'Loans Outstanding after final instalment date - Email'), (SELECT id from stretchy_parameter where parameter_name = 'fromXSelect'), 'fromX'), +( (SELECT id from stretchy_report where report_name = 'Loans Outstanding after final instalment date - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan Rejected - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loan Rejected - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan Rejected - Email'), (SELECT id from stretchy_parameter where parameter_name = 'DefaultLoan'), 'loanId'), +( (SELECT id from stretchy_report where report_name = 'Loan Rejected - Email'), (SELECT id from stretchy_parameter where parameter_name = 'DefaultClient'), 'clientId'), +( (SELECT id from stretchy_report where report_name = 'Loan Rejected - Email'), (SELECT id from stretchy_parameter where parameter_name = 'DefaultGroup'), 'groupId'), +( (SELECT id from stretchy_report where report_name = 'Loan Rejected - Email'), (SELECT id from stretchy_parameter where parameter_name = 'SelectLoanType'), 'loanType'), +( (SELECT id from stretchy_report where report_name = 'Loan Approved - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loan Approved - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan Approved - Email'), (SELECT id from stretchy_parameter where parameter_name = 'DefaultLoan'), 'loanId'), +( (SELECT id from stretchy_report where report_name = 'Loan Approved - Email'), (SELECT id from stretchy_parameter where parameter_name = 'DefaultClient'), 'clientId'), +( (SELECT id from stretchy_report where report_name = 'Loan Approved - Email'), (SELECT id from stretchy_parameter where parameter_name = 'DefaultGroup'), 'groupId'), +( (SELECT id from stretchy_report where report_name = 'Loan Approved - Email'), (SELECT id from stretchy_parameter where parameter_name = 'SelectLoanType'), 'loanType'), +( (SELECT id from stretchy_report where report_name = 'Loan Repayment - Email'), (SELECT id from stretchy_parameter where parameter_name = 'OfficeIdSelectOne'), 'Office'), +( (SELECT id from stretchy_report where report_name = 'Loan Repayment - Email'), (SELECT id from stretchy_parameter where parameter_name = 'loanOfficerIdSelectAll'), 'Loanofficer'), +( (SELECT id from stretchy_report where report_name = 'Loan Repayment - Email'), (SELECT id from stretchy_parameter where parameter_name = 'SelectLoanType'), 'loanType'); + +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES +('report', 'READ_Active Clients - Email', 'Active Clients - Email', 'READ', 0), +('report', 'READ_Prospective Clients - Email', 'Prospective Clients - Email', 'READ', 0), +('report', 'READ_Active Loan Clients - Email', 'Active Loan Clients - Email', 'READ', 0), +('report', 'READ_Loans in arrears - Email', 'Loans in arrears - Email', 'READ', 0), +('report', 'READ_Loans disbursed to clients - Email', 'Loans disbursed to clients - Email', 'READ', 0), +('report', 'READ_Loan payments due - Email', 'Loan payments due - Email', 'READ', 0), +('report', 'READ_Dormant Prospects - Email', 'Dormant Prospects - Email', 'READ', 0), +('report', 'READ_Active Group Leaders - Email', 'Active Group Leaders - Email', 'READ', 0), +('report', 'READ_Loan Payments Due (Overdue Loans) - Email', 'Loan Payments Due (Overdue Loans) - Email', 'READ', 0), +('report', 'READ_Loan Payments Received (Active Loans) - Email', 'Loan Payments Received (Active Loans) - Email', 'READ', 0), +('report', 'READ_Loan Payments Received (Overdue Loans) - Email', 'Loan Payments Received (Overdue Loans) - Email', 'READ', 0), +('report', 'READ_Loan Fully Repaid - Email', 'Loan Fully Repaid - Email', 'READ', 0), +('report', 'READ_Loans Outstanding after final instalment date - Email', 'Loans Outstanding after final instalment date - Email', 'READ', 0), +('report', 'READ_Happy Birthday - Email', 'Happy Birthday - Email', 'READ', 0), +('report', 'READ_Loan Rejected - Email', 'Loan Rejected - Email', 'READ', 0), +('report', 'READ_Loan Approved - Email', 'Loan Approved - Email', 'READ', 0), +('report', 'READ_Loan Repayment - Email', 'Loan Repayment - Email', 'READ', 0); \ No newline at end of file diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V336__sms_campaign_notification.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V336__sms_campaign_notification.sql new file mode 100644 index 000000000..37ab49629 --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V336__sms_campaign_notification.sql @@ -0,0 +1,48 @@ +-- +-- 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. +-- + +CREATE TABLE `client_device_registration` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `client_id` BIGINT(20) NOT NULL, + `updatedon_date` DATETIME NOT NULL, + `registration_id` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `registration_key_client_device_registration` (`registration_id`), + UNIQUE INDEX `client_key_client_device_registration` (`client_id`), + INDEX `FK1_client_device_registration_m_client` (`client_id`), + CONSTRAINT `FK1_client_device_registration_m_client` FOREIGN KEY (`client_id`) REFERENCES `m_client` (`id`) +); + +INSERT INTO `c_external_service` (`name`) VALUES ('NOTIFICATION'); + +INSERT INTO `c_external_service_properties` (`name`, `value`, `external_service_id`) VALUES ('server_key', +'AAAAToBmqQQ:APA91bEodkE12CwFl8VHqanUbeJYg1E05TiheVz59CZZYrBnCq3uM40UYhHfdP-JfeTQ0L0zoLqS8orjvW_ze0_VF8DSuyyqkrDibflhtUainsI0lwgVz5u1YP3q3c3erqjlySEuRShS', +(select id from `c_external_service` where name ='NOTIFICATION') +),('gcm_end_point','https://gcm-http.googleapis.com/gcm/send',(select id from `c_external_service` where name ='NOTIFICATION') +),('fcm_end_point','https://fcm.googleapis.com/fcm/send',(select id from `c_external_service` where name ='NOTIFICATION') +); + +ALTER TABLE sms_campaign +MODIFY COLUMN provider_id BIGINT(20) NULL DEFAULT NULL, +ADD COLUMN is_notification TINYINT(1) NULL DEFAULT '0'; + +ALTER TABLE sms_messages_outbound +MODIFY COLUMN mobile_no VARCHAR(50) NULL DEFAULT NULL, +ADD COLUMN is_notification TINYINT(1) NOT NULL DEFAULT '0' + 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/main/resources/sql/migrations/core_db/V338__two_factor_authentication.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V338__two_factor_authentication.sql new file mode 100644 index 000000000..bd5d35950 --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V338__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 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