diff --git a/apps/user-ms/src/main/java/it/pagopa/selfcare/user/constant/TemplateMailConstant.java b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/constant/TemplateMailConstant.java index cf314402..b0341f1d 100644 --- a/apps/user-ms/src/main/java/it/pagopa/selfcare/user/constant/TemplateMailConstant.java +++ b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/constant/TemplateMailConstant.java @@ -6,9 +6,9 @@ @NoArgsConstructor(access = AccessLevel.NONE) public class TemplateMailConstant { - private static final String ACTIVATE_SUBJECT = "Il tuo ruolo è stato riabilitato"; - private static final String DELETE_SUBJECT = "Il tuo ruolo è stato rimosso"; - private static final String SUSPEND_SUBJECT = "Il tuo ruolo è sospeso"; + public static final String ACTIVATE_SUBJECT = "Il tuo ruolo è stato riabilitato"; + public static final String DELETE_SUBJECT = "Il tuo ruolo è stato rimosso"; + public static final String SUSPEND_SUBJECT = "Il tuo ruolo è sospeso"; public static final String ACTIVATE_TEMPLATE = "user_activated.ftlh"; public static final String DELETE_TEMPLATE = "user_deleted.ftlh"; public static final String SUSPEND_TEMPLATE = "user_suspended.ftlh"; diff --git a/apps/user-ms/src/main/java/it/pagopa/selfcare/user/model/LoggedUser.java b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/model/LoggedUser.java new file mode 100644 index 00000000..05b9566a --- /dev/null +++ b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/model/LoggedUser.java @@ -0,0 +1,11 @@ +package it.pagopa.selfcare.user.model; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class LoggedUser { + String uid; + String name; + String familyName; +} \ No newline at end of file diff --git a/apps/user-ms/src/main/java/it/pagopa/selfcare/user/model/notification/PrepareNotificationData.java b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/model/notification/PrepareNotificationData.java new file mode 100644 index 00000000..0ba93915 --- /dev/null +++ b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/model/notification/PrepareNotificationData.java @@ -0,0 +1,15 @@ +package it.pagopa.selfcare.user.model.notification; + +import it.pagopa.selfcare.product.entity.Product; +import it.pagopa.selfcare.user.entity.UserInstitution; +import lombok.Builder; +import lombok.Getter; +import org.openapi.quarkus.user_registry_json.model.UserResource; + +@Builder +@Getter +public class PrepareNotificationData { + private UserInstitution userInstitution; + private UserResource userResource; + private Product product; +} \ No newline at end of file diff --git a/apps/user-ms/src/main/java/it/pagopa/selfcare/user/service/UserNotificationService.java b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/service/UserNotificationService.java new file mode 100644 index 00000000..47daa210 --- /dev/null +++ b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/service/UserNotificationService.java @@ -0,0 +1,13 @@ +package it.pagopa.selfcare.user.service; + +import io.smallrye.mutiny.Uni; +import it.pagopa.selfcare.product.entity.Product; +import it.pagopa.selfcare.user.constant.OnboardedProductState; +import it.pagopa.selfcare.user.entity.UserInstitution; +import it.pagopa.selfcare.user.model.notification.UserNotificationToSend; +import org.openapi.quarkus.user_registry_json.model.UserResource; + +public interface UserNotificationService { + Uni sendKafkaNotification(UserNotificationToSend userNotificationToSend, String userId); + Uni sendEmailNotification(UserResource user, UserInstitution institution, Product product, OnboardedProductState status, String loggedUserName, String loggedUserSurname); +} \ No newline at end of file diff --git a/apps/user-ms/src/main/java/it/pagopa/selfcare/user/service/UserNotificationServiceImpl.java b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/service/UserNotificationServiceImpl.java new file mode 100644 index 00000000..b9e53d2e --- /dev/null +++ b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/service/UserNotificationServiceImpl.java @@ -0,0 +1,142 @@ +package it.pagopa.selfcare.user.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import freemarker.template.Configuration; +import freemarker.template.Template; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import io.smallrye.reactive.messaging.MutinyEmitter; +import it.pagopa.selfcare.product.entity.Product; +import it.pagopa.selfcare.product.entity.ProductRole; +import it.pagopa.selfcare.user.conf.CloudTemplateLoader; +import it.pagopa.selfcare.user.constant.OnboardedProductState; +import it.pagopa.selfcare.user.entity.OnboardedProduct; +import it.pagopa.selfcare.user.entity.UserInstitution; +import it.pagopa.selfcare.user.exception.InvalidRequestException; +import it.pagopa.selfcare.user.model.notification.UserNotificationToSend; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; + +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static it.pagopa.selfcare.user.constant.TemplateMailConstant.*; + +import org.openapi.quarkus.user_registry_json.model.UserResource; +import org.openapi.quarkus.user_registry_json.model.WorkContactResource; + + +@Slf4j +@ApplicationScoped +public class UserNotificationServiceImpl implements UserNotificationService { + + public static final String ERROR_DURING_SEND_DATA_LAKE_NOTIFICATION_FOR_USER = "error during send dataLake notification for user {}"; + + @Inject + @Channel("sc-users") + MutinyEmitter usersEmitter; + + private final ObjectMapper objectMapper; + + private final MailService mailService; + + private final Configuration freemarkerConfig; + + public UserNotificationServiceImpl(Configuration freemarkerConfig, CloudTemplateLoader cloudTemplateLoader, ObjectMapper objectMapper, MailService mailService) { + this.mailService = mailService; + this.freemarkerConfig = freemarkerConfig; + freemarkerConfig.setTemplateLoader(cloudTemplateLoader); + this.objectMapper = objectMapper; + } + + private String convertNotificationToJson(UserNotificationToSend userNotificationToSend) { + try { + return objectMapper.writeValueAsString(userNotificationToSend); + } catch (JsonProcessingException e) { + log.warn(ERROR_DURING_SEND_DATA_LAKE_NOTIFICATION_FOR_USER, userNotificationToSend.getUser().getUserId()); + throw new InvalidRequestException(ERROR_DURING_SEND_DATA_LAKE_NOTIFICATION_FOR_USER); + } + } + + @Override + public Uni sendKafkaNotification(UserNotificationToSend userNotificationToSend, String userId) { + String message = convertNotificationToJson(userNotificationToSend); + return usersEmitter.sendMessage(Message.of(message)) + .onItem().invoke(() -> log.info("sent dataLake notification for user : {}", userId)) + .onFailure().invoke(throwable -> log.warn("error during send dataLake notification for user {}: {} ", userId, throwable.getMessage(), throwable)) + .replaceWith(userNotificationToSend); + } + + @Override + public Uni sendEmailNotification(UserResource user, UserInstitution institution, Product product, OnboardedProductState status, String loggedUserName, String loggedUserSurname) { + log.info("sendMailNotification {}", status.name()); + return switch (status) { + case ACTIVE -> + buildAndSendEmailNotification(user, institution, product, ACTIVATE_TEMPLATE, ACTIVATE_SUBJECT, loggedUserName, loggedUserSurname); + case SUSPENDED -> + buildAndSendEmailNotification(user, institution, product, DELETE_TEMPLATE, DELETE_SUBJECT, loggedUserName, loggedUserSurname); + case DELETED -> + buildAndSendEmailNotification(user, institution, product, SUSPEND_TEMPLATE, SUSPEND_SUBJECT, loggedUserName, loggedUserSurname); + case PENDING, TOBEVALIDATED, REJECTED -> Uni.createFrom().voidItem(); + }; + } + + private Map buildEmailDataModel(UserInstitution institution, Product product, String loggedUserName, String loggedUserSurname) { + Optional productDb = institution.getProducts().stream().filter(p -> StringUtils.equals(p.getProductId(), product.getId())).findFirst(); + + Optional roleLabel = Optional.empty(); + if (productDb.isPresent()) { + roleLabel = product.getRoleMappings().values().stream() + .flatMap(productRoleInfo -> productRoleInfo.getRoles().stream()) + .filter(productRole -> productRole.getCode().equals(productDb.get().getProductRole())) + .map(ProductRole::getLabel).findAny(); + } + + Map dataModel = new HashMap<>(); + dataModel.put("productName", product.getTitle()); + dataModel.put("productRole", roleLabel.orElse("no_role_found")); + dataModel.put("institutionName", institution.getInstitutionDescription()); + dataModel.put("requesterName", loggedUserName); + dataModel.put("requesterSurname", loggedUserSurname); + return dataModel; + } + + private Uni buildAndSendEmailNotification(org.openapi.quarkus.user_registry_json.model.UserResource user, UserInstitution institution, Product product, String templateName, String subject, String loggedUserName, String loggedUserSurname) { + return Uni.createFrom().voidItem().onItem().transformToUni(Unchecked.function(x -> { + String email = retrieveMail(user, institution); + Map dataModel = buildEmailDataModel(institution, product, loggedUserName, loggedUserSurname); + return Uni.createFrom().item(getContent(templateName, dataModel)) + .onItem().transformToUni(content -> mailService.sendMail(email, content.toString(), subject)); + })); + } + + private StringWriter getContent(String templateName, Map dataModel) { + StringWriter stringWriter = null; + try { + Template template = freemarkerConfig.getTemplate(templateName); + stringWriter = new StringWriter(); + template.process(dataModel, stringWriter); + } catch (Exception e) { + log.error("Unable to fetch template {}", templateName, e); + } + return stringWriter; + } + + private static String retrieveMail(UserResource user, UserInstitution institution) { + WorkContactResource certEmail = user.getWorkContacts().getOrDefault(institution.getUserMailUuid(), null); + String email; + if (certEmail == null || certEmail.getEmail() == null || StringUtils.isBlank(certEmail.getEmail().getValue())) { + throw new InvalidRequestException("Missing mail for userId: " + user.getId()); + } else { + email = certEmail.getEmail().getValue(); + } + return email; + } +} \ No newline at end of file diff --git a/apps/user-ms/src/main/java/it/pagopa/selfcare/user/service/UserRegistryServiceImpl.java b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/service/UserRegistryServiceImpl.java index 64965741..878fa4c5 100644 --- a/apps/user-ms/src/main/java/it/pagopa/selfcare/user/service/UserRegistryServiceImpl.java +++ b/apps/user-ms/src/main/java/it/pagopa/selfcare/user/service/UserRegistryServiceImpl.java @@ -1,27 +1,19 @@ package it.pagopa.selfcare.user.service; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; -import io.smallrye.reactive.messaging.MutinyEmitter; import it.pagopa.selfcare.user.constant.QueueEvent; import it.pagopa.selfcare.user.entity.filter.UserInstitutionFilter; -import it.pagopa.selfcare.user.exception.InvalidRequestException; import it.pagopa.selfcare.user.model.notification.UserNotificationToSend; import it.pagopa.selfcare.user.util.UserUtils; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.eclipse.microprofile.reactive.messaging.Channel; -import org.eclipse.microprofile.reactive.messaging.Message; import org.eclipse.microprofile.rest.client.inject.RestClient; import org.gradle.internal.impldep.org.apache.commons.lang.StringUtils; import org.openapi.quarkus.user_registry_json.model.MutableUserFieldsDto; -import java.util.ArrayList; import java.util.List; - import org.openapi.quarkus.user_registry_json.api.UserApi; @@ -29,21 +21,17 @@ @RequiredArgsConstructor @Slf4j public class UserRegistryServiceImpl implements UserRegistryService { - public static final String ERROR_DURING_SEND_DATA_LAKE_NOTIFICATION_FOR_USER = "error during send dataLake notification for user {}"; private static final String USERS_FIELD_LIST_WITHOUT_FISCAL_CODE = "name,familyName,email,workContacts"; - - private final ObjectMapper objectMapper; private final UserInstitutionService userInstitutionService; private final UserUtils userUtils; + private final UserNotificationService userNotificationService; + @RestClient @Inject private UserApi userRegistryApi; - @Inject - @Channel("sc-users") - private MutinyEmitter usersEmitter; @Override public Uni> updateUserRegistryAndSendNotificationToQueue(MutableUserFieldsDto userDto, String userId, String institutionId) { @@ -57,25 +45,7 @@ public Uni> updateUserRegistryAndSendNotificationTo .onItem().transformToMulti(response -> userInstitutionService.findAllWithFilter(userInstitutionFilter.constructMap())) .onItem().transformToMultiAndMerge(userInstitution -> userRegistryApi.findByIdUsingGET(USERS_FIELD_LIST_WITHOUT_FISCAL_CODE, userInstitution.getUserId()) .onItem().transformToMulti(userResource -> Multi.createFrom().iterable(userUtils.buildUsersNotificationResponse(userInstitution, userResource, QueueEvent.UPDATE)))) - .onItem().transformToUniAndMerge(notification -> sendUserNotification(notification, userId)) + .onItem().transformToUniAndMerge(notification -> userNotificationService.sendKafkaNotification(notification, userId)) .collect().asList(); } - - - private String convertNotificationToJson(UserNotificationToSend userNotificationToSend) { - try { - return objectMapper.writeValueAsString(userNotificationToSend); - } catch (JsonProcessingException e) { - log.warn(ERROR_DURING_SEND_DATA_LAKE_NOTIFICATION_FOR_USER, userNotificationToSend.getUser().getUserId()); - throw new InvalidRequestException(ERROR_DURING_SEND_DATA_LAKE_NOTIFICATION_FOR_USER); - } - } - - private Uni sendUserNotification(UserNotificationToSend userNotificationToSend, String userId) { - String message = convertNotificationToJson(userNotificationToSend); - return usersEmitter.sendMessage(Message.of(message)) - .onItem().invoke(() -> log.info("sent dataLake notification for user : {}", userId)) - .onFailure().invoke(throwable -> log.warn("error during send dataLake notification for user {}: {} ", userId, throwable.getMessage(), throwable)) - .replaceWith(userNotificationToSend); - } } diff --git a/apps/user-ms/src/test/java/it/pagopa/selfcare/user/service/UserNotificationServiceImplTest.java b/apps/user-ms/src/test/java/it/pagopa/selfcare/user/service/UserNotificationServiceImplTest.java new file mode 100644 index 00000000..cbf72c62 --- /dev/null +++ b/apps/user-ms/src/test/java/it/pagopa/selfcare/user/service/UserNotificationServiceImplTest.java @@ -0,0 +1,173 @@ +package it.pagopa.selfcare.user.service; + +import freemarker.template.Configuration; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; +import io.smallrye.reactive.messaging.memory.InMemoryConnector; +import io.smallrye.reactive.messaging.memory.InMemorySink; +import it.pagopa.selfcare.onboarding.common.PartyRole; +import it.pagopa.selfcare.product.entity.Product; +import it.pagopa.selfcare.product.entity.ProductRole; +import it.pagopa.selfcare.product.entity.ProductRoleInfo; +import it.pagopa.selfcare.user.constant.OnboardedProductState; +import it.pagopa.selfcare.user.entity.OnboardedProduct; +import it.pagopa.selfcare.user.entity.UserInstitution; +import it.pagopa.selfcare.user.model.notification.UserNotificationToSend; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import org.bson.types.ObjectId; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.openapi.quarkus.user_registry_json.model.CertifiableFieldResourceOfstring; +import org.openapi.quarkus.user_registry_json.model.UserResource; +import org.openapi.quarkus.user_registry_json.model.WorkContactResource; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@QuarkusTest +class UserNotificationServiceImplTest { + @Inject + UserNotificationService userNotificationService; + + @InjectMock + private MailService mailService; + + @Inject + @Any + InMemoryConnector connector; + + private static final UserResource userResource; + private static final UserInstitution userInstitution; + private static final Product product; + + @Produces + @ApplicationScoped + Configuration freemarkerConfig() { + return mock(Configuration.class); + } + + static { + userResource = new UserResource(); + userResource.setId(UUID.randomUUID()); + CertifiableFieldResourceOfstring certifiedName = new CertifiableFieldResourceOfstring(); + certifiedName.setValue("name"); + userResource.setName(certifiedName); + userResource.setFamilyName(certifiedName); + userResource.setFiscalCode("taxCode"); + CertifiableFieldResourceOfstring certifiedEmail = new CertifiableFieldResourceOfstring(); + certifiedEmail.setValue("test@test.it"); + WorkContactResource workContactResource = new WorkContactResource(); + workContactResource.setEmail(certifiedEmail); + userResource.setEmail(certifiedEmail); + userResource.setWorkContacts(Map.of("IdMail", workContactResource)); + + userInstitution = new UserInstitution(); + userInstitution.setId(ObjectId.get()); + userInstitution.setUserId("userId"); + userInstitution.setUserMailUuid("IdMail"); + userInstitution.setInstitutionId("institutionId"); + userInstitution.setInstitutionRootName("institutionRootName"); + OnboardedProduct onboardedProduct = new OnboardedProduct(); + onboardedProduct.setProductId("test"); + onboardedProduct.setProductRole("code"); + userInstitution.setProducts(List.of(onboardedProduct)); + + + ProductRoleInfo productRoleInfo = new ProductRoleInfo(); + var productRole = new ProductRole(); + productRole.setCode("code"); + productRole.setDescription("description"); + productRole.setLabel("label"); + productRoleInfo.setRoles(List.of(productRole)); + + product = new Product(); + product.setId("test"); + product.setRoleMappings(new HashMap<>() {{ + put(PartyRole.MANAGER, productRoleInfo); + }}); + } + + @Test + void testSendMailNotification() { + String loggedUserName = "loggedUserName"; + String loggedUserSurname = "loggedUserSurname"; + + when(mailService.sendMail(anyString(), anyString(), anyString())).thenReturn(Uni.createFrom().voidItem()); + + userNotificationService.sendEmailNotification( + userResource, + userInstitution, + product, + OnboardedProductState.ACTIVE, + loggedUserName, + loggedUserSurname + ) + .subscribe() + .withSubscriber(UniAssertSubscriber.create()) + .assertCompleted(); + userNotificationService.sendEmailNotification( + userResource, + userInstitution, + product, + OnboardedProductState.DELETED, + loggedUserName, + loggedUserSurname + ) + .subscribe() + .withSubscriber(UniAssertSubscriber.create()) + .assertCompleted(); + userNotificationService.sendEmailNotification( + userResource, + userInstitution, + product, + OnboardedProductState.SUSPENDED, + loggedUserName, + loggedUserSurname + ) + .subscribe() + .withSubscriber(UniAssertSubscriber.create()) + .assertCompleted(); + userNotificationService.sendEmailNotification( + userResource, + userInstitution, + product, + OnboardedProductState.PENDING, + loggedUserName, + loggedUserSurname + ) + .subscribe() + .withSubscriber(UniAssertSubscriber.create()) + .assertCompleted(); + verify(mailService, times(3)).sendMail(anyString(), anyString(), anyString()); + } + + @Test + void testSendKafkaNotification(){ + InMemorySink usersOut = connector.sink("sc-users"); + UserNotificationToSend userNotificationToSend = new UserNotificationToSend(); + userNotificationToSend.setId("userId"); + + UniAssertSubscriber subscriber = userNotificationService.sendKafkaNotification( + userNotificationToSend, + "userId" + ).subscribe().withSubscriber(UniAssertSubscriber.create()); + subscriber.assertCompleted(); + + await().>>until(usersOut::received, t -> t.size() == 1); + + String queuedMessage = usersOut.received().get(0).getPayload(); + Assertions.assertTrue(queuedMessage.contains("userId")); + } +} diff --git a/apps/user-ms/src/test/java/it/pagopa/selfcare/user/service/UserRegistryServiceTest.java b/apps/user-ms/src/test/java/it/pagopa/selfcare/user/service/UserRegistryServiceTest.java index 1ea59ba7..fca286eb 100644 --- a/apps/user-ms/src/test/java/it/pagopa/selfcare/user/service/UserRegistryServiceTest.java +++ b/apps/user-ms/src/test/java/it/pagopa/selfcare/user/service/UserRegistryServiceTest.java @@ -28,7 +28,7 @@ import java.util.UUID; import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.when; @QuarkusTest @@ -43,6 +43,9 @@ public class UserRegistryServiceTest { @InjectMock private UserInstitutionService userInstitutionService; + @InjectMock + private UserNotificationService userNotificationService; + @RestClient @InjectMock private org.openapi.quarkus.user_registry_json.api.UserApi userRegistryApi; @@ -83,6 +86,8 @@ public static void switchMyChannels() { @Test void testSendUpdateUserNotificationToQueue() { + when(userNotificationService.sendKafkaNotification(any(UserNotificationToSend.class), anyString())).thenReturn(Uni.createFrom().item(new UserNotificationToSend())); + MutableUserFieldsDto mutableUserFieldsDto = new MutableUserFieldsDto(); InMemorySink usersOut = connector.sink("sc-users"); when(userInstitutionService.findAllWithFilter(anyMap())).thenReturn(Multi.createFrom().item(userInstitution)); @@ -91,12 +96,6 @@ void testSendUpdateUserNotificationToQueue() { UniAssertSubscriber> subscriber = userRegistryService.updateUserRegistryAndSendNotificationToQueue(mutableUserFieldsDto, "userId", "institutionId") .subscribe().withSubscriber(UniAssertSubscriber.create()); subscriber.assertCompleted(); - - // Wait that the event is sent on kafka. - await().>>until(usersOut::received, t -> t.size() == 1); - - String queuedMessage = usersOut.received().get(0).getPayload(); - Assertions.assertTrue(queuedMessage.contains("userId")); }