diff --git a/dao/src/main/java/greencity/entity/Notification.java b/dao/src/main/java/greencity/entity/Notification.java index 3af3e5191..68885efb4 100644 --- a/dao/src/main/java/greencity/entity/Notification.java +++ b/dao/src/main/java/greencity/entity/Notification.java @@ -19,7 +19,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; @@ -62,7 +62,7 @@ public class Notification { private boolean viewed; @Column - private LocalDateTime time; + private ZonedDateTime time; @Column private boolean emailSent; diff --git a/dao/src/main/java/greencity/enums/NotificationType.java b/dao/src/main/java/greencity/enums/NotificationType.java index a8a579f93..0a3e7a5f0 100644 --- a/dao/src/main/java/greencity/enums/NotificationType.java +++ b/dao/src/main/java/greencity/enums/NotificationType.java @@ -1,5 +1,7 @@ package greencity.enums; +import java.util.EnumSet; + public enum NotificationType { ECONEWS_COMMENT_REPLY, ECONEWS_COMMENT_LIKE, @@ -27,5 +29,12 @@ public enum NotificationType { HABIT_COMMENT_USER_TAG, HABIT_LAST_DAY_OF_PRIMARY_DURATION, PLACE_STATUS, - PLACE_ADDED + PLACE_ADDED; + + private static final EnumSet COMMENT_LIKE_TYPES = EnumSet.of( + ECONEWS_COMMENT_LIKE, EVENT_COMMENT_LIKE, HABIT_COMMENT_LIKE); + + public static boolean isCommentLike(final NotificationType notificationType) { + return COMMENT_LIKE_TYPES.contains(notificationType); + } } diff --git a/dao/src/main/java/greencity/repository/NotificationRepo.java b/dao/src/main/java/greencity/repository/NotificationRepo.java index a78a969b3..dba616c7d 100644 --- a/dao/src/main/java/greencity/repository/NotificationRepo.java +++ b/dao/src/main/java/greencity/repository/NotificationRepo.java @@ -4,10 +4,7 @@ import greencity.enums.NotificationType; import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.*; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -54,6 +51,27 @@ public interface NotificationRepo extends CustomNotificationRepo, JpaRepository< Notification findNotificationByTargetUserIdAndNotificationTypeAndTargetId(Long targetUserId, NotificationType notificationType, Long targetId); + /** + * Finds a {@link Notification} based on a unique combination of the target + * user's ID, the notification type, and an identifier that either matches the + * {@code secondMessageId} (if present) or the {@code targetId}. This query is + * designed to find a unique notification by the following criteria: + * + * @param targetUserId user, that should receive Notification + * @param notificationType type of Notification + * @param identifier identifier: either {@code secondMessageId} or + * {@code targetId} + * @return {@link Notification} + */ + @Query("SELECT n " + + "FROM Notification AS n " + + "WHERE n.targetUser.id = :targetUserId " + + "AND n.notificationType = :notificationType " + + "AND ((n.secondMessageId IS NOT NULL AND n.secondMessageId = :identifier) " + + "OR (n.secondMessageId IS NULL AND n.targetId = :identifier))") + Notification findNotificationByTargetUserIdAndNotificationTypeAndIdentifier(Long targetUserId, + NotificationType notificationType, Long identifier); + /** * Method to find specific not viewed Notification. * @@ -130,4 +148,21 @@ long countActionUsersByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFals * false otherwise */ boolean existsByIdAndTargetUserId(Long notificationId, Long targetUserId); + + /** + * Finds an unviewed {@link Notification} by target user's ID, notification + * type, target ID, and second message ID. Returns a notification that matches + * the provided {@code targetUserId}, {@code notificationType}, + * {@code targetId}, and {@code secondMessageId}, with the {@code viewed} flag + * set to {@code false}. + * + * @param targetUserId the ID of the target user. + * @param notificationType the type of the notification. + * @param targetId the identifier of the notification's target. + * @param secondMessageId the secondary message ID. + * @return an {@link Optional} containing the matching notification, or empty if + * not found. + */ + Optional findByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalseAndSecondMessageId( + Long targetUserId, NotificationType notificationType, Long targetId, Long secondMessageId); } diff --git a/dao/src/main/resources/db/changelog/db.changelog-master.xml b/dao/src/main/resources/db/changelog/db.changelog-master.xml index 798810425..b4e00147f 100644 --- a/dao/src/main/resources/db/changelog/db.changelog-master.xml +++ b/dao/src/main/resources/db/changelog/db.changelog-master.xml @@ -246,6 +246,7 @@ + diff --git a/dao/src/main/resources/db/changelog/logs/ch-change-notification-time-Fedyk.xml b/dao/src/main/resources/db/changelog/logs/ch-change-notification-time-Fedyk.xml new file mode 100644 index 000000000..3ed1011c4 --- /dev/null +++ b/dao/src/main/resources/db/changelog/logs/ch-change-notification-time-Fedyk.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/service-api/src/main/java/greencity/dto/notification/EmailNotificationDto.java b/service-api/src/main/java/greencity/dto/notification/EmailNotificationDto.java index 340524201..6e3b4f780 100644 --- a/service-api/src/main/java/greencity/dto/notification/EmailNotificationDto.java +++ b/service-api/src/main/java/greencity/dto/notification/EmailNotificationDto.java @@ -9,7 +9,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; @@ -40,7 +40,7 @@ public class EmailNotificationDto { private boolean viewed; - private LocalDateTime time; + private ZonedDateTime time; private boolean emailSent; } diff --git a/service-api/src/main/java/greencity/dto/notification/NotificationDto.java b/service-api/src/main/java/greencity/dto/notification/NotificationDto.java index 994dfcddf..e25d6aac2 100644 --- a/service-api/src/main/java/greencity/dto/notification/NotificationDto.java +++ b/service-api/src/main/java/greencity/dto/notification/NotificationDto.java @@ -6,7 +6,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.List; @NoArgsConstructor @@ -19,7 +19,7 @@ public class NotificationDto { private Long notificationId; private String projectName; private String notificationType; - private LocalDateTime time; + private ZonedDateTime time; private Boolean viewed; private String titleText; diff --git a/service-api/src/main/java/greencity/enums/NotificationType.java b/service-api/src/main/java/greencity/enums/NotificationType.java index c32978f7e..be0fa5f76 100644 --- a/service-api/src/main/java/greencity/enums/NotificationType.java +++ b/service-api/src/main/java/greencity/enums/NotificationType.java @@ -1,5 +1,7 @@ package greencity.enums; +import java.util.EnumSet; + public enum NotificationType { ECONEWS_COMMENT_REPLY, ECONEWS_COMMENT_LIKE, @@ -27,5 +29,12 @@ public enum NotificationType { HABIT_COMMENT_USER_TAG, HABIT_LAST_DAY_OF_PRIMARY_DURATION, PLACE_STATUS, - PLACE_ADDED + PLACE_ADDED; + + private static final EnumSet COMMENT_LIKE_TYPES = EnumSet.of( + ECONEWS_COMMENT_LIKE, EVENT_COMMENT_LIKE, HABIT_COMMENT_LIKE); + + public static boolean isCommentLike(final NotificationType notificationType) { + return COMMENT_LIKE_TYPES.contains(notificationType); + } } diff --git a/service-api/src/main/java/greencity/service/UserNotificationService.java b/service-api/src/main/java/greencity/service/UserNotificationService.java index a07cf97e7..c63384b73 100644 --- a/service-api/src/main/java/greencity/service/UserNotificationService.java +++ b/service-api/src/main/java/greencity/service/UserNotificationService.java @@ -86,20 +86,40 @@ void createNotification(UserVO targetUser, UserVO actionUser, NotificationType n Long targetId, String customMessage); /** - * Method to create Notification. + * Creates a notification for a target user. Notifications are uniquely + * identified by the combination of {@code targetUserId}, + * {@code notificationType}, {@code targetId}, and {@code secondMessageId}. * - * @param targetUser user, that should receive Notification - * @param actionUser user, that performed action - * @param notificationType type of Notification + * @param targetUser the user who will receive the notification + * @param actionUser the user who performed the action triggering the + * notification + * @param notificationType the type of notification to be created * @param targetId represent the corresponding object's ID - * @param customMessage text of Notification, {message} in template - * @param secondMessageId if to secondMessageText - * @param secondMessageText additional text, {secondMessage} in template - * @author Volodymyr Mladonov + * @param customMessage a custom message for the notification + * @param secondMessageId a secondary identifier for additional context + * @param secondMessageText a secondary text for additional context + * @author Vitalii Fedyk */ void createNotification(UserVO targetUser, UserVO actionUser, NotificationType notificationType, Long targetId, String customMessage, Long secondMessageId, String secondMessageText); + /** + * Creates a notification for a target user. Notifications are uniquely + * identified by the combination of {@code targetUserId}, + * {@code notificationType}, and {@code targetId}. + * + * @param targetUser the user who will receive the notification + * @param actionUser the user who performed the action triggering the + * notification + * @param notificationType the type of notification to be created + * @param targetId represent the corresponding object's ID + * @param customMessage a custom message for the notification + * @param secondMessageText a secondary text for additional context + * @author Vitalii Fedyk + */ + void createNotification(UserVO targetUser, UserVO actionUser, NotificationType notificationType, + Long targetId, String customMessage, String secondMessageText); + /** * Method to create Notification without actionUser. * diff --git a/service-api/src/test/java/greencity/enums/NotificationTypeTest.java b/service-api/src/test/java/greencity/enums/NotificationTypeTest.java new file mode 100644 index 000000000..78c841b28 --- /dev/null +++ b/service-api/src/test/java/greencity/enums/NotificationTypeTest.java @@ -0,0 +1,29 @@ +package greencity.enums; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NotificationTypeTest { + @ParameterizedTest + @EnumSource(names = { + "ECONEWS_COMMENT_LIKE", + "EVENT_COMMENT_LIKE", + "HABIT_COMMENT_LIKE" + }) + void isCommentLike_ShouldReturnTrueForCommentLikeType(final NotificationType notificationType) { + assertTrue(NotificationType.isCommentLike(notificationType)); + } + + @ParameterizedTest + @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = { + "ECONEWS_COMMENT_LIKE", + "EVENT_COMMENT_LIKE", + "HABIT_COMMENT_LIKE" + }) + void isCommentLike_ShouldReturnFalseForCommentLikeType(final NotificationType notificationType) { + assertFalse(NotificationType.isCommentLike(notificationType)); + } +} diff --git a/service/src/main/java/greencity/service/CommentServiceImpl.java b/service/src/main/java/greencity/service/CommentServiceImpl.java index 397b6fd0a..57d33c98b 100644 --- a/service/src/main/java/greencity/service/CommentServiceImpl.java +++ b/service/src/main/java/greencity/service/CommentServiceImpl.java @@ -136,9 +136,9 @@ public AddCommentDtoResponse save(ArticleType articleType, Long articleId, commentRepo.save(comment), AddCommentDtoResponse.class); addCommentDtoResponse.setAuthor(modelMapper.map(userVO, CommentAuthorDto.class)); if (checkUserIsNotAuthor(userVO, articleAuthor) && !isCommentReply) { - createCommentNotification(articleType, articleId, comment, userVO, locale); + createCommentNotification(articleType, articleId, userVO, locale); } - sendNotificationToTaggedUser(modelMapper.map(comment, CommentVO.class), articleType, locale); + sendNotificationToTaggedUser(comment, articleType, locale); return addCommentDtoResponse; } @@ -181,23 +181,28 @@ private void addImagesToComment(Comment comment, MultipartFile[] images) { /** * Sends a notification to users tagged in a comment on a specific article. * - * @param commentVO the comment containing the tag, {@link CommentVO}. * @param articleType the type of the article where the comment is made, * {@link ArticleType}. * @param locale the locale used for localization of the notification, * {@link Locale}. * @throws NotFoundException if a tagged user is not found by ID. */ - private void sendNotificationToTaggedUser(CommentVO commentVO, ArticleType articleType, Locale locale) { - String commentText = commentVO.getText(); + private void sendNotificationToTaggedUser(Comment comment, ArticleType articleType, Locale locale) { + String commentText = comment.getText(); Set usersId = getUserIdFromComment(commentText); + NotificationType notificationType = getNotificationType(articleType, CommentActionType.COMMENT_USER_TAG); + CommentVO commentVO = modelMapper.map(comment, CommentVO.class); if (!usersId.isEmpty()) { for (Long userId : usersId) { User user = userRepo.findById(userId) .orElseThrow(() -> new NotFoundException(ErrorMessage.USER_NOT_FOUND_BY_ID + userId)); - createNotification(articleType, commentVO.getArticleId(), modelMapper.map(commentVO, Comment.class), - modelMapper.map(user, UserVO.class), commentVO.getUser(), - getNotificationType(articleType, CommentActionType.COMMENT_USER_TAG), locale); + userNotificationService.createNotification( + modelMapper.map(user, UserVO.class), + commentVO.getUser(), + notificationType, + commentVO.getArticleId(), + null, + getArticleTitle(articleType, commentVO.getArticleId(), locale)); } } } @@ -265,32 +270,6 @@ protected String getArticleTitle(ArticleType articleType, Long articleId, Locale return articleName; } - /** - * Generic method for creating a notification for various comment-related - * actions on an article. - * - * @param articleType the type of the article, {@link ArticleType}. - * @param articleId the ID of the article, {@link Long}. - * @param comment the comment that triggered the notification, - * {@link Comment}. - * @param receiver the user receiving the notification, {@link UserVO}. - * @param sender the user sending the notification, {@link UserVO}. - * @param notificationType the type of notification, {@link NotificationType}. - * @throws BadRequestException if the article type is not supported. - */ - private void createNotification(ArticleType articleType, Long articleId, Comment comment, UserVO receiver, - UserVO sender, NotificationType notificationType, Locale locale) { - boolean hasParentComment = comment.getParentComment() != null; - userNotificationService.createNotification( - receiver, - sender, - notificationType, - articleId, - hasParentComment ? comment.getParentComment().getText() : comment.getText(), - hasParentComment ? comment.getParentComment().getId() : comment.getId(), - getArticleTitle(articleType, articleId, locale)); - } - /** * Determines the appropriate notification type based on article and action * type. @@ -331,16 +310,15 @@ private NotificationType getNotificationType(ArticleType articleType, CommentAct * * @param articleType the type of the article, {@link ArticleType}. * @param articleId the ID of the article, {@link Long}. - * @param comment the comment that was made, {@link Comment}. * @param userVO the user who made the comment, {@link UserVO}. * @param locale the locale used for localization of the notification, * {@link Locale}. */ - private void createCommentNotification(ArticleType articleType, Long articleId, Comment comment, UserVO userVO, - Locale locale) { + private void createCommentNotification(ArticleType articleType, Long articleId, UserVO userVO, Locale locale) { UserVO receiver = modelMapper.map(getArticleAuthor(articleType, articleId), UserVO.class); String message = null; - ResourceBundle bundle = ResourceBundle.getBundle("notification", Locale.forLanguageTag(locale.getLanguage()), + ResourceBundle bundle = ResourceBundle.getBundle("notification", + Locale.forLanguageTag(locale.getLanguage()), ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT)); long commentsCount = notificationRepo .countActionUsersByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(receiver.getId(), @@ -356,7 +334,6 @@ private void createCommentNotification(ArticleType articleType, Long articleId, getNotificationType(articleType, CommentActionType.COMMENT), articleId, message, - comment.getId(), getArticleTitle(articleType, articleId, locale)); } @@ -397,8 +374,14 @@ private void createCommentLikeNotification(ArticleType articleType, Long article */ private void createCommentReplyNotification(ArticleType articleType, Long articleId, Comment comment, UserVO sender, UserVO receiver, Locale locale) { - createNotification(articleType, articleId, comment, receiver, - sender, getNotificationType(articleType, CommentActionType.COMMENT_REPLY), locale); + userNotificationService.createNotification( + receiver, + sender, + getNotificationType(articleType, CommentActionType.COMMENT_REPLY), + articleId, + comment.getParentComment().getText(), + comment.getParentComment().getId(), + getArticleTitle(articleType, articleId, locale)); } /** diff --git a/service/src/main/java/greencity/service/NotificationServiceImpl.java b/service/src/main/java/greencity/service/NotificationServiceImpl.java index f19ef5286..5f66702dd 100644 --- a/service/src/main/java/greencity/service/NotificationServiceImpl.java +++ b/service/src/main/java/greencity/service/NotificationServiceImpl.java @@ -350,7 +350,7 @@ private ScheduledEmailMessage createScheduledEmailMessage(Notification notificat String subject = bundle.getString(notification.getNotificationType() + "_TITLE"); String bodyTemplate = bundle.getString(notification.getNotificationType().toString()); String actionUserText; - long actionUsersSize = notification.getActionUsers().size(); + long actionUsersSize = notification.getActionUsers().stream().distinct().toList().size(); if (actionUsersSize > 1) { actionUserText = actionUsersSize + " " + bundle.getString("USERS"); } else if (actionUsersSize == 1) { diff --git a/service/src/main/java/greencity/service/UserNotificationServiceImpl.java b/service/src/main/java/greencity/service/UserNotificationServiceImpl.java index 1c2aa5679..79eaf66bd 100644 --- a/service/src/main/java/greencity/service/UserNotificationServiceImpl.java +++ b/service/src/main/java/greencity/service/UserNotificationServiceImpl.java @@ -24,12 +24,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.security.Principal; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.ResourceBundle; /** @@ -89,7 +90,7 @@ public void createNotificationForAttenders(List attendersList, String me .notificationType(notificationType) .projectName(ProjectName.GREENCITY) .targetUser(modelMapper.map(targetUserVO, User.class)) - .time(LocalDateTime.now()) + .time(ZonedDateTime.now()) .targetId(targetId) .customMessage(message) .secondMessage(title) @@ -110,7 +111,7 @@ public void createNotification(UserVO targetUser, UserVO actionUser, Notificatio .notificationType(notificationType) .projectName(ProjectName.GREENCITY) .targetUser(modelMapper.map(targetUser, User.class)) - .time(LocalDateTime.now()) + .time(ZonedDateTime.now()) .actionUsers(new ArrayList<>(List.of(modelMapper.map(actionUser, User.class)))) .emailSent(false) .build(); @@ -138,7 +139,7 @@ public void createNotification(UserVO targetUserVO, UserVO actionUserVO, Notific .emailSent(false) .build()); notification.getActionUsers().add(modelMapper.map(actionUserVO, User.class)); - notification.setTime(LocalDateTime.now()); + notification.setTime(ZonedDateTime.now()); notificationService.sendEmailNotification( modelMapper.map(notificationRepo.save(notification), EmailNotificationDto.class)); sendNotification(notification.getTargetUser().getId()); @@ -151,27 +152,42 @@ public void createNotification(UserVO targetUserVO, UserVO actionUserVO, Notific public void createNotification(UserVO targetUserVO, UserVO actionUserVO, NotificationType notificationType, Long targetId, String customMessage, Long secondMessageId, String secondMessageText) { Notification notification = notificationRepo - .findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(targetUserVO.getId(), - notificationType, targetId) - .orElse(Notification.builder() - .notificationType(notificationType) - .projectName(ProjectName.GREENCITY) - .targetUser(modelMapper.map(targetUserVO, User.class)) - .actionUsers(new ArrayList<>()) - .targetId(targetId) - .customMessage(customMessage) - .secondMessageId(secondMessageId) - .secondMessage(secondMessageText) - .emailSent(false) - .build()); + .findByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalseAndSecondMessageId( + targetUserVO.getId(), notificationType, targetId, secondMessageId) + .orElse(buildNotification( + notificationType, + targetUserVO, + targetId, + customMessage, + secondMessageId, + secondMessageText)); notification.getActionUsers().add(modelMapper.map(actionUserVO, User.class)); - notification.setTime(LocalDateTime.now()); + notification.setTime(ZonedDateTime.now()); notification.setCustomMessage(customMessage); notificationService.sendEmailNotification( modelMapper.map(notificationRepo.save(notification), EmailNotificationDto.class)); sendNotification(notification.getTargetUser().getId()); } + /** + * {@inheritDoc} + */ + @Override + public void createNotification(UserVO targetUserVO, UserVO actionUserVO, NotificationType notificationType, + Long targetId, String customMessage, String secondMessageText) { + final Notification notification = notificationRepo + .findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(targetUserVO.getId(), + notificationType, targetId) + .orElse(buildNotification(notificationType, targetUserVO, targetId, customMessage, null, + secondMessageText)); + notification.getActionUsers().add(modelMapper.map(actionUserVO, User.class)); + notification.setTime(ZonedDateTime.now()); + notification.setCustomMessage(customMessage); + notificationService + .sendEmailNotification(modelMapper.map(notificationRepo.save(notification), EmailNotificationDto.class)); + sendNotification(notification.getTargetUser().getId()); + } + /** * {@inheritDoc} */ @@ -194,7 +210,7 @@ public void createNewNotification(UserVO targetUserVO, NotificationType notifica .targetId(targetId) .customMessage(customMessage) .secondMessage(secondMessage) - .time(LocalDateTime.now()) + .time(ZonedDateTime.now()) .emailSent(false) .build(); notificationService.sendEmailNotification( @@ -212,7 +228,7 @@ public void createNewNotificationForPlaceAdded(List targetUsers, Long ta .notificationType(NotificationType.PLACE_ADDED) .projectName(ProjectName.GREENCITY) .targetUser(modelMapper.map(targetUser, User.class)) - .time(LocalDateTime.now()) + .time(ZonedDateTime.now()) .targetId(targetId) .customMessage(customMessage) .secondMessage(secondMessage) @@ -230,7 +246,7 @@ public void createNewNotificationForPlaceAdded(List targetUsers, Long ta @Override public void removeActionUserFromNotification(UserVO targetUserVO, UserVO actionUserVO, Long targetId, NotificationType notificationType) { - Notification notification = notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndTargetId( + Notification notification = notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndIdentifier( targetUserVO.getId(), notificationType, targetId); if (notification != null) { if (notification.getActionUsers().size() == 1) { @@ -348,10 +364,45 @@ private NotificationDto createNotificationDto(Notification notification, String case 2 -> bodyText = bodyTextTemplate.replace("{user}", bundle.getString("TWO_USERS")); default -> bodyText = bodyTextTemplate.replace("{user}", bundle.getString("THREE_OR_MORE_USERS")); } + final int messagesCount = notification.getActionUsers().size(); + final String times_in_dto = "{times}"; + if (bodyText.contains(times_in_dto)) { + if (size == 1) { + bodyText = bodyText.replace(times_in_dto, language.equals("ua") + ? resolveTimesInUkrainian(messagesCount) + : resolveTimesInEnglish(messagesCount)); + } else { + bodyText = bodyText.replace(times_in_dto, ""); + } + } dto.setBodyText(bodyText); return dto; } + private String resolveTimesInEnglish(final int number) { + return switch (number) { + case 1 -> ""; + case 2 -> "twice"; + default -> number + " times"; + }; + } + + private String resolveTimesInUkrainian(int number) { + number = Math.abs(number); + final int lastTwoDigits = number % 100; + final int lastDigit = number % 10; + + if (lastTwoDigits >= 11 && lastTwoDigits <= 19) { + return number + " разів"; + } + + return switch (lastDigit) { + case 1 -> ""; + case 2, 3, 4 -> number + " рази"; + default -> number + " разів"; + }; + } + /** * Sends a new notification to a specified user. * @@ -364,30 +415,73 @@ private void sendNotification(Long userId) { @Override public void createOrUpdateLikeNotification(final LikeNotificationDto likeNotificationDto) { - notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse( - likeNotificationDto.getTargetUserVO().getId(), likeNotificationDto.getNotificationType(), - likeNotificationDto.getNewsId()) - .ifPresentOrElse(notification -> { - List actionUsers = notification.getActionUsers(); - actionUsers.removeIf(user -> user.getId().equals(likeNotificationDto.getActionUserVO().getId())); - if (likeNotificationDto.isLike()) { - actionUsers.add(modelMapper.map(likeNotificationDto.getActionUserVO(), User.class)); - } + boolean isCommentLike = NotificationType.isCommentLike(likeNotificationDto.getNotificationType()); + Optional baseNotification = (isCommentLike + ? notificationRepo.findByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalseAndSecondMessageId( + likeNotificationDto.getTargetUserVO().getId(), likeNotificationDto.getNotificationType(), + likeNotificationDto.getNewsId(), likeNotificationDto.getSecondMessageId()) + : notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse( + likeNotificationDto.getTargetUserVO().getId(), likeNotificationDto.getNotificationType(), + likeNotificationDto.getNewsId())); + baseNotification.ifPresentOrElse(notification -> { + List actionUsers = notification.getActionUsers(); + actionUsers.removeIf(user -> user.getId().equals(likeNotificationDto.getActionUserVO().getId())); + if (likeNotificationDto.isLike()) { + actionUsers.add(modelMapper.map(likeNotificationDto.getActionUserVO(), User.class)); + } - if (actionUsers.isEmpty()) { - notificationRepo.delete(notification); - } else { - notification.setCustomMessage(likeNotificationDto.getNewsTitle()); - notification.setTime(LocalDateTime.now()); - notificationRepo.save(notification); - } - }, () -> { - if (likeNotificationDto.isLike()) { - createNotification(likeNotificationDto.getTargetUserVO(), likeNotificationDto.getActionUserVO(), - likeNotificationDto.getNotificationType(), likeNotificationDto.getNewsId(), - likeNotificationDto.getNewsTitle(), likeNotificationDto.getSecondMessageId(), - likeNotificationDto.getSecondMessageText()); - } - }); + if (actionUsers.isEmpty()) { + notificationRepo.delete(notification); + } else { + notification.setCustomMessage(likeNotificationDto.getNewsTitle()); + notification.setTime(ZonedDateTime.now()); + notificationRepo.save(notification); + } + }, () -> { + if (likeNotificationDto.isLike()) { + generateNotification(likeNotificationDto.getTargetUserVO(), likeNotificationDto.getActionUserVO(), + likeNotificationDto.getNotificationType(), likeNotificationDto.getNewsId(), + likeNotificationDto.getNewsTitle(), likeNotificationDto.getSecondMessageId(), + likeNotificationDto.getSecondMessageText()); + } + }); + } + + private void generateNotification(final UserVO targetUserVO, final UserVO actionUserVO, + final NotificationType notificationType, final Long targetId, + final String customMessage, final Long secondMessageId, + final String secondMessageText) { + final Notification notification = Notification.builder() + .notificationType(notificationType) + .projectName(ProjectName.GREENCITY) + .targetUser(modelMapper.map(targetUserVO, User.class)) + .actionUsers(new ArrayList<>()) + .targetId(targetId) + .customMessage(customMessage) + .secondMessageId(secondMessageId) + .secondMessage(secondMessageText) + .emailSent(false) + .build(); + notification.getActionUsers().add(modelMapper.map(actionUserVO, User.class)); + notification.setTime(ZonedDateTime.now()); + notification.setCustomMessage(customMessage); + notificationService.sendEmailNotification( + modelMapper.map(notificationRepo.save(notification), EmailNotificationDto.class)); + sendNotification(notification.getTargetUser().getId()); + } + + private Notification buildNotification(NotificationType notificationType, UserVO targetUserVO, Long targetId, + String customMessage, Long secondMessageId, String secondMessageText) { + return Notification.builder() + .notificationType(notificationType) + .projectName(ProjectName.GREENCITY) + .targetUser(modelMapper.map(targetUserVO, User.class)) + .actionUsers(new ArrayList<>()) + .targetId(targetId) + .customMessage(customMessage) + .secondMessageId(secondMessageId) + .secondMessage(secondMessageText) + .emailSent(false) + .build(); } } diff --git a/service/src/main/resources/notification.properties b/service/src/main/resources/notification.properties index a3cafd7e4..1e3facddd 100644 --- a/service/src/main/resources/notification.properties +++ b/service/src/main/resources/notification.properties @@ -13,9 +13,9 @@ ECONEWS_COMMENT_REPLY={user} commented your comment EVENT_COMMENT_LIKE_TITLE=You received a like. EVENT_COMMENT_LIKE={user} liked your comment {message} to event {secondMessage}. EVENT_COMMENT_REPLY_TITLE=You received a comment reply -EVENT_COMMENT_REPLY={user} commented your comment {message} to event {secondMessage}. +EVENT_COMMENT_REPLY={user} commented your comment {message} in the event {secondMessage}. EVENT_COMMENT_USER_TAG_TITLE=You have been tagged in the comment. -EVENT_COMMENT_USER_TAG={user} mention you in comment {message} to event {secondMessage}. +EVENT_COMMENT_USER_TAG={user} tagged you {times} in the event {secondMessage}. ECONEWS_COMMENT_TITLE=You received a comment ECONEWS_COMMENT=You received {message} from {user} on your news {secondMessage}. @@ -24,7 +24,7 @@ ECONEWS_CREATED=You successfully created eco news {message}. ECONEWS_LIKE_TITLE=Your news received a like ECONEWS_LIKE=News {message} received a like from {user}. ECONEWS_COMMENT_USER_TAG_TITLE=You have been tagged in the comment. -ECONEWS_COMMENT_USER_TAG={user} mentioned you in comment {message} to eco news {secondMessage}. +ECONEWS_COMMENT_USER_TAG={user} tagged you {times} in the eco news {secondMessage}. EVENT_COMMENT_TITLE=You received a comment EVENT_COMMENT=You received {message} from {user} on your event {secondMessage}. @@ -57,7 +57,7 @@ HABIT_COMMENT_LIKE={user} liked your comment HABIT_COMMENT_REPLY_TITLE=You received a comment reply HABIT_COMMENT_REPLY={user} commented your comment {message} to Habit {secondMessage}. HABIT_COMMENT_USER_TAG_TITLE=You have been tagged in the comment. -HABIT_COMMENT_USER_TAG={user} mention you in comment {message} to Habit {secondMessage}. +HABIT_COMMENT_USER_TAG={user} tagged you {times} in the habit {secondMessage}. HABIT_LAST_DAY_OF_PRIMARY_DURATION_TITLE=Today is last day of your habit primary duration HABIT_LAST_DAY_OF_PRIMARY_DURATION=Today the duration of your habit {message} is run out. To get more information go to Habit's page diff --git a/service/src/main/resources/notification_ua.properties b/service/src/main/resources/notification_ua.properties index 784dafba6..cb159a53e 100644 --- a/service/src/main/resources/notification_ua.properties +++ b/service/src/main/resources/notification_ua.properties @@ -11,13 +11,13 @@ ECONEWS_COMMENT_LIKE=\u0412\u0456\u0434 {user} \u043e\u0442\u0440\u0438\u043c\u0 ECONEWS_COMMENT_REPLY_TITLE=\u0412\u0438 \u043e\u0442\u0440\u0438\u043c\u0430\u043b\u0438 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c \u043d\u0430 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440 ECONEWS_COMMENT_REPLY={user} \u043f\u0440\u043e\u043a\u043e\u043c\u0435\u043d\u0442\u0443\u0432\u0430\u0432 \u0432\u0430\u0448 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440 {message} \u0434\u043e \u0435\u043a\u043e \u043d\u043e\u0432\u0438\u043d\u0438 {secondMessage}. ECONEWS_COMMENT_USER_TAG_TITLE=\u0412\u0430\u0441\u0020\u0442\u0435\u0433\u043d\u0443\u043b\u0438\u0020\u0432\u0020\u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440\u0456 -ECONEWS_COMMENT_USER_TAG={user} \u0437\u0433\u0430\u0434\u0430\u0432\u0020\u0432\u0430\u0441\u0020\u0432\u0020\u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440\u0456 {message} \u0434\u043e\u0020\u043d\u043e\u0432\u0438\u043d\u0438 {secondMessage} \u002e +ECONEWS_COMMENT_USER_TAG={user} \u043f\u043e\u0437\u043d\u0430\u0447\u0438\u0432\u0020\u0432\u0430\u0441 {times} \u0432\u0020\u0435\u043a\u043e\u043b\u043e\u0433\u0456\u0447\u043d\u0438\u0445\u0020\u043d\u043e\u0432\u0438\u043d\u0430\u0445 {secondMessage}. EVENT_COMMENT_LIKE_TITLE=\u0412\u0438 \u043e\u0442\u0440\u0438\u043c\u0430\u043b\u0438 \u043b\u0430\u0439\u043a EVENT_COMMENT_LIKE=\u0412\u0456\u0434 {user} \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043e \u043b\u0430\u0439\u043a \u043d\u0430 \u0432\u0430\u0448 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440 {message} \u0434\u043e \u043f\u043e\u0434\u0456\u0457 {secondMessage}. EVENT_COMMENT_REPLY_TITLE=\u0412\u0438 \u043e\u0442\u0440\u0438\u043c\u0430\u043b\u0438 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c \u043d\u0430 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440 EVENT_COMMENT_REPLY={user} \u043f\u0440\u043e\u043a\u043e\u043c\u0435\u043d\u0442\u0443\u0432\u0430\u0432 \u0432\u0430\u0448 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440 {message} \u0434\u043e \u043f\u043e\u0434\u0456\u0457 {secondMessage}. EVENT_COMMENT_USER_TAG_TITLE=\u0412\u0430\u0441\u0020\u0442\u0435\u0433\u043d\u0443\u043b\u0438\u0020\u0432\u0020\u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440\u0456 -EVENT_COMMENT_USER_TAG={user} \u0437\u0433\u0430\u0434\u0430\u0432\u0020\u0432\u0430\u0441\u0020\u0432\u0020\u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440\u0456 {message} \u0434\u043e\u0020\u043f\u043e\u0434\u0456\u0457 {secondMessage} \u002e +EVENT_COMMENT_USER_TAG={user} \u043f\u043e\u0437\u043d\u0430\u0447\u0438\u0432\u0020\u0432\u0430\u0441 {times} \u0443\u0020\u043f\u043e\u0434\u0456\u0457 {secondMessage}. ECONEWS_COMMENT_TITLE=\u0412\u0438 \u043e\u0442\u0440\u0438\u043c\u0430\u043b\u0438 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440 ECONEWS_COMMENT=\u0414\u043E \u0432\u0430\u0448\u043e\u0457 \u043D\u043E\u0432\u0438\u043D\u0438 \u00AB{secondMessage}\u00BB {user} \u0437\u0430\u043B\u0438\u0448\u0438\u0432(\u043B\u0438) {message}. @@ -57,7 +57,7 @@ HABIT_COMMENT_LIKE=\u0412\u0456\u0434 {user} \u043e\u0442\u0440\u0438\u043c\u043 HABIT_COMMENT_REPLY_TITLE=\u0412\u0438 \u043e\u0442\u0440\u0438\u043c\u0430\u043b\u0438 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c \u043d\u0430 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440 HABIT_COMMENT_REPLY={user} \u043f\u0440\u043e\u043a\u043e\u043c\u0435\u043d\u0442\u0443\u0432\u0430\u0432 \u0432\u0430\u0448 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440 {message} \u0434\u043e\u0020\u0437\u0432\u0438\u0447\u043a\u0438 {secondMessage}. HABIT_COMMENT_USER_TAG_TITLE=\u0412\u0430\u0441\u0020\u0442\u0435\u0433\u043d\u0443\u043b\u0438\u0020\u0432\u0020\u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440\u0456 -HABIT_COMMENT_USER_TAG={user} \u0437\u0433\u0430\u0434\u0430\u0432\u0020\u0432\u0430\u0441\u0020\u0432\u0020\u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440\u0456 {message} \u0434\u043e\u0020\u0437\u0432\u0438\u0447\u043a\u0438 {secondMessage} \u002e +HABIT_COMMENT_USER_TAG={user} \u043f\u043e\u0437\u043d\u0430\u0447\u0438\u0432\u0020\u0432\u0430\u0441 {times} \u0443\u0020\u0437\u0432\u0438\u0447\u0446\u0456 {secondMessage}. HABIT_LAST_DAY_OF_PRIMARY_DURATION_TITLE=\u0421\u044c\u043e\u0433\u043e\u0434\u043d\u0456\u0020\u043e\u0441\u0442\u0430\u043d\u043d\u0456\u0439\u0020\u0434\u0435\u043d\u044c\u0020\u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0457\u0020\u0442\u0440\u0438\u0432\u0430\u043b\u043e\u0441\u0442\u0456\u0020\u0432\u0430\u0448\u043e\u0457\u0020\u0437\u0432\u0438\u0447\u043a\u0438 HABIT_LAST_DAY_OF_PRIMARY_DURATION=\u0421\u044c\u043e\u0433\u043e\u0434\u043d\u0456\u0020\u0442\u0435\u0440\u043c\u0456\u043d\u0020\u0432\u0430\u0448\u043e\u0457\u0020\u0437\u0432\u0438\u0447\u043a\u0438\u0020{message}\u0020\u0437\u0430\u043a\u0456\u043d\u0447\u0438\u0432\u0441\u044f\u002e\u0020\u0414\u043b\u044f\u0020\u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f\u0020\u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e\u0457\u0020\u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457\u0020\u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c\u0020\u043d\u0430\u0020\u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443\u0020\u0048\u0061\u0062\u0069\u0074 diff --git a/service/src/test/java/greencity/ModelUtils.java b/service/src/test/java/greencity/ModelUtils.java index 60b810400..9a25bfee9 100644 --- a/service/src/test/java/greencity/ModelUtils.java +++ b/service/src/test/java/greencity/ModelUtils.java @@ -258,6 +258,7 @@ import static greencity.constant.EventTupleConstant.type; import static greencity.enums.EventStatus.OPEN; import static greencity.enums.EventTime.PAST; +import static greencity.enums.NotificationType.EVENT_COMMENT_USER_TAG; import static greencity.enums.NotificationType.EVENT_CREATED; import static greencity.enums.ProjectName.GREENCITY; import static greencity.enums.UserStatus.ACTIVATED; @@ -2940,7 +2941,7 @@ public static NotificationDto getNotificationDto() { .notificationId(1L) .projectName(String.valueOf(GREENCITY)) .notificationType(String.valueOf(EVENT_CREATED)) - .time(LocalDateTime.of(2100, 1, 31, 12, 0)) + .time(ZonedDateTime.of(2100, 1, 31, 12, 0, 0, 0, ZoneId.of("UTC"))) .viewed(true) .titleText("You have created event") .bodyText("You successfully created event {message}.") @@ -2953,6 +2954,41 @@ public static NotificationDto getNotificationDto() { .build(); } + public static NotificationDto getBaseOfNotificationDtoForEventCommentUserTag( + String titleText, String bodyText, List actionUserId, List actionUserText) { + return NotificationDto.builder() + .notificationId(1L) + .projectName(String.valueOf(GREENCITY)) + .notificationType(EVENT_COMMENT_USER_TAG.name()) + .time(ZonedDateTime.of(2100, 1, 31, 12, 0, 0, 0, ZoneId.of("UTC"))) + .viewed(true) + .titleText(titleText) + .bodyText(bodyText) + .actionUserId(actionUserId) + .actionUserText(actionUserText) + .build(); + } + + public static Notification getBaseOfNotificationForEventCommentUserTag(List actionUsers) { + return Notification.builder() + .id(1L) + .actionUsers(actionUsers) + .targetId(1L) + .secondMessageId(null) + .notificationType(EVENT_COMMENT_USER_TAG) + .viewed(true) + .time(ZonedDateTime.of(2050, 6, 23, 12, 4, 0, 0, ZoneId.of("UTC"))) + .emailSent(true) + .build(); + } + + public static PageableAdvancedDto getPageableAdvancedDtoWithNotificationForEventCommentUserTag( + NotificationDto notificationDto) { + return new PageableAdvancedDto<>(Collections.singletonList(notificationDto), + 1, 0, 1, 0, + false, false, true, true); + } + public static Notification getNotification() { return Notification.builder() .id(1L) @@ -2963,7 +2999,7 @@ public static Notification getNotification() { .notificationType(EVENT_CREATED) .projectName(GREENCITY) .viewed(true) - .time(LocalDateTime.of(2100, 1, 31, 12, 0)) + .time(ZonedDateTime.of(2100, 1, 31, 12, 0, 0, 0, ZoneId.of("UTC"))) .actionUsers(List.of(getUser())) .emailSent(true) .build(); @@ -2987,7 +3023,7 @@ public static Notification getNotificationWithSeveralActionUsers(int numberOfUse .notificationType(EVENT_CREATED) .projectName(GREENCITY) .viewed(true) - .time(LocalDateTime.of(2100, 1, 31, 12, 0)) + .time(ZonedDateTime.of(2100, 1, 31, 12, 0, 0, 0, ZoneId.of("UTC"))) .actionUsers(actionUsers) .emailSent(true) .build(); diff --git a/service/src/test/java/greencity/service/CommentServiceImplTest.java b/service/src/test/java/greencity/service/CommentServiceImplTest.java index 564624620..a99dfe355 100644 --- a/service/src/test/java/greencity/service/CommentServiceImplTest.java +++ b/service/src/test/java/greencity/service/CommentServiceImplTest.java @@ -169,6 +169,44 @@ void save() { verify(modelMapper).map(any(Comment.class), eq(AddCommentDtoResponse.class)); } + @Test + void saveCommentReplyNotification() { + Event event = getEvent(); + User user = getUser(); + UserVO userVO = getUserVO(); + User parentCommentCreator = getUser().setId(2L); + Comment parentComment = getComment() + .setUser(parentCommentCreator) + .setArticleId(1L) + .setArticleType(ArticleType.EVENT); + Comment comment = getComment(); + CommentVO commentVO = getCommentVO(); + AddCommentDtoRequest addCommentDtoRequest = ModelUtils.getAddCommentDtoRequest(); + addCommentDtoRequest.setParentCommentId(1L); + + when(eventRepo.findById(1L)).thenReturn(Optional.of(event)); + when(userRepo.findById(1L)).thenReturn(Optional.of(user)); + when(modelMapper.map(addCommentDtoRequest, Comment.class)).thenReturn(comment); + when(modelMapper.map(userVO, User.class)).thenReturn(user); + when(commentRepo.findById(1L)) + .thenReturn(Optional.of(parentComment)); + when(modelMapper.map(any(Comment.class), eq(CommentVO.class))).thenReturn(commentVO); + when(commentRepo.save(any(Comment.class))).then(AdditionalAnswers.returnsFirstArg()); + when(modelMapper.map(commentRepo.save(comment), AddCommentDtoResponse.class)) + .thenReturn(getAddCommentDtoResponse()); + + commentService.save( + ArticleType.EVENT, + 1L, + addCommentDtoRequest, + null, + getUserVO(), + Locale.of("en")); + + verify(userNotificationService) + .createNotification(any(), any(), any(), anyLong(), anyString(), anyLong(), anyString()); + } + @Test void saveWithNullElementOfImages() { UserVO userVO = getUserVO(); diff --git a/service/src/test/java/greencity/service/UserNotificationServiceImplTest.java b/service/src/test/java/greencity/service/UserNotificationServiceImplTest.java index 281bda25a..352b478d7 100644 --- a/service/src/test/java/greencity/service/UserNotificationServiceImplTest.java +++ b/service/src/test/java/greencity/service/UserNotificationServiceImplTest.java @@ -20,6 +20,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -29,26 +32,17 @@ import org.springframework.data.domain.PageRequest; import org.springframework.messaging.simp.SimpMessagingTemplate; import java.security.Principal; -import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; -import static greencity.ModelUtils.getActionDto; -import static greencity.ModelUtils.getHabit; -import static greencity.ModelUtils.getHabitAssign; -import static greencity.ModelUtils.getHabitTranslation; -import static greencity.ModelUtils.getLanguage; -import static greencity.ModelUtils.getLanguageVO; -import static greencity.ModelUtils.getNotification; -import static greencity.ModelUtils.getNotificationDto; -import static greencity.ModelUtils.getNotificationWithSeveralActionUsers; -import static greencity.ModelUtils.getPageableAdvancedDtoForNotificationDto; -import static greencity.ModelUtils.getPrincipal; -import static greencity.ModelUtils.getUser; -import static greencity.ModelUtils.getUserVO; -import static greencity.ModelUtils.testUser; -import static greencity.ModelUtils.testUserVo; +import java.util.stream.Stream; + +import static greencity.ModelUtils.*; +import static greencity.enums.NotificationType.EVENT_COMMENT_USER_TAG; +import static greencity.enums.ProjectName.GREENCITY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -108,7 +102,170 @@ void getNotificationsFilteredTest() { verify(modelMapper).map(notification, NotificationDto.class); } - @Test + static Stream getNotificationScenariosInEnglish() { + return Stream.of( + Arguments.of( + "You have been tagged in the comment.", + "{user} tagged you in the event «{secondMessage}».", + List.of(getUser()), + List.of(1L), + List.of("Taras")), + Arguments.of( + "You have been tagged in the comment.", + "{user1} and {user2} tagged you in the event «{secondMessage}».", + List.of(getUser(), getUser().setId(2L)), + List.of(1L, 2L), + List.of("Taras", "Taras")), + Arguments.of( + "You have been tagged in the comment.", + "{user} tagged you twice in the event «{secondMessage}».", + List.of(getUser(), getUser()), + List.of(1L), + List.of("Taras"))); + } + + @ParameterizedTest + @MethodSource("getNotificationScenariosInEnglish") + void getNotificationsWithTaggingFilteredInEnglish( + String titleText, + String bodyText, + List actionUsers, + List actionUsersId, + List actionUserText) { + Notification notification = getBaseOfNotificationForEventCommentUserTag(actionUsers); + NotificationDto notificationDto = NotificationDto.builder() + .notificationId(1L) + .projectName(String.valueOf(GREENCITY)) + .notificationType(EVENT_COMMENT_USER_TAG.name()) + .time(ZonedDateTime.of(2100, 1, 31, 12, 0, 0, 0, ZoneId.of("UTC"))) + .viewed(true) + .build(); + List list = List.of(notification); + + PageRequest pageRequest = PageRequest.of(0, 1); + PageImpl page = new PageImpl<>(list, pageRequest, 1); + + when(userService.findByEmail("danylo@gmail.com")).thenReturn(testUserVo); + when(notificationRepo.findNotificationsByFilter(testUserVo.getId(), + ProjectName.GREENCITY, + null, + true, + pageRequest)) + .thenReturn(page); + when(modelMapper.map(notification, NotificationDto.class)).thenReturn(notificationDto); + + PageableAdvancedDto actual = userNotificationService + .getNotificationsFiltered( + pageRequest, + getPrincipal(), + "en", + ProjectName.GREENCITY, + null, + true); + PageableAdvancedDto expected = + getPageableAdvancedDtoWithNotificationForEventCommentUserTag( + getBaseOfNotificationDtoForEventCommentUserTag( + titleText, + bodyText, + actionUsersId, + actionUserText)); + + assertEquals(expected, actual); + + verify(userService).findByEmail("danylo@gmail.com"); + verify(notificationRepo) + .findNotificationsByFilter(testUser.getId(), ProjectName.GREENCITY, null, true, pageRequest); + verify(modelMapper).map(notification, NotificationDto.class); + } + + static Stream getNotificationScenariosInUkrainian() { + return Stream.of( + Arguments.of( + "Вас тегнули в коментарі", + "{user} позначив вас у події «{secondMessage}».", + List.of(getUser()), + List.of(1L), + List.of("Taras")), + Arguments.of( + "Вас тегнули в коментарі", + "{user1} та {user2} позначив вас у події «{secondMessage}».", + List.of(getUser(), getUser().setId(2L)), + List.of(1L, 2L), + List.of("Taras", "Taras")), + Arguments.of( + "Вас тегнули в коментарі", + "{user} позначив вас 2 рази у події «{secondMessage}».", + List.of(getUser(), getUser()), + List.of(1L), + List.of("Taras")), + Arguments.of( + "Вас тегнули в коментарі", + "{user} позначив вас 5 разів у події «{secondMessage}».", + List.of(getUser(), getUser(), getUser(), getUser(), getUser()), + List.of(1L), + List.of("Taras")), + Arguments.of( + "Вас тегнули в коментарі", + "{user} позначив вас 10 разів у події «{secondMessage}».", + List.of(getUser(), getUser(), getUser(), getUser(), getUser(), getUser(), getUser(), getUser(), + getUser(), getUser()), + List.of(1L), + List.of("Taras"))); + } + + @ParameterizedTest + @MethodSource("getNotificationScenariosInUkrainian") + void getNotificationsWithTaggingFilteredInUkrainian( + String titleText, + String bodyText, + List actionUsers, + List actionUsersId, + List actionUserText) { + Notification notification = getBaseOfNotificationForEventCommentUserTag(actionUsers); + NotificationDto notificationDto = NotificationDto.builder() + .notificationId(1L) + .projectName(String.valueOf(GREENCITY)) + .notificationType(EVENT_COMMENT_USER_TAG.name()) + .time(ZonedDateTime.of(2100, 1, 31, 12, 0, 0, 0, ZoneId.of("UTC"))) + .viewed(true) + .build(); + List list = List.of(notification); + + PageRequest pageRequest = PageRequest.of(0, 1); + PageImpl page = new PageImpl<>(list, pageRequest, 1); + + when(userService.findByEmail("danylo@gmail.com")).thenReturn(testUserVo); + when(notificationRepo.findNotificationsByFilter(testUserVo.getId(), + ProjectName.GREENCITY, + null, + true, + pageRequest)) + .thenReturn(page); + when(modelMapper.map(notification, NotificationDto.class)).thenReturn(notificationDto); + + PageableAdvancedDto actual = userNotificationService + .getNotificationsFiltered( + pageRequest, + getPrincipal(), + "ua", + ProjectName.GREENCITY, + null, + true); + PageableAdvancedDto expected = + getPageableAdvancedDtoWithNotificationForEventCommentUserTag( + getBaseOfNotificationDtoForEventCommentUserTag( + titleText, + bodyText, + actionUsersId, + actionUserText)); + assertEquals(expected, actual); + + verify(userService).findByEmail("danylo@gmail.com"); + verify(notificationRepo) + .findNotificationsByFilter(testUser.getId(), ProjectName.GREENCITY, null, true, pageRequest); + verify(modelMapper).map(notification, NotificationDto.class); + } + void notificationSocketTest() { ActionDto dto = getActionDto(); @@ -184,8 +341,8 @@ void createNotificationWithCustomMessageTest() { @Test void createNotificationWithSecondMessageTest() { when(notificationRepo - .findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(1L, - NotificationType.EVENT_CREATED, 1L)).thenReturn(Optional.empty()); + .findByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalseAndSecondMessageId(1L, + NotificationType.EVENT_CREATED, 1L, 1L)).thenReturn(Optional.empty()); when(notificationRepo.countByTargetUserIdAndViewedIsFalse(testUserVo.getId())).thenReturn(1L); when(modelMapper.map(testUserVo, User.class)).thenReturn(testUser); userNotificationService.createNotification(testUserVo, testUserVo, @@ -193,8 +350,8 @@ void createNotificationWithSecondMessageTest() { "Second Message"); verify(notificationRepo) - .findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(1L, - NotificationType.EVENT_CREATED, 1L); + .findByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalseAndSecondMessageId(1L, + NotificationType.EVENT_CREATED, 1L, 1L); verify(modelMapper, times(2)).map(testUserVo, User.class); verify(messagingTemplate, times(1)) .convertAndSend(TOPIC + testUser.getId() + NOTIFICATION, 1L); @@ -215,38 +372,38 @@ void createNewNotificationTest() { void removeActionUserFromNotificationTest() { var notification = getNotification(); when(notificationRepo - .findNotificationByTargetUserIdAndNotificationTypeAndTargetId(testUser.getId(), + .findNotificationByTargetUserIdAndNotificationTypeAndIdentifier(testUser.getId(), NotificationType.EVENT_CREATED, 1L)) .thenReturn(notification); userNotificationService .removeActionUserFromNotification(testUserVo, testUserVo, 1L, NotificationType.EVENT_CREATED); - verify(notificationRepo).findNotificationByTargetUserIdAndNotificationTypeAndTargetId(testUser.getId(), + verify(notificationRepo).findNotificationByTargetUserIdAndNotificationTypeAndIdentifier(testUser.getId(), NotificationType.EVENT_CREATED, 1L); } @Test void removeActionUserFromNotificationWithSeveralActionUsersTest() { var notification = getNotificationWithSeveralActionUsers(3); - when(notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndTargetId(testUser.getId(), + when(notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndIdentifier(testUser.getId(), NotificationType.EVENT_CREATED, 1L)) .thenReturn(notification); when(modelMapper.map(testUserVo, User.class)).thenReturn(testUser); userNotificationService .removeActionUserFromNotification(testUserVo, testUserVo, 1L, NotificationType.EVENT_CREATED); - verify(notificationRepo).findNotificationByTargetUserIdAndNotificationTypeAndTargetId(testUser.getId(), + verify(notificationRepo).findNotificationByTargetUserIdAndNotificationTypeAndIdentifier(testUser.getId(), NotificationType.EVENT_CREATED, 1L); verify(modelMapper).map(testUserVo, User.class); } @Test void removeActionUserFromNotificationIfNotificationIsNullTest() { - when(notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndTargetId(testUser.getId(), + when(notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndIdentifier(testUser.getId(), NotificationType.EVENT_CREATED, 1L)).thenReturn(null); userNotificationService.removeActionUserFromNotification(testUserVo, testUserVo, 1L, NotificationType.EVENT_CREATED); - verify(notificationRepo).findNotificationByTargetUserIdAndNotificationTypeAndTargetId(testUser.getId(), + verify(notificationRepo, times(0)).findNotificationByTargetUserIdAndNotificationTypeAndTargetId(testUser.getId(), NotificationType.EVENT_CREATED, 1L); } @@ -273,6 +430,66 @@ void deleteNonExistentNotificationAndGetNotFoundExceptionTest() { () -> userNotificationService.deleteNotification(principal, notificationId)); } + @Test + void createNotification_ShouldUpdateExistingNotificationWithoutIdentifierSecondMessageId() { + Notification notification = mock(Notification.class); + UserVO targetUserVO = mock(UserVO.class); + UserVO actionUserVO = mock(UserVO.class); + NotificationType notificationType = NotificationType.HABIT_LIKE; + Long targetId = 1L; + String customMessage = "Custom Message"; + String secondMessageText = "Second Message"; + User targetUser = mock(User.class); + + when(targetUserVO.getId()).thenReturn(1L); + when(notification.getTargetUser()).thenReturn(targetUser); + when(notification.getTargetUser().getId()).thenReturn(1L); + when(notificationRepo + .findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(targetUserVO.getId(), + notificationType, targetId)) + .thenReturn(Optional.of(notification)); + + userNotificationService.createNotification(targetUserVO, actionUserVO, notificationType, targetId, + customMessage, secondMessageText); + + verify(notificationRepo).findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse( + targetUserVO.getId(), + notificationType, targetId); + verify(notificationService).sendEmailNotification( + modelMapper.map(notificationRepo.save(notification), EmailNotificationDto.class)); + verify(messagingTemplate).convertAndSend(TOPIC + targetUser.getId() + NOTIFICATION, 0L); + } + + @Test + void createNotification_ShouldCreateNotificationWithoutIdentifierSecondMessageId() { + Notification notification = mock(Notification.class); + UserVO targetUserVO = mock(UserVO.class); + UserVO actionUserVO = mock(UserVO.class); + NotificationType notificationType = NotificationType.HABIT_LIKE; + Long targetId = 1L; + String customMessage = "Custom Message"; + String secondMessageText = "Second Message"; + User targetUser = mock(User.class); + + when(targetUserVO.getId()).thenReturn(1L); + when(notification.getTargetUser()).thenReturn(targetUser); + when(notification.getTargetUser().getId()).thenReturn(1L); + when(modelMapper.map(targetUserVO, User.class)).thenReturn(targetUser); + when(notificationRepo + .findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(targetUserVO.getId(), + notificationType, targetId)) + .thenReturn(Optional.ofNullable(null)); + + userNotificationService.createNotification(targetUserVO, actionUserVO, notificationType, targetId, + customMessage, secondMessageText); + + verify(notificationRepo).findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse( + targetUserVO.getId(), notificationType, targetId); + verify(notificationService).sendEmailNotification( + modelMapper.map(notificationRepo.save(notification), EmailNotificationDto.class)); + verify(messagingTemplate).convertAndSend(TOPIC + targetUser.getId() + NOTIFICATION, 0L); + } + @Test void unreadNotificationTest() { Long notificationId = 1L; @@ -321,12 +538,13 @@ void testCreateOrUpdateLikeNotification_UpdateExistingNotification_AddLike() { User actionUser = mock(User.class); Long newsId = 1L; String newsTitle = "Test News"; + long secondMessageId = 1L; Notification existingNotification = mock(Notification.class); List actionUsers = new ArrayList<>(); when(existingNotification.getActionUsers()).thenReturn(actionUsers); - when(notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(anyLong(), - any(), anyLong())) + when(notificationRepo.findByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalseAndSecondMessageId( + anyLong(), any(), anyLong(), anyLong())) .thenReturn(Optional.of(existingNotification)); when(modelMapper.map(actionUserVO, User.class)).thenReturn(actionUser); @@ -336,13 +554,14 @@ void testCreateOrUpdateLikeNotification_UpdateExistingNotification_AddLike() { .newsId(newsId) .newsTitle(newsTitle) .notificationType(NotificationType.ECONEWS_COMMENT_LIKE) + .secondMessageId(secondMessageId) .isLike(true) .build()); assertTrue(actionUsers.contains(actionUser), "Action users should contain the actionUser."); verify(existingNotification).setCustomMessage(anyString()); - verify(existingNotification).setTime(any(LocalDateTime.class)); + verify(existingNotification).setTime(any(ZonedDateTime.class)); verify(notificationRepo).save(existingNotification); verify(notificationRepo, never()).delete(existingNotification); } @@ -355,13 +574,15 @@ void testCreateOrUpdateLikeNotification_UpdateExistingNotification_RemoveLike() User actionUser = mock(User.class); Long newsId = 1L; String newsTitle = "Test News"; + long secondMessageId = 1L; Notification existingNotification = mock(Notification.class); List actionUsers = new ArrayList<>(); actionUsers.add(actionUser); when(existingNotification.getActionUsers()).thenReturn(actionUsers); - when(notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(anyLong(), - any(), anyLong())) + when(notificationRepo.findByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalseAndSecondMessageId( + anyLong(), + any(), anyLong(), anyLong())) .thenReturn(Optional.of(existingNotification)); when(actionUserVO.getId()).thenReturn(1L); when(actionUser.getId()).thenReturn(1L); @@ -372,6 +593,7 @@ void testCreateOrUpdateLikeNotification_UpdateExistingNotification_RemoveLike() .newsId(newsId) .newsTitle(newsTitle) .notificationType(NotificationType.ECONEWS_COMMENT_LIKE) + .secondMessageId(secondMessageId) .isLike(false) .build()); @@ -389,9 +611,10 @@ void testCreateOrUpdateLikeNotification_CreateNewNotification_AddLike() { User actionUser = mock(User.class); Long newsId = 1L; String newsTitle = "Test News"; + long secondMessageId = 1L; - when(notificationRepo.findNotificationByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(anyLong(), - any(), anyLong())) + when(notificationRepo.findByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalseAndSecondMessageId( + anyLong(), any(), anyLong(), anyLong())) .thenReturn(Optional.empty()); when(modelMapper.map(any(UserVO.class), eq(User.class))).thenReturn(actionUser); @@ -401,6 +624,7 @@ void testCreateOrUpdateLikeNotification_CreateNewNotification_AddLike() { .newsId(newsId) .newsTitle(newsTitle) .notificationType(NotificationType.ECONEWS_COMMENT_LIKE) + .secondMessageId(secondMessageId) .isLike(true) .build());