FINERACT-1724: Put user notification feature behind feature toggle

This commit is contained in:
Adam Saghy
2023-03-23 09:42:52 +01:00
committed by Arnold Gálovics
parent 3a176f4cbf
commit d21110f7b2
9 changed files with 170 additions and 83 deletions
@@ -58,6 +58,8 @@ public class FineractProperties {
private FineractQueryProperties query; private FineractQueryProperties query;
private FineractApiProperties api; private FineractApiProperties api;
private FineractNotificationProperties notification;
@Getter @Getter
@Setter @Setter
public static class FineractTenantProperties { public static class FineractTenantProperties {
@@ -280,4 +282,17 @@ public class FineractProperties {
private int inlineLoanCob; private int inlineLoanCob;
} }
@Getter
@Setter
public static class FineractNotificationProperties {
private UserNotificationSystemProperties userNotificationSystem;
}
@Getter
@Setter
public static class UserNotificationSystemProperties {
private boolean enabled;
}
} }
@@ -37,7 +37,7 @@ import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.security.data.PlatformRequestLog; import org.apache.fineract.infrastructure.security.data.PlatformRequestLog;
import org.apache.fineract.infrastructure.security.exception.InvalidTenantIdentifierException; import org.apache.fineract.infrastructure.security.exception.InvalidTenantIdentifierException;
import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService; import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService;
import org.apache.fineract.notification.service.NotificationReadPlatformService; import org.apache.fineract.notification.service.UserNotificationService;
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -79,7 +79,7 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil
private CacheWritePlatformService cacheWritePlatformService; private CacheWritePlatformService cacheWritePlatformService;
@Autowired @Autowired
private NotificationReadPlatformService notificationReadPlatformService; private UserNotificationService userNotificationService;
@Autowired @Autowired
private BasicAuthTenantDetailsService basicAuthTenantDetailsService; private BasicAuthTenantDetailsService basicAuthTenantDetailsService;
@@ -170,7 +170,7 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil
super.onSuccessfulAuthentication(request, response, authResult); super.onSuccessfulAuthentication(request, response, authResult);
AppUser user = (AppUser) authResult.getPrincipal(); AppUser user = (AppUser) authResult.getPrincipal();
if (notificationReadPlatformService.hasUnreadNotifications(user.getId())) { if (userNotificationService.hasUnreadUserNotifications(user.getId())) {
response.addHeader("X-Notification-Refresh", "true"); response.addHeader("X-Notification-Refresh", "true");
} else { } else {
response.addHeader("X-Notification-Refresh", "false"); response.addHeader("X-Notification-Refresh", "false");
@@ -18,46 +18,19 @@
*/ */
package org.apache.fineract.notification.eventandlistener; package org.apache.fineract.notification.eventandlistener;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.notification.data.NotificationData; import org.apache.fineract.notification.data.NotificationData;
import org.apache.fineract.notification.service.NotificationWritePlatformService; import org.apache.fineract.notification.service.UserNotificationService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.AppUserRepository;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class NotificationEventListener { public class NotificationEventListener {
private final NotificationWritePlatformService notificationWritePlatformService; private final UserNotificationService userNotificationService;
private final AppUserRepository appUserRepository;
public void receive(NotificationData notificationData) { public void receive(NotificationData notificationData) {
Long appUserId = notificationData.getActorId(); userNotificationService.notifyUsers(notificationData);
Set<Long> userIds = notificationData.getUserIds();
if (notificationData.getOfficeId() != null) {
List<Long> tempUserIds = new ArrayList<>(userIds);
for (Long userId : tempUserIds) {
AppUser appUser = appUserRepository.findById(userId).orElseThrow();
if (!Objects.equals(appUser.getOffice().getId(), notificationData.getOfficeId())) {
userIds.remove(userId);
}
}
}
// Don't notify the same user who triggered the event
if (userIds.contains(appUserId)) {
userIds.remove(appUserId);
}
notificationWritePlatformService.notify(userIds, notificationData.getObjectType(), notificationData.getObjectId(),
notificationData.getAction(), notificationData.getActorId(), notificationData.getContent(),
notificationData.isSystemGenerated());
} }
} }
@@ -18,15 +18,10 @@
*/ */
package org.apache.fineract.notification.service; package org.apache.fineract.notification.service;
import static java.util.stream.Collectors.toSet;
import java.util.Collection;
import java.util.Set;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.event.business.BusinessEventListener; import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
import org.apache.fineract.infrastructure.event.business.domain.client.ClientCreateBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.client.ClientCreateBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.deposit.FixedDepositAccountCreateBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.deposit.FixedDepositAccountCreateBusinessEvent;
@@ -50,8 +45,6 @@ import org.apache.fineract.infrastructure.event.business.domain.share.ShareAccou
import org.apache.fineract.infrastructure.event.business.domain.share.ShareProductDividentsCreateBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.share.ShareProductDividentsCreateBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.notification.data.NotificationData;
import org.apache.fineract.notification.eventandlistener.NotificationEventPublisher;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -62,8 +55,6 @@ import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccount; import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccount;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.AppUserRepository;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@@ -73,8 +64,7 @@ public class NotificationDomainServiceImpl implements NotificationDomainService
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final NotificationEventPublisher notificationEventPublisher; private final UserNotificationService userNotificationService;
private final AppUserRepository appUserRepository;
@PostConstruct @PostConstruct
public void addListeners() { public void addListeners() {
@@ -327,22 +317,7 @@ public class NotificationDomainServiceImpl implements NotificationDomainService
private void buildNotification(String permission, String objectType, Long objectIdentifier, String notificationContent, private void buildNotification(String permission, String objectType, Long objectIdentifier, String notificationContent,
String eventType, Long appUserId, Long officeId) { String eventType, Long appUserId, Long officeId) {
String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier(); userNotificationService.notifyUsers(permission, objectType, objectIdentifier, notificationContent, eventType, appUserId, officeId);
Set<Long> userIds = getNotifiableUserIds(officeId, permission);
NotificationData notificationData = new NotificationData().setObjectType(objectType).setObjectId(objectIdentifier)
.setAction(eventType).setActorId(appUserId).setContent(notificationContent).setRead(false).setSystemGenerated(false)
.setTenantIdentifier(tenantIdentifier).setOfficeId(officeId).setUserIds(userIds);
try {
notificationEventPublisher.broadcastNotification(notificationData);
} catch (Exception e) {
// We want to avoid rethrowing the exception to stop the business transaction from rolling back
log.error("Error while broadcasting notification event", e);
}
} }
private Set<Long> getNotifiableUserIds(Long officeId, String permission) {
Collection<AppUser> users = appUserRepository.findByOfficeId(officeId);
Collection<AppUser> usersWithPermission = users.stream().filter(aU -> aU.hasAnyPermission(permission, "ALL_FUNCTIONS")).toList();
return usersWithPermission.stream().map(AppUser::getId).collect(toSet());
}
} }
@@ -22,9 +22,6 @@ import java.util.Collection;
public interface NotificationWritePlatformService { public interface NotificationWritePlatformService {
Long notify(Long userId, String objectType, Long objectId, String action, Long actorId, String notificationContent,
boolean isSystemGenerated);
Long notify(Collection<Long> userIds, String objectType, Long objectId, String action, Long actorId, String notificationContent, Long notify(Collection<Long> userIds, String objectType, Long objectId, String action, Long actorId, String notificationContent,
boolean isSystemGenerated); boolean isSystemGenerated);
} }
@@ -40,26 +40,6 @@ public class NotificationWritePlatformServiceImpl implements NotificationWritePl
private final AppUserRepository appUserRepository; private final AppUserRepository appUserRepository;
private final NotificationMapperWritePlatformService notificationMapperWritePlatformService; private final NotificationMapperWritePlatformService notificationMapperWritePlatformService;
@Override
public Long notify(Long userId, String objectType, Long objectIdentifier, String action, Long actorId, String notificationContent,
boolean isSystemGenerated) {
Long generatedNotificationId = insertIntoNotificationGenerator(objectType, objectIdentifier, action, actorId, notificationContent,
isSystemGenerated);
insertIntoNotificationMapper(userId, generatedNotificationId);
return generatedNotificationId;
}
private Long insertIntoNotificationMapper(Long userId, Long generatedNotificationId) {
AppUser appUser = this.appUserRepository.findById(userId).orElse(null);
NotificationMapper notificationMapper = new NotificationMapper()
.setNotification(this.notificationGeneratorReadRepositoryWrapper.findById(generatedNotificationId)).setUserId(appUser)
.setRead(false).setCreatedAt(DateUtils.getLocalDateTimeOfSystem());
this.notificationMapperWritePlatformService.create(notificationMapper);
return notificationMapper.getId();
}
private Long insertIntoNotificationGenerator(String objectType, Long objectIdentifier, String action, Long actorId, private Long insertIntoNotificationGenerator(String objectType, Long objectIdentifier, String action, Long actorId,
String notificationContent, boolean isSystemGenerated) { String notificationContent, boolean isSystemGenerated) {
@@ -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.notification.service;
import org.apache.fineract.notification.data.NotificationData;
public interface UserNotificationService {
void notifyUsers(String permission, String objectType, Long objectIdentifier, String notificationContent, String eventType,
Long appUserId, Long officeId);
boolean hasUnreadUserNotifications(Long appUserId);
void notifyUsers(NotificationData notificationData);
}
@@ -0,0 +1,114 @@
/**
* 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.notification.service;
import static java.util.stream.Collectors.toSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.notification.data.NotificationData;
import org.apache.fineract.notification.eventandlistener.NotificationEventPublisher;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.AppUserRepository;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserNotificationServiceImpl implements UserNotificationService {
private final NotificationEventPublisher notificationEventPublisher;
private final AppUserRepository appUserRepository;
private final FineractProperties fineractProperties;
private final NotificationReadPlatformService notificationReadPlatformService;
private final NotificationWritePlatformService notificationWritePlatformService;
@Override
public void notifyUsers(String permission, String objectType, Long objectIdentifier, String notificationContent, String eventType,
Long appUserId, Long officeId) {
if (userNotificationSystemIsEnabled()) {
String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
Set<Long> userIds = getNotifiableUserIds(officeId, permission);
NotificationData notificationData = new NotificationData().setObjectType(objectType).setObjectId(objectIdentifier)
.setAction(eventType).setActorId(appUserId).setContent(notificationContent).setRead(false).setSystemGenerated(false)
.setTenantIdentifier(tenantIdentifier).setOfficeId(officeId).setUserIds(userIds);
try {
notificationEventPublisher.broadcastNotification(notificationData);
} catch (Exception e) {
// We want to avoid rethrowing the exception to stop the business transaction from rolling back
log.error("Error while broadcasting notification event", e);
}
}
}
@Override
public boolean hasUnreadUserNotifications(Long appUserId) {
if (userNotificationSystemIsEnabled()) {
return notificationReadPlatformService.hasUnreadNotifications(appUserId);
} else {
return false;
}
}
@Override
public void notifyUsers(NotificationData notificationData) {
if (userNotificationSystemIsEnabled()) {
Long appUserId = notificationData.getActorId();
Set<Long> userIds = notificationData.getUserIds();
if (notificationData.getOfficeId() != null) {
List<Long> tempUserIds = new ArrayList<>(userIds);
for (Long userId : tempUserIds) {
AppUser appUser = appUserRepository.findById(userId).orElseThrow();
if (!Objects.equals(appUser.getOffice().getId(), notificationData.getOfficeId())) {
userIds.remove(userId);
}
}
}
// Don't notify the same user who triggered the event
if (userIds.contains(appUserId)) {
userIds.remove(appUserId);
}
notificationWritePlatformService.notify(userIds, notificationData.getObjectType(), notificationData.getObjectId(),
notificationData.getAction(), notificationData.getActorId(), notificationData.getContent(),
notificationData.isSystemGenerated());
}
}
private boolean userNotificationSystemIsEnabled() {
return fineractProperties.getNotification().getUserNotificationSystem().isEnabled();
}
private Set<Long> getNotifiableUserIds(Long officeId, String permission) {
Collection<AppUser> users = appUserRepository.findByOfficeId(officeId);
Collection<AppUser> usersWithPermission = users.stream().filter(aU -> aU.hasAnyPermission(permission, "ALL_FUNCTIONS")).toList();
return usersWithPermission.stream().map(AppUser::getId).collect(toSet());
}
}
@@ -102,6 +102,8 @@ fineract.report.export.s3.enabled=${FINERACT_REPORT_EXPORT_S3_ENABLED:false}
fineract.jpa.statementLoggingEnabled=${FINERACT_STATEMENT_LOGGING_ENABLED:false} fineract.jpa.statementLoggingEnabled=${FINERACT_STATEMENT_LOGGING_ENABLED:false}
fineract.notification.user-notification-system.enabled=${FINERACT_USER_NOTIFICATION_SYSTEM_ENABLED:true}
# Logging pattern for the console # Logging pattern for the console
logging.pattern.console=${CONSOLE_LOG_PATTERN:%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(%replace([%X{correlationId}]){'\\[\\]', ''}) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}} logging.pattern.console=${CONSOLE_LOG_PATTERN:%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(%replace([%X{correlationId}]){'\\[\\]', ''}) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}}