diff --git a/application/openapi.yaml b/application/openapi.yaml index b399ef72168f5be67a2b25143b3c84a95117554b..36bdf258e437fda4dabad8cb33b300d6decfee4f 100644 --- a/application/openapi.yaml +++ b/application/openapi.yaml @@ -14,6 +14,7 @@ components: type: string description: Status of application enum: + - pending - rejected - accepted @@ -26,6 +27,7 @@ components: - name - surname - fillingOutDate + - email properties: id: type: integer @@ -45,6 +47,9 @@ components: description: Date of birth example: 09-30-1997 format: date + email: + type: string + description: Email address of the person fillingOutDate: type: string description: Date of filling out the application @@ -66,9 +71,13 @@ components: description: Date of birth example: 1977-09-30 format: date + email: + type: string + description: Email address of the person required: - name - surname + - email responses: NotFound: diff --git a/application/src/main/java/cz/muni/pa165/config/ServiceConfig.java b/application/src/main/java/cz/muni/pa165/config/ServiceConfig.java index e1616efff4d506b601288943b694e3cd295d0320..9113f55d6e41232aa18163acd24753af63aaaec9 100644 --- a/application/src/main/java/cz/muni/pa165/config/ServiceConfig.java +++ b/application/src/main/java/cz/muni/pa165/config/ServiceConfig.java @@ -1,8 +1,10 @@ package cz.muni.pa165.config; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.web.client.RestTemplate; /** * @author Michal Badin @@ -11,4 +13,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @ComponentScan @EnableTransactionManagement public class ServiceConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } } \ No newline at end of file diff --git a/application/src/main/java/cz/muni/pa165/data/enums/ApplicationStatusEnum.java b/application/src/main/java/cz/muni/pa165/data/enums/ApplicationStatusEnum.java index 9302a4d523f432ea65a3f864e57119d148c7cf28..02c0421eeafa7c3157434dc1fd48811df51a03cf 100644 --- a/application/src/main/java/cz/muni/pa165/data/enums/ApplicationStatusEnum.java +++ b/application/src/main/java/cz/muni/pa165/data/enums/ApplicationStatusEnum.java @@ -7,8 +7,11 @@ import com.fasterxml.jackson.annotation.JsonValue; * @author Michal Badin */ public enum ApplicationStatusEnum { - ACCEPTED("accepted"), - REJECTED("rejected"); + PENDING("pending"), + + REJECTED("rejected"), + + ACCEPTED("accepted"); private final String value; diff --git a/application/src/main/java/cz/muni/pa165/data/model/Application.java b/application/src/main/java/cz/muni/pa165/data/model/Application.java index 77cd6839d9d5b2684443b0664a3ea00cd3c1470f..c597fbb4ad52e08dc26dc9f9e8b81d55b2533217 100644 --- a/application/src/main/java/cz/muni/pa165/data/model/Application.java +++ b/application/src/main/java/cz/muni/pa165/data/model/Application.java @@ -2,10 +2,7 @@ package cz.muni.pa165.data.model; import cz.muni.pa165.data.enums.ApplicationStatusEnum; import jakarta.persistence.*; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Past; -import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.*; import java.io.Serializable; import java.time.LocalDate; @@ -34,17 +31,22 @@ public class Application implements Serializable { @NotNull private LocalDate birthday; + @Email + private String email; + @Column(name = "filling_out_date") @PastOrPresent private LocalDate fillingOutDate; - public Application() {} + public Application() { + } - public Application(ApplicationStatusEnum status, String name, String surname, LocalDate birthday, LocalDate fillingOutDate) { + public Application(ApplicationStatusEnum status, String name, String surname, LocalDate birthday, String email, LocalDate fillingOutDate) { this.status = status; this.name = name; this.surname = surname; this.birthday = birthday; + this.email = email; this.fillingOutDate = fillingOutDate; } @@ -88,6 +90,14 @@ public class Application implements Serializable { this.birthday = birthday; } + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + public LocalDate getFillingOutDate() { return fillingOutDate; } diff --git a/application/src/main/java/cz/muni/pa165/mappers/ApplicationMapper.java b/application/src/main/java/cz/muni/pa165/mappers/ApplicationMapper.java index fa948aef311f9e4fc1ded146620532582790fe9a..d5aba3105ac0aa498cb2d7dcfec79c1bcebda9f9 100644 --- a/application/src/main/java/cz/muni/pa165/mappers/ApplicationMapper.java +++ b/application/src/main/java/cz/muni/pa165/mappers/ApplicationMapper.java @@ -4,8 +4,6 @@ import cz.muni.pa165.data.model.Application; import cz.muni.pa165.generated.model.ApplicationCreateDto; import cz.muni.pa165.generated.model.ApplicationDto; import org.mapstruct.Mapper; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import java.util.List; @@ -19,9 +17,5 @@ public interface ApplicationMapper { Application mapFromCreateDto(ApplicationCreateDto carCreateDto); List<ApplicationDto> mapToList(List<Application> cars); - - default Page<ApplicationDto> mapToPageDto(Page<Application> cars) { - return new PageImpl<>(mapToList(cars.getContent()), cars.getPageable(), cars.getTotalPages()); - } } diff --git a/application/src/main/java/cz/muni/pa165/service/ApplicationService.java b/application/src/main/java/cz/muni/pa165/service/ApplicationService.java index e8d0932a64fca156e51583dad1fa1cbd01211947..56ba43b0882cd54fb17753744c62adbed1e7af5e 100644 --- a/application/src/main/java/cz/muni/pa165/service/ApplicationService.java +++ b/application/src/main/java/cz/muni/pa165/service/ApplicationService.java @@ -4,12 +4,21 @@ import cz.muni.pa165.data.enums.ApplicationStatusEnum; import cz.muni.pa165.data.model.Application; import cz.muni.pa165.data.repository.ApplicationRepository; import cz.muni.pa165.exceptions.ResourceNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; import java.time.LocalDate; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; /** @@ -17,12 +26,18 @@ import java.util.Optional; */ @Service public class ApplicationService { + private static final Logger log = LoggerFactory.getLogger(ApplicationService.class); + private static final String NOTIFICATION_MODULE_URL_NEW = "http://localhost:8083/notification/application/new"; + private static final String NOTIFICATION_MODULE_URL_STATUS = "http://localhost:8083/notification/application/status"; + private static final List<String> NOTIFICATION_RECEIVERS = List.of("formula.team.management@gmail.com"); private final ApplicationRepository applicationRepository; + private final RestTemplate restTemplate; @Autowired - public ApplicationService(ApplicationRepository applicationRepository) { + public ApplicationService(ApplicationRepository applicationRepository, RestTemplate restTemplate) { this.applicationRepository = applicationRepository; + this.restTemplate = restTemplate; } @Transactional(readOnly = true) @@ -32,7 +47,17 @@ public class ApplicationService { public Application create(Application application) { application.setFillingOutDate(LocalDate.now()); - return applicationRepository.save(application); + application.setStatus(ApplicationStatusEnum.PENDING); + + var savedApplication = applicationRepository.save(application); + + try { + sendPostRequest(savedApplication, NOTIFICATION_MODULE_URL_NEW, List.of(savedApplication.getEmail())); + } catch (RestClientException | IllegalArgumentException e) { + log.debug(String.format("The notification module is not reachable on the URL: %s", NOTIFICATION_MODULE_URL_NEW)); + } + + return savedApplication; } @Transactional(readOnly = true) @@ -50,6 +75,27 @@ public class ApplicationService { applicationFromDb.get().setStatus(applicationStatusEnum); - return applicationRepository.save(applicationFromDb.get()); + var savedApplication = applicationRepository.save(applicationFromDb.get()); + + if (savedApplication.getStatus() != ApplicationStatusEnum.PENDING) { + try { + sendPostRequest(savedApplication, NOTIFICATION_MODULE_URL_STATUS, NOTIFICATION_RECEIVERS); + } catch (RestClientException | IllegalArgumentException e) { + log.debug(String.format("The notification module is not reachable on the URL: %s", NOTIFICATION_MODULE_URL_NEW)); + } + } + + return savedApplication; + } + + private void sendPostRequest(Application application, String url, List<String> receivers) { + Map<String, Object> notification = new HashMap<>(); + notification.put("application", application); + notification.put("receivers", receivers); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity<Map<String, Object>> request = new HttpEntity<>(notification, headers); + restTemplate.postForObject(url, request, String.class); } } diff --git a/application/src/test/java/cz/muni/pa165/rest/IntegrationTests.java b/application/src/test/java/cz/muni/pa165/rest/IntegrationTests.java index 346e5d1601cc04a68c82c04331a38750aff8c5f5..6f4f7b57568282711caef39e5320fa06a9d4d00c 100644 --- a/application/src/test/java/cz/muni/pa165/rest/IntegrationTests.java +++ b/application/src/test/java/cz/muni/pa165/rest/IntegrationTests.java @@ -32,7 +32,7 @@ class IntegrationTests { void testCreateApplication() throws Exception { log.info("testApplicationModuleResponse() running"); - String testRequestContent = "{\"name\":\"John\",\"surname\":\"Doe\",\"birthday\":\"2000-01-01\"}"; + String testRequestContent = "{\"name\":\"John\",\"surname\":\"Doe\",\"birthday\":\"2000-01-01\",\"email\":\"john.doe@aaa.com\"}"; String response = mockMvc.perform( post("/application") @@ -43,7 +43,7 @@ class IntegrationTests { .andReturn().getResponse().getContentAsString(); log.info("response: {}", response); - String expectedResponse = "{\"id\":1,\"name\":\"John\",\"surname\":\"Doe\",\"birthday\":\"2000-01-01\",\"fillingOutDate\":\"" + LocalDate.now() + "\",\"status\":null}"; + String expectedResponse = "{\"id\":1,\"name\":\"John\",\"surname\":\"Doe\",\"birthday\":\"2000-01-01\",\"email\":\"john.doe@aaa.com\",\"fillingOutDate\":\"" + LocalDate.now() + "\",\"status\":\"pending\"}"; assertEquals(expectedResponse, response); } diff --git a/application/src/test/java/cz/muni/pa165/service/ApplicationServiceTest.java b/application/src/test/java/cz/muni/pa165/service/ApplicationServiceTest.java index 6766fae6e3107bf1f8f9eccfb26d9bb0e57f05ed..4bfd428db5d6c690a6b9e88e30b7d6c7c7dddbc2 100644 --- a/application/src/test/java/cz/muni/pa165/service/ApplicationServiceTest.java +++ b/application/src/test/java/cz/muni/pa165/service/ApplicationServiceTest.java @@ -6,6 +6,7 @@ import cz.muni.pa165.data.repository.ApplicationRepository; import cz.muni.pa165.exceptions.ResourceNotFoundException; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.springframework.web.client.RestTemplate; import java.time.LocalDate; import java.util.Optional; @@ -16,8 +17,10 @@ import static org.mockito.Mockito.when; class ApplicationServiceTest { ApplicationRepository applicationRepository = Mockito.mock(ApplicationRepository.class); - ApplicationService service = new ApplicationService(applicationRepository); - Application application = new Application(ApplicationStatusEnum.ACCEPTED, "John", "Doe", LocalDate.now().minusYears(20), LocalDate.now()); + RestTemplate restTemplate = Mockito.mock(RestTemplate.class); + ApplicationService service = new ApplicationService(applicationRepository, restTemplate); + Application application = new Application(ApplicationStatusEnum.ACCEPTED, "John", "Doe", + LocalDate.now().minusYears(20), "john.doe@aaa.com", LocalDate.now()); @Test @@ -27,11 +30,13 @@ class ApplicationServiceTest { Application created = service.create(application); assertEquals(application, created); + assertEquals(ApplicationStatusEnum.PENDING, created.getStatus()); } @Test void setStatus() { - Application expected = new Application(ApplicationStatusEnum.REJECTED, "John", "Doe", LocalDate.now().minusYears(20), LocalDate.now()); + Application expected = new Application(ApplicationStatusEnum.REJECTED, "John", "Doe", + LocalDate.now().minusYears(20), "john.doe@aaa.com", LocalDate.now()); when(applicationRepository.save(application)).thenReturn(expected); when(applicationRepository.findById(application.getId())).thenReturn(java.util.Optional.ofNullable(application)); @@ -44,7 +49,7 @@ class ApplicationServiceTest { void setStatusUnknownApplication() { when(applicationRepository.findById(application.getId())).thenReturn(Optional.empty()); - assertThrows(ResourceNotFoundException.class, () -> service.setStatus(application.getId(), ApplicationStatusEnum.REJECTED)); + assertThrows(ResourceNotFoundException.class, + () -> service.setStatus(application.getId(), ApplicationStatusEnum.REJECTED)); } - } \ No newline at end of file diff --git a/core/openapi.yaml b/core/openapi.yaml index 480007a2b2deba16f490f4bc18319d2746a10ab3..d27d98e0488bb000a87b1b366f793fa0c12bfc82 100644 --- a/core/openapi.yaml +++ b/core/openapi.yaml @@ -69,7 +69,7 @@ components: $ref: '#/components/schemas/CarComponentType' weight: type: number - description: car's weight + description: car component's weight information: type: string description: Specific information about component diff --git a/core/src/main/java/cz/muni/pa165/service/CarComponentService.java b/core/src/main/java/cz/muni/pa165/service/CarComponentService.java index 4e7405eafb158c9cdb973cff6bc8f64ab6128b05..eb8c89a38ef514ccde47c96634252e02fa05d2f3 100644 --- a/core/src/main/java/cz/muni/pa165/service/CarComponentService.java +++ b/core/src/main/java/cz/muni/pa165/service/CarComponentService.java @@ -23,7 +23,9 @@ public class CarComponentService extends DomainService<CarComponent> { private final CarComponentRepository componentRepository; private final RestTemplate restTemplate; - private final String NOTIFICATION_MODULE_URL = "http://localhost:8083/notification"; + private static final String NOTIFICATION_MODULE_URL = "http://localhost:8083/notification/component"; + private static final List<String> NOTIFICATION_RECEIVERS = List.of("formula.team.management@gmail.com"); + @Autowired public CarComponentService(CarComponentRepository repository, RestTemplate restTemplate) { super(repository, CarComponent.class); @@ -41,18 +43,6 @@ public class CarComponentService extends DomainService<CarComponent> { return savedComponent; } - private void sendPostRequest(CarComponent component) { - Map<String, Object> notification = new HashMap<>(); - notification.put("subject", "New CarComponent added"); - notification.put("message", "A new CarComponent with ID " + component.getId() + " has been added."); - notification.put("receivers", Collections.singletonList(Collections.singletonMap("emailAddress", "manager@example.com"))); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - HttpEntity<Map<String, Object>> request = new HttpEntity<>(notification, headers); - restTemplate.postForObject(NOTIFICATION_MODULE_URL, request, String.class); - } - public CarComponent update(Long id, CarComponent component) { Optional<CarComponent> dbComponent = componentRepository.findById(id); if (dbComponent.isEmpty()) { @@ -76,4 +66,15 @@ public class CarComponentService extends DomainService<CarComponent> { public List<CarComponent> findAllByType(ComponentTypeEnum type) { return componentRepository.findByType(type); } + + private void sendPostRequest(CarComponent component) { + Map<String, Object> notification = new HashMap<>(); + notification.put("carComponent", component); + notification.put("receivers", NOTIFICATION_RECEIVERS); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity<Map<String, Object>> request = new HttpEntity<>(notification, headers); + restTemplate.postForObject(NOTIFICATION_MODULE_URL, request, String.class); + } } diff --git a/notification/openapi.yaml b/notification/openapi.yaml index 3493a9f3addddd2f3e045c88be509bd16f4fb6cd..975d5f7cbfac27de5761ce59eb5ce89611b680fb 100644 --- a/notification/openapi.yaml +++ b/notification/openapi.yaml @@ -5,31 +5,11 @@ info: Hand-made OpenAPI document for Notification module. version: 1.0.0 servers: - - url: "http://localhost:8080" + - url: "http://localhost:8083" tags: - name: NotificationService components: schemas: - ReceiverDto: - type: object - title: Receiver - description: Receiver of notification - required: - - emailAddress - properties: - emailAddress: - type: string - description: Email address where to send the Notification - SenderDto: - type: object - title: Sender - description: Receiver of email - required: - - emailAddress - properties: - emailAddress: - type: string - description: Email address from which was sent the Notification NotificationDto: type: object title: Notification @@ -41,13 +21,45 @@ components: properties: message: type: string - description: Content of notification + description: Content of the email receivers: type: array items: - $ref: '#/components/schemas/ReceiverDto' + type: string + description: Email addresses the notification is sent to subject: type: string + description: Subject of the email + CarComponentNotificationDto: + type: object + title: CarComponentNotification + description: Notification about new component + required: + - carComponent + - receivers + properties: + carComponent: + $ref: "#/components/schemas/CarComponentDto" + receivers: + type: array + items: + type: string + description: Email addresses the notification is sent to + ApplicationNotificationDto: + type: object + title: ApplicationNotification + description: Application notification + required: + - application + - receivers + properties: + application: + $ref: "#/components/schemas/ApplicationDto" + receivers: + type: array + items: + type: string + description: Email addresses the notification is sent to ConfirmationDto: type: object title: Confirmation @@ -56,12 +68,107 @@ components: type: string subject: type: string + description: Subject of the email receivers: type: array items: - $ref: '#/components/schemas/ReceiverDto' + type: string + description: Email addresses the notification was sent to sender: - $ref: '#/components/schemas/SenderDto' + type: string + description: Email address from which the notification was sent + + CarComponentType: + type: string + description: Type of a component + enum: + - chassis + - engine + - frontWing + - suspension + CarComponentDto: + type: object + title: Component + description: Component of a car + required: + - id + - componentType + - information + properties: + id: + type: integer + format: int64 + description: Id of the component + example: 1 + componentType: + $ref: '#/components/schemas/CarComponentType' + weight: + type: number + description: Weight of the component + example: 50.5 + information: + type: string + description: Specific information about component + example: lightweight front wing v2 (black) + + ApplicationStatus: + type: string + description: Status of application + enum: + - pending + - rejected + - accepted + + ApplicationDto: + type: object + title: Application + description: Application for becoming driver + required: + - id + - name + - surname + - fillingOutDate + - email + properties: + id: + type: integer + description: Id of the application + example: 1 + format: int64 + name: + type: string + description: Name of the person + example: Max + surname: + type: string + description: Surname of the person + example: Verstappen + birthday: + type: string + description: Date of birth + example: 09-30-1997 + format: date + email: + type: string + description: Email address of the person + fillingOutDate: + type: string + description: Date of filling out the application + example: 09-30-1997 + format: date + status: + $ref: '#/components/schemas/ApplicationStatus' + responses: + Unexpected: + description: Unexpected error + content: + application/json: + schema: + type: object + title: Unexpected + properties: + message: + type: string paths: /notification: @@ -85,12 +192,71 @@ paths: schema: $ref: '#/components/schemas/ConfirmationDto' default: - description: Unexpected error + $ref: '#/components/responses/Unexpected' + /notification/component: + post: + tags: + - NotificationService + summary: Creates and sends notification(s) about new component + operationId: notifyNewComponent + requestBody: + required: true + description: Request body with receiver and car component for new notification + content: + application/json: + schema: + $ref: '#/components/schemas/CarComponentNotificationDto' + responses: + "200": + description: OK content: application/json: schema: - type: object - title: Unexpected - properties: - message: - type: string + $ref: '#/components/schemas/ConfirmationDto' + default: + $ref: '#/components/responses/Unexpected' + /notification/application/new: + post: + tags: + - NotificationService + summary: Creates and sends notification(s) + operationId: notifyNewApplication + requestBody: + required: true + description: Request body with receiver and message for new notification + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationNotificationDto' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ConfirmationDto' + default: + $ref: '#/components/responses/Unexpected' + /notification/application/status: + post: + tags: + - NotificationService + summary: Creates and sends notification + operationId: notifyApplicationStatusChange + requestBody: + required: true + description: Request body with receiver and message for new notification + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationNotificationDto' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ConfirmationDto' + default: + $ref: '#/components/responses/Unexpected' + diff --git a/notification/pom.xml b/notification/pom.xml index 12afd6ff2cfb2fd0f37d219724e9f4a08ddc318f..0c1d0dbd2234e4f447e52c6181ce69de08ea0395 100644 --- a/notification/pom.xml +++ b/notification/pom.xml @@ -33,7 +33,7 @@ <apiPackage>cz.muni.pa165.generated.api</apiPackage> <modelPackage>cz.muni.pa165.generated.model</modelPackage> <configOptions> - <basePackage>cz.muni.pa165.rest</basePackage> + <basePackage>cz.muni.pa165</basePackage> <configPackage>cz.muni.pa165.generated.config</configPackage> <useSpringBoot3>true</useSpringBoot3> <useTags>true</useTags> @@ -98,6 +98,12 @@ <scope>test</scope> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-mail</artifactId> + <version>3.0.4</version> + </dependency> + </dependencies> </project> diff --git a/notification/src/main/java/cz/muni/pa165/config/MailConfig.java b/notification/src/main/java/cz/muni/pa165/config/MailConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..46df3353e6a02993e8e78a4bc6bb941c9551c2bc --- /dev/null +++ b/notification/src/main/java/cz/muni/pa165/config/MailConfig.java @@ -0,0 +1,29 @@ +package cz.muni.pa165.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class MailConfig { + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost("smtp.gmail.com"); + mailSender.setPort(587); + + mailSender.setUsername("formula.team.management@gmail.com"); + mailSender.setPassword("cwnlsvcodawlphvu"); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.transport.protocol", "smtp"); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.debug", "true"); + + return mailSender; + } +} \ No newline at end of file diff --git a/notification/src/main/java/cz/muni/pa165/facade/NotificationFacade.java b/notification/src/main/java/cz/muni/pa165/facade/NotificationFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..8f802001e0360df82c238376ed98824ce4215da0 --- /dev/null +++ b/notification/src/main/java/cz/muni/pa165/facade/NotificationFacade.java @@ -0,0 +1,38 @@ +package cz.muni.pa165.facade; + +import cz.muni.pa165.generated.model.ApplicationNotificationDto; +import cz.muni.pa165.generated.model.CarComponentNotificationDto; +import cz.muni.pa165.generated.model.ConfirmationDto; +import cz.muni.pa165.generated.model.NotificationDto; +import cz.muni.pa165.service.NotificationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class NotificationFacade { + + private final NotificationService notificationService; + + @Autowired + public NotificationFacade(NotificationService notificationService) { + + this.notificationService = notificationService; + } + + public ConfirmationDto notify(NotificationDto notificationDto) { + return notificationService.notify(notificationDto); + } + + public ConfirmationDto notifyNewComponent(CarComponentNotificationDto carComponentNotificationDto) { + return notificationService.notifyNewComponent(carComponentNotificationDto); + } + + public ConfirmationDto notifyNewApplication(ApplicationNotificationDto applicationDto) { + return notificationService.notifyNewApplication(applicationDto); + } + + public ConfirmationDto notifyApplicationStatusChange(ApplicationNotificationDto applicationDto) { + return notificationService.notifyApplicationStatusChange(applicationDto); + + } +} diff --git a/notification/src/main/java/cz/muni/pa165/rest/NotificationController.java b/notification/src/main/java/cz/muni/pa165/rest/NotificationController.java index 981d1bbe673256f20393e9def5ec6211c1ea3270..975e713dff0051c480f41672c3fd9e8b2d1a9f89 100644 --- a/notification/src/main/java/cz/muni/pa165/rest/NotificationController.java +++ b/notification/src/main/java/cz/muni/pa165/rest/NotificationController.java @@ -1,12 +1,14 @@ package cz.muni.pa165.rest; +import cz.muni.pa165.facade.NotificationFacade; import cz.muni.pa165.generated.api.NotificationServiceApiDelegate; +import cz.muni.pa165.generated.model.ApplicationNotificationDto; +import cz.muni.pa165.generated.model.CarComponentNotificationDto; import cz.muni.pa165.generated.model.ConfirmationDto; import cz.muni.pa165.generated.model.NotificationDto; -import cz.muni.pa165.generated.model.ReceiverDto; -import cz.muni.pa165.generated.model.SenderDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; @@ -19,24 +21,38 @@ import org.springframework.stereotype.Component; public class NotificationController implements NotificationServiceApiDelegate { private static final Logger log = LoggerFactory.getLogger(NotificationController.class); + private final NotificationFacade notificationFacade; + + @Autowired + public NotificationController(NotificationFacade notificationFacade) { + this.notificationFacade = notificationFacade; + } @Override public ResponseEntity<ConfirmationDto> notify(NotificationDto notificationDto) { log.debug("notify() called"); - SenderDto sender = new SenderDto().emailAddress("noreply@formula1.com"); + return new ResponseEntity<>(notificationFacade.notify(notificationDto), HttpStatus.OK); + } + + @Override + public ResponseEntity<ConfirmationDto> notifyNewComponent(CarComponentNotificationDto carComponentNotificationDto) { + log.debug("notifyComponentChange() called"); - for (ReceiverDto receiver : notificationDto.getReceivers()) { - log.debug("Sending mail with subject " + notificationDto.getSubject() + " and body " - + notificationDto.getMessage() + " to " + receiver.getEmailAddress() - + " from " + sender.getEmailAddress()); - } + return new ResponseEntity<>(notificationFacade.notifyNewComponent(carComponentNotificationDto), HttpStatus.OK); + } - ConfirmationDto confirmation = new ConfirmationDto() - .message(notificationDto.getMessage()) - .receivers(notificationDto.getReceivers()) - .sender(sender); + @Override + public ResponseEntity<ConfirmationDto> notifyNewApplication(ApplicationNotificationDto applicationDto) { + log.debug("notifyNewApplication() called"); + + return new ResponseEntity<>(notificationFacade.notifyNewApplication(applicationDto), HttpStatus.OK); + } + + @Override + public ResponseEntity<ConfirmationDto> notifyApplicationStatusChange(ApplicationNotificationDto applicationDto) { + log.debug("notifyApplicationStatusChange() called"); - return new ResponseEntity<>(confirmation, HttpStatus.OK); + return new ResponseEntity<>(notificationFacade.notifyApplicationStatusChange(applicationDto), HttpStatus.OK); } } diff --git a/notification/src/main/java/cz/muni/pa165/rest/exceptionhandling/ApiError.java b/notification/src/main/java/cz/muni/pa165/rest/exceptionhandling/ApiError.java new file mode 100644 index 0000000000000000000000000000000000000000..5e1161dcad4805649ce3757c0c7983dd52f30e9c --- /dev/null +++ b/notification/src/main/java/cz/muni/pa165/rest/exceptionhandling/ApiError.java @@ -0,0 +1,62 @@ +package cz.muni.pa165.rest.exceptionhandling; + +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +public class ApiError { + + private LocalDateTime timestamp; + private HttpStatus status; + private String message; + private String path; + + public ApiError(LocalDateTime timestamp, HttpStatus status, String message, String path) { + this.timestamp = timestamp; + this.status = status; + this.message = message; + this.path = path; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public HttpStatus getStatus() { + return status; + } + + public void setStatus(HttpStatus status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + @Override + public String toString() { + return "ApiError{" + + "timestamp=" + timestamp + + ", status=" + status + + ", message='" + message + '\'' + + ", path='" + path + '\'' + + '}'; + } +} diff --git a/notification/src/main/java/cz/muni/pa165/rest/exceptionhandling/CustomRestGlobalExceptionHandling.java b/notification/src/main/java/cz/muni/pa165/rest/exceptionhandling/CustomRestGlobalExceptionHandling.java new file mode 100644 index 0000000000000000000000000000000000000000..323b98f637b01a42b134dcd22cb77f5c6dba82f0 --- /dev/null +++ b/notification/src/main/java/cz/muni/pa165/rest/exceptionhandling/CustomRestGlobalExceptionHandling.java @@ -0,0 +1,35 @@ +package cz.muni.pa165.rest.exceptionhandling; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.util.UrlPathHelper; + +import java.time.Clock; +import java.time.LocalDateTime; + +@RestControllerAdvice +public class CustomRestGlobalExceptionHandling { + + private static final UrlPathHelper URL_PATH_HELPER = new UrlPathHelper(); + + @ExceptionHandler({Exception.class}) + public ResponseEntity<ApiError> handleAll(final Exception ex, HttpServletRequest request) { + final ApiError apiError = new ApiError( + LocalDateTime.now(Clock.systemUTC()), + HttpStatus.INTERNAL_SERVER_ERROR, + getInitialException(ex).getLocalizedMessage(), + URL_PATH_HELPER.getRequestUri(request)); + return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus()); + } + + private Exception getInitialException(Exception ex) { + while (ex.getCause() != null) { + ex = (Exception) ex.getCause(); + } + return ex; + } +} diff --git a/notification/src/main/java/cz/muni/pa165/service/NotificationService.java b/notification/src/main/java/cz/muni/pa165/service/NotificationService.java new file mode 100644 index 0000000000000000000000000000000000000000..444378a4e7bdba907aa31b0becb481b9395d3279 --- /dev/null +++ b/notification/src/main/java/cz/muni/pa165/service/NotificationService.java @@ -0,0 +1,107 @@ +package cz.muni.pa165.service; + +import cz.muni.pa165.generated.model.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class NotificationService { + private static final String EMAIL = "formula.team.management@gmail.com"; + private final JavaMailSender emailSender; + + @Autowired + public NotificationService(JavaMailSender emailSender) { + this.emailSender = emailSender; + } + + public ConfirmationDto notify(NotificationDto notificationDto) { + return sendEmail(notificationDto.getSubject(), notificationDto.getMessage(), notificationDto.getReceivers()); + } + + public ConfirmationDto notifyNewComponent(CarComponentNotificationDto carComponentNotificationDto) { + var c = carComponentNotificationDto.getCarComponent(); + var subject = "New component: " + c.getInformation(); + var text = "New component was added to Formula team management core.\n" + + "\nType: " + c.getComponentType() + + "\nInformation: " + c.getInformation() + + "\nWeight: " + c.getWeight(); + + return sendEmail(subject, text, carComponentNotificationDto.getReceivers()); + } + + public ConfirmationDto notifyNewApplication(ApplicationNotificationDto applicationNotificationDto) { + var a = applicationNotificationDto.getApplication(); + var subject = "New application: " + getFullName(a); + var text = "New application was received.\n" + + "\nId: " + a.getId() + + "\nName: " + a.getName() + + "\nSurname: " + a.getSurname() + + "\nBirthday: " + a.getBirthday() + + "\nEmail: " + a.getEmail() + + "\nFilling out date: " + a.getFillingOutDate(); + + + return sendEmail(subject, text, applicationNotificationDto.getReceivers()); + } + + public ConfirmationDto notifyApplicationStatusChange(ApplicationNotificationDto applicationNotificationDto) { + var a = applicationNotificationDto.getApplication(); + var subject = "Application for Formula Driver Position"; + var text = ""; + if (a.getStatus() == ApplicationStatus.ACCEPTED) { + text = "Dear " + getFullName(a) + ",\n" + + "\n" + + "I am pleased to inform you that your application for the position of Formula Driver has been accepted. We are excited to have you join our team and look forward to working with you.\n" + + "\n" + + "Your experience, skills, and passion for racing make you an excellent fit for our team. We believe that you will be a valuable asset to our organization and we are eager to see what you can accomplish on the track.\n" + + "\n" + + "As a next step, we will contact you shortly to discuss the details of your employment contract and to schedule your training and orientation. In the meantime, please do not hesitate to contact us if you have any questions or concerns.\n" + + "\n" + + "Thank you for your interest in our team and for taking the time to apply. We look forward to a long and successful partnership with you.\n" + + "\n" + + "Best regards,\n" + + "\n" + + "Formula Team Management"; + } else { + text = "Dear " + getFullName(a) + ",\n" + + "\n" + + "I regret to inform you that your application for the Formula Driver position has been declined. While we appreciate your interest in the role, we have decided to pursue other candidates whose skills and experience more closely match our requirements.\n" + + "\n" + + "I want to personally thank you for taking the time to apply for the position and for your passion for motorsports. We would like to keep your resume on file in case any suitable openings arise in the future.\n" + + "\n" + + "If you have any questions or would like feedback on your application, please feel free to reach out to us.\n" + + "\n" + + "Thank you again for considering Formula Team Management, and we wish you all the best in your future endeavors.\n" + + "\n" + + "Best regards,\n" + + "\n" + + "Formula Team Management"; + } + + return sendEmail(subject, text, applicationNotificationDto.getReceivers()); + } + + private ConfirmationDto sendEmail(String subject, String content, List<String> receivers) { + var message = new SimpleMailMessage(); + + message.setFrom(EMAIL); + message.setTo(receivers.toArray(String[]::new)); + message.setSubject(subject); + message.setText(content); + emailSender.send(message); + + return new ConfirmationDto() + .sender(EMAIL) + .receivers(receivers) + .subject(subject) + .message(content); + } + + private String getFullName(ApplicationDto applicationDto) { + return applicationDto.getName() + " " + applicationDto.getSurname(); + } +} diff --git a/notification/src/main/resources/application.properties b/notification/src/main/resources/application.properties index ba586ded3f7e451a9bed7ef6a899c79064b07f07..67b74b143553fda6ca63c21b731f11201fa723e1 100644 --- a/notification/src/main/resources/application.properties +++ b/notification/src/main/resources/application.properties @@ -1 +1,9 @@ -server.port=8083 \ No newline at end of file +server.port=8083 +spring.mail.host=smtp.gmail.com +spring.mail.port=587 +spring.mail.username=formula.team.management@gmail.com +spring.mail.password=cwnlsvcodawlphvu +spring.mail.protocol=smtps +spring.mail.properties.mail.from.email=formula.team.management@gmail.com +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=false \ No newline at end of file diff --git a/notification/src/test/java/cz/muni/pa165/rest/IntegrationTests.java b/notification/src/test/java/cz/muni/pa165/rest/IntegrationTests.java index ab495e930bf5c6068971a152d52fb1c6057b9a4d..f05e0787f0cb302dfa48f455fefe2cfdb97e0cb5 100644 --- a/notification/src/test/java/cz/muni/pa165/rest/IntegrationTests.java +++ b/notification/src/test/java/cz/muni/pa165/rest/IntegrationTests.java @@ -30,7 +30,7 @@ class IntegrationTests { void testNotificationModuleResponse() throws Exception { log.info("testNotificationModuleResponse() running"); - String testRequestContent = "{\"message\":\"Hello\",\"receivers\":[]}"; + String testRequestContent = "{\"message\": \"string\",\"receivers\": [\"formula.team.management@gmail.com\"],\"subject\": \"string\"}"; String response = mockMvc.perform( post("/notification") @@ -41,8 +41,22 @@ class IntegrationTests { .andReturn().getResponse().getContentAsString(); log.info("response: {}", response); - String expectedResponse = "{\"message\":\"Hello\",\"subject\":null,\"receivers\":[],\"sender\":{\"emailAddress\":\"noreply@formula1.com\"}}"; + String expectedResponse = "{\"message\":\"string\",\"subject\":\"string\",\"receivers\":[\"formula.team.management@gmail.com\"],\"sender\":\"formula.team.management@gmail.com\"}"; assertEquals(expectedResponse, response); } + + @Test + void testNotificationModuleNoRecipient() throws Exception { + log.info("testNotificationModuleResponse() running"); + + String testRequestContent = "{\"message\": \"string\",\"receivers\": [],\"subject\": \"string\"}"; + + mockMvc.perform( + post("/notification") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(testRequestContent)) + .andExpect(status().isInternalServerError()); + } } \ No newline at end of file diff --git a/notification/src/test/java/cz/muni/pa165/rest/NotificationControllerTest.java b/notification/src/test/java/cz/muni/pa165/rest/NotificationControllerTest.java index dc133fc03aaa463398a3c8925df6911410be5ef2..043d7ebfcc0274df100ea28d78daafd9fcac3231 100644 --- a/notification/src/test/java/cz/muni/pa165/rest/NotificationControllerTest.java +++ b/notification/src/test/java/cz/muni/pa165/rest/NotificationControllerTest.java @@ -1,30 +1,57 @@ package cz.muni.pa165.rest; -import cz.muni.pa165.generated.model.ConfirmationDto; -import cz.muni.pa165.generated.model.NotificationDto; -import cz.muni.pa165.generated.model.ReceiverDto; +import cz.muni.pa165.facade.NotificationFacade; +import cz.muni.pa165.generated.model.*; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; +import java.math.BigDecimal; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertEquals; class NotificationControllerTest { - - NotificationController controller = new NotificationController(); private static final Logger log = LoggerFactory.getLogger(NotificationControllerTest.class); + private final NotificationFacade mockFacade = Mockito.mock(NotificationFacade.class); + private final NotificationController controller = new NotificationController(mockFacade); + + @Test + void testNotifyNewComponent() { + log.debug("testNotifyNewComponent() running"); + + var dto = new CarComponentNotificationDto() + .carComponent(new CarComponentDto()) + .receivers(List.of("aaa@aaa.com")); + + controller.notifyNewComponent(dto); + Mockito.verify(mockFacade, Mockito.times(1)).notifyNewComponent(dto); + } + + @Test + void testNotifyNewApplication() { + log.debug("testNotifyNewApplication() running"); + + var dto = new ApplicationNotificationDto() + .application(new ApplicationDto()) + .receivers(List.of("aaa@aaa.com")); + + controller.notifyNewApplication(dto); + Mockito.verify(mockFacade, Mockito.times(1)).notifyNewApplication(dto); + } + @Test - void testNotify() { - log.debug("testNotify() running"); + void testNotifyApplicationStatusChange() { + log.debug("testNotifyApplicationStatusChange() running"); - ResponseEntity<ConfirmationDto> received = controller.notify(new NotificationDto() - .message("Hello") - .addReceiversItem(new ReceiverDto().emailAddress("test@doamin.com"))); + var dto = new ApplicationNotificationDto() + .application(new ApplicationDto()) + .receivers(List.of("aaa@aaa.com")); - assertEquals("Hello", received.getBody().getMessage()); - assertEquals(1, received.getBody().getReceivers().size()); - assertEquals("test@doamin.com", received.getBody().getReceivers().get(0).getEmailAddress()); + controller.notifyApplicationStatusChange(dto); + Mockito.verify(mockFacade, Mockito.times(1)).notifyApplicationStatusChange(dto); } -} \ No newline at end of file +}