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 FineractApiProperties api;
private FineractNotificationProperties notification;
@Getter
@Setter
public static class FineractTenantProperties {
@@ -280,4 +282,17 @@ public class FineractProperties {
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.exception.InvalidTenantIdentifierException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -79,7 +79,7 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil
private CacheWritePlatformService cacheWritePlatformService;
@Autowired
private NotificationReadPlatformService notificationReadPlatformService;
private UserNotificationService userNotificationService;
@Autowired
private BasicAuthTenantDetailsService basicAuthTenantDetailsService;
@@ -170,7 +170,7 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil
super.onSuccessfulAuthentication(request, response, authResult);
AppUser user = (AppUser) authResult.getPrincipal();
if (notificationReadPlatformService.hasUnreadNotifications(user.getId())) {
if (userNotificationService.hasUnreadUserNotifications(user.getId())) {
response.addHeader("X-Notification-Refresh", "true");
} else {
response.addHeader("X-Notification-Refresh", "false");
@@ -18,46 +18,19 @@
*/
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 org.apache.fineract.notification.data.NotificationData;
import org.apache.fineract.notification.service.NotificationWritePlatformService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.AppUserRepository;
import org.apache.fineract.notification.service.UserNotificationService;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class NotificationEventListener {
private final NotificationWritePlatformService notificationWritePlatformService;
private final AppUserRepository appUserRepository;
private final UserNotificationService userNotificationService;
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;
import static java.util.stream.Collectors.toSet;
import java.util.Collection;
import java.util.Set;
import javax.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.domain.client.ClientCreateBusinessEvent;
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.service.BusinessEventNotifierService;
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.loanaccount.domain.Loan;
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.SavingsAccountTransaction;
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;
@Service
@@ -73,8 +64,7 @@ public class NotificationDomainServiceImpl implements NotificationDomainService
private final BusinessEventNotifierService businessEventNotifierService;
private final PlatformSecurityContext context;
private final NotificationEventPublisher notificationEventPublisher;
private final AppUserRepository appUserRepository;
private final UserNotificationService userNotificationService;
@PostConstruct
public void addListeners() {
@@ -327,22 +317,7 @@ public class NotificationDomainServiceImpl implements NotificationDomainService
private void buildNotification(String permission, String objectType, Long objectIdentifier, String notificationContent,
String eventType, Long appUserId, Long officeId) {
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);
}
userNotificationService.notifyUsers(permission, objectType, objectIdentifier, notificationContent, eventType, appUserId, officeId);
}
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 {
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,
boolean isSystemGenerated);
}
@@ -40,26 +40,6 @@ public class NotificationWritePlatformServiceImpl implements NotificationWritePl
private final AppUserRepository appUserRepository;
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,
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.notification.user-notification-system.enabled=${FINERACT_USER_NOTIFICATION_SYSTEM_ENABLED:true}
# 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}}