diff --git a/core/src/main/java/greencity/controller/EmailController.java b/core/src/main/java/greencity/controller/EmailController.java index 74da26fcc..4a3c88fc0 100644 --- a/core/src/main/java/greencity/controller/EmailController.java +++ b/core/src/main/java/greencity/controller/EmailController.java @@ -85,4 +85,30 @@ public ResponseEntity sendScheduledNotification(@RequestBody ScheduledEmai emailService.sendScheduledNotificationEmail(message); return ResponseEntity.ok().build(); } + + /** + * Method for sending an email notification about the status change of a place + * to the user. + * + * @param dto Object containing the necessary information for sending the status + * change notification email. The object includes: - userName: The + * name of the user. - userEmail: The email of the user who will + * receive the notification. - placeName: The name of the place whose + * status has been changed. - newStatus: The new status of the place. + * + * @return ResponseEntity with HTTP status 200 OK if the email was successfully + * sent. If any error occurs, an appropriate error response will be + * returned. + */ + @Operation(summary = "Send email notification to user if place status changed") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = HttpStatuses.OK), + @ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST), + @ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND) + }) + @PostMapping("/sendPlaceStatusChange") + public ResponseEntity sendPlaceStatusChange(@RequestBody PlaceStatusChangeDto dto) { + emailService.sendPlaceStatusChangeNotification(dto); + return ResponseEntity.ok().build(); + } } diff --git a/core/src/main/resources/messages_en.properties b/core/src/main/resources/messages_en.properties index 8f733c360..eeaf62439 100644 --- a/core/src/main/resources/messages_en.properties +++ b/core/src/main/resources/messages_en.properties @@ -54,4 +54,5 @@ advice.for.block=You received this email for security reasons. To protect your a profile.text=If you no longer wish to receive these emails, you can unsubscribe from them in your profile=profile advice.general.ubs=You are receiving this email because you are a registered member of Pick Up City. -read.more=READ MORE \ No newline at end of file +read.more=READ MORE +your.place.status.changed=The status of your place has been updated. You are receiving this email because the status of your place was changed. Thank you for using our service! \ No newline at end of file diff --git a/core/src/main/resources/messages_uk.properties b/core/src/main/resources/messages_uk.properties index f559a1290..510b8e356 100644 --- a/core/src/main/resources/messages_uk.properties +++ b/core/src/main/resources/messages_uk.properties @@ -54,4 +54,5 @@ advice.for.block=\u0412\u0438\u0020\u043E\u0442\u0440\u0438\u043C\u0430\u043B\u0 profile.text=\u042f\u043a\u0449\u043e\u0020\u0432\u0438\u0020\u0431\u0456\u043b\u044c\u0448\u0435\u0020\u043d\u0435\u0020\u0445\u043e\u0447\u0435\u0442\u0435\u0020\u043e\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u0442\u0438\u0020\u0440\u043e\u0437\u0441\u0438\u043b\u043a\u0443\u002c\u0020\u0432\u0438\u0020\u043c\u043e\u0436\u0435\u0442\u0435\u0020\u0432\u0456\u0434\u043f\u0438\u0441\u0430\u0442\u0438\u0441\u044f\u0020\u0432\u0456\u0434\u0020\u043d\u0435\u0457\u0020\u0443\u0020\u0441\u0432\u043e\u0454\u043c\u0443 profile=\u043f\u0440\u043e\u0444\u0456\u043b\u0456 advice.general.ubs=\u0412\u0438\u0020\u043e\u0442\u0440\u0438\u043c\u0430\u043b\u0438\u0020\u0446\u0435\u0439\u0020\u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0438\u0439\u0020\u043b\u0438\u0441\u0442\u002c\u0020\u0442\u043e\u043c\u0443\u0020\u0449\u043e\u0020\u0454\u0020\u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u0438\u043c\u0020\u043a\u043b\u0456\u0454\u043d\u0442\u043e\u043c\u0020\u0050\u0069\u0063\u006b\u0020\u0055\u0070\u0020\u0043\u0069\u0074\u0079\u002e -read.more=\u0427\u0418\u0422\u0410\u0422\u0418 \u0414\u0410\u041b\u0406 \ No newline at end of file +read.more=\u0427\u0418\u0422\u0410\u0422\u0418 \u0414\u0410\u041b\u0406 +your.place.status.changed=\u0421\u0442\u0430\u0442\u0443\u0441\u0020\u0432\u0430\u0448\u043E\u0433\u043E\u0020\u043C\u0456\u0441\u0446\u044F\u0020\u0431\u0443\u0432\u0020\u043E\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0439\u002E\u0020\u0412\u0438\u0020\u043E\u0442\u0440\u0438\u043C\u0430\u043B\u0438\u0020\u0446\u0435\u0439\u0020\u043B\u0438\u0441\u0442\u002C\u0020\u0442\u043E\u043C\u0443\u0020\u0449\u043E\u0020\u0441\u0442\u0430\u0442\u0443\u0441\u0020\u0432\u0430\u0448\u043E\u0433\u043E\u0020\u043C\u0456\u0441\u0446\u044F\u0020\u0431\u0443\u043B\u043E\u0020\u0437\u043C\u0456\u043D\u0435\u043D\u043E\u002E\u0020\u0414\u044F\u043A\u0443\u0454\u043C\u043E\u002C\u0020\u0449\u043E\u0020\u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0454\u0442\u0435\u0441\u044C\u0020\u043D\u0430\u0448\u0438\u043C\u0020\u0441\u0435\u0440\u0432\u0456\u0441\u043E\u043C\u0021 \ No newline at end of file diff --git a/core/src/main/resources/templates/email/place-status-change.html b/core/src/main/resources/templates/email/place-status-change.html new file mode 100644 index 000000000..da68e8332 --- /dev/null +++ b/core/src/main/resources/templates/email/place-status-change.html @@ -0,0 +1,105 @@ + + + + + + + + + + + + diff --git a/core/src/test/java/greencity/controller/EmailControllerTest.java b/core/src/test/java/greencity/controller/EmailControllerTest.java index c5eafe73e..814b8e807 100644 --- a/core/src/test/java/greencity/controller/EmailControllerTest.java +++ b/core/src/test/java/greencity/controller/EmailControllerTest.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import greencity.dto.econews.InterestingEcoNewsDto; import greencity.dto.violation.UserViolationMailDto; +import greencity.enums.PlaceStatus; +import greencity.message.PlaceStatusChangeDto; import greencity.message.ScheduledEmailMessage; import greencity.message.SendHabitNotification; import greencity.message.SendReportEmailMessage; @@ -176,4 +178,26 @@ void sendUserReceivedScheduledNotification() { .content(content)) .andExpect(status().isOk()); } + + @Test + @SneakyThrows + void sendPlaceStatusChangeTest() { + PlaceStatusChangeDto dto = new PlaceStatusChangeDto(); + dto.setUserName("John Doe"); + dto.setEmail("test@example.com"); + dto.setPlaceName("Green Park"); + dto.setNewStatus(PlaceStatus.APPROVED); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + String content = objectMapper.writeValueAsString(dto); + + mockMvc.perform(MockMvcRequestBuilders.post(LINK + "/sendPlaceStatusChange") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer your_token_here") + .content(content)) + .andExpect(status().isOk()); + + verify(emailService).sendPlaceStatusChangeNotification(dto); + } } diff --git a/service-api/src/main/java/greencity/constant/EmailConstants.java b/service-api/src/main/java/greencity/constant/EmailConstants.java index eb4d2f66f..2e2649776 100644 --- a/service-api/src/main/java/greencity/constant/EmailConstants.java +++ b/service-api/src/main/java/greencity/constant/EmailConstants.java @@ -40,6 +40,8 @@ public class EmailConstants { public static final String BODY = "body"; public static final String UNLOCK_USER_LINK = "unlockUserLink"; public static final String NOTIFICATIONS_LINK = "notificationsLink"; + public static final String PLACE_NAME = "placeName"; + public static final String PLACE_STATUS = "placeStatus"; // templates public static final String VERIFY_EMAIL_PAGE = "verify-email-page"; public static final String RESTORE_EMAIL_PAGE = "restore-email-page"; @@ -53,4 +55,5 @@ public class EmailConstants { public static final String SCHEDULED_NOTIFICATION_PAGE = "scheduled-notification-email-page"; public static final String RECEIVE_INTERESTING_NEWS_EMAIL_PAGE = "receive-interesting-news-email-page"; public static final String BLOCKED_USER_PAGE = "blocked-user-page"; + public static final String PLACE_STATUS_CHANGE_PAGE = "place-status-change"; } diff --git a/service-api/src/main/java/greencity/message/PlaceStatusChangeDto.java b/service-api/src/main/java/greencity/message/PlaceStatusChangeDto.java new file mode 100644 index 000000000..cf346e01e --- /dev/null +++ b/service-api/src/main/java/greencity/message/PlaceStatusChangeDto.java @@ -0,0 +1,22 @@ +package greencity.message; + +import greencity.enums.PlaceStatus; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PlaceStatusChangeDto { + @NotNull + private String placeName; + + @NotNull + private PlaceStatus newStatus; + + @NotNull + private String userName; + + @NotNull + private String email; +} diff --git a/service-api/src/main/java/greencity/service/EmailService.java b/service-api/src/main/java/greencity/service/EmailService.java index 08c259d34..31f58528e 100644 --- a/service-api/src/main/java/greencity/service/EmailService.java +++ b/service-api/src/main/java/greencity/service/EmailService.java @@ -4,6 +4,7 @@ import greencity.dto.user.UserActivationDto; import greencity.dto.user.UserDeactivationReasonDto; import greencity.dto.violation.UserViolationMailDto; +import greencity.message.PlaceStatusChangeDto; import greencity.message.ScheduledEmailMessage; import greencity.message.SendReportEmailMessage; @@ -85,7 +86,7 @@ void sendRestoreEmail(Long userId, String userFistName, String userEmail, String /** * Method for send violation to user. - * + * * @param dto {@link UserViolationMailDto}-includes all information about * Violation. */ @@ -138,4 +139,13 @@ void sendCreateNewPasswordForEmployee(Long employeeId, String employeeFistName, void sendBlockAccountNotificationWithUnblockLinkEmail( Long userId, String userFistName, String userEmail, String token, String language, boolean isUbs); + + /** + * Sends an email notification to a user regarding the status change of a place. + * + * @param dto the data transfer object containing information about the user, + * place, and the new status of the place (e.g., PROPOSED, DECLINED, + * APPROVED, DELETED). + */ + void sendPlaceStatusChangeNotification(PlaceStatusChangeDto dto); } diff --git a/service/src/main/java/greencity/service/EmailServiceImpl.java b/service/src/main/java/greencity/service/EmailServiceImpl.java index 29eb2889c..e2cba6f95 100644 --- a/service/src/main/java/greencity/service/EmailServiceImpl.java +++ b/service/src/main/java/greencity/service/EmailServiceImpl.java @@ -1,14 +1,19 @@ package greencity.service; import greencity.constant.EmailConstants; +import greencity.constant.ErrorMessage; import greencity.constant.LogMessage; import greencity.dto.econews.InterestingEcoNewsDto; import greencity.dto.user.SubscriberDto; import greencity.dto.user.UserActivationDto; import greencity.dto.user.UserDeactivationReasonDto; import greencity.dto.violation.UserViolationMailDto; +import greencity.entity.User; +import greencity.message.PlaceStatusChangeDto; import greencity.message.ScheduledEmailMessage; import greencity.message.SendReportEmailMessage; +import greencity.repository.LanguageRepo; +import greencity.repository.UserRepo; import greencity.validator.EmailAddressValidator; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; @@ -42,6 +47,7 @@ public class EmailServiceImpl implements EmailService { private final String senderEmailAddress; private final MessageSource messageSource; private static final String PARAM_USER_ID = "&user_id="; + private final UserRepo userRepo; /** * Constructor. @@ -51,13 +57,15 @@ public EmailServiceImpl(JavaMailSender javaMailSender, ITemplateEngine templateEngine, @Qualifier("sendEmailExecutor") Executor executor, @Value("${client.address}") String clientLink, - @Value("${sender.email.address}") String senderEmailAddress, MessageSource messageSource) { + @Value("${sender.email.address}") String senderEmailAddress, MessageSource messageSource, UserRepo userRepo, + LanguageRepo languageRepo) { this.javaMailSender = javaMailSender; this.templateEngine = templateEngine; this.executor = executor; this.clientLink = clientLink; this.senderEmailAddress = senderEmailAddress; this.messageSource = messageSource; + this.userRepo = userRepo; } /** @@ -317,6 +325,24 @@ private Map buildModelMapForPasswordRestore(Long userId, String return model; } + @Override + public void sendPlaceStatusChangeNotification(PlaceStatusChangeDto dto) { + Map model = new HashMap<>(); + String userEmail = dto.getEmail(); + User user = userRepo.findByEmail(userEmail) + .orElseThrow(() -> new RuntimeException(ErrorMessage.USER_NOT_FOUND_BY_EMAIL + userEmail)); + String userLanguageCode = user.getLanguage().getCode(); + model.put(EmailConstants.CLIENT_LINK, clientLink); + model.put(EmailConstants.USER_NAME, dto.getUserName()); + model.put(EmailConstants.PLACE_NAME, dto.getPlaceName()); + model.put(EmailConstants.PLACE_STATUS, dto.getNewStatus().name()); + model.put(EmailConstants.LANGUAGE, userLanguageCode); + + String template = createEmailTemplate(model, EmailConstants.PLACE_STATUS_CHANGE_PAGE); + sendEmail(userEmail, messageSource.getMessage(EmailConstants.PLACE_STATUS, null, + getLocale(userLanguageCode)), template); + } + private String getClientLinkByIsUbs(boolean isUbs) { return clientLink + "/#" + (isUbs ? "/ubs" : "/greenCity"); } diff --git a/service/src/test/java/greencity/ModelUtils.java b/service/src/test/java/greencity/ModelUtils.java index c1f886ed2..a3dc94151 100644 --- a/service/src/test/java/greencity/ModelUtils.java +++ b/service/src/test/java/greencity/ModelUtils.java @@ -711,4 +711,5 @@ public static TestersSignInRequest getTestersSignInRequestWithInvalidSecretKey() .secretKey("invalid-secret-key") .build(); } + } diff --git a/service/src/test/java/greencity/TestConst.java b/service/src/test/java/greencity/TestConst.java index 938363805..68086053a 100644 --- a/service/src/test/java/greencity/TestConst.java +++ b/service/src/test/java/greencity/TestConst.java @@ -7,4 +7,6 @@ public final class TestConst { public static final Long SIMPLE_LONG_NUMBER = 1L; public static final Long SIMPLE_LONG_TWO_NUMBER = 2L; public static final Long SIMPLE_LONG_NUMBER_BAD_VALUE = 100L; + public static final String ENGLISH_CODE = "en"; + public static final String PLACE_NAME = "Central Park"; } diff --git a/service/src/test/java/greencity/service/EmailServiceImplTest.java b/service/src/test/java/greencity/service/EmailServiceImplTest.java index 002fe6c54..c532e0de9 100644 --- a/service/src/test/java/greencity/service/EmailServiceImplTest.java +++ b/service/src/test/java/greencity/service/EmailServiceImplTest.java @@ -9,10 +9,16 @@ import greencity.dto.user.UserActivationDto; import greencity.dto.user.UserDeactivationReasonDto; import greencity.dto.violation.UserViolationMailDto; +import greencity.entity.Language; +import greencity.entity.User; import greencity.enums.EmailPreferencePeriodicity; +import greencity.enums.PlaceStatus; import greencity.exception.exceptions.WrongEmailException; +import greencity.message.PlaceStatusChangeDto; import greencity.message.ScheduledEmailMessage; import greencity.message.SendReportEmailMessage; +import greencity.repository.LanguageRepo; +import greencity.repository.UserRepo; import jakarta.mail.Session; import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.BeforeEach; @@ -29,13 +35,25 @@ import org.thymeleaf.ITemplateEngine; import java.util.*; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import static greencity.ModelUtils.getSubscriberDto; + +import static greencity.TestConst.ENGLISH_CODE; +import static greencity.TestConst.NAME; +import static greencity.TestConst.EMAIL; +import static greencity.TestConst.SIMPLE_LONG_NUMBER; +import static greencity.TestConst.PLACE_NAME; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.doAnswer; + import org.thymeleaf.context.Context; @ExtendWith(MockitoExtension.class) @@ -48,13 +66,23 @@ class EmailServiceImplTest { private ITemplateEngine templateEngine; @Mock private MessageSource messageSource; + @Mock + private UserRepo userRepo; + @Mock + LanguageRepo languageRepo; private static final Locale UA_LOCALE = Locale.of("uk", "UA"); @BeforeEach public void setup() { - service = new EmailServiceImpl(javaMailSender, templateEngine, Executors.newCachedThreadPool(), + service = new EmailServiceImpl( + javaMailSender, + templateEngine, + Executors.newCachedThreadPool(), "http://localhost:4200", - "test@email.com", messageSource); + "test@email.com", + messageSource, + userRepo, + languageRepo); when(javaMailSender.createMimeMessage()).thenReturn(new MimeMessage((Session) null)); when(templateEngine.process(any(String.class), any(Context.class))).thenReturn(""); } @@ -241,6 +269,61 @@ void sendBlockAccountNotificationWithUnblockLinkEmailTest(Long id, String name, verify(javaMailSender).createMimeMessage(); } + @Test + void sendPlaceStatusChangeNotificationTest() throws InterruptedException { + PlaceStatusChangeDto dto = new PlaceStatusChangeDto(); + dto.setUserName(NAME); + dto.setPlaceName(PLACE_NAME); + dto.setNewStatus(PlaceStatus.APPROVED); + dto.setEmail(EMAIL); + + User user = new User(); + user.setEmail(EMAIL); + user.setName(NAME); + Language language = new Language(SIMPLE_LONG_NUMBER, ENGLISH_CODE, List.of(user)); + user.setLanguage(language); + + when(userRepo.findByEmail(dto.getEmail())).thenReturn(Optional.of(user)); + + MimeMessage mimeMessage = mock(MimeMessage.class); + when(javaMailSender.createMimeMessage()).thenReturn(mimeMessage); + + doNothing().when(javaMailSender).send(any(MimeMessage.class)); + + CountDownLatch latch = new CountDownLatch(1); + doAnswer(invocation -> { + latch.countDown(); + return null; + }).when(javaMailSender).send(any(MimeMessage.class)); + + String subject = "Place Status Change Notification"; + when(messageSource.getMessage(eq(EmailConstants.PLACE_STATUS), any(), eq(getLocale(ENGLISH_CODE)))) + .thenReturn(subject); + + service.sendPlaceStatusChangeNotification(dto); + + latch.await(); + + verify(userRepo).findByEmail(dto.getEmail()); + verify(javaMailSender).createMimeMessage(); + verify(javaMailSender).send(mimeMessage); + verify(messageSource).getMessage(eq(EmailConstants.PLACE_STATUS), any(), eq(getLocale(ENGLISH_CODE))); + } + + @Test + void sendPlaceStatusChangeNotificationUserNotFoundTest() { + PlaceStatusChangeDto dto = new PlaceStatusChangeDto(); + dto.setUserName(NAME); + dto.setPlaceName(PLACE_NAME); + dto.setNewStatus(PlaceStatus.APPROVED); + dto.setEmail(EMAIL); + when(userRepo.findByEmail(dto.getEmail())).thenReturn(Optional.empty()); + assertThrows(RuntimeException.class, () -> { + service.sendPlaceStatusChangeNotification(dto); + }); + verify(userRepo).findByEmail(dto.getEmail()); + } + private static Locale getLocale(String language) { return switch (language) { case "ua" -> UA_LOCALE;