diff --git a/core/src/main/java/greencity/config/SecurityConfig.java b/core/src/main/java/greencity/config/SecurityConfig.java index 7950baae96..86ee16c5e4 100644 --- a/core/src/main/java/greencity/config/SecurityConfig.java +++ b/core/src/main/java/greencity/config/SecurityConfig.java @@ -269,6 +269,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti EVENTS + EVENT_ID + LIKES, EVENTS + EVENT_ID + LIKES + COUNT, EVENTS + EVENT_ID + DISLIKES + COUNT, + EVENTS + EVENT_ID + "/requested-users", "/user/to-do-list-items/{userId}/get-all-inprogress", "/habit/assign/{habitAssignId}/allUserAndCustomList", "/habit/assign/allUserAndCustomToDoListsInprogress", @@ -307,6 +308,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti EVENTS_COMMENTS + LIKE + COMMENT_ID, EVENTS, EVENTS + EVENT_ID + ATTENDERS, + "/events/{eventId}/requested-users/{userId}/decline", + "/events/{eventId}/requested-users/{userId}/approve", + EVENTS + EVENT_ID + "/addToRequested", EVENTS + EVENT_ID + FAVORITES, EVENTS + EVENT_ID + RATINGS, EVENTS + EVENT_ID + LIKE, @@ -402,7 +406,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/notification/{notificationId}", "/ownSecurity/user", NOTIFICATIONS + NOTIFICATION_ID, - HABIT_INVITE + INVITATION_ID + "/reject") + HABIT_INVITE + INVITATION_ID + "/reject", + EVENTS + EVENT_ID + "/removeFromRequested") .hasAnyRole(USER, ADMIN, MODERATOR, UBS_EMPLOYEE) .requestMatchers(HttpMethod.GET, COMMENTS, diff --git a/core/src/main/java/greencity/controller/EventController.java b/core/src/main/java/greencity/controller/EventController.java index efe4cdb498..ee05380b97 100644 --- a/core/src/main/java/greencity/controller/EventController.java +++ b/core/src/main/java/greencity/controller/EventController.java @@ -8,12 +8,14 @@ import greencity.constant.HttpStatuses; import greencity.constant.SwaggerExampleModel; import greencity.dto.PageableAdvancedDto; +import greencity.dto.PageableDto; import greencity.dto.event.AddEventDtoRequest; import greencity.dto.event.AddressDto; import greencity.dto.event.EventAttenderDto; import greencity.dto.event.EventDto; import greencity.dto.event.UpdateEventRequestDto; import greencity.dto.filter.FilterEventDto; +import greencity.dto.user.UserForListDto; import greencity.dto.user.UserVO; import greencity.enums.EventStatus; import greencity.exception.exceptions.BadRequestException; @@ -494,4 +496,108 @@ public ResponseEntity getAllAttendersCount(@RequestParam(name = "user-id") public ResponseEntity getOrganizersCount(@RequestParam(name = "user-id") Long userId) { return ResponseEntity.ok().body(eventService.getCountOfOrganizedEventsByUserId(userId)); } + + /** + * Method for adding an event to requested by event id. + * + * @author Olha Pitsyk. + */ + @Operation(summary = "Add an event to requested") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = HttpStatuses.OK), + @ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST), + @ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED), + @ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN), + + @ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND) + }) + @PostMapping("/{eventId}/addToRequested") + public ResponseEntity addToRequested(@PathVariable Long eventId, + @Parameter(hidden = true) Principal principal) { + eventService.addToRequested(eventId, principal.getName()); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + /** + * Method for removing an event from requested by event id. + * + * @author Olha Pitsyk. + */ + @Operation(summary = "Remove an event from requested") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = HttpStatuses.OK), + @ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST), + @ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED), + @ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN), + + @ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND) + }) + @DeleteMapping("/{eventId}/removeFromRequested") + public ResponseEntity removeFromRequested(@PathVariable Long eventId, + @Parameter(hidden = true) Principal principal) { + eventService.removeFromRequested(eventId, principal.getName()); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + /** + * Method for getting all users who made request for joining the event. + * + * @author Olha Pitsyk. + */ + @Operation(summary = "List of requested users") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = HttpStatuses.OK), + @ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST), + @ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED), + @ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN), + @ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND) + }) + @GetMapping("/{eventId}/requested-users") + public ResponseEntity> getRequestedUsers( + @PathVariable Long eventId, + @Parameter(hidden = true) Principal principal, + @Parameter(hidden = true) Pageable pageable) { + return ResponseEntity.status(HttpStatus.OK) + .body(eventService.getRequestedUsers(eventId, principal.getName(), pageable)); + } + + /** + * Method for approving join request. + * + * @author Olha Pitsyk. + */ + @Operation(summary = "Approve join request") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = HttpStatuses.OK), + @ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST), + @ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED), + @ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN), + @ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND) + }) + @PostMapping("/{eventId}/requested-users/{userId}/approve") + public ResponseEntity approveRequest(@PathVariable Long eventId, @PathVariable Long userId, + @Parameter(hidden = true) Principal principal) { + eventService.approveRequest(eventId, principal.getName(), userId); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + /** + * Method for declining join request. + * + * @author Olha Pitsyk. + */ + @Operation(summary = "Decline join request") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = HttpStatuses.OK), + @ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST), + @ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED), + @ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN), + @ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND) + }) + @PostMapping("/{eventId}/requested-users/{userId}/decline") + public ResponseEntity declineRequest(@PathVariable Long eventId, @PathVariable Long userId, + @Parameter(hidden = true) Principal principal) { + eventService.declineRequest(eventId, principal.getName(), userId); + return ResponseEntity.status(HttpStatus.OK).build(); + } } \ No newline at end of file diff --git a/core/src/test/java/greencity/controller/EventControllerTest.java b/core/src/test/java/greencity/controller/EventControllerTest.java index 169c615ad6..91ae6768cf 100644 --- a/core/src/test/java/greencity/controller/EventControllerTest.java +++ b/core/src/test/java/greencity/controller/EventControllerTest.java @@ -703,4 +703,57 @@ private EventDto getEventDto() { objectMapper.findAndRegisterModules(); return objectMapper.readValue(json, EventDto.class); } + + @Test + @SneakyThrows + void addToRequestedTest() { + Long eventId = 1L; + mockMvc.perform(post(EVENTS_CONTROLLER_LINK + "/{eventId}/addToRequested", eventId) + .principal(principal)) + .andExpect(status().isOk()); + verify(eventService).addToRequested(eventId, principal.getName()); + } + + @Test + @SneakyThrows + void removeFromRequestedTest() { + Long eventId = 1L; + mockMvc.perform(delete(EVENTS_CONTROLLER_LINK + "/{eventId}/removeFromRequested", eventId) + .principal(principal)) + .andExpect(status().isOk()); + verify(eventService).removeFromRequested(eventId, principal.getName()); + } + + @Test + @SneakyThrows + void getRequestedUsersTest() { + Long eventId = 1L; + Pageable pageable = PageRequest.of(0, 20); + mockMvc.perform(get(EVENTS_CONTROLLER_LINK + "/{eventId}/requested-users", eventId) + .principal(principal)) + .andExpect(status().isOk()); + verify(eventService).getRequestedUsers(eventId, principal.getName(), pageable); + } + + @Test + @SneakyThrows + void approveRequestTest() { + Long eventId = 1L; + Long userId = 1L; + mockMvc.perform(post(EVENTS_CONTROLLER_LINK + "/{eventId}/requested-users/{userId}/approve", eventId, userId) + .principal(principal)) + .andExpect(status().isOk()); + verify(eventService).approveRequest(eventId, principal.getName(), userId); + } + + @Test + @SneakyThrows + void declineRequestTest() { + Long eventId = 1L; + Long userId = 1L; + mockMvc.perform(post(EVENTS_CONTROLLER_LINK + "/{eventId}/requested-users/{userId}/decline", eventId, userId) + .principal(principal)) + .andExpect(status().isOk()); + verify(eventService).declineRequest(eventId, principal.getName(), userId); + } } diff --git a/dao/src/main/java/greencity/entity/User.java b/dao/src/main/java/greencity/entity/User.java index 9d567f4ff8..622202d6f3 100644 --- a/dao/src/main/java/greencity/entity/User.java +++ b/dao/src/main/java/greencity/entity/User.java @@ -190,10 +190,12 @@ u.id IN (:users) @Table(name = "users") @EqualsAndHashCode( exclude = {"verifyEmail", "ownSecurity", "ecoNewsLiked", "refreshTokenKey", "estimates", "restorePasswordEmail", - "customToDoListItems", "eventOrganizerRating", "favoriteEcoNews", "favoriteEvents", "subscribedEvents"}) + "customToDoListItems", "eventOrganizerRating", "favoriteEcoNews", "favoriteEvents", "requestedEvents", + "subscribedEvents"}) @ToString( exclude = {"verifyEmail", "ownSecurity", "refreshTokenKey", "ecoNewsLiked", "estimates", "restorePasswordEmail", - "customToDoListItems", "eventOrganizerRating", "favoriteEcoNews", "favoriteEvents", "subscribedEvents"}) + "customToDoListItems", "eventOrganizerRating", "favoriteEcoNews", "favoriteEvents", "requestedEvents", + "subscribedEvents"}) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -317,4 +319,7 @@ public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default private Set emailPreference = new HashSet<>(); + + @ManyToMany(mappedBy = "requesters", fetch = FetchType.LAZY) + private Set requestedEvents; } diff --git a/dao/src/main/java/greencity/entity/event/Event.java b/dao/src/main/java/greencity/entity/event/Event.java index e8ca1b5d8c..dbc61dc8a5 100644 --- a/dao/src/main/java/greencity/entity/event/Event.java +++ b/dao/src/main/java/greencity/entity/event/Event.java @@ -33,7 +33,7 @@ @Entity @Table(name = "events") -@EqualsAndHashCode(exclude = {"attenders", "followers", "dates"}) +@EqualsAndHashCode(exclude = {"attenders", "followers", "requesters", "dates"}) @AllArgsConstructor @NoArgsConstructor @Getter @@ -72,6 +72,12 @@ public class Event { inverseJoinColumns = @JoinColumn(name = "user_id")) private Set followers = new HashSet<>(); + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "events_requesters", + joinColumns = @JoinColumn(name = "event_id"), + inverseJoinColumns = @JoinColumn(name = "user_id")) + private Set requesters = new HashSet<>(); + @NonNull @OrderBy("finishDate ASC") @OneToMany(mappedBy = "event", cascade = CascadeType.ALL) @@ -96,8 +102,7 @@ public class Event { private List eventGrades = new ArrayList<>(); @ManyToMany - @JoinTable( - name = "events_users_likes", + @JoinTable(name = "events_users_likes", joinColumns = @JoinColumn(name = "event_id"), inverseJoinColumns = @JoinColumn(name = "users_id")) private Set usersLikedEvents = new HashSet<>(); diff --git a/dao/src/main/java/greencity/enums/NotificationType.java b/dao/src/main/java/greencity/enums/NotificationType.java index 0a3e7a5f0e..a791edb674 100644 --- a/dao/src/main/java/greencity/enums/NotificationType.java +++ b/dao/src/main/java/greencity/enums/NotificationType.java @@ -29,7 +29,10 @@ public enum NotificationType { HABIT_COMMENT_USER_TAG, HABIT_LAST_DAY_OF_PRIMARY_DURATION, PLACE_STATUS, - PLACE_ADDED; + PLACE_ADDED, + EVENT_REQUEST_ACCEPTED, + EVENT_REQUEST_DECLINED, + EVENT_INVITE; private static final EnumSet COMMENT_LIKE_TYPES = EnumSet.of( ECONEWS_COMMENT_LIKE, EVENT_COMMENT_LIKE, HABIT_COMMENT_LIKE); diff --git a/dao/src/main/java/greencity/repository/UserRepo.java b/dao/src/main/java/greencity/repository/UserRepo.java index 600cc319d8..223ad1a640 100644 --- a/dao/src/main/java/greencity/repository/UserRepo.java +++ b/dao/src/main/java/greencity/repository/UserRepo.java @@ -900,4 +900,16 @@ uep.emailPreference, uep.periodicity, COUNT(uep.id) */ @Query("SELECT COUNT(u) FROM User u WHERE u.userStatus IN (greencity.enums.UserStatus.ACTIVATED) ") Long countActiveUsers(); + + /** + * Method for getting all users who made request for joining the event. + * + * @param eventId - id of the event + * @param pageable + * + */ + @Query(nativeQuery = true, value = "SELECT users.* FROM users " + + "JOIN events_requesters ON users.id = events_requesters.user_id " + + "WHERE events_requesters.event_id = :eventId") + Page findUsersByRequestedEvents(Long eventId, Pageable pageable); } 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 41f0546dbc..7f5fb2a493 100644 --- a/dao/src/main/resources/db/changelog/db.changelog-master.xml +++ b/dao/src/main/resources/db/changelog/db.changelog-master.xml @@ -260,4 +260,5 @@ + diff --git a/dao/src/main/resources/db/changelog/logs/ch-add-table-events-requesters-Pitsyk.xml b/dao/src/main/resources/db/changelog/logs/ch-add-table-events-requesters-Pitsyk.xml new file mode 100644 index 0000000000..9eab31b5ec --- /dev/null +++ b/dao/src/main/resources/db/changelog/logs/ch-add-table-events-requesters-Pitsyk.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/service-api/src/main/java/greencity/constant/EmailNotificationMessagesConstants.java b/service-api/src/main/java/greencity/constant/EmailNotificationMessagesConstants.java index e7986bf0db..77d82957da 100644 --- a/service-api/src/main/java/greencity/constant/EmailNotificationMessagesConstants.java +++ b/service-api/src/main/java/greencity/constant/EmailNotificationMessagesConstants.java @@ -27,4 +27,12 @@ public class EmailNotificationMessagesConstants { public static final String FRIEND_REQUEST_RECEIVED_MESSAGE = "%s sent you a friend request"; public static final String FRIEND_REQUEST_ACCEPTED_SUBJECT = "Your friend request was accepted"; public static final String FRIEND_REQUEST_ACCEPTED_MESSAGE = "Now you are friends with %s"; + public static final String JOIN_REQUEST_APPROVED_SUBJECT = "You have successfully joined the event"; + public static final String JOIN_REQUEST_APPROVED_MESSAGE = "You have successfully joined %s"; + public static final String JOIN_REQUEST_DECLINED_SUBJECT = "The organizer didn't approve your request"; + public static final String JOIN_REQUEST_DECLINED_MESSAGE = + "While we can't confirm your attendance this time, there's another way to stay connected! Join our " + + "organizer's friend list for future updates and opportunities."; + public static final String NEW_JOIN_REQUEST_SUBJECT = "New people want to join your event"; + public static final String NEW_JOIN_REQUEST_MESSAGE = "%s wants to join your event"; } diff --git a/service-api/src/main/java/greencity/constant/ErrorMessage.java b/service-api/src/main/java/greencity/constant/ErrorMessage.java index 94b5d3d3ef..c85344a041 100644 --- a/service-api/src/main/java/greencity/constant/ErrorMessage.java +++ b/service-api/src/main/java/greencity/constant/ErrorMessage.java @@ -175,6 +175,11 @@ public class ErrorMessage { public static final String USER_HAS_ALREADY_ADDED_EVENT_TO_FAVORITES = "User has already added this event to favorites."; public static final String EVENT_IS_NOT_IN_FAVORITES = "This event is not in favorites."; + public static final String USER_HAS_ALREADY_ADDED_EVENT_TO_REQUESTED = + "User has already added this event to requested."; + public static final String EVENT_IS_NOT_IN_REQUESTED = "This event is not in requested."; + public static final String USER_DID_NOT_REQUEST_FOR_EVENT = "User with this id did not request to join event: "; + public static final String EVENT_COMMENT_NOT_FOUND_BY_ID = "Event comment doesn't exist by this id: "; public static final String EVENT_IS_FINISHED = "Finished event cannot be modified"; public static final String USER_HAS_NO_FRIEND_WITH_ID = "User has no friend with this id: "; public static final String INVALID_DURATION = "The duration for such habit is lower than previously set"; diff --git a/service-api/src/main/java/greencity/dto/user/UserForListDto.java b/service-api/src/main/java/greencity/dto/user/UserForListDto.java new file mode 100644 index 0000000000..916549a889 --- /dev/null +++ b/service-api/src/main/java/greencity/dto/user/UserForListDto.java @@ -0,0 +1,45 @@ +package greencity.dto.user; + +import greencity.constant.ServiceValidationConstants; +import greencity.enums.Role; +import greencity.enums.UserStatus; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import java.time.LocalDateTime; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@EqualsAndHashCode +public class UserForListDto { + @NotNull + private Long id; + + @NotBlank + @Size( + min = ServiceValidationConstants.USERNAME_MIN_LENGTH, + max = ServiceValidationConstants.USERNAME_MAX_LENGTH) + private String name; + + private LocalDateTime dateOfRegistration; + + @Email(message = ServiceValidationConstants.INVALID_EMAIL) + @NotBlank + private String email; + + @NotNull + private UserStatus userStatus; + + @NotNull + private Role role; + + private String userCredo; +} \ No newline at end of file diff --git a/service-api/src/main/java/greencity/enums/NotificationType.java b/service-api/src/main/java/greencity/enums/NotificationType.java index be0fa5f763..24badd192b 100644 --- a/service-api/src/main/java/greencity/enums/NotificationType.java +++ b/service-api/src/main/java/greencity/enums/NotificationType.java @@ -29,7 +29,10 @@ public enum NotificationType { HABIT_COMMENT_USER_TAG, HABIT_LAST_DAY_OF_PRIMARY_DURATION, PLACE_STATUS, - PLACE_ADDED; + PLACE_ADDED, + EVENT_REQUEST_ACCEPTED, + EVENT_REQUEST_DECLINED, + EVENT_INVITE; private static final EnumSet COMMENT_LIKE_TYPES = EnumSet.of( ECONEWS_COMMENT_LIKE, EVENT_COMMENT_LIKE, HABIT_COMMENT_LIKE); diff --git a/service-api/src/main/java/greencity/service/EventService.java b/service-api/src/main/java/greencity/service/EventService.java index 4ecc0e648e..460f476b5a 100644 --- a/service-api/src/main/java/greencity/service/EventService.java +++ b/service-api/src/main/java/greencity/service/EventService.java @@ -13,6 +13,7 @@ import java.security.Principal; import java.util.List; import java.util.Set; +import greencity.dto.user.UserForListDto; import greencity.dto.user.UserVO; import org.springframework.data.domain.Pageable; import org.springframework.web.multipart.MultipartFile; @@ -209,4 +210,43 @@ public interface EventService { * @return user liked event or not. */ boolean isEventDislikedByUser(Long eventId, UserVO userVO); + + /** + * Method for adding an event to requested by event id. + * + * @param eventId - event id. + * @param email - user email. + * @author Olha Pitsyk. + */ + void addToRequested(Long eventId, String email); + + /** + * Method for removing an event from requested by event id. + * + * @param eventId - event id. + * @param email - user email. + * @author Olha Pitsyk. + */ + void removeFromRequested(Long eventId, String email); + + /** + * Method for getting all users who made request for joining the event. + * + * @author Olha Pitsyk. + */ + PageableDto getRequestedUsers(Long eventId, String email, Pageable pageable); + + /** + * Method for approving request for joining the event. + * + * @author Olha Pitsyk. + */ + void approveRequest(Long eventId, String email, Long userId); + + /** + * Method for declining request for joining the event. + * + * @author Olha Pitsyk. + */ + void declineRequest(Long eventId, String email, Long userId); } diff --git a/service/src/main/java/greencity/service/EventServiceImpl.java b/service/src/main/java/greencity/service/EventServiceImpl.java index 5b3970b62d..a1bef001dd 100644 --- a/service/src/main/java/greencity/service/EventServiceImpl.java +++ b/service/src/main/java/greencity/service/EventServiceImpl.java @@ -23,6 +23,7 @@ import greencity.dto.tag.TagDto; import greencity.dto.tag.TagUaEnDto; import greencity.dto.tag.TagVO; +import greencity.dto.user.UserForListDto; import greencity.dto.user.UserVO; import greencity.entity.Tag; import greencity.entity.User; @@ -46,6 +47,7 @@ import greencity.repository.UserRepo; import jakarta.persistence.Tuple; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ObjectUtils; import org.modelmapper.ModelMapper; @@ -110,6 +112,7 @@ import static greencity.constant.EventTupleConstant.titleImage; import static greencity.constant.EventTupleConstant.type; +@Slf4j @Service @Transactional @RequiredArgsConstructor @@ -864,6 +867,115 @@ public boolean isEventDislikedByUser(Long eventId, UserVO userVO) { return event.getUsersDislikedEvents().stream().anyMatch(u -> u.getId().equals(userVO.getId())); } + @Override + public void addToRequested(Long eventId, String email) { + Event event = eventRepo.findById(eventId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.EVENT_NOT_FOUND_BY_ID + eventId)); + + User currentUser = userRepo.findByEmail(email) + .orElseThrow(() -> new NotFoundException(ErrorMessage.USER_NOT_FOUND_BY_EMAIL + email)); + + if (event.getRequesters().contains(currentUser)) { + throw new BadRequestException(ErrorMessage.USER_HAS_ALREADY_ADDED_EVENT_TO_REQUESTED); + } + + event.getRequesters().add(currentUser); + eventRepo.save(event); + + userNotificationService.createNotification(modelMapper.map(event.getOrganizer(), UserVO.class), + modelMapper.map(currentUser, UserVO.class), NotificationType.EVENT_INVITE, eventId, event.getTitle()); + } + + @Override + public void removeFromRequested(Long eventId, String email) { + Event event = eventRepo.findById(eventId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.EVENT_NOT_FOUND_BY_ID + eventId)); + + User currentUser = userRepo.findByEmail(email) + .orElseThrow(() -> new NotFoundException(ErrorMessage.USER_NOT_FOUND_BY_EMAIL + email)); + + if (!event.getRequesters().contains(currentUser)) { + throw new BadRequestException(ErrorMessage.EVENT_IS_NOT_IN_REQUESTED); + } + + event.getRequesters().remove(currentUser); + eventRepo.save(event); + } + + @Override + public PageableDto getRequestedUsers(Long eventId, String email, Pageable pageable) { + User user = userRepo.findByEmail(email) + .orElseThrow(() -> new NotFoundException(ErrorMessage.USER_NOT_FOUND_BY_EMAIL + email)); + + Event event = eventRepo.findById(eventId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.EVENT_NOT_FOUND)); + + if (!user.equals(event.getOrganizer())) { + throw new UserHasNoPermissionToAccessException(ErrorMessage.USER_HAS_NO_PERMISSION); + } + + Page usersPage = userRepo.findUsersByRequestedEvents(eventId, pageable); + List userList = usersPage.stream() + .map(users -> modelMapper.map(users, UserForListDto.class)) + .collect(Collectors.toList()); + + return new PageableDto<>( + userList, + usersPage.getTotalElements(), + usersPage.getPageable().getPageNumber(), + usersPage.getTotalPages()); + } + + @Override + public void approveRequest(Long eventId, String email, Long userId) { + UserVO userVO = restClient.findByEmail(email); + User currentUser = modelMapper.map(userVO, User.class); + + Event event = eventRepo.findById(eventId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.EVENT_NOT_FOUND)); + + if (!Objects.equals(currentUser.getId(), event.getOrganizer().getId())) { + throw new UserHasNoPermissionToAccessException(ErrorMessage.USER_HAS_NO_PERMISSION); + } + if (event.getRequesters().stream().noneMatch(u -> Objects.equals(u.getId(), userId))) { + throw new BadRequestException(ErrorMessage.USER_DID_NOT_REQUEST_FOR_EVENT + userId); + } + User userToJoin = userRepo.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.USER_NOT_FOUND_BY_ID + userId)); + + event.getRequesters().remove(userToJoin); + event.getAttenders().add(userToJoin); + + eventRepo.save(event); + + userNotificationService.createNotification(modelMapper.map(userToJoin, UserVO.class), userVO, + NotificationType.EVENT_REQUEST_ACCEPTED, eventId, event.getTitle()); + } + + @Override + public void declineRequest(Long eventId, String email, Long userId) { + UserVO userVO = restClient.findByEmail(email); + User currentUser = modelMapper.map(userVO, User.class); + Event event = eventRepo.findById(eventId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.EVENT_NOT_FOUND)); + + if (!Objects.equals(currentUser.getId(), event.getOrganizer().getId())) { + throw new UserHasNoPermissionToAccessException(ErrorMessage.USER_HAS_NO_PERMISSION); + } + if (event.getRequesters().stream().noneMatch(u -> Objects.equals(u.getId(), userId))) { + throw new BadRequestException(ErrorMessage.USER_DID_NOT_REQUEST_FOR_EVENT + userId); + } + User userToJoin = + userRepo.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.USER_NOT_FOUND_BY_ID + userId)); + + event.getRequesters().remove(userToJoin); + eventRepo.save(event); + + userNotificationService.createNotification(modelMapper.map(userToJoin, UserVO.class), userVO, + NotificationType.EVENT_REQUEST_DECLINED, eventId, event.getTitle()); + } + private void sendEventLikeNotification(User targetUser, UserVO actionUser, Long eventId, Event event) { final LikeNotificationDto likeNotificationDto = LikeNotificationDto.builder() .targetUserVO(modelMapper.map(targetUser, UserVO.class)) diff --git a/service/src/main/java/greencity/service/NotificationServiceImpl.java b/service/src/main/java/greencity/service/NotificationServiceImpl.java index b7f5961a77..cd254559bf 100644 --- a/service/src/main/java/greencity/service/NotificationServiceImpl.java +++ b/service/src/main/java/greencity/service/NotificationServiceImpl.java @@ -269,6 +269,7 @@ public void sendEmailNotification(EmailNotificationDto notificationDto) { List invites = List.of( NotificationType.FRIEND_REQUEST_RECEIVED, NotificationType.FRIEND_REQUEST_ACCEPTED, + NotificationType.EVENT_INVITE, NotificationType.HABIT_INVITE); List systems = List.of( NotificationType.ECONEWS_CREATED, @@ -277,7 +278,9 @@ public void sendEmailNotification(EmailNotificationDto notificationDto) { NotificationType.EVENT_NAME_UPDATED, NotificationType.EVENT_UPDATED, NotificationType.EVENT_JOINED, - NotificationType.HABIT_LAST_DAY_OF_PRIMARY_DURATION); + NotificationType.HABIT_LAST_DAY_OF_PRIMARY_DURATION, + NotificationType.EVENT_REQUEST_ACCEPTED, + NotificationType.EVENT_REQUEST_DECLINED); List places = List.of( NotificationType.PLACE_STATUS, NotificationType.PLACE_ADDED); diff --git a/service/src/main/resources/notification.properties b/service/src/main/resources/notification.properties index 9301efe249..a32be185cc 100644 --- a/service/src/main/resources/notification.properties +++ b/service/src/main/resources/notification.properties @@ -49,6 +49,13 @@ FRIEND_REQUEST_ACCEPTED={user} accepted your friend request. FRIEND_REQUEST_RECEIVED_TITLE=You have received a friend request FRIEND_REQUEST_RECEIVED={user} sent you a friend request. +EVENT_REQUEST_ACCEPTED_TITLE=You have successfully joined the event +EVENT_REQUEST_ACCEPTED=You have successfully joined {message} +EVENT_REQUEST_DECLINED_TITLE=The organizer didn't approve your request +EVENT_REQUEST_DECLINED= While we can't confirm your attendance this time, there's another way to stay connected! Join our organizer's friend list for future updates and opportunities. +EVENT_INVITE_TITLE= New people want to join your event +EVENT_INVITE= {user} wants to join your event + HABIT_LIKE=Habit {message} received a like from {user}. HABIT_LIKE_TITLE=Your habit received a like HABIT_INVITE_TITLE=Habit invitation. diff --git a/service/src/main/resources/notification_ua.properties b/service/src/main/resources/notification_ua.properties index 82bdedce8f..df831c021e 100644 --- a/service/src/main/resources/notification_ua.properties +++ b/service/src/main/resources/notification_ua.properties @@ -29,6 +29,13 @@ ECONEWS_CREATED=\u0412\u0438 \u0443\u0441\u043F\u0456\u0448\u043D\u043E \u0441\u ECONEWS_LIKE_TITLE=\u0412\u0430\u0448\u0430 \u043D\u043E\u0432\u0438\u043D\u0430 \u043E\u0442\u0440\u0438\u043C\u0430\u043B\u0430 \u043B\u0430\u0439\u043A ECONEWS_LIKE=\u041d\u043e\u0432\u0438\u043d\u0430 {message} \u043e\u0442\u0440\u0438\u043c\u0430\u043b\u0430 \u043b\u0430\u0439\u043a \u0432\u0456\u0434 {user}. +EVENT_REQUEST_ACCEPTED_TITLE=\u0412\u0438 \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0434\u043e\u043B\u0443\u0447\u0438\u043B\u0438\u0441\u044F \u0434\u043E \u043F\u043E\u0434\u0456\u0457 +EVENT_REQUEST_ACCEPTED=\u0412\u0438 \u0443\u0441\u043F\u0456\u0448\u043D\u043E \u0434\u043E\u043B\u0443\u0447\u0438\u043B\u0438\u0441\u044F \u0434\u043E {message} +EVENT_REQUEST_DECLINED_TITLE=\u041E\u0440\u0433\u0430\u043D\u0456\u0437\u0430\u0442\u043E\u0440 \u043D\u0435 \u043F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0432 \u0432\u0430\u0448 \u0437\u0430\u043F\u0438\u0442 +EVENT_REQUEST_DECLINED=\u0425\u043E\u0447\u0430 \u043C\u0438 \u043D\u0435 \u043C\u043E\u0436\u0435\u043C\u043E \u043F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438 \u0432\u0430\u0448\u0443 \u0443\u0447\u0430\u0441\u0442\u044C \u0446\u044C\u043E\u0433\u043E \u0440\u0430\u0437\u0443, \u0454 \u0456\u043D\u0448\u0438\u0439 \u0441\u043F\u043E\u0441\u0456\u0431 \u0437\u0430\u043B\u0438\u0448\u0430\u0442\u0438\u0441\u044F \u043D\u0430 \u0437\u0432'\u044F\u0437\u043A\u0443! \u0414\u043E\u043B\u0443\u0447\u0456\u0442\u044C\u0441\u044F \u0434\u043E \u0441\u043F\u0438\u0441\u043A\u0443 \u0434\u0440\u0443\u0437\u0456\u0432 \u043E\u0440\u0433\u0430\u043D\u0456\u0437\u0430\u0442\u043E\u0440\u0430 \u0434\u043B\u044F \u043E\u0442\u0440\u0438\u043C\u0430\u043D\u043D\u044F \u043C\u0430\u0439\u0431\u0443\u0442\u043D\u0456\u0445 \u043E\u043D\u043E\u0432\u043B\u0435\u043D\u044C \u0442\u0430 \u043C\u043E\u0436\u043B\u0438\u0432\u043E\u0441\u0442\u0435\u0439. +EVENT_INVITE_TITLE=\u041D\u043E\u0432\u0456 \u043B\u044E\u0434\u0438 \u0445\u043E\u0447\u0443\u0442\u044C \u0434\u043E\u043B\u0443\u0447\u0438\u0442\u0438\u0441\u044F \u0434\u043E \u0432\u0430\u0448\u043E\u0457 \u043F\u043E\u0434\u0456\u0457 +EVENT_INVITE={user} \u0445\u043E\u0447\u0435 \u0434\u043E\u043B\u0443\u0447\u0438\u0442\u0438\u0441\u044F \u0434\u043E \u0432\u0430\u0448\u043E\u0457 \u043F\u043E\u0434\u0456\u0457 + EVENT_COMMENT_TITLE=\u0412\u0438 \u043e\u0442\u0440\u0438\u043c\u0430\u043b\u0438 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440 EVENT_COMMENT=\u0414\u043E \u0432\u0430\u0448\u043e\u0457 \u043F\u043E\u0434\u0456\u0457 \u00AB{secondMessage}\u00BB {user} \u0437\u0430\u043B\u0438\u0448\u0438\u0432(\u043B\u0438) {message}. EVENT_CANCELED_TITLE=\u041f\u043e\u0434\u0456\u044e \u0431\u0443\u043b\u043e \u0441\u043a\u0430\u0441\u043e\u0432\u0430\u043d\u043e diff --git a/service/src/test/java/greencity/ModelUtils.java b/service/src/test/java/greencity/ModelUtils.java index f0cc16a09c..494ac150b9 100644 --- a/service/src/test/java/greencity/ModelUtils.java +++ b/service/src/test/java/greencity/ModelUtils.java @@ -1798,6 +1798,7 @@ public static Event getEvent() { event.setId(1L); event.setOrganizer(getUser()); event.setFollowers(followers); + event.setRequesters(followers); event.setTitle("Title"); event.setAttenders(new HashSet<>(Collections.singleton(getUser()))); List dates = new ArrayList<>(); diff --git a/service/src/test/java/greencity/service/EventServiceImplTest.java b/service/src/test/java/greencity/service/EventServiceImplTest.java index 427409b7f2..2f2209dbb3 100644 --- a/service/src/test/java/greencity/service/EventServiceImplTest.java +++ b/service/src/test/java/greencity/service/EventServiceImplTest.java @@ -1583,4 +1583,271 @@ void checkIsEventDislikedByUserTest_ThrowNotFoundException_Test() { assertTrue(exception.getMessage().contains(ErrorMessage.EVENT_NOT_FOUND_BY_ID + event.getId())); verify(eventRepo, times(1)).findById(event.getId()); } + + @Test + void addToRequestedTest() { + Event event = ModelUtils.getEvent(); + User user = ModelUtils.getUser(); + user.setId(2L); + + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + when(userRepo.findByEmail(TestConst.EMAIL)).thenReturn(Optional.of(user)); + when(eventRepo.save(event)).thenReturn(event); + + eventService.addToRequested(1L, TestConst.EMAIL); + + verify(eventRepo).findById(any()); + verify(userRepo).findByEmail(TestConst.EMAIL); + verify(eventRepo).save(event); + } + + @Test + void addToRequestedThrowsExceptionWhenEventNotFoundTest() { + when(eventRepo.findById(any())).thenThrow(NotFoundException.class); + assertThrows(NotFoundException.class, () -> eventService.addToRequested(1L, TestConst.EMAIL)); + verify(eventRepo).findById(any()); + } + + @Test + void addToRequestedThrowsExceptionWhenUserNotFoundTest() { + when(userRepo.findById(any())).thenThrow(NotFoundException.class); + assertThrows(NotFoundException.class, () -> eventService.addToRequested(1L, TestConst.EMAIL)); + verify(eventRepo).findById(any()); + } + + @Test + void addToRequestedThrowsExceptionWhenUserHasAlreadyAddedEventToRequestedTest() { + Event event = ModelUtils.getEvent(); + User user = ModelUtils.getUser(); + + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + when(userRepo.findByEmail(TestConst.EMAIL)).thenReturn(Optional.of(user)); + + assertThrows(BadRequestException.class, () -> eventService.addToRequested(1L, TestConst.EMAIL)); + + verify(eventRepo).findById(any()); + verify(userRepo).findByEmail(TestConst.EMAIL); + } + + @Test + void removeFromRequestedTest() { + Event event = ModelUtils.getEvent(); + User user = ModelUtils.getUser(); + + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + when(userRepo.findByEmail(TestConst.EMAIL)).thenReturn(Optional.of(user)); + when(eventRepo.save(event)).thenReturn(event); + + eventService.removeFromRequested(1L, TestConst.EMAIL); + + verify(eventRepo).findById(any()); + verify(userRepo).findByEmail(TestConst.EMAIL); + verify(eventRepo).save(event); + } + + @Test + void removeFromRequestedThrowsExceptionWhenEventNotFoundTest() { + when(eventRepo.findById(any())).thenThrow(NotFoundException.class); + assertThrows(NotFoundException.class, () -> eventService.removeFromRequested(1L, TestConst.EMAIL)); + verify(eventRepo).findById(any()); + } + + @Test + void removeFromRequestedThrowsExceptionWhenUserNotFoundTest() { + when(userRepo.findById(any())).thenThrow(NotFoundException.class); + assertThrows(NotFoundException.class, () -> eventService.removeFromRequested(1L, TestConst.EMAIL)); + verify(eventRepo).findById(any()); + } + + @Test + void removeFromRequestedThrowsExceptionWhenEventIsNotInRequestedTest() { + Event event = ModelUtils.getEvent(); + User user = ModelUtils.getUser(); + user.setId(2L); + + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + when(userRepo.findByEmail(TestConst.EMAIL)).thenReturn(Optional.of(user)); + + assertThrows(BadRequestException.class, () -> eventService.removeFromRequested(1L, TestConst.EMAIL)); + + verify(eventRepo).findById(any()); + verify(userRepo).findByEmail(TestConst.EMAIL); + } + + @Test + void getRequestedUsersTest() { + Event event = ModelUtils.getEvent(); + User user = ModelUtils.getUser(); + Pageable pageable = PageRequest.of(0, 20); + + Page userPage = new PageImpl<>(List.of(user), pageable, 1); + when(userRepo.findByEmail(anyString())).thenReturn(Optional.of(user)); + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + when(userRepo.findUsersByRequestedEvents(any(), any(Pageable.class))) + .thenReturn(userPage); + + assertEquals(1, eventService.getRequestedUsers(1L, "", pageable).getTotalElements()); + + verify(userRepo).findByEmail(anyString()); + verify(eventRepo).findById(any()); + verify(userRepo).findUsersByRequestedEvents(any(), any(Pageable.class)); + } + + @Test + void getRequestedUsersThrowsUserHasNoPermissionExceptionTest() { + Event event = ModelUtils.getEvent(); + User user = User.builder().id(20L).build(); + Pageable pageable = PageRequest.of(0, 20); + + when(userRepo.findByEmail(anyString())).thenReturn(Optional.of(user)); + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + + assertThrows(UserHasNoPermissionToAccessException.class, + () -> eventService.getRequestedUsers(1L, "", pageable)); + + verify(userRepo).findByEmail(anyString()); + verify(eventRepo).findById(any()); + } + + @Test + void getRequestedUsersThrowsUserNotFoundExceptionTest() { + when(userRepo.findByEmail(anyString())).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> eventService.getRequestedUsers(1L, "", null)); + } + + @Test + void approveRequestTest() { + UserVO userVO = ModelUtils.getUserVO(); + User user = ModelUtils.getUser(); + User userToJoin = User.builder().build(); + Event event = ModelUtils.getEvent(); + event.getRequesters().add(userToJoin); + + when(restClient.findByEmail(anyString())).thenReturn(userVO); + when(modelMapper.map(userVO, User.class)).thenReturn(user); + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + when(userRepo.findById(any())) + .thenReturn(Optional.of(userToJoin)); + when(eventRepo.save(any())).thenReturn(null); + + eventService.approveRequest(event.getId(), userVO.getEmail(), userToJoin.getId()); + + verify(restClient).findByEmail(anyString()); + verify(modelMapper).map(userVO, User.class); + verify(eventRepo).findById(any()); + verify(userRepo).findById(any()); + verify(eventRepo).save(any()); + } + + @Test + void approveRequestUserHasNoAccessTest() { + UserVO userVO = ModelUtils.getUserVO(); + User user = ModelUtils.getTestUser(); + Event event = ModelUtils.getEvent(); + + when(restClient.findByEmail(anyString())).thenReturn(userVO); + when(modelMapper.map(userVO, User.class)).thenReturn(user); + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + + Long eventId = event.getId(); + String email = userVO.getEmail(); + assertThrows(UserHasNoPermissionToAccessException.class, + () -> eventService.approveRequest(eventId, email, null)); + + verify(restClient).findByEmail(anyString()); + verify(modelMapper).map(userVO, User.class); + verify(eventRepo).findById(any()); + } + + @Test + void approveRequestUserDidNotRequestTest() { + UserVO userVO = ModelUtils.getUserVO(); + User user = ModelUtils.getUser(); + User userToJoin = User.builder().build(); + Event event = ModelUtils.getEvent(); + event.getRequesters().remove(userToJoin); + + when(restClient.findByEmail(anyString())).thenReturn(userVO); + when(modelMapper.map(userVO, User.class)).thenReturn(user); + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + + Long eventId = event.getId(); + Long userToJoinId = userToJoin.getId(); + String email = userVO.getEmail(); + assertThrows(BadRequestException.class, + () -> eventService.approveRequest(eventId, email, userToJoinId)); + + verify(restClient).findByEmail(anyString()); + verify(modelMapper).map(userVO, User.class); + verify(eventRepo).findById(any()); + } + + @Test + void declineRequestTest() { + UserVO userVO = ModelUtils.getUserVO(); + User user = ModelUtils.getUser(); + User userToJoin = User.builder().build(); + Event event = ModelUtils.getEvent(); + event.getRequesters().add(userToJoin); + + when(restClient.findByEmail(anyString())).thenReturn(userVO); + when(modelMapper.map(userVO, User.class)).thenReturn(user); + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + when(userRepo.findById(any())) + .thenReturn(Optional.of(userToJoin)); + when(eventRepo.save(any())).thenReturn(null); + + eventService.declineRequest(event.getId(), userVO.getEmail(), userToJoin.getId()); + + verify(restClient).findByEmail(anyString()); + verify(modelMapper).map(userVO, User.class); + verify(eventRepo).findById(any()); + verify(userRepo).findById(any()); + verify(eventRepo).save(any()); + } + + @Test + void declineRequestUserHasNoAccessTest() { + UserVO userVO = ModelUtils.getUserVO(); + User user = ModelUtils.getTestUser(); + Event event = ModelUtils.getEvent(); + + when(restClient.findByEmail(anyString())).thenReturn(userVO); + when(modelMapper.map(userVO, User.class)).thenReturn(user); + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + + Long eventId = event.getId(); + String email = userVO.getEmail(); + assertThrows(UserHasNoPermissionToAccessException.class, + () -> eventService.declineRequest(eventId, email, null)); + + verify(restClient).findByEmail(anyString()); + verify(modelMapper).map(userVO, User.class); + verify(eventRepo).findById(any()); + } + + @Test + void declineRequestUserDidNotRequestTest() { + UserVO userVO = ModelUtils.getUserVO(); + User user = ModelUtils.getUser(); + User userToJoin = User.builder().build(); + Event event = ModelUtils.getEvent(); + event.getRequesters().remove(userToJoin); + + when(restClient.findByEmail(anyString())).thenReturn(userVO); + when(modelMapper.map(userVO, User.class)).thenReturn(user); + when(eventRepo.findById(any())).thenReturn(Optional.of(event)); + + Long eventId = event.getId(); + Long userToJoinId = userToJoin.getId(); + String email = userVO.getEmail(); + assertThrows(BadRequestException.class, + () -> eventService.declineRequest(eventId, email, userToJoinId)); + + verify(restClient).findByEmail(anyString()); + verify(modelMapper).map(userVO, User.class); + verify(eventRepo).findById(any()); + } + }