diff --git a/db/installationInit.sql b/db/installationInit.sql
index 26333ba3..8843bb94 100644
--- a/db/installationInit.sql
+++ b/db/installationInit.sql
@@ -115,7 +115,10 @@ INSERT INTO `configuration` (`id`, `type`, `configuration_group_id`, `parent`, `
(90, 'GENERAL', 11, 87, 9, 'FHIRViaHL7v2SendingFacility', 'STRING', NULL, 'de.imi.mopat.io.impl.EncounterExporterTemplateFHIR', 'configuration.label.ODMviaHL7SendingFacility', NULL, NULL, '87e35940-97e2-4c3e-ae6c-b35ea65717e3', '', NULL),
(91, 'GENERAL', 11, 87, 10, 'FHIRViaHL7v2ReceivingApplication', 'STRING', NULL, 'de.imi.mopat.io.impl.EncounterExporterTemplateFHIR', 'configuration.label.ODMviaHL7ReceivingApplication', NULL, NULL, 'd500a3a1-c1ce-4091-ace9-4557481dba37', '', NULL),
(92, 'GENERAL', 11, 87, 11, 'FHIRViaHL7v2ReceivingFacility', 'STRING', NULL, 'de.imi.mopat.io.impl.EncounterExporterTemplateFHIR', 'configuration.label.ODMviaHL7ReceivingFacility', NULL, NULL, '613cc529-396e-4a06-b785-158c15629d02', '', NULL),
-(93, 'GENERAL', 11, 87, 11, 'FHIRViaHL7v2OBRFillerOrderNumber', 'STRING', NULL, 'de.imi.mopat.io.impl.EncounterExporterTemplateFHIR', 'configuration.label.ODMviaHL7OBRFillerOrderNumber', NULL, NULL, 'e78a6310-b2b1-4ee7-aea0-378ab33c3f73', '', NULL);
+(93, 'GENERAL', 11, 87, 11, 'FHIRViaHL7v2OBRFillerOrderNumber', 'STRING', NULL, 'de.imi.mopat.io.impl.EncounterExporterTemplateFHIR', 'configuration.label.ODMviaHL7OBRFillerOrderNumber', NULL, NULL, 'e78a6310-b2b1-4ee7-aea0-378ab33c3f73', '', NULL),
+(94, 'GENERAL', 1, NULL, 17, 'enableApiTokenAccess', 'BOOLEAN', 'configuration.description.enableApiTokenAccess', 'GLOBAL', 'configuration.label.enableApiTokenAccess', NULL, NULL, '25fd8f5e-2e90-4cd1-8a30-d84c88889c5c', false, NULL),
+(95, 'GENERAL', 1, 94, 18, 'apiKey', 'STRING', 'configuration.description.apiKey', 'GLOBAL', 'configuration.label.apiKey', NULL, NULL, '9b48f864-8cb7-4704-82e2-12045755e2aa', '', NULL);
+
diff --git a/db/installationInitTest.sql b/db/installationInitTest.sql
index e5e04531..e05b5d38 100644
--- a/db/installationInitTest.sql
+++ b/db/installationInitTest.sql
@@ -122,7 +122,9 @@ INSERT INTO `configuration` (`id`, `type`, `configuration_group_id`, `parent`, `
(90, 'GENERAL', 11, 87, 9, 'FHIRViaHL7v2SendingFacility', 'STRING', NULL, 'de.imi.mopat.io.impl.EncounterExporterTemplateFHIR', 'configuration.label.ODMviaHL7SendingFacility', NULL, NULL, '87e35940-97e2-4c3e-ae6c-b35ea65717e3', '', NULL),
(91, 'GENERAL', 11, 87, 10, 'FHIRViaHL7v2ReceivingApplication', 'STRING', NULL, 'de.imi.mopat.io.impl.EncounterExporterTemplateFHIR', 'configuration.label.ODMviaHL7ReceivingApplication', NULL, NULL, 'd500a3a1-c1ce-4091-ace9-4557481dba37', '', NULL),
(92, 'GENERAL', 11, 87, 11, 'FHIRViaHL7v2ReceivingFacility', 'STRING', NULL, 'de.imi.mopat.io.impl.EncounterExporterTemplateFHIR', 'configuration.label.ODMviaHL7ReceivingFacility', NULL, NULL, '613cc529-396e-4a06-b785-158c15629d02', '', NULL),
-(93, 'GENERAL', 11, 87, 11, 'FHIRViaHL7v2OBRFillerOrderNumber', 'STRING', NULL, 'de.imi.mopat.io.impl.EncounterExporterTemplateFHIR', 'configuration.label.ODMviaHL7OBRFillerOrderNumber', NULL, NULL, 'e78a6310-b2b1-4ee7-aea0-378ab33c3f73', '', NULL);
+(93, 'GENERAL', 11, 87, 11, 'FHIRViaHL7v2OBRFillerOrderNumber', 'STRING', NULL, 'de.imi.mopat.io.impl.EncounterExporterTemplateFHIR', 'configuration.label.ODMviaHL7OBRFillerOrderNumber', NULL, NULL, 'e78a6310-b2b1-4ee7-aea0-378ab33c3f73', '', NULL),
+(94, 'GENERAL', 1, NULL, 17, 'enableApiTokenAccess', 'BOOLEAN', 'configuration.description.enableApiTokenAccess', 'GLOBAL', 'configuration.label.enableApiTokenAccess', NULL, NULL, '25fd8f5e-2e90-4cd1-8a30-d84c88889c5c', false, NULL),
+(95, 'GENERAL', 1, 94, 18, 'apiKey', 'STRING', 'configuration.description.apiKey', 'GLOBAL', 'configuration.label.apiKey', NULL, NULL, '9b48f864-8cb7-4704-82e2-12045755e2aa', '', NULL);
CREATE TABLE IF NOT EXISTS `clinic_configuration` (
diff --git a/db/update/v3.4.0.sql b/db/update/v3.4.0.sql
new file mode 100644
index 00000000..024395eb
--- /dev/null
+++ b/db/update/v3.4.0.sql
@@ -0,0 +1,24 @@
+USE `moPat`;
+
+SELECT MAX(id)
+INTO @largest_id
+FROM moPat.configuration;
+
+
+-- Determine the highest position in configuration group 1
+SELECT COALESCE(MAX(position), 0)
+INTO @max_position_group_1
+FROM `moPat`.`configuration`
+WHERE `configuration_group_id` = 1;
+
+
+-- Configuration group 1 is used here.
+-- If your target server uses a different configuration group for these settings,
+-- please adjust configuration_group_id accordingly.
+
+-- Inserts:
+-- 1) enableApiTokenAccess
+-- 2) apiKey as child of enableApiTokenAccess
+INSERT INTO moPat.configuration (`id`, `type`, `configuration_group_id`, `parent`, `position`, `attribute`, `configuration_type`, `description_message_code`, `class`, `label_message_code`, `test_method`, `update_method`, `uuid`, `value`, `pattern`) VALUES
+ (@largest_id + 1, 'GENERAL', 1, NULL, @max_position_group_1 +1, 'enableApiTokenAccess', 'BOOLEAN', 'configuration.description.enableApiTokenAccess', 'GLOBAL', 'configuration.label.enableApiTokenAccess', NULL, NULL, '25fd8f5e-2e90-4cd1-8a30-d84c88889c5c', false, NULL),
+ (@largest_id + 2, 'GENERAL', 1, @largest_id + 1, @max_position_group_1 +2, 'apiKey', 'STRING', 'configuration.description.apiKey', 'GLOBAL', 'configuration.label.apiKey', NULL, NULL, '9b48f864-8cb7-4704-82e2-12045755e2aa', '', NULL);
diff --git a/pom.xml b/pom.xml
index b9952fc9..decd0314 100644
--- a/pom.xml
+++ b/pom.xml
@@ -966,6 +966,17 @@
jar
test
+
+ org.junit.jupiter
+ junit-jupiter
+ RELEASE
+ test
+
+
+ com.bucket4j
+ bucket4j_jdk17-core
+ 8.18.0
+
diff --git a/src/main/java/de/imi/mopat/auth/ApiRateLimitFilter.java b/src/main/java/de/imi/mopat/auth/ApiRateLimitFilter.java
new file mode 100644
index 00000000..1d5dd450
--- /dev/null
+++ b/src/main/java/de/imi/mopat/auth/ApiRateLimitFilter.java
@@ -0,0 +1,104 @@
+package de.imi.mopat.auth;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.imi.mopat.service.ApiRateLimitService;
+import io.github.bucket4j.Bucket;
+import io.github.bucket4j.ConsumptionProbe;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+/**
+ * Filter that enforces rate limiting on the API endpoint {@code POST /encounter/schedule/api}.
+ * Applies both a global limit and a per-IP limit using token buckets.
+ * Requests exceeding either limit are rejected with HTTP 429.
+ */
+public class ApiRateLimitFilter extends OncePerRequestFilter {
+
+ private static final String API_PATH = "/encounter/schedule/api";
+ private static final String HTTP_METHOD = "POST";
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private final ApiRateLimitService apiRateLimitService;
+
+ public ApiRateLimitFilter(ApiRateLimitService apiRateLimitService) {
+ this.apiRateLimitService = apiRateLimitService;
+ }
+ /**
+ * Skips filtering for all requests that do not target the rate-limited endpoint.
+ *
+ * @param request the incoming HTTP request
+ * @return {@code true} if the request should not be filtered
+ */
+ @Override
+ protected boolean shouldNotFilter(HttpServletRequest request) {
+ return !API_PATH.equals(request.getServletPath())
+ || !HTTP_METHOD.equalsIgnoreCase(request.getMethod());
+ }
+ /**
+ * Checks the global and per-IP rate limits. Forwards the request if both limits
+ * are satisfied, otherwise responds with HTTP 429.
+ *
+ * @param request the incoming HTTP request
+ * @param response the HTTP response
+ * @param filterChain the filter chain
+ */
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain)
+ throws ServletException, IOException {
+
+ ConsumptionProbe globalProbe =
+ apiRateLimitService.getGlobalBucket().tryConsumeAndReturnRemaining(1);
+
+ if (!globalProbe.isConsumed()) {
+ writeTooManyRequestsResponse(response, globalProbe, "Global rate limit exceeded");
+ return;
+ }
+
+ String clientIp = request.getRemoteAddr();
+ Bucket ipBucket = apiRateLimitService.resolveIpBucket(clientIp);
+ ConsumptionProbe ipProbe = ipBucket.tryConsumeAndReturnRemaining(1);
+
+ if (!ipProbe.isConsumed()) {
+ writeTooManyRequestsResponse(response, ipProbe, "IP rate limit exceeded");
+ return;
+ }
+
+ response.setHeader("X-Rate-Limit-Global-Remaining",
+ String.valueOf(globalProbe.getRemainingTokens()));
+ response.setHeader("X-Rate-Limit-IP-Remaining",
+ String.valueOf(ipProbe.getRemainingTokens()));
+
+ filterChain.doFilter(request, response);
+ }
+ /**
+ * Writes a {@code 429 Too Many Requests} response including a retry-after header
+ * and a JSON error message.
+ *
+ * @param response the HTTP response
+ * @param probe the consumption probe containing refill timing information
+ * @param message the error message to include in the response body
+ */
+ private void writeTooManyRequestsResponse(HttpServletResponse response,
+ ConsumptionProbe probe,
+ String message) throws IOException {
+ response.setStatus(429);
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ response.setHeader(
+ "X-Rate-Limit-Retry-After-Seconds",
+ String.valueOf(TimeUnit.NANOSECONDS.toSeconds(probe.getNanosToWaitForRefill()))
+ );
+
+ objectMapper.writeValue(response.getWriter(), Map.of(
+ "error", message
+ ));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/imi/mopat/config/AppConfig.java b/src/main/java/de/imi/mopat/config/AppConfig.java
index 781daa6a..34c102aa 100644
--- a/src/main/java/de/imi/mopat/config/AppConfig.java
+++ b/src/main/java/de/imi/mopat/config/AppConfig.java
@@ -52,7 +52,8 @@
@ComponentScan(basePackages = {"de.imi.mopat.auth", "de.imi.mopat.config",
"de.imi.mopat.controller", "de.imi.mopat.cron", "de.imi.mopat.dao", "de.imi.mopat.helper.model",
"de.imi.mopat.helper.controller", "de.imi.mopat.io", "de.imi.mopat.io.impl", "de.imi.mopat.io.importer",
- "de.imi.mopat.io.importer.fhir", "de.imi.mopat.io.importer.odm", "de.imi.mopat.model", "de.imi.mopat.validator"})
+ "de.imi.mopat.io.importer.fhir", "de.imi.mopat.io.importer.odm", "de.imi.mopat.model",
+ "de.imi.mopat.validator","de.imi.mopat.service"})
@PropertySources({
@PropertySource("classpath:mopat.properties"),
diff --git a/src/main/java/de/imi/mopat/config/ApplicationSecurityConfig.java b/src/main/java/de/imi/mopat/config/ApplicationSecurityConfig.java
index 2368ef6f..8a6d1acf 100644
--- a/src/main/java/de/imi/mopat/config/ApplicationSecurityConfig.java
+++ b/src/main/java/de/imi/mopat/config/ApplicationSecurityConfig.java
@@ -1,7 +1,7 @@
package de.imi.mopat.config;
import com.mchange.v2.c3p0.ComboPooledDataSource;
-import de.imi.mopat.helper.controller.NoOpAclCache;
+import de.imi.mopat.auth.ApiRateLimitFilter;
import de.imi.mopat.auth.CustomAuthenticationFailureHandler;
import de.imi.mopat.auth.CustomPostAuthenticationChecks;
import de.imi.mopat.auth.CustomPreAuthenticationChecks;
@@ -11,8 +11,11 @@
import de.imi.mopat.auth.PinAuthorizationFilter;
import de.imi.mopat.auth.PepperedBCryptPasswordEncoder;
import de.imi.mopat.auth.RoleBasedAuthenticationSuccessHandler;
+import de.imi.mopat.helper.controller.NoOpAclCache;
import java.beans.PropertyVetoException;
import java.util.Properties;
+
+import de.imi.mopat.service.ApiRateLimitService;
import org.apache.groovy.util.Maps;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
@@ -59,10 +62,12 @@ public class ApplicationSecurityConfig {
CacheManager cacheManager;
private final Environment environment;
+ private final ApiRateLimitService apiRateLimitService;
- public ApplicationSecurityConfig(Environment environment) {
+ public ApplicationSecurityConfig(Environment environment, ApiRateLimitService apiRateLimitService) {
this.environment = environment;
+ this.apiRateLimitService = apiRateLimitService;
}
/**
@@ -343,7 +348,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/mobile/survey/pseudonym",
"/error/maintenance",
"/error/internalservererror",
- "/error/accessdenied"
+ "/error/accessdenied",
+ "/encounter/schedule/api"
).permitAll()
// Login page GET
.requestMatchers(HttpMethod.GET, "/mobile/user/login").permitAll()
@@ -383,6 +389,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// CSRF
.csrf(csrf -> csrf.disable());
+
+ // Add custom filter before authentification
+ http.addFilterBefore(
+ new ApiRateLimitFilter(apiRateLimitService),
+ UsernamePasswordAuthenticationFilter.class
+ );
+
// Add custom filter after authentication
http.addFilterAfter(pinAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
diff --git a/src/main/java/de/imi/mopat/controller/BundleController.java b/src/main/java/de/imi/mopat/controller/BundleController.java
index ea31605c..df54549e 100644
--- a/src/main/java/de/imi/mopat/controller/BundleController.java
+++ b/src/main/java/de/imi/mopat/controller/BundleController.java
@@ -3,18 +3,13 @@
import de.imi.mopat.dao.AnswerDao;
import de.imi.mopat.dao.BundleDao;
import de.imi.mopat.dao.ConditionDao;
-import de.imi.mopat.dao.ExportTemplateDao;
import de.imi.mopat.dao.QuestionnaireDao;
import de.imi.mopat.dao.ScoreDao;
import de.imi.mopat.dao.user.AclClassDao;
import de.imi.mopat.dao.user.AclObjectIdentityDao;
-import de.imi.mopat.helper.controller.AuthService;
-import de.imi.mopat.helper.controller.BundleService;
+import de.imi.mopat.service.BundleService;
import de.imi.mopat.helper.controller.LocaleHelper;
-import de.imi.mopat.helper.controller.UserService;
-import de.imi.mopat.helper.controller.ClinicService;
-import de.imi.mopat.helper.model.BundleDTOMapper;
-import de.imi.mopat.helper.model.QuestionnaireDTOMapper;
+import de.imi.mopat.service.ClinicService;
import de.imi.mopat.model.Answer;
import de.imi.mopat.model.Bundle;
import de.imi.mopat.model.BundleClinic;
@@ -26,7 +21,6 @@
import de.imi.mopat.model.conditions.SelectAnswerCondition;
import de.imi.mopat.model.conditions.SliderAnswerThresholdCondition;
import de.imi.mopat.model.dto.BundleDTO;
-import de.imi.mopat.model.dto.BundleQuestionnaireDTO;
import de.imi.mopat.model.dto.QuestionnaireDTO;
import de.imi.mopat.validator.BundleDTOValidator;
diff --git a/src/main/java/de/imi/mopat/controller/ClinicController.java b/src/main/java/de/imi/mopat/controller/ClinicController.java
index 251dc736..e64e5746 100644
--- a/src/main/java/de/imi/mopat/controller/ClinicController.java
+++ b/src/main/java/de/imi/mopat/controller/ClinicController.java
@@ -13,14 +13,14 @@
import de.imi.mopat.helper.model.BundleDTOMapper;
import de.imi.mopat.helper.model.ClinicConfigurationDTOMapper;
import de.imi.mopat.helper.model.ClinicDTOMapper;
-import de.imi.mopat.helper.controller.UserService;
+import de.imi.mopat.service.UserService;
import de.imi.mopat.model.Bundle;
import de.imi.mopat.model.BundleClinic;
import de.imi.mopat.model.Clinic;
-import de.imi.mopat.helper.controller.ClinicConfigurationMappingService;
-import de.imi.mopat.helper.controller.ClinicConfigurationService;
-import de.imi.mopat.helper.controller.ClinicService;
-import de.imi.mopat.helper.controller.ConfigurationService;
+import de.imi.mopat.service.ClinicConfigurationMappingService;
+import de.imi.mopat.service.ClinicConfigurationService;
+import de.imi.mopat.service.ClinicService;
+import de.imi.mopat.service.ConfigurationService;
import de.imi.mopat.model.*;
import de.imi.mopat.model.dto.*;
import de.imi.mopat.model.user.User;
diff --git a/src/main/java/de/imi/mopat/controller/ConditionController.java b/src/main/java/de/imi/mopat/controller/ConditionController.java
index 50a78c66..c4aebfd3 100644
--- a/src/main/java/de/imi/mopat/controller/ConditionController.java
+++ b/src/main/java/de/imi/mopat/controller/ConditionController.java
@@ -5,7 +5,7 @@
import de.imi.mopat.dao.ConditionDao;
import de.imi.mopat.dao.QuestionDao;
import de.imi.mopat.dao.QuestionnaireDao;
-import de.imi.mopat.helper.controller.ConditionService;
+import de.imi.mopat.service.ConditionService;
import de.imi.mopat.helper.model.BundleDTOMapper;
import de.imi.mopat.helper.model.ConditionDTOMapper;
import de.imi.mopat.model.Answer;
diff --git a/src/main/java/de/imi/mopat/controller/ConfigurationController.java b/src/main/java/de/imi/mopat/controller/ConfigurationController.java
index 254044f0..8a1a4fdf 100644
--- a/src/main/java/de/imi/mopat/controller/ConfigurationController.java
+++ b/src/main/java/de/imi/mopat/controller/ConfigurationController.java
@@ -3,7 +3,7 @@
import com.mchange.v1.db.sql.UnsupportedTypeException;
import de.imi.mopat.dao.ConfigurationDao;
import de.imi.mopat.dao.ConfigurationGroupDao;
-import de.imi.mopat.helper.controller.ConfigurationService;
+import de.imi.mopat.service.ConfigurationService;
import de.imi.mopat.helper.controller.MailSender;
import de.imi.mopat.helper.controller.MultiPartFileUploadBean;
import de.imi.mopat.helper.controller.StringUtilities;
diff --git a/src/main/java/de/imi/mopat/controller/EncounterController.java b/src/main/java/de/imi/mopat/controller/EncounterController.java
index b3440db8..0b61f863 100644
--- a/src/main/java/de/imi/mopat/controller/EncounterController.java
+++ b/src/main/java/de/imi/mopat/controller/EncounterController.java
@@ -9,10 +9,9 @@
import de.imi.mopat.dao.EncounterScheduledDao;
import de.imi.mopat.dao.ExportTemplateDao;
import de.imi.mopat.helper.controller.ApplicationMailer;
-import de.imi.mopat.helper.controller.ClinicService;
import de.imi.mopat.helper.model.BundleDTOMapper;
-import de.imi.mopat.helper.model.EncounterScheduledDTOMapper;
import de.imi.mopat.helper.model.EncounterDTOMapper;
+import de.imi.mopat.helper.model.EncounterScheduledDTOMapper;
import de.imi.mopat.io.EncounterExporter;
import de.imi.mopat.model.Bundle;
import de.imi.mopat.model.BundleClinic;
@@ -23,6 +22,7 @@
import de.imi.mopat.model.dto.BundleDTO;
import de.imi.mopat.model.dto.ClinicDTO;
import de.imi.mopat.model.dto.EncounterDTO;
+import de.imi.mopat.model.dto.EncounterScheduledApiRequestDTO;
import de.imi.mopat.model.dto.EncounterScheduledDTO;
import de.imi.mopat.model.enumeration.AuditEntryActionType;
import de.imi.mopat.model.enumeration.AuditPatientAttribute;
@@ -31,12 +31,16 @@
import de.imi.mopat.model.user.Authority;
import de.imi.mopat.model.user.User;
import de.imi.mopat.model.user.UserRole;
+import de.imi.mopat.service.AuditService;
+import de.imi.mopat.service.ClinicService;
+import de.imi.mopat.service.EncounterScheduledService;
+import de.imi.mopat.service.MailSendingStatus;
+import de.imi.mopat.validator.EncounterScheduledApiRequestDTOValidator;
import de.imi.mopat.validator.EncounterScheduledDTOValidator;
import jakarta.validation.Valid;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
@@ -44,21 +48,27 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
@@ -84,6 +94,8 @@ public class EncounterController {
@Autowired
private EncounterScheduledDTOValidator encounterScheduledDTOValidator;
@Autowired
+ private EncounterScheduledApiRequestDTOValidator encounterScheduledApiRequestDTOValidator;
+ @Autowired
private EncounterScheduledExecutor encounterScheduledExecutor;
@Autowired
private ExportTemplateDao exportTemplateDao;
@@ -99,6 +111,10 @@ public class EncounterController {
private ClinicService clinicService;
@Autowired
private ClinicDao clinicDao;
+ @Autowired
+ private EncounterScheduledService encounterSchedulingService;
+ @Autowired
+ private AuditService auditService;
/**
* Collects all emails to set for the encounterScheduledDTOs replyMails.
@@ -148,11 +164,7 @@ public Map> getReplyMails() {
public String listEncounter(final Model model) {
// Initialize containers
Set patientAttributes = new HashSet<>(
- Arrays.asList(
- AuditPatientAttribute.CASE_NUMBER,
- AuditPatientAttribute.EMAIL_ADDRESS
- )
- );
+ Arrays.asList(AuditPatientAttribute.CASE_NUMBER, AuditPatientAttribute.EMAIL_ADDRESS));
Set caseNumbers = new HashSet<>();
List encounterDTOs = new ArrayList<>();
@@ -178,9 +190,8 @@ public String listEncounter(final Model model) {
});
// Add EncounterScheduled to DTOs based on already grouped data by bundle
- encounterScheduledByBundle.getOrDefault(bundle.getId(), Collections.emptyList()).stream()
- .map(encounterScheduledDTOMapper)
- .forEach(dto -> {
+ encounterScheduledByBundle.getOrDefault(bundle.getId(), Collections.emptyList())
+ .stream().map(encounterScheduledDTOMapper).forEach(dto -> {
encounterScheduledDTOs.add(dto);
encounterScheduledJSONSet.add(dto.getJSON());
});
@@ -271,13 +282,12 @@ public String scheduleEncounter(@RequestParam(value = "id", required = false) fi
@RequestParam(value = "pseudonym", required = false) final String pseudonym,
@RequestParam(value = "email", required = false) final String email, final Model model) {
- addClinicInfoToModel(model,getCurrentUser());
+ addClinicInfoToModel(model, getCurrentUser());
EncounterScheduledDTO encounterScheduledDTO = new EncounterScheduledDTO();
if (id != null && id > 0) {
EncounterScheduled encounterScheduled = encounterScheduledDao.getElementById(id);
if (encounterScheduled != null) {
- encounterScheduledDTO = encounterScheduledDTOMapper.apply(
- encounterScheduled);
+ encounterScheduledDTO = encounterScheduledDTOMapper.apply(encounterScheduled);
Set patientAttributes = new HashSet<>();
patientAttributes.add(AuditPatientAttribute.CASE_NUMBER);
patientAttributes.add(AuditPatientAttribute.EMAIL_ADDRESS);
@@ -338,25 +348,9 @@ public String saveScheduledEncounter(@RequestParam final String action,
final BindingResult result, final Model model,
final RedirectAttributes redirectAttributes) {
- if (action.equalsIgnoreCase("cancel")) {
+ if (isCancelAction(action)) {
return "redirect:/encounter/list?series=true";
}
-
- switch (encounterScheduledDTO.getEncounterScheduledSerialType()) {
- case UNIQUELY:
- encounterScheduledDTO.setRepeatPeriod(null);
- encounterScheduledDTO.setEndDate(null);
- break;
- case WEEKLY:
- encounterScheduledDTO.setRepeatPeriod(7);
- break;
- case MONTHLY:
- encounterScheduledDTO.setRepeatPeriod(30);
- break;
- default:
- break;
- }
-
encounterScheduledDTO.setReplyMails(getReplyMails());
encounterScheduledDTOValidator.validate(encounterScheduledDTO, result);
@@ -373,123 +367,80 @@ public String saveScheduledEncounter(@RequestParam final String action,
new ArrayList<>(Arrays.asList(EncounterScheduledSerialType.values())));
return "encounter/schedule";
}
+ MailSendingStatus status = encounterSchedulingService.save(encounterScheduledDTO,
+ encounterScheduledExecutor);
+
+ switch (status) {
+ case SUCCESS -> redirectAttributes.addFlashAttribute("success",
+ messageSource.getMessage("encounterScheduled.mail.success", new Object[]{},
+ LocaleContextHolder.getLocale()));
+ case INVALID_ADDRESS -> redirectAttributes.addFlashAttribute("failure",
+ messageSource.getMessage("encounterScheduled.mail.invalidMail",
+ new Object[]{encounterScheduledDTO.getEmail()},
+ LocaleContextHolder.getLocale()));
+ case FAILURE -> redirectAttributes.addFlashAttribute("failure",
+ messageSource.getMessage("encounterScheduled.mail.fail", null,
+ LocaleContextHolder.getLocale()));
- EncounterScheduled encounterScheduled = null;
- Bundle bundle = bundleDao.getElementById(encounterScheduledDTO.getBundleDTO().getId());
- Clinic clinic = clinicDao.getElementById(encounterScheduledDTO.getClinicDTO().getId());
- if (encounterScheduledDTO.getId() == null) {
- encounterScheduled = new EncounterScheduled(encounterScheduledDTO.getCaseNumber(),
- bundle, clinic, encounterScheduledDTO.getStartDate(),
- encounterScheduledDTO.getEncounterScheduledSerialType(),
- encounterScheduledDTO.getEndDate(), encounterScheduledDTO.getRepeatPeriod(),
- encounterScheduledDTO.getEmail(), encounterScheduledDTO.getLocale().toString(),
- encounterScheduledDTO.getPersonalText(), encounterScheduledDTO.getReplyMail());
- } else {
- encounterScheduled = encounterScheduledDao.getElementById(
- encounterScheduledDTO.getId());
- encounterScheduled.setCaseNumber(encounterScheduledDTO.getCaseNumber());
- encounterScheduled.setBundle(bundle);
- encounterScheduled.setClinic(clinic);
- encounterScheduled.setEmail(encounterScheduledDTO.getEmail());
- encounterScheduled.setEncounterScheduledSerialType(
- encounterScheduledDTO.getEncounterScheduledSerialType());
- encounterScheduled.setStartDate(encounterScheduledDTO.getStartDate());
- encounterScheduled.setEndDate(encounterScheduledDTO.getEndDate());
- encounterScheduled.setRepeatPeriod(encounterScheduledDTO.getRepeatPeriod());
- encounterScheduled.setLocale(encounterScheduledDTO.getLocale().toString());
- encounterScheduled.setPersonalText(encounterScheduledDTO.getPersonalText());
- if (encounterScheduledDTO.getReplyMail().equalsIgnoreCase("empty")) {
- encounterScheduled.setReplyMail(null);
- } else {
- encounterScheduled.setReplyMail(encounterScheduledDTO.getReplyMail());
- }
}
- Calendar now = Calendar.getInstance();
- now.setTime(new Date());
+ return "redirect:/encounter/list";
+ }
- Calendar startDay = Calendar.getInstance();
- startDay.setTime(encounterScheduledDTO.getStartDate());
+ private boolean isCancelAction(String action) {
+ return "cancel".equalsIgnoreCase(action);
+ }
- // If the scheduled encounter is scheduled for today,
- // possibly send the notification mail immediately
-
- if (startDay.get(Calendar.DAY_OF_MONTH) == now.get(Calendar.DAY_OF_MONTH) &&
- startDay.get(Calendar.MONTH) == now.get(Calendar.MONTH) &&
- startDay.get(Calendar.YEAR) == now.get(Calendar.YEAR)
- ) {
- Calendar lastExecutionTime = Calendar.getInstance();
- if (encounterScheduledExecutor.getLastExecutionTime() != null) {
- lastExecutionTime.setTime(encounterScheduledExecutor.getLastExecutionTime());
+ /**
+ * Controls the HTTP POST requests for the URL /encounter/schedule/api. Used to enable
+ * automatic scheduled encounters through an API.
+ *
+ * @param request The API request payload for creating a scheduled encounter.
+ * @param result The validation result.
+ * @param key The API key from the request header.
+ * @return A response entity describing the result of the request.
+ */
+ @PostMapping(value = "/encounter/schedule/api")
+ @ResponseBody
+ public ResponseEntity> scheduleEncounterFromApi(
+ @RequestBody EncounterScheduledApiRequestDTO request, BindingResult result,
+ @RequestHeader(value = "x-api-key", required = false) String key) {
+ if (!configurationDao.isApiKeyAccessEnabled()) {
+ return ResponseEntity.status(403).body(Map.of("error", "API access is disabled"));
+ }
+ encounterScheduledApiRequestDTOValidator.validate(request, result);
+ if (result.hasErrors()) {
+ Map fieldErrors = new LinkedHashMap<>();
+ for (FieldError error : result.getFieldErrors()) {
+ fieldErrors.put(error.getField(), error.getDefaultMessage());
}
+ return ResponseEntity.badRequest()
+ .body(Map.of("error", "Validation failed", "details", fieldErrors));
+ }
- Calendar nextExecutionTime = Calendar.getInstance();
- nextExecutionTime.setTime(encounterScheduledExecutor.getNextExecutionTime());
-
- // When the last execution time of the encounterScheduledExecutor
- // is not known
- // and the next run will be tomorrow or
- // if the last execution time was today,
- // we have to send the notification email immediately.
- if ((encounterScheduledExecutor.getLastExecutionTime() == null
- && now.get(Calendar.DAY_OF_MONTH) != nextExecutionTime.get(Calendar.DAY_OF_MONTH))
- || (encounterScheduledExecutor.getLastExecutionTime() != null
- && now.get(Calendar.DAY_OF_MONTH) == lastExecutionTime.get(
- Calendar.DAY_OF_MONTH))) {
- // Get date today at midnight to set the encounter's time
- now.set(Calendar.MILLISECOND, 0);
- now.set(Calendar.SECOND, 0);
- now.set(Calendar.MINUTE, 0);
- now.set(Calendar.HOUR_OF_DAY, 0);
- Date today = now.getTime();
-
- Encounter encounter = new Encounter();
- encounter.setEncounterScheduled(encounterScheduled);
- encounter.setBundle(bundle);
- encounter.setClinic(encounterScheduled.getClinic());
- bundle.addEncounter(encounter);
- encounter.setCaseNumber(encounterScheduled.getCaseNumber());
- encounter.setStartTime(new Timestamp(today.getTime()));
- if (encounter.sendMail(applicationMailer, messageSource,
- configurationDao.getBaseURL())) {
- redirectAttributes.addFlashAttribute("success",
- messageSource.getMessage("encounterScheduled.mail.success", new Object[]{},
- LocaleContextHolder.getLocale()));
- } else {
- String failMessage = messageSource.getMessage("encounterScheduled.mail.fail",
- new Object[]{}, LocaleContextHolder.getLocale());
- if (encounter.getEncounterScheduled().getMailStatus() != null
- && encounter.getEncounterScheduled().getMailStatus()
- .equals(EncounterScheduledMailStatus.ADDRESS_REJECTED)) {
- encounterScheduled.setMailStatus(
- EncounterScheduledMailStatus.ADDRESS_REJECTED);
- failMessage = messageSource.getMessage(
- "encounterScheduled.mail.invalidMail",
- new Object[]{encounterScheduled.getEmail()},
- LocaleContextHolder.getLocale());
- }
- redirectAttributes.addFlashAttribute("failure", failMessage);
- }
- }
+ String apiKey = configurationDao.getApiKey();
+
+ if (!Objects.equals(apiKey, key)) {
+ return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
}
- Set patientAttributes = new HashSet<>();
- patientAttributes.add(AuditPatientAttribute.CASE_NUMBER);
- patientAttributes.add(AuditPatientAttribute.EMAIL_ADDRESS);
- patientAttributes.add(AuditPatientAttribute.FIRST_NAME);
- patientAttributes.add(AuditPatientAttribute.LAST_NAME);
- patientAttributes.add(AuditPatientAttribute.DATE_OF_BIRTH);
- auditEntryDao.writeAuditEntry(this.getClass().getSimpleName(),
- "saveScheduledEncounter(encounterScheduledDTO, result, model," + " redirectAttributes)",
- encounterScheduled.getCaseNumber(), patientAttributes, AuditEntryActionType.WRITE);
- encounterScheduledDao.merge(encounterScheduled);
- bundleDao.merge(bundle);
- return "redirect:/encounter/list";
+ EncounterScheduledDTO dto = encounterScheduledDTOMapper.mapFromApiRequest(request);
+ MailSendingStatus status = encounterSchedulingService.save(dto, encounterScheduledExecutor);
+
+ return switch (status) {
+ case SUCCESS ->
+ ResponseEntity.ok(Map.of("message", "Scheduled encounter created successfully"));
+ case INVALID_ADDRESS ->
+ ResponseEntity.badRequest().body(Map.of("error", "Invalid email address"));
+ case FAILURE -> ResponseEntity.internalServerError()
+ .body(Map.of("error", "Failed to create scheduled encounter"));
+ };
+
}
/**
- * Controls the HTTP GET Request for the URL /encounter/sendEmail. Sends a remind mail
+ * Controls the HTTP GET Request for the URL /encounter/sendEmail. Sends a reminder mail
* for a given {@link Encounter} object to the patient.
*
* @param encounterId :The id of the {@link Encounter} object.
@@ -773,28 +724,29 @@ public String editEmail(
/**
* Adds ClinicDTOS to the model
+ *
* @param model
*/
- private void addClinicInfoToModel(Model model, User user){
+ private void addClinicInfoToModel(Model model, User user) {
boolean isAdmin = false;
- for(Authority authority: user.getAuthority()){
- if(authority.getAuthority().equals(UserRole.ROLE_ADMIN.getTextValue())){
- isAdmin=true;
+ for (Authority authority : user.getAuthority()) {
+ if (authority.getAuthority().equals(UserRole.ROLE_ADMIN.getTextValue())) {
+ isAdmin = true;
break;
}
}
- if(isAdmin){
+ if (isAdmin) {
model.addAttribute("clinicDTOs", clinicService.getAllClinicsWithoutBundle());
} else {
List assignedClinics = clinicService.getAssignedClinics(user);
- List clinicDTOs = clinicService.transformClinicsToDTOs(false, assignedClinics);
+ List clinicDTOs = clinicService.transformClinicsToDTOs(false,
+ assignedClinics);
model.addAttribute("clinicDTOs", clinicDTOs);
}
}
private User getCurrentUser() {
- return (User) SecurityContextHolder.getContext().getAuthentication()
- .getPrincipal();
+ return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
diff --git a/src/main/java/de/imi/mopat/controller/InvitationController.java b/src/main/java/de/imi/mopat/controller/InvitationController.java
index 4eaceb89..cd4de606 100644
--- a/src/main/java/de/imi/mopat/controller/InvitationController.java
+++ b/src/main/java/de/imi/mopat/controller/InvitationController.java
@@ -8,7 +8,7 @@
import de.imi.mopat.dao.user.InvitationDao;
import de.imi.mopat.dao.user.UserDao;
import de.imi.mopat.helper.controller.ApplicationMailer;
-import de.imi.mopat.helper.controller.InvitationService;
+import de.imi.mopat.service.InvitationService;
import de.imi.mopat.helper.model.ClinicDTOMapper;
import de.imi.mopat.helper.controller.LocaleHelper;
import de.imi.mopat.model.Clinic;
diff --git a/src/main/java/de/imi/mopat/controller/QuestionnaireController.java b/src/main/java/de/imi/mopat/controller/QuestionnaireController.java
index fe56c78e..d615cbbb 100644
--- a/src/main/java/de/imi/mopat/controller/QuestionnaireController.java
+++ b/src/main/java/de/imi/mopat/controller/QuestionnaireController.java
@@ -10,11 +10,11 @@
import de.imi.mopat.dao.QuestionDao;
import de.imi.mopat.dao.QuestionnaireDao;
import de.imi.mopat.dao.ScoreDao;
-import de.imi.mopat.helper.controller.AuthService;
+import de.imi.mopat.service.AuthService;
import de.imi.mopat.helper.controller.FhirVersionHelper;
import de.imi.mopat.helper.controller.LocaleHelper;
import de.imi.mopat.helper.controller.QuestionnaireService;
-import de.imi.mopat.helper.controller.QuestionnaireVersionGroupService;
+import de.imi.mopat.service.QuestionnaireVersionGroupService;
import de.imi.mopat.helper.controller.StringUtilities;
import de.imi.mopat.io.MetadataExporter;
import de.imi.mopat.io.impl.MetadataExporterFactory;
diff --git a/src/main/java/de/imi/mopat/controller/SurveyController.java b/src/main/java/de/imi/mopat/controller/SurveyController.java
index 7ac94d20..c522b639 100644
--- a/src/main/java/de/imi/mopat/controller/SurveyController.java
+++ b/src/main/java/de/imi/mopat/controller/SurveyController.java
@@ -3,7 +3,6 @@
import de.imi.mopat.dao.*;
import de.imi.mopat.dao.user.AclEntryDao;
import de.imi.mopat.dao.user.UserDao;
-import de.imi.mopat.helper.controller.*;
import de.imi.mopat.auth.PinAuthorizationService;
import de.imi.mopat.dao.AnswerDao;
import de.imi.mopat.dao.AuditEntryDao;
@@ -34,6 +33,9 @@
import de.imi.mopat.model.dto.ResponseDTO;
import de.imi.mopat.model.score.Score;
import de.imi.mopat.model.user.User;
+import de.imi.mopat.service.AuthService;
+import de.imi.mopat.service.ClinicConfigurationMappingService;
+import de.imi.mopat.service.ClinicService;
import de.imi.mopat.validator.MoPatValidator;
import java.sql.Timestamp;
diff --git a/src/main/java/de/imi/mopat/controller/UserController.java b/src/main/java/de/imi/mopat/controller/UserController.java
index a424ace3..297dcddb 100644
--- a/src/main/java/de/imi/mopat/controller/UserController.java
+++ b/src/main/java/de/imi/mopat/controller/UserController.java
@@ -12,11 +12,11 @@
import de.imi.mopat.dao.user.PinAuthorizationDao;
import de.imi.mopat.dao.user.UserDao;
import de.imi.mopat.helper.controller.ApplicationMailer;
-import de.imi.mopat.helper.controller.AuthService;
+import de.imi.mopat.service.AuthService;
import de.imi.mopat.helper.controller.Constants;
import de.imi.mopat.model.Clinic;
import de.imi.mopat.model.enumeration.PermissionType;
-import de.imi.mopat.helper.controller.UserService;
+import de.imi.mopat.service.UserService;
import de.imi.mopat.model.user.AclEntry;
import de.imi.mopat.model.user.Authority;
import de.imi.mopat.model.user.ForgotPasswordToken;
diff --git a/src/main/java/de/imi/mopat/dao/ConfigurationDao.java b/src/main/java/de/imi/mopat/dao/ConfigurationDao.java
index 50d753f1..cec3d8e6 100644
--- a/src/main/java/de/imi/mopat/dao/ConfigurationDao.java
+++ b/src/main/java/de/imi/mopat/dao/ConfigurationDao.java
@@ -169,4 +169,17 @@ public Configuration getConfigurationByGroupName(final Long clinicId, final Stri
* @return The imprint text to display.
*/
String getImprintText();
+
+ /**
+ * Returns the flag, whether automated access via Api-token is enabled
+ * @return true, if access is enabled, false otherwise
+ */
+ Boolean isApiKeyAccessEnabled();
+
+ /**
+ * returns String of API-Key for automated encounters.
+ * @return String of API-Key
+ */
+
+ String getApiKey();
}
diff --git a/src/main/java/de/imi/mopat/dao/impl/ConfigurationDaoImpl.java b/src/main/java/de/imi/mopat/dao/impl/ConfigurationDaoImpl.java
index cb6d542b..208b6b89 100644
--- a/src/main/java/de/imi/mopat/dao/impl/ConfigurationDaoImpl.java
+++ b/src/main/java/de/imi/mopat/dao/impl/ConfigurationDaoImpl.java
@@ -235,4 +235,18 @@ public String getImprintText() {
Constants.CLASS_GLOBAL);
return configuration.getValue();
}
+
+ @Override
+ public Boolean isApiKeyAccessEnabled() {
+ Configuration configuration = getConfigurationByAttributeAndClass(Constants.ENABLE_API_TOKEN_ACCESS,
+ Constants.CLASS_GLOBAL);
+ return Boolean.valueOf(configuration.getValue());
+ }
+
+ @Override
+ public String getApiKey() {
+ Configuration configuration = getConfigurationByAttributeAndClass(Constants.API_KEY,
+ Constants.CLASS_GLOBAL);
+ return configuration.getValue();
+ }
}
diff --git a/src/main/java/de/imi/mopat/helper/controller/Constants.java b/src/main/java/de/imi/mopat/helper/controller/Constants.java
index 768ed53e..db72ae30 100644
--- a/src/main/java/de/imi/mopat/helper/controller/Constants.java
+++ b/src/main/java/de/imi/mopat/helper/controller/Constants.java
@@ -92,4 +92,8 @@ public abstract class Constants {
// Configuration: The name of the attribute for the Imprint text message
public static final String IMPRINT_TEXT = "imprintText";
+
+ public static final String ENABLE_API_TOKEN_ACCESS="enableApiTokenAccess";
+
+ public static final String API_KEY="apiKey";
}
\ No newline at end of file
diff --git a/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetriever.java b/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetriever.java
index e42b4a4e..7d6f7897 100644
--- a/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetriever.java
+++ b/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetriever.java
@@ -29,6 +29,7 @@
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import de.imi.mopat.service.ApplicationContextService;
import org.slf4j.Logger;
import org.springframework.security.core.context.SecurityContextHolder;
diff --git a/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java b/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java
index cd4746fe..ced5a31f 100644
--- a/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java
+++ b/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java
@@ -23,6 +23,7 @@
import de.imi.mopat.model.enumeration.AuditPatientAttribute;
import de.imi.mopat.model.enumeration.Gender;
import de.imi.mopat.model.user.User;
+import de.imi.mopat.service.ApplicationContextService;
import org.slf4j.Logger;
import org.springframework.security.core.context.SecurityContextHolder;
diff --git a/src/main/java/de/imi/mopat/helper/controller/QuestionnaireService.java b/src/main/java/de/imi/mopat/helper/controller/QuestionnaireService.java
index c08e472a..2d7c110b 100644
--- a/src/main/java/de/imi/mopat/helper/controller/QuestionnaireService.java
+++ b/src/main/java/de/imi/mopat/helper/controller/QuestionnaireService.java
@@ -26,6 +26,7 @@
import de.imi.mopat.model.user.UserRole;
import de.imi.mopat.validator.LogoValidator;
import de.imi.mopat.validator.QuestionnaireDTOValidator;
+import de.imi.mopat.service.*;
import java.awt.image.BufferedImage;
import java.io.File;
diff --git a/src/main/java/de/imi/mopat/helper/model/ClinicConfigurationMappingDTOMapper.java b/src/main/java/de/imi/mopat/helper/model/ClinicConfigurationMappingDTOMapper.java
index e1fc2828..44adb5e2 100644
--- a/src/main/java/de/imi/mopat/helper/model/ClinicConfigurationMappingDTOMapper.java
+++ b/src/main/java/de/imi/mopat/helper/model/ClinicConfigurationMappingDTOMapper.java
@@ -1,8 +1,8 @@
package de.imi.mopat.helper.model;
import de.imi.mopat.dao.ConfigurationGroupDao;
-import de.imi.mopat.helper.controller.ClinicConfigurationMappingService;
-import de.imi.mopat.helper.controller.ConfigurationService;
+import de.imi.mopat.service.ClinicConfigurationMappingService;
+import de.imi.mopat.service.ConfigurationService;
import de.imi.mopat.model.ClinicConfigurationMapping;
import de.imi.mopat.model.Configuration;
import de.imi.mopat.model.ConfigurationGroup;
diff --git a/src/main/java/de/imi/mopat/helper/model/ClinicDTOMapper.java b/src/main/java/de/imi/mopat/helper/model/ClinicDTOMapper.java
index 516decf9..f8201c93 100644
--- a/src/main/java/de/imi/mopat/helper/model/ClinicDTOMapper.java
+++ b/src/main/java/de/imi/mopat/helper/model/ClinicDTOMapper.java
@@ -1,6 +1,6 @@
package de.imi.mopat.helper.model;
-import de.imi.mopat.helper.controller.ClinicConfigurationMappingService;
+import de.imi.mopat.service.ClinicConfigurationMappingService;
import de.imi.mopat.model.Clinic;
import de.imi.mopat.model.ClinicConfiguration;
import de.imi.mopat.model.ClinicConfigurationMapping;
diff --git a/src/main/java/de/imi/mopat/helper/model/EncounterScheduledDTOMapper.java b/src/main/java/de/imi/mopat/helper/model/EncounterScheduledDTOMapper.java
index 7a9f5d1a..d986527f 100644
--- a/src/main/java/de/imi/mopat/helper/model/EncounterScheduledDTOMapper.java
+++ b/src/main/java/de/imi/mopat/helper/model/EncounterScheduledDTOMapper.java
@@ -1,10 +1,19 @@
package de.imi.mopat.helper.model;
+import de.imi.mopat.dao.BundleDao;
+import de.imi.mopat.dao.ClinicDao;
+import de.imi.mopat.dao.EncounterScheduledDao;
import de.imi.mopat.helper.controller.LocaleHelper;
+import de.imi.mopat.model.Bundle;
+import de.imi.mopat.model.Clinic;
import de.imi.mopat.model.Encounter;
import de.imi.mopat.model.EncounterScheduled;
+import de.imi.mopat.model.dto.BundleDTO;
+import de.imi.mopat.model.dto.ClinicDTO;
import de.imi.mopat.model.dto.EncounterDTO;
+import de.imi.mopat.model.dto.EncounterScheduledApiRequestDTO;
import de.imi.mopat.model.dto.EncounterScheduledDTO;
+import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -16,11 +25,23 @@
@Component
public class EncounterScheduledDTOMapper implements Function {
- @Autowired
- private BundleDTOMapper bundleDTOMapper;
-
- @Autowired
- private EncounterDTOMapper encounterDTOMapper;
+ private final BundleDTOMapper bundleDTOMapper;
+ private final EncounterDTOMapper encounterDTOMapper;
+ private final BundleDao bundleDao;
+ private final ClinicDao clinicDao;
+ private final EncounterScheduledDao encounterScheduledDao;
+
+ public EncounterScheduledDTOMapper(BundleDTOMapper bundleDTOMapper,
+ EncounterDTOMapper encounterDTOMapper,
+ BundleDao bundleDao,
+ ClinicDao clinicDao,
+ EncounterScheduledDao encounterScheduledDao) {
+ this.bundleDTOMapper = bundleDTOMapper;
+ this.encounterDTOMapper = encounterDTOMapper;
+ this.bundleDao = bundleDao;
+ this.clinicDao = clinicDao;
+ this.encounterScheduledDao = encounterScheduledDao;
+ }
@Override
public EncounterScheduledDTO apply(EncounterScheduled encounterScheduled) {
@@ -60,5 +81,73 @@ public EncounterScheduledDTO apply(EncounterScheduled encounterScheduled) {
return encounterScheduledDTO;
}
+ public EncounterScheduled mapToEntity(EncounterScheduledDTO dto) {
+ if (dto.getBundleDTO() == null || dto.getBundleDTO().getId() == null) {
+ throw new IllegalArgumentException("EncounterScheduledDTO must contain a bundle id");
+ }
+
+ if (dto.getClinicDTO() == null || dto.getClinicDTO().getId() == null) {
+ throw new IllegalArgumentException("EncounterScheduledDTO must contain a clinic id");
+ }
+ Clinic clinic = clinicDao.getElementById(dto.getClinicDTO().getId());
+ Bundle bundle = bundleDao.getElementById(dto.getBundleDTO().getId());
+
+ if (dto.getId() == null) {
+ return new EncounterScheduled(
+ dto.getCaseNumber(), bundle, clinic, dto.getStartDate(),
+ dto.getEncounterScheduledSerialType(), dto.getEndDate(), dto.getRepeatPeriod(),
+ dto.getEmail(), dto.getLocale().toString(), dto.getPersonalText(),
+ dto.getReplyMail());
+ }
+
+ EncounterScheduled entity = encounterScheduledDao.getElementById(dto.getId());
+ updateEntity(entity, dto, bundle, clinic);
+ return entity;
+ }
+
+ private void updateEntity(EncounterScheduled entity, EncounterScheduledDTO dto, Bundle bundle, Clinic clinic) {
+ entity.setCaseNumber(dto.getCaseNumber());
+ entity.setBundle(bundle);
+ entity.setClinic(clinic);
+ entity.setStartDate(dto.getStartDate());
+ entity.setEncounterScheduledSerialType(dto.getEncounterScheduledSerialType());
+ entity.setEndDate(dto.getEndDate());
+ entity.setRepeatPeriod(dto.getRepeatPeriod());
+ entity.setEmail(dto.getEmail());
+ entity.setLocale(dto.getLocale().toString());
+ entity.setPersonalText(dto.getPersonalText());
+
+ if ("empty".equalsIgnoreCase(dto.getReplyMail())) {
+ entity.setReplyMail(null);
+ } else {
+ entity.setReplyMail(dto.getReplyMail());
+ }
+ }
+
+ public EncounterScheduledDTO mapFromApiRequest(EncounterScheduledApiRequestDTO request) {
+ EncounterScheduledDTO dto = new EncounterScheduledDTO();
+
+ dto.setCaseNumber(request.getCaseNumber());
+ dto.setEmail(request.getEmail());
+ dto.setStartDate(request.getStartDate());
+ dto.setEndDate(request.getEndDate());
+ dto.setEncounterScheduledSerialType(request.getEncounterScheduledSerialType());
+ dto.setReplyMail(request.getReplyMail());
+ dto.setPersonalText(request.getPersonalText());
+
+ BundleDTO bundleDTO = new BundleDTO();
+ bundleDTO.setId(request.getBundleId());
+ dto.setBundleDTO(bundleDTO);
+
+ ClinicDTO clinicDTO = new ClinicDTO();
+ clinicDTO.setId(request.getClinicId());
+ dto.setClinicDTO(clinicDTO);
+ dto.setLocale(request.getLocale());
+
+ dto.setLocale(request.getLocale() != null ? request.getLocale() : Locale.getDefault());
+
+ return dto;
+ }
+
}
diff --git a/src/main/java/de/imi/mopat/io/importer/MoPatQuestionnaireImporter.java b/src/main/java/de/imi/mopat/io/importer/MoPatQuestionnaireImporter.java
index 62409894..09052957 100644
--- a/src/main/java/de/imi/mopat/io/importer/MoPatQuestionnaireImporter.java
+++ b/src/main/java/de/imi/mopat/io/importer/MoPatQuestionnaireImporter.java
@@ -5,7 +5,7 @@
import de.imi.mopat.dao.OperatorDao;
import de.imi.mopat.dao.QuestionnaireDao;
import de.imi.mopat.helper.controller.Constants;
-import de.imi.mopat.helper.controller.QuestionnaireVersionGroupService;
+import de.imi.mopat.service.QuestionnaireVersionGroupService;
import de.imi.mopat.helper.controller.StringUtilities;
import de.imi.mopat.model.Answer;
import de.imi.mopat.model.ImageAnswer;
diff --git a/src/main/java/de/imi/mopat/io/importer/fhir/FhirImporter.java b/src/main/java/de/imi/mopat/io/importer/fhir/FhirImporter.java
index 33025938..cb3d857c 100644
--- a/src/main/java/de/imi/mopat/io/importer/fhir/FhirImporter.java
+++ b/src/main/java/de/imi/mopat/io/importer/fhir/FhirImporter.java
@@ -7,8 +7,7 @@
import de.imi.mopat.dao.QuestionnaireDao;
import de.imi.mopat.helper.controller.Constants;
import de.imi.mopat.helper.controller.FhirVersionHelper;
-import de.imi.mopat.helper.controller.QuestionnaireVersionGroupService;
-import de.imi.mopat.helper.controller.StringUtilities;
+import de.imi.mopat.service.QuestionnaireVersionGroupService;
import de.imi.mopat.io.importer.ImportFailedException;
import de.imi.mopat.io.importer.ImportQuestionnaireResult;
import de.imi.mopat.io.importer.ImportQuestionnaireValidation;
diff --git a/src/main/java/de/imi/mopat/io/importer/odm/OdmQuestionnaireImporter.java b/src/main/java/de/imi/mopat/io/importer/odm/OdmQuestionnaireImporter.java
index 018b56c5..4bafd3d7 100644
--- a/src/main/java/de/imi/mopat/io/importer/odm/OdmQuestionnaireImporter.java
+++ b/src/main/java/de/imi/mopat/io/importer/odm/OdmQuestionnaireImporter.java
@@ -4,7 +4,7 @@
import de.imi.mopat.dao.ConfigurationGroupDao;
import de.imi.mopat.dao.ExportTemplateDao;
import de.imi.mopat.dao.QuestionnaireDao;
-import de.imi.mopat.helper.controller.QuestionnaireVersionGroupService;
+import de.imi.mopat.service.QuestionnaireVersionGroupService;
import de.imi.mopat.helper.controller.StringUtilities;
import de.imi.mopat.io.importer.ImportQuestionnaireResult;
import de.imi.mopat.model.ExportTemplate;
diff --git a/src/main/java/de/imi/mopat/model/dto/EncounterScheduledApiRequestDTO.java b/src/main/java/de/imi/mopat/model/dto/EncounterScheduledApiRequestDTO.java
new file mode 100644
index 00000000..f6b1e627
--- /dev/null
+++ b/src/main/java/de/imi/mopat/model/dto/EncounterScheduledApiRequestDTO.java
@@ -0,0 +1,117 @@
+package de.imi.mopat.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import de.imi.mopat.model.enumeration.EncounterScheduledSerialType;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import java.util.Date;
+import java.util.Locale;
+
+public class EncounterScheduledApiRequestDTO {
+
+ @NotNull
+ private String caseNumber;
+
+ @NotNull
+ private Long bundleId;
+
+ @NotNull
+ private Long clinicId;
+
+ @Pattern(regexp = "[A-Za-z0-9.!#$%&'*+\\-/=?^_`{|}~]+@[A-Za-z0-9.!#$%&'*+\\-/=?^_`{|}~]+\\.[A-Za-z]{2,}")
+ private String email;
+
+ @NotNull
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
+ private Date startDate;
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
+ private Date endDate;
+
+ @NotNull
+ private EncounterScheduledSerialType encounterScheduledSerialType = EncounterScheduledSerialType.UNIQUELY;
+
+ private String replyMail;
+
+ private String personalText;
+
+ private Locale locale;
+
+ public String getCaseNumber() {
+ return caseNumber;
+ }
+
+ public void setCaseNumber(String caseNumber) {
+ this.caseNumber = caseNumber;
+ }
+
+ public Long getBundleId() {
+ return bundleId;
+ }
+
+ public void setBundleId(Long bundleId) {
+ this.bundleId = bundleId;
+ }
+
+ public Long getClinicId() {
+ return clinicId;
+ }
+
+ public void setClinicId(Long clinicId) {
+ this.clinicId = clinicId;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public Date getStartDate() {
+ return startDate;
+ }
+
+ public void setStartDate(Date startDate) {
+ this.startDate = startDate;
+ }
+
+ public Date getEndDate() {
+ return endDate;
+ }
+
+ public void setEndDate(Date endDate) {
+ this.endDate = endDate;
+ }
+
+ public EncounterScheduledSerialType getEncounterScheduledSerialType() {
+ return encounterScheduledSerialType;
+ }
+
+ public void setEncounterScheduledSerialType(EncounterScheduledSerialType type) {
+ this.encounterScheduledSerialType = type;
+ }
+
+ public String getReplyMail() {
+ return replyMail;
+ }
+
+ public void setReplyMail(String replyMail) {
+ this.replyMail = replyMail;
+ }
+
+ public String getPersonalText() {
+ return personalText;
+ }
+
+ public void setPersonalText(String personalText) {
+ this.personalText = personalText;
+ }
+ public Locale getLocale() {
+ return locale;
+ }
+ public void setLocale (Locale locale){
+ this.locale = locale;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/imi/mopat/model/enumeration/ConfigurationType.java b/src/main/java/de/imi/mopat/model/enumeration/ConfigurationType.java
index a5ac76bd..ce3a7324 100644
--- a/src/main/java/de/imi/mopat/model/enumeration/ConfigurationType.java
+++ b/src/main/java/de/imi/mopat/model/enumeration/ConfigurationType.java
@@ -25,7 +25,8 @@ public enum ConfigurationType {
PASSWORD("PASSWORD"), // Path to the File. Application must have read and
// write rights
FILE("FILE"),
- RICH_TEXT("RICH_TEXT");
+ RICH_TEXT("RICH_TEXT"),
+ API_KEY("API_KEY");
private final String textValue;
diff --git a/src/main/java/de/imi/mopat/service/ApiRateLimitService.java b/src/main/java/de/imi/mopat/service/ApiRateLimitService.java
new file mode 100644
index 00000000..1665832b
--- /dev/null
+++ b/src/main/java/de/imi/mopat/service/ApiRateLimitService.java
@@ -0,0 +1,55 @@
+package de.imi.mopat.service;
+
+import io.github.bucket4j.Bandwidth;
+import io.github.bucket4j.Bucket;
+import java.time.Duration;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.springframework.stereotype.Service;
+
+/**
+ * Service that manages token buckets for API rate limiting.
+ * Maintains a global bucket shared across all clients and individual buckets per IP address.
+ */
+@Service
+public class ApiRateLimitService {
+
+ private final Bucket globalBucket;
+ private final Map ipBuckets = new ConcurrentHashMap<>();
+
+ public ApiRateLimitService() {
+ this.globalBucket = Bucket.builder()
+ .addLimit(Bandwidth.builder()
+ .capacity(200)
+ .refillGreedy(200, Duration.ofMinutes(1))
+ .build())
+ .build();
+ }
+
+ //Returns the global token bucket shared across all API requests.
+ public Bucket getGlobalBucket() {
+ return globalBucket;
+ }
+ /**
+ * Returns the token bucket for the given IP address, creating one if it does not exist.
+ *
+ * @param ipAddress the client's IP address
+ * @return the {@link Bucket} associated with the given IP address
+ */
+ public Bucket resolveIpBucket(String ipAddress) {
+ return ipBuckets.computeIfAbsent(ipAddress, key -> createIpBucket());
+ }
+ /**
+ * Creates a new token bucket for a single IP address.
+ *
+ * @return a new {@link Bucket} with the per-IP rate limit configuration
+ */
+ private Bucket createIpBucket() {
+ return Bucket.builder()
+ .addLimit(Bandwidth.builder()
+ .capacity(100)
+ .refillGreedy(100, Duration.ofMinutes(1))
+ .build())
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/imi/mopat/helper/controller/ApplicationContextService.java b/src/main/java/de/imi/mopat/service/ApplicationContextService.java
similarity index 95%
rename from src/main/java/de/imi/mopat/helper/controller/ApplicationContextService.java
rename to src/main/java/de/imi/mopat/service/ApplicationContextService.java
index dc598f1e..fd46072d 100644
--- a/src/main/java/de/imi/mopat/helper/controller/ApplicationContextService.java
+++ b/src/main/java/de/imi/mopat/service/ApplicationContextService.java
@@ -1,4 +1,4 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
diff --git a/src/main/java/de/imi/mopat/service/AuditService.java b/src/main/java/de/imi/mopat/service/AuditService.java
new file mode 100644
index 00000000..6b4c0fc5
--- /dev/null
+++ b/src/main/java/de/imi/mopat/service/AuditService.java
@@ -0,0 +1,32 @@
+package de.imi.mopat.service;
+
+import de.imi.mopat.dao.AuditEntryDao;
+import de.imi.mopat.model.EncounterScheduled;
+import de.imi.mopat.model.enumeration.AuditEntryActionType;
+import de.imi.mopat.model.enumeration.AuditPatientAttribute;
+import java.util.EnumSet;
+import java.util.Set;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AuditService {
+
+ private final AuditEntryDao auditEntryDao;
+
+ public AuditService(AuditEntryDao auditEntryDao) {
+ this.auditEntryDao = auditEntryDao;
+ }
+
+ @Transactional("MoPat_Audit")
+ public void writeScheduledEncounterAudit(Class> sourceClass, String methodName,
+ EncounterScheduled scheduled) {
+
+ Set patientAttributes = EnumSet.of(AuditPatientAttribute.CASE_NUMBER,
+ AuditPatientAttribute.EMAIL_ADDRESS, AuditPatientAttribute.FIRST_NAME,
+ AuditPatientAttribute.LAST_NAME, AuditPatientAttribute.DATE_OF_BIRTH);
+
+ auditEntryDao.writeAuditEntry(sourceClass.getSimpleName(), methodName,
+ scheduled.getCaseNumber(), patientAttributes, AuditEntryActionType.WRITE);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/imi/mopat/helper/controller/AuthService.java b/src/main/java/de/imi/mopat/service/AuthService.java
similarity index 98%
rename from src/main/java/de/imi/mopat/helper/controller/AuthService.java
rename to src/main/java/de/imi/mopat/service/AuthService.java
index 830a302d..64162e7d 100644
--- a/src/main/java/de/imi/mopat/helper/controller/AuthService.java
+++ b/src/main/java/de/imi/mopat/service/AuthService.java
@@ -1,4 +1,4 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import de.imi.mopat.model.user.UserRole;
import org.springframework.beans.factory.annotation.Autowired;
diff --git a/src/main/java/de/imi/mopat/helper/controller/BundleService.java b/src/main/java/de/imi/mopat/service/BundleService.java
similarity index 99%
rename from src/main/java/de/imi/mopat/helper/controller/BundleService.java
rename to src/main/java/de/imi/mopat/service/BundleService.java
index f4ecd99c..4b9f7bf5 100644
--- a/src/main/java/de/imi/mopat/helper/controller/BundleService.java
+++ b/src/main/java/de/imi/mopat/service/BundleService.java
@@ -1,4 +1,4 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import de.imi.mopat.dao.user.AclClassDao;
import de.imi.mopat.dao.user.AclObjectIdentityDao;
@@ -6,6 +6,7 @@
import de.imi.mopat.dao.BundleQuestionnaireDao;
import de.imi.mopat.dao.ExportTemplateDao;
import de.imi.mopat.dao.QuestionnaireDao;
+import de.imi.mopat.helper.controller.QuestionnaireService;
import de.imi.mopat.dao.ScoreDao;
import de.imi.mopat.helper.model.BundleDTOMapper;
import de.imi.mopat.helper.model.QuestionnaireDTOMapper;
diff --git a/src/main/java/de/imi/mopat/helper/controller/ClinicConfigurationMappingService.java b/src/main/java/de/imi/mopat/service/ClinicConfigurationMappingService.java
similarity index 99%
rename from src/main/java/de/imi/mopat/helper/controller/ClinicConfigurationMappingService.java
rename to src/main/java/de/imi/mopat/service/ClinicConfigurationMappingService.java
index 98cfc807..49d16e59 100644
--- a/src/main/java/de/imi/mopat/helper/controller/ClinicConfigurationMappingService.java
+++ b/src/main/java/de/imi/mopat/service/ClinicConfigurationMappingService.java
@@ -1,4 +1,4 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import com.fasterxml.jackson.annotation.JsonIgnore;
import de.imi.mopat.dao.ClinicConfigurationDao;
diff --git a/src/main/java/de/imi/mopat/helper/controller/ClinicConfigurationService.java b/src/main/java/de/imi/mopat/service/ClinicConfigurationService.java
similarity index 98%
rename from src/main/java/de/imi/mopat/helper/controller/ClinicConfigurationService.java
rename to src/main/java/de/imi/mopat/service/ClinicConfigurationService.java
index 27105d1e..2b607ee9 100644
--- a/src/main/java/de/imi/mopat/helper/controller/ClinicConfigurationService.java
+++ b/src/main/java/de/imi/mopat/service/ClinicConfigurationService.java
@@ -1,4 +1,4 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import de.imi.mopat.dao.ConfigurationGroupDao;
import de.imi.mopat.helper.model.ClinicConfigurationDTOMapper;
diff --git a/src/main/java/de/imi/mopat/helper/controller/ClinicService.java b/src/main/java/de/imi/mopat/service/ClinicService.java
similarity index 98%
rename from src/main/java/de/imi/mopat/helper/controller/ClinicService.java
rename to src/main/java/de/imi/mopat/service/ClinicService.java
index 9716368e..281479a0 100644
--- a/src/main/java/de/imi/mopat/helper/controller/ClinicService.java
+++ b/src/main/java/de/imi/mopat/service/ClinicService.java
@@ -1,4 +1,4 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import de.imi.mopat.dao.user.AclEntryDao;
import de.imi.mopat.dao.ClinicDao;
diff --git a/src/main/java/de/imi/mopat/helper/controller/ConditionService.java b/src/main/java/de/imi/mopat/service/ConditionService.java
similarity index 99%
rename from src/main/java/de/imi/mopat/helper/controller/ConditionService.java
rename to src/main/java/de/imi/mopat/service/ConditionService.java
index 4589d447..8b556fcd 100644
--- a/src/main/java/de/imi/mopat/helper/controller/ConditionService.java
+++ b/src/main/java/de/imi/mopat/service/ConditionService.java
@@ -1,4 +1,4 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import de.imi.mopat.dao.AnswerDao;
import de.imi.mopat.dao.BundleDao;
diff --git a/src/main/java/de/imi/mopat/helper/controller/ConfigurationService.java b/src/main/java/de/imi/mopat/service/ConfigurationService.java
similarity index 96%
rename from src/main/java/de/imi/mopat/helper/controller/ConfigurationService.java
rename to src/main/java/de/imi/mopat/service/ConfigurationService.java
index 5365b02e..5579ed32 100644
--- a/src/main/java/de/imi/mopat/helper/controller/ConfigurationService.java
+++ b/src/main/java/de/imi/mopat/service/ConfigurationService.java
@@ -1,4 +1,4 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import de.imi.mopat.dao.ConfigurationGroupDao;
import de.imi.mopat.model.Configuration;
diff --git a/src/main/java/de/imi/mopat/service/EncounterScheduledService.java b/src/main/java/de/imi/mopat/service/EncounterScheduledService.java
new file mode 100644
index 00000000..0173a3b4
--- /dev/null
+++ b/src/main/java/de/imi/mopat/service/EncounterScheduledService.java
@@ -0,0 +1,66 @@
+package de.imi.mopat.service;
+
+import de.imi.mopat.cron.EncounterScheduledExecutor;
+import de.imi.mopat.dao.BundleDao;
+import de.imi.mopat.dao.ClinicDao;
+import de.imi.mopat.dao.EncounterScheduledDao;
+import de.imi.mopat.helper.model.EncounterScheduledDTOMapper;
+import de.imi.mopat.model.Bundle;
+import de.imi.mopat.model.EncounterScheduled;
+import de.imi.mopat.model.dto.EncounterScheduledDTO;
+import de.imi.mopat.service.helper.SetRepeatConfiguration;
+import de.imi.mopat.validator.EncounterScheduledDTOValidator;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+
+@Service
+public class EncounterScheduledService {
+
+ private final BundleDao bundleDao;
+ private final EncounterScheduledDao encounterScheduledDao;
+ private final EncounterScheduledDTOMapper mapper;
+ private final ImmediateMailService immediateMailService;
+ private final AuditService auditService;
+ private final SetRepeatConfiguration setRepeatConfiguration;
+
+ public EncounterScheduledService(BundleDao bundleDao,
+ EncounterScheduledDao encounterScheduledDao,
+ EncounterScheduledDTOMapper mapper, ImmediateMailService immediateMailService,
+ AuditService auditService, SetRepeatConfiguration setRepeatConfiguration) {
+ this.bundleDao = bundleDao;
+ this.encounterScheduledDao = encounterScheduledDao;
+ this.mapper = mapper;
+ this.immediateMailService = immediateMailService;
+ this.auditService = auditService;
+ this.setRepeatConfiguration = setRepeatConfiguration;
+ }
+
+ public MailSendingStatus save(EncounterScheduledDTO dto, EncounterScheduledExecutor executor) {
+ setRepeatConfiguration.apply(dto);
+
+ EncounterScheduled scheduled = mapper.mapToEntity(dto);
+
+ encounterScheduledDao.merge(scheduled);
+ Bundle bundle = scheduled.getBundle();
+
+ if (bundle != null) {
+ bundleDao.merge(bundle);
+ }
+
+ auditService.writeScheduledEncounterAudit(EncounterScheduledService.class,
+ "save", scheduled);
+
+ if (immediateMailService.shouldSendEmailImmediately(scheduled, executor)) {
+ return immediateMailService.createAndSendEncounter(scheduled);
+ }
+ return MailSendingStatus.SUCCESS;
+ }
+
+
+}
+
+
+
+
+
diff --git a/src/main/java/de/imi/mopat/service/ImmediateMailService.java b/src/main/java/de/imi/mopat/service/ImmediateMailService.java
new file mode 100644
index 00000000..e928a11d
--- /dev/null
+++ b/src/main/java/de/imi/mopat/service/ImmediateMailService.java
@@ -0,0 +1,102 @@
+package de.imi.mopat.service;
+
+import de.imi.mopat.cron.EncounterScheduledExecutor;
+import de.imi.mopat.dao.BundleDao;
+import de.imi.mopat.model.Bundle;
+import de.imi.mopat.model.Encounter;
+import de.imi.mopat.model.EncounterScheduled;
+import de.imi.mopat.model.dto.EncounterScheduledDTO;
+import de.imi.mopat.service.MailService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Date;
+@Service
+public class ImmediateMailService {
+ private final MailService mailService;
+ private final BundleDao bundleDao;
+
+ public ImmediateMailService(MailService mailService, BundleDao bundleDao) {
+ this.mailService = mailService;
+ this.bundleDao = bundleDao;
+ }
+
+ public boolean shouldSendEmailImmediately(EncounterScheduled scheduled, EncounterScheduledExecutor executor){
+ return (shouldSendMailToday(scheduled) && (shouldSendBecauseNeverExecuted(executor)
+ || shouldSendBecauseAlreadyExecutedToday(executor))
+ );
+ }
+
+ private boolean shouldSendMailToday(EncounterScheduled scheduled){
+ return (isSameDay(scheduled.getStartDate(), new Date()));
+ }
+
+ private boolean shouldSendBecauseNeverExecuted(EncounterScheduledExecutor executor){
+ if (executor.getLastExecutionTime() != null) return false;
+ return (!isSameDay(executor.getNextExecutionTime(), new Date()));
+ }
+
+ private boolean shouldSendBecauseAlreadyExecutedToday(EncounterScheduledExecutor executor){
+ if (executor.getLastExecutionTime() == null) return false;
+ return (isSameDay(executor.getLastExecutionTime(), new Date()));
+ };
+
+ private boolean isSameDay(Date dateOne, Date dateTwo) {
+
+ Calendar cal1 = Calendar.getInstance();
+ cal1.setTime(dateOne);
+
+ Calendar cal2 = Calendar.getInstance();
+ cal2.setTime(dateTwo);
+
+ return cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH)
+ && cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH)
+ && cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR);
+ }
+
+public MailSendingStatus createAndSendEncounter(
+ EncounterScheduled scheduled
+) {
+
+ Encounter encounter = createEncounterForImmediateSending(scheduled);
+ bundleDao.merge(scheduled.getBundle());
+
+ return mailService.sendEncounterMail(encounter);
+}
+
+ private Encounter createEncounterForImmediateSending(
+ EncounterScheduled scheduled
+ ) {
+
+ Encounter encounter = new Encounter();
+
+ encounter.setEncounterScheduled(scheduled);
+ encounter.setBundle(scheduled.getBundle());
+ encounter.setClinic(scheduled.getClinic());
+ encounter.setCaseNumber(scheduled.getCaseNumber());
+
+ Bundle bundle = scheduled.getBundle();
+ bundle.addEncounter(encounter);
+
+
+ encounter.setStartTime(
+ new Timestamp(getTodayAtMidnight().getTime())
+ );
+
+ return encounter;
+ }
+
+ private Date getTodayAtMidnight() {
+
+ Calendar calendar = Calendar.getInstance();
+
+ calendar.set(Calendar.MILLISECOND, 0);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+
+ return calendar.getTime();
+ }
+}
diff --git a/src/main/java/de/imi/mopat/helper/controller/InvitationService.java b/src/main/java/de/imi/mopat/service/InvitationService.java
similarity index 93%
rename from src/main/java/de/imi/mopat/helper/controller/InvitationService.java
rename to src/main/java/de/imi/mopat/service/InvitationService.java
index b0bcc576..fdcbb70a 100644
--- a/src/main/java/de/imi/mopat/helper/controller/InvitationService.java
+++ b/src/main/java/de/imi/mopat/service/InvitationService.java
@@ -1,9 +1,8 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import de.imi.mopat.model.user.Invitation;
import java.util.Comparator;
import java.util.List;
-import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/src/main/java/de/imi/mopat/service/MailSendingStatus.java b/src/main/java/de/imi/mopat/service/MailSendingStatus.java
new file mode 100644
index 00000000..ff3d90c8
--- /dev/null
+++ b/src/main/java/de/imi/mopat/service/MailSendingStatus.java
@@ -0,0 +1,7 @@
+package de.imi.mopat.service;
+
+public enum MailSendingStatus {
+ SUCCESS,
+ INVALID_ADDRESS,
+ FAILURE
+}
diff --git a/src/main/java/de/imi/mopat/service/MailService.java b/src/main/java/de/imi/mopat/service/MailService.java
new file mode 100644
index 00000000..98148d14
--- /dev/null
+++ b/src/main/java/de/imi/mopat/service/MailService.java
@@ -0,0 +1,38 @@
+package de.imi.mopat.service;
+
+import de.imi.mopat.dao.ConfigurationDao;
+import de.imi.mopat.helper.controller.ApplicationMailer;
+import de.imi.mopat.model.Encounter;
+import de.imi.mopat.model.enumeration.EncounterScheduledMailStatus;
+import org.springframework.context.MessageSource;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MailService {
+
+ private final ApplicationMailer applicationMailer;
+ private final MessageSource messageSource;
+ private final ConfigurationDao configurationDao;
+
+ public MailService(ApplicationMailer applicationMailer, MessageSource messageSource,
+ ConfigurationService configurationService, ConfigurationDao configurationDao) {
+ this.applicationMailer = applicationMailer;
+ this.messageSource = messageSource;
+ this.configurationDao = configurationDao;
+ }
+
+ public MailSendingStatus sendEncounterMail(Encounter encounter) {
+ boolean success = encounter.sendMail(applicationMailer, messageSource,
+ configurationDao.getBaseURL());
+ if (success) {
+ return MailSendingStatus.SUCCESS;
+ }
+ if (EncounterScheduledMailStatus.ADDRESS_REJECTED.equals(
+ encounter.getEncounterScheduled().getMailStatus())) {
+ return MailSendingStatus.INVALID_ADDRESS;
+ } else {
+ return MailSendingStatus.FAILURE;
+ }
+ }
+
+}
diff --git a/src/main/java/de/imi/mopat/helper/controller/QuestionnaireVersionGroupService.java b/src/main/java/de/imi/mopat/service/QuestionnaireVersionGroupService.java
similarity index 99%
rename from src/main/java/de/imi/mopat/helper/controller/QuestionnaireVersionGroupService.java
rename to src/main/java/de/imi/mopat/service/QuestionnaireVersionGroupService.java
index 97f09d2c..609e6212 100644
--- a/src/main/java/de/imi/mopat/helper/controller/QuestionnaireVersionGroupService.java
+++ b/src/main/java/de/imi/mopat/service/QuestionnaireVersionGroupService.java
@@ -1,4 +1,4 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import de.imi.mopat.dao.QuestionnaireDao;
import de.imi.mopat.dao.QuestionnaireVersionGroupDao;
diff --git a/src/main/java/de/imi/mopat/helper/controller/UserService.java b/src/main/java/de/imi/mopat/service/UserService.java
similarity index 99%
rename from src/main/java/de/imi/mopat/helper/controller/UserService.java
rename to src/main/java/de/imi/mopat/service/UserService.java
index 00f413b3..b1c28091 100644
--- a/src/main/java/de/imi/mopat/helper/controller/UserService.java
+++ b/src/main/java/de/imi/mopat/service/UserService.java
@@ -1,4 +1,4 @@
-package de.imi.mopat.helper.controller;
+package de.imi.mopat.service;
import de.imi.mopat.dao.ClinicDao;
import de.imi.mopat.dao.user.AclEntryDao;
diff --git a/src/main/java/de/imi/mopat/service/helper/SetRepeatConfiguration.java b/src/main/java/de/imi/mopat/service/helper/SetRepeatConfiguration.java
new file mode 100644
index 00000000..ab27ac0b
--- /dev/null
+++ b/src/main/java/de/imi/mopat/service/helper/SetRepeatConfiguration.java
@@ -0,0 +1,32 @@
+package de.imi.mopat.service.helper;
+
+import de.imi.mopat.model.dto.EncounterScheduledDTO;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SetRepeatConfiguration {
+
+ // in case of REPEATEDLY, RepeatPeriod is already set in dto
+ public void apply(EncounterScheduledDTO dto) {
+ int WEEKLY_PERIOD_DAYS = 7;
+ int MONTHLY_PERIOD_DAYS = 30;
+ switch (dto.getEncounterScheduledSerialType()) {
+ case UNIQUELY -> {
+ dto.setRepeatPeriod(null);
+ dto.setEndDate(null);
+ break;
+ }
+ case WEEKLY -> {
+ dto.setRepeatPeriod(WEEKLY_PERIOD_DAYS);
+ break;
+ }
+ case MONTHLY -> {
+ dto.setRepeatPeriod(MONTHLY_PERIOD_DAYS);
+ break;
+ }
+ default -> {
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/imi/mopat/validator/ConfigurationDTOValidator.java b/src/main/java/de/imi/mopat/validator/ConfigurationDTOValidator.java
index dac0fa20..303dbb46 100644
--- a/src/main/java/de/imi/mopat/validator/ConfigurationDTOValidator.java
+++ b/src/main/java/de/imi/mopat/validator/ConfigurationDTOValidator.java
@@ -100,6 +100,14 @@ public void validate(final Object o, final Errors errors) {
}
}
break;
+ case API_KEY:
+ String apiKeyValue = configurationDTO.getValue();
+ String apiKeyPattern = "^[A-Za-z0-9_\\-.]{32,128}$";
+ if (apiKeyValue == null || !apiKeyValue.matches(apiKeyPattern)) {
+ message = messageSource.getMessage("configuration.validate" + ".apiKey",
+ new Object[]{}, LocaleContextHolder.getLocale());
+ }
+ break;
default:
break;
}
diff --git a/src/main/java/de/imi/mopat/validator/EncounterScheduledApiRequestDTOValidator.java b/src/main/java/de/imi/mopat/validator/EncounterScheduledApiRequestDTOValidator.java
new file mode 100644
index 00000000..ad63b386
--- /dev/null
+++ b/src/main/java/de/imi/mopat/validator/EncounterScheduledApiRequestDTOValidator.java
@@ -0,0 +1,107 @@
+package de.imi.mopat.validator;
+
+import de.imi.mopat.model.EncounterScheduled;
+import de.imi.mopat.model.dto.EncounterScheduledApiRequestDTO;
+import de.imi.mopat.model.dto.EncounterScheduledDTO;
+import java.util.Calendar;
+import java.util.Date;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.Errors;
+import org.springframework.validation.Validator;
+
+/**
+ * The validator for {@link de.imi.mopat.model.dto.EncounterScheduledApiRequestDTO} objects.
+ */
+@Component
+public class EncounterScheduledApiRequestDTOValidator implements Validator {
+
+ @Autowired
+ private MessageSource messageSource;
+
+ @Override
+ public boolean supports(final Class> type) {
+ return EncounterScheduledApiRequestDTO.class.isAssignableFrom(type);
+ }
+
+ @Override
+ public void validate(Object target, Errors errors) {
+
+ EncounterScheduledApiRequestDTO dto = (EncounterScheduledApiRequestDTO) target;
+
+ if (dto.getCaseNumber() == null) {
+ errors.rejectValue("caseNumber", "encounterScheduledApi.caseNumber.required",
+ messageSource.getMessage("encounterScheduledApi.validate.caseNumber.required",
+ new Object[]{}, LocaleContextHolder.getLocale()));
+ }
+ if (dto.getBundleId() == null) {
+ errors.rejectValue("bundleId", "encounterScheduledApi.bundleId.required",
+ messageSource.getMessage("encounterScheduledApi.validate.bundleId.required",
+ new Object[]{}, LocaleContextHolder.getLocale()));
+ }
+ if (dto.getClinicId() == null) {
+ errors.rejectValue("clinicId", "encounterScheduledApi.clinicId.required",
+ messageSource.getMessage("encounterScheduledApi.validate.clinicId.required",
+ new Object[]{}, LocaleContextHolder.getLocale()));
+ }
+ if (dto.getEmail() != null && !dto.getEmail().matches(
+ "[A-Za-z0-9.!#$%&'*+\\-/=?^_`{|}~]+@[A-Za-z0-9.!#$%&'*+\\-/=?^_`{|}~]+\\.[A-Za-z]{2,}")) {
+ errors.rejectValue("email", "encounterScheduledApi.invalidEmail",
+ messageSource.getMessage("encounterScheduledApi.validate.invalidEmail",
+ new Object[]{}, LocaleContextHolder.getLocale()));
+ }
+ if (dto.getStartDate() == null) {
+ errors.rejectValue("startDate", "encounterScheduledApi.startDate.required",
+ messageSource.getMessage("encounterScheduledApi.validate.startDate.required",
+ new Object[]{}, LocaleContextHolder.getLocale()));
+ }
+ if (dto.getEncounterScheduledSerialType() == null) {
+ errors.rejectValue("encounterScheduledSerialType", "encounterScheduledApi.serialType.required",
+ messageSource.getMessage("encounterScheduledApi.validate.serialType.required",
+ new Object[]{}, LocaleContextHolder.getLocale()));
+ }
+ if (dto.getLocale() == null) {
+ errors.rejectValue("locale", "encounterScheduledApi.locale.required",
+ messageSource.getMessage("encounterScheduledApi.validate.locale.required",
+ new Object[]{}, LocaleContextHolder.getLocale()));
+ }
+
+ Date now = new Date();
+ // Generate date today at midnight
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(now);
+ calendar.set(Calendar.MILLISECOND, 0);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+ Date today = calendar.getTime();
+
+ if (dto.getStartDate() != null
+ && dto.getStartDate().getTime() < today.getTime()) {
+ errors.rejectValue("startDate", MoPatValidator.ERRORCODE_ERRORMESSAGE,
+ messageSource.getMessage(
+ "encounterScheduledApi.validate.startEndDate.CanNotBeInThePast", new Object[]{},
+ LocaleContextHolder.getLocale()));
+ }
+
+ if (dto.getEndDate() != null
+ && dto.getEndDate().getTime() < today.getTime()) {
+ errors.rejectValue("endDate", MoPatValidator.ERRORCODE_ERRORMESSAGE,
+ messageSource.getMessage(
+ "encounterScheduledApi.validate.startEndDate.CanNotBeInThePast", new Object[]{},
+ LocaleContextHolder.getLocale()));
+ }
+
+ if (dto.getEndDate() != null && dto.getStartDate() !=null
+ && dto.getEndDate().getTime() < dto.getStartDate().getTime()) {
+ errors.rejectValue("endDate", MoPatValidator.ERRORCODE_ERRORMESSAGE,
+ messageSource.getMessage(
+ "encounterScheduledApi.validate.startEndDate.enddateBiggerThanStartdate", new Object[]{},
+ LocaleContextHolder.getLocale()));
+ }
+ }
+
+
+}
diff --git a/src/main/resources/message/messages.properties b/src/main/resources/message/messages.properties
index ae8b09f8..f4ab212c 100644
--- a/src/main/resources/message/messages.properties
+++ b/src/main/resources/message/messages.properties
@@ -265,7 +265,9 @@ condition.warning.changeTrigger=If you change the condition trigger, any newly c
configuration.alert.uploadImagePath=WARNING: If you change the path, the previously saved images can no longer be used by MoPat. They are still saved in the old location and can be copied manually to the new directory to make them accessible again.
configuration.button.add=Add
configuration.button.remove=Remove
+configuration.description.apiKey=Defines the API key required to access the encounter API.
configuration.description.baseUrl=This is the base URL including the context path of the application.
+configuration.description.enableApiTokenAccess=This enables access to the encounter API via API key. Anyone in possession of the configured API key can create new encounters through the API. Enable this feature only if external access is explicitly intended.
configuration.description.exportPath=This is the path completed questionnaires will be exported to. The path has to be a directory, so it has to ends with a slash.
configuration.description.finishedEncounterMailaddressTimeWindowInMillis=Any email address of a finished encounter older than this timeframe will be deleted (default: 30 days). The value -1 deactivates the deletion.
configuration.description.finishedEncounterScheduledTimeWindowInMillis=Any scheduled encounter finished and older than this timeframe will be deleted (default: 90 days). The value -1 deactivates the deletion.
@@ -282,6 +284,8 @@ configuration.error.wrongValidationSchemaType=The file format doesn't accord wit
configuration.file.notUploaded=No file uploaded
configuration.file.path=Path to the uploaded file
configuration.file.uploaded=File uploaded
+configuration.label.apiKey=API-Key for scheduled encounters
+configuration.label.enableApiTokenAccess=Enablement of API-Key for scheduled encounters
configuration.label.FHIRsystemURI=System URI for FHIR export
configuration.label.HL7v22PatientInformationRetrieverHostname=Host for the HL7v22PatientInformationRetriever
configuration.label.HL7v22PatientInformationRetrieverPort=Port for the HL7v22PatientInformationRetriever
@@ -366,6 +370,7 @@ configuration.label.usePatientDataLookup=Look up patient data of a connected hos
configuration.label.validationSchema=Path for schema base validation
configuration.label.webappRootPath=Path for the main directory of the application. Please enter the absolute path (not the relative path).
configuration.success.changed=Configuration changed successfully.
+configuration.validate.apiKey=The value of the field is no valid API-key.
configuration.validate.boolean=The value of the field {field} has to either be 'true' or 'false'.
configuration.validate.double=The value of the field {field} is not decimal.
configuration.validate.integer=The value of the field {field} has to be between 0 and 2E31-1.
@@ -475,6 +480,14 @@ encounterScheduled.validator.repeatPeriodGreaterThanZero=Number of days must be
encounterScheduled.validator.startDateEmpty=The start date can not be empty
encounterScheduled.validator.startdateCanNotBeInThePast=Startdate can not be in the past
encounterScheduled.warning.daysShorterThanThePeriod=The given repeat period is larger than the period between start- and enddate. Thus, only one survey will be scheduled on the startdate.
+encounterScheduledApi.validate.bundleId.required= Bundle-ID can not be empty
+encounterScheduledApi.validate.caseNumber.required= Case Number can not be empty
+encounterScheduledApi.validate.clinicId.required=Clinic-ID can not be empty
+encounterScheduledApi.validate.invalidEmail=Illegal e-mail address
+encounterScheduledApi.validate.serialType.required= Serial Type can not be empty
+encounterScheduledApi.validate.startEndDate.CanNotBeInThePast=Startdate and enddate can not be in the past
+encounterScheduledApi.validate.startEndDate.enddateBiggerThanStartdate=Startdate must be greater than enddate
+encounterScheduledApi.validate.locale.required=Locale can not be null
encountermanager.welcome=Welcome to Mobile Patient Survey (MoPat)!
This is the administration interface of MoPat.
From this interface on, you are able toMoPat is developed by the Institute of Medical Informatics, M\u00fcnster, managed by Univ.-Prof. Dr. rer. nat. Dominik Heider.
You can contact us at {1} or {2}.
error.heading.denied=Access denied!
error.heading.internalservererror=Oops! There was something wrong. The support team has been informed.
diff --git a/src/main/resources/message/messages_de_DE.properties b/src/main/resources/message/messages_de_DE.properties
index 6274aaea..1b6631f0 100644
--- a/src/main/resources/message/messages_de_DE.properties
+++ b/src/main/resources/message/messages_de_DE.properties
@@ -1158,4 +1158,17 @@ value=Wert
valueOf=Wert der Frage
valueOfScore=Wert des Scores
sliderAnswer.validator.localizedMinMaxText=Der eingegebene Text hat mehr als 255 Zeichen.
-login.demo.infoText=Willkommen zu MoPat Demo!
Dies ist eine Testinstallation mit uneingeschr\u00e4nktem Zugriff zur Anwendung. Sie k\u00f6nnen hier alle Funktionen von MoPat ausprobieren. Ben\u00f6tigen Sie Hilfe, k\u00f6nnen Sie unser Handbuch zurate ziehen, in welchem sich ausf\u00fchrliche Informationen zu allen Funktionen finden lassen.
Achtung: Die Daten werden regelm\u00e4\u00dfig zur\u00fcckgesetzt. Von Ihnen angelegte Frageb\u00f6gen und Befragungen werden demnach gel\u00f6scht und es gibt keine M\u00f6glichkeit diese wiederherzustellen. Nutzen Sie dieses System ausschlie\u00dflich f\u00fcr Tests.
Ben\u00f6tigen Sie weitere Informationen, kontaktieren Sie uns: mopat@uni-muenster.de
\ No newline at end of file
+login.demo.infoText=Willkommen zu MoPat Demo!
Dies ist eine Testinstallation mit uneingeschr\u00e4nktem Zugriff zur Anwendung. Sie k\u00f6nnen hier alle Funktionen von MoPat ausprobieren. Ben\u00f6tigen Sie Hilfe, k\u00f6nnen Sie unser Handbuch zurate ziehen, in welchem sich ausf\u00fchrliche Informationen zu allen Funktionen finden lassen.
Achtung: Die Daten werden regelm\u00e4\u00dfig zur\u00fcckgesetzt. Von Ihnen angelegte Frageb\u00f6gen und Befragungen werden demnach gel\u00f6scht und es gibt keine M\u00f6glichkeit diese wiederherzustellen. Nutzen Sie dieses System ausschlie\u00dflich f\u00fcr Tests.
Ben\u00f6tigen Sie weitere Informationen, kontaktieren Sie uns: mopat@uni-muenster.de
+configuration.label.apiKey=API-Key f\u00fcr geplante Befragungen
+configuration.label.enableApiTokenAccess=Automatisierte geplante Befragungen unter Anwendung eines API-Keys erlauben
+configuration.validate.apiKey=Der eingegebene Wert ist kein gültiger API-Key.
+encounterScheduledApi.validate.bundleId.required=Bundle-ID farf nicht leer sein
+encounterScheduledApi.validate.caseNumber.required=Fallnummer darf nicht leer sein
+encounterScheduledApi.validate.clinicId.required=Clinic-ID darf nicht leer sein
+encounterScheduledApi.validate.invalidEmail=E-Mail Adresse ung\u00fcltig
+encounterScheduledApi.validate.serialType.required=SerialType darf nicht leer sein
+encounterScheduledApi.validate.startEndDate.CanNotBeInThePast=Das Start- und Enddatum darf nicht in der Vergangenheit liegen
+encounterScheduledApi.validate.startEndDate.enddateBiggerThanStartdate=Endzeitpunkt darf nicht kleiner als Startzeitpunkt sein
+configuration.description.enableApiTokenAccess=Aktiviert den Zugriff auf die Encounter-API per API-Key. Jede Person mit Kenntnis des API-Keys kann \u00fcber die API neue Encounters anlegen. Sollte nur aktiviert werden, wenn ein externer Zugriff ausdr\u00fccklich gew\u00fcnscht ist.
+configuration.description.apiKey=Definiert den API-Key, der f\u00fcr r den Encounter-Zugriff per API vorausgesetzt wird.
+encounterScheduledApi.validate.locale.required=Locale darf nicht null sein
\ No newline at end of file
diff --git a/src/main/resources/message/messages_en_GB.properties b/src/main/resources/message/messages_en_GB.properties
index ae8b09f8..2049cbb0 100644
--- a/src/main/resources/message/messages_en_GB.properties
+++ b/src/main/resources/message/messages_en_GB.properties
@@ -1155,4 +1155,17 @@ configuration.label.FHIRViaHL7v2Port=Port of the HL7 communication server for FH
configuration.label.FHIRViaHL7v2SendingFacility=
configuration.description.exportFHIRViaHL7v2=For FHIR, export can be set up using an HL7v2 communication server. In this case, the FHIR resource is embedded as a blob in the HL7 message.
encounter.error.caseNumberInvalid=The case number was invalid. Please try again!
-survey.questionnaire.button.completeQuestionnaireInBundle=Complete the survey section
\ No newline at end of file
+survey.questionnaire.button.completeQuestionnaireInBundle=Complete the survey section
+configuration.label.apiKey=API-Key for automated scheduled encounters
+configuration.label.enableApiTokenAccess=Enablement of API-Key for automated scheduled encounters
+configuration.validate.apiKey=The value of the field is no valid API-key.
+encounterScheduledApi.validate.bundleId.required=Bundle-ID can not be empty
+encounterScheduledApi.validate.caseNumber.required=Case Number can not be empty
+encounterScheduledApi.validate.clinicId.required=Clinic-ID can not be empty
+encounterScheduledApi.validate.invalidEmail=Illegal e-mail address
+encounterScheduledApi.validate.serialType.required=Serial Type can not be empty
+encounterScheduledApi.validate.startEndDate.CanNotBeInThePast=Startdate and Enddate can not be in the past
+encounterScheduledApi.validate.startEndDate.enddateBiggerThanStartdate=Startdate must be greater than enddate
+configuration.description.enableApiTokenAccess=This enables access to the encounter API via API key. Anyone in possession of the configured API key can create new encounters through the API. Enable this feature only if external access is explicitly intended.
+configuration.description.apiKey=Defines the API key required to access the encounter API.
+encounterScheduledApi.validate.locale.required=Locale can not be null
\ No newline at end of file
diff --git a/src/test/java/de/imi/mopat/controller/EncounterControllerTest.java b/src/test/java/de/imi/mopat/controller/EncounterControllerTest.java
new file mode 100644
index 00000000..64f33da6
--- /dev/null
+++ b/src/test/java/de/imi/mopat/controller/EncounterControllerTest.java
@@ -0,0 +1,134 @@
+package de.imi.mopat.controller;
+
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import de.imi.mopat.dao.ConfigurationDao;
+import de.imi.mopat.helper.model.EncounterScheduledDTOMapper;
+import de.imi.mopat.model.dto.EncounterScheduledApiRequestDTO;
+import de.imi.mopat.model.dto.EncounterScheduledDTO;
+import de.imi.mopat.service.EncounterScheduledService;
+import de.imi.mopat.service.MailSendingStatus;
+import de.imi.mopat.validator.EncounterScheduledApiRequestDTOValidator;
+
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.validation.BeanPropertyBindingResult;
+import org.springframework.validation.BindingResult;
+
+public class EncounterControllerTest {
+
+ private static final String CONFIGURED_API_KEY = "secret-key";
+
+ private ConfigurationDao configurationDao;
+ private EncounterScheduledDTOMapper encounterScheduledDTOMapper;
+ private EncounterScheduledService encounterScheduledService;
+ private BindingResult result;
+ private EncounterController encounterController;
+
+ private EncounterScheduledApiRequestDTO requestDto;
+
+ @Before
+ public void setUp() {
+ configurationDao = mock(ConfigurationDao.class);
+ encounterScheduledDTOMapper = mock(EncounterScheduledDTOMapper.class);
+ encounterScheduledService = mock(EncounterScheduledService.class);
+ EncounterScheduledApiRequestDTOValidator encounterScheduledApiRequestDTOValidator = mock(
+ EncounterScheduledApiRequestDTOValidator.class);
+
+ requestDto = new EncounterScheduledApiRequestDTO();
+ requestDto.setEmail("test@example.com");
+ result = new BeanPropertyBindingResult(requestDto, "encounterScheduledApiRequestDTO");
+
+ encounterController = new EncounterController();
+ ReflectionTestUtils.setField(encounterController, "configurationDao", configurationDao);
+ ReflectionTestUtils.setField(encounterController, "encounterScheduledDTOMapper",
+ encounterScheduledDTOMapper);
+ ReflectionTestUtils.setField(encounterController, "encounterSchedulingService",
+ encounterScheduledService);
+ ReflectionTestUtils.setField(encounterController, "encounterScheduledApiRequestDTOValidator",
+ encounterScheduledApiRequestDTOValidator);
+
+ requestDto = new EncounterScheduledApiRequestDTO();
+ requestDto.setEmail("test@example.com");
+ }
+
+ /**
+ * isApiKeyAccessEnabled() == true && apiKey.equals(key) == true
+ * -> Access granted, encounter is created successfully
+ */
+ @Test
+ public void accessEnabledAndKeyMatches_returnsOk() {
+ when(configurationDao.isApiKeyAccessEnabled()).thenReturn(true);
+ when(configurationDao.getApiKey()).thenReturn(CONFIGURED_API_KEY);
+ when(encounterScheduledDTOMapper.mapFromApiRequest(any()))
+ .thenReturn(new EncounterScheduledDTO());
+ when(encounterScheduledService.save(any(), any()))
+ .thenReturn(MailSendingStatus.SUCCESS);
+
+ ResponseEntity> response = encounterController.scheduleEncounterFromApi(requestDto, result,
+ CONFIGURED_API_KEY);
+
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertEquals(Map.of("message", "Scheduled encounter created successfully"),
+ response.getBody());
+ }
+
+ /**
+ * isApiKeyAccessEnabled() == true && apiKey.equals(key) == false
+ * -> Access denied, wrong key provided
+ */
+ @Test
+ public void accessEnabledAndKeyDoesNotMatch_returnsUnauthorized() {
+ when(configurationDao.isApiKeyAccessEnabled()).thenReturn(true);
+ when(configurationDao.getApiKey()).thenReturn(CONFIGURED_API_KEY);
+
+ ResponseEntity> response = encounterController.scheduleEncounterFromApi(requestDto, result,
+ "wrong-key");
+
+ assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
+ assertEquals(Map.of("error", "Unauthorized"), response.getBody());
+ }
+
+ /**
+ * isApiKeyAccessEnabled() == false && apiKey.equals(key) == true
+ * -> Access denied because the feature is disabled, even though the key would match
+ */
+ @Test
+ public void accessDisabledAndKeyWouldMatch_returnsUnauthorized() {
+ when(configurationDao.isApiKeyAccessEnabled()).thenReturn(false);
+ when(configurationDao.getApiKey()).thenReturn(CONFIGURED_API_KEY);
+
+ ResponseEntity> response = encounterController.scheduleEncounterFromApi(requestDto, result,
+ CONFIGURED_API_KEY);
+
+ assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
+ assertEquals(Map.of("error", "Unauthorized"), response.getBody());
+ }
+
+ /**
+ * isApiKeyAccessEnabled() == true && apiKey.equals(null) == false
+ * -> No x-api-key header sent (key == null), access denied.
+ * Assumes the condition is implemented as "apiKey.equals(key)"
+ * (instead of "key.equals(apiKey)"), since the latter would throw
+ * a NullPointerException.
+ */
+ @Test
+ public void accessEnabledAndNoApiKeyHeaderSent_returnsUnauthorized() {
+ when(configurationDao.isApiKeyAccessEnabled()).thenReturn(true);
+ when(configurationDao.getApiKey()).thenReturn(CONFIGURED_API_KEY);
+
+ ResponseEntity> response = encounterController.scheduleEncounterFromApi(requestDto, result,
+ null);
+
+ assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
+ assertEquals(Map.of("error", "Unauthorized"), response.getBody());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/imi/mopat/helper/controller/AuthServiceTest.java b/src/test/java/de/imi/mopat/helper/controller/AuthServiceTest.java
index 9d32b959..8ac6b66c 100644
--- a/src/test/java/de/imi/mopat/helper/controller/AuthServiceTest.java
+++ b/src/test/java/de/imi/mopat/helper/controller/AuthServiceTest.java
@@ -4,6 +4,7 @@
import de.imi.mopat.model.user.User;
import de.imi.mopat.model.user.UserRole;
import de.imi.mopat.model.user.UserTest;
+import de.imi.mopat.service.AuthService;
import de.imi.mopat.utils.Helper;
import org.junit.Before;
import org.junit.Test;
diff --git a/src/test/java/de/imi/mopat/helper/controller/QuestionServiceTest.java b/src/test/java/de/imi/mopat/helper/controller/QuestionServiceTest.java
index d6de6814..dc73ed18 100644
--- a/src/test/java/de/imi/mopat/helper/controller/QuestionServiceTest.java
+++ b/src/test/java/de/imi/mopat/helper/controller/QuestionServiceTest.java
@@ -14,6 +14,7 @@
import de.imi.mopat.model.conditions.Condition;
import de.imi.mopat.model.conditions.ConditionTest;
import java.util.Random;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/src/test/java/de/imi/mopat/helper/controller/QuestionnaireServiceTest.java b/src/test/java/de/imi/mopat/helper/controller/QuestionnaireServiceTest.java
index 99b80d03..2dbd93f7 100644
--- a/src/test/java/de/imi/mopat/helper/controller/QuestionnaireServiceTest.java
+++ b/src/test/java/de/imi/mopat/helper/controller/QuestionnaireServiceTest.java
@@ -15,6 +15,7 @@
import de.imi.mopat.utils.MultipartFileUtils;
import de.imi.mopat.validator.LogoValidator;
import de.imi.mopat.validator.QuestionnaireDTOValidator;
+import de.imi.mopat.service.*;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -38,6 +39,7 @@
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
+
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
diff --git a/src/test/java/de/imi/mopat/helper/controller/QuestionnaireVersionGroupServiceTest.java b/src/test/java/de/imi/mopat/helper/controller/QuestionnaireVersionGroupServiceTest.java
index 74cd766f..b8af4068 100644
--- a/src/test/java/de/imi/mopat/helper/controller/QuestionnaireVersionGroupServiceTest.java
+++ b/src/test/java/de/imi/mopat/helper/controller/QuestionnaireVersionGroupServiceTest.java
@@ -13,6 +13,7 @@
import de.imi.mopat.model.QuestionnaireTest;
import de.imi.mopat.model.dto.QuestionnaireDTO;
import de.imi.mopat.model.dto.QuestionnaireVersionGroupDTO;
+import de.imi.mopat.service.QuestionnaireVersionGroupService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/src/test/java/de/imi/mopat/io/FhirImporterTest.java b/src/test/java/de/imi/mopat/io/FhirImporterTest.java
index 2143cb46..33a51443 100644
--- a/src/test/java/de/imi/mopat/io/FhirImporterTest.java
+++ b/src/test/java/de/imi/mopat/io/FhirImporterTest.java
@@ -18,7 +18,7 @@
import de.imi.mopat.dao.QuestionDao;
import de.imi.mopat.dao.QuestionnaireDao;
import de.imi.mopat.dao.ScoreDao;
-import de.imi.mopat.helper.controller.QuestionnaireVersionGroupService;
+import de.imi.mopat.service.QuestionnaireVersionGroupService;
import de.imi.mopat.io.impl.MetadataExporterFhirDstu3;
import de.imi.mopat.io.impl.MetadataExporterFhirR4b;
import de.imi.mopat.io.impl.MetadataExporterFhirR5;
diff --git a/src/test/java/de/imi/mopat/model/BundleTest.java b/src/test/java/de/imi/mopat/model/BundleTest.java
index bd3148b4..7c3c4513 100644
--- a/src/test/java/de/imi/mopat/model/BundleTest.java
+++ b/src/test/java/de/imi/mopat/model/BundleTest.java
@@ -11,7 +11,7 @@
import de.imi.mopat.config.ApplicationSecurityConfig;
import de.imi.mopat.config.MvcWebApplicationInitializer;
import de.imi.mopat.config.PersistenceConfig;
-import de.imi.mopat.helper.controller.BundleService;
+import de.imi.mopat.service.BundleService;
import de.imi.mopat.helper.model.BundleDTOMapper;
import de.imi.mopat.model.dto.BundleDTO;
import de.imi.mopat.utils.Helper;
diff --git a/src/test/java/de/imi/mopat/service/AuditServiceTest.java b/src/test/java/de/imi/mopat/service/AuditServiceTest.java
new file mode 100644
index 00000000..397d54fa
--- /dev/null
+++ b/src/test/java/de/imi/mopat/service/AuditServiceTest.java
@@ -0,0 +1,93 @@
+package de.imi.mopat.service;
+
+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 de.imi.mopat.dao.AuditEntryDao;
+import de.imi.mopat.model.EncounterScheduled;
+import de.imi.mopat.model.enumeration.AuditEntryActionType;
+import de.imi.mopat.model.enumeration.AuditPatientAttribute;
+import java.util.EnumSet;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AuditServiceTest {
+
+ @Mock
+ private AuditEntryDao auditEntryDao;
+
+ @InjectMocks
+ private AuditService auditService;
+
+ @Mock
+ private EncounterScheduled encounterScheduled;
+
+ private static final Set EXPECTED_ATTRIBUTES = EnumSet.of(
+ AuditPatientAttribute.CASE_NUMBER,
+ AuditPatientAttribute.EMAIL_ADDRESS,
+ AuditPatientAttribute.FIRST_NAME,
+ AuditPatientAttribute.LAST_NAME,
+ AuditPatientAttribute.DATE_OF_BIRTH
+ );
+
+ /**
+ * Test of {@link AuditService#writeScheduledEncounterAudit(Class, String, EncounterScheduled)}
+ */
+
+ @Test
+ public void testWriteScheduledEncounterAuditWritesCorrectAuditEntry() {
+ String caseNumber = "TEST-CASE-123";
+ when(encounterScheduled.getCaseNumber()).thenReturn(caseNumber);
+
+ auditService.writeScheduledEncounterAudit(
+ EncounterScheduledService.class, "save", encounterScheduled);
+
+ verify(auditEntryDao).writeAuditEntry(
+ EncounterScheduledService.class.getSimpleName(),
+ "save",
+ caseNumber,
+ EXPECTED_ATTRIBUTES,
+ AuditEntryActionType.WRITE
+ );
+ }
+
+ @Test
+ public void testWriteScheduledEncounterAuditUsesSimpleNameOfSourceClass() {
+ String caseNumber = "CASE-456";
+ when(encounterScheduled.getCaseNumber()).thenReturn(caseNumber);
+
+ auditService.writeScheduledEncounterAudit(
+ AuditService.class, "testMethod", encounterScheduled);
+
+ verify(auditEntryDao).writeAuditEntry(
+ "AuditService",
+ "testMethod",
+ caseNumber,
+ EXPECTED_ATTRIBUTES,
+ AuditEntryActionType.WRITE
+ );
+ }
+
+ @Test
+ public void testWriteScheduledEncounterAuditAlwaysUsesActionTypeWrite() {
+ when(encounterScheduled.getCaseNumber()).thenReturn("ANY");
+
+ auditService.writeScheduledEncounterAudit(
+ AuditService.class, "anyMethod", encounterScheduled);
+
+ verify(auditEntryDao).writeAuditEntry(
+ any(),
+ any(),
+ any(),
+ any(),
+ eq(AuditEntryActionType.WRITE)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/imi/mopat/service/EncounterScheduledServiceTest.java b/src/test/java/de/imi/mopat/service/EncounterScheduledServiceTest.java
new file mode 100644
index 00000000..0053bb38
--- /dev/null
+++ b/src/test/java/de/imi/mopat/service/EncounterScheduledServiceTest.java
@@ -0,0 +1,166 @@
+package de.imi.mopat.service;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import de.imi.mopat.cron.EncounterScheduledExecutor;
+import de.imi.mopat.dao.BundleDao;
+import de.imi.mopat.dao.EncounterScheduledDao;
+import de.imi.mopat.helper.model.EncounterScheduledDTOMapper;
+import de.imi.mopat.model.Bundle;
+import de.imi.mopat.model.EncounterScheduled;
+import de.imi.mopat.model.dto.EncounterScheduledDTO;
+import de.imi.mopat.service.helper.SetRepeatConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class EncounterScheduledServiceTest {
+
+ @Mock
+ private BundleDao bundleDao;
+
+ @Mock
+ private EncounterScheduledDao encounterScheduledDao;
+
+ @Mock
+ private EncounterScheduledDTOMapper mapper;
+
+ @Mock
+ private ImmediateMailService immediateMailService;
+
+ @Mock
+ private AuditService auditService;
+
+ @Mock
+ private SetRepeatConfiguration setRepeatConfiguration;
+
+ @InjectMocks
+ private EncounterScheduledService encounterScheduledService;
+
+ @Mock
+ private EncounterScheduledDTO dto;
+
+ @Mock
+ private EncounterScheduled scheduled;
+
+ @Mock
+ private Bundle bundle;
+
+ @Mock
+ private EncounterScheduledExecutor executor;
+
+ @Before
+ public void setUp() {
+ when(mapper.mapToEntity(dto)).thenReturn(scheduled);
+ when(scheduled.getBundle()).thenReturn(bundle);
+ }
+
+ /**
+ * Test of {@link EncounterScheduledService#save(EncounterScheduledDTO, EncounterScheduledExecutor)} )},
+ */
+
+ @Test
+ public void testSaveWhenShouldSendImmediatelyAndMailSucceeds() {
+ when(immediateMailService.shouldSendEmailImmediately(scheduled, executor)).thenReturn(true);
+ when(immediateMailService.createAndSendEncounter(scheduled)).thenReturn(MailSendingStatus.SUCCESS);
+
+ MailSendingStatus result = encounterScheduledService.save(dto, executor);
+
+ assertEquals("should return SUCCESS", MailSendingStatus.SUCCESS, result);
+ }
+
+ @Test
+ public void testSaveWhenShouldSendImmediatelyAndAddressInvalid() {
+ when(immediateMailService.shouldSendEmailImmediately(scheduled, executor)).thenReturn(true);
+ when(immediateMailService.createAndSendEncounter(scheduled)).thenReturn(MailSendingStatus.INVALID_ADDRESS);
+
+ MailSendingStatus result = encounterScheduledService.save(dto, executor);
+
+ assertEquals("Should return INVALID_ADDRESS", MailSendingStatus.INVALID_ADDRESS, result);
+ }
+
+ @Test
+ public void testSaveWhenShouldSendImmediatelyAndMailFails() {
+ when(immediateMailService.shouldSendEmailImmediately(scheduled, executor)).thenReturn(true);
+ when(immediateMailService.createAndSendEncounter(scheduled)).thenReturn(MailSendingStatus.FAILURE);
+
+ MailSendingStatus result = encounterScheduledService.save(dto, executor);
+
+ assertEquals("Should return FAILURE", MailSendingStatus.FAILURE, result);
+ }
+
+ @Test
+ public void testSaveWhenShouldNotSendImmediately() {
+ when(immediateMailService.shouldSendEmailImmediately(scheduled, executor)).thenReturn(false);
+
+ MailSendingStatus result = encounterScheduledService.save(dto, executor);
+
+ assertEquals("Should return SUCCESS when not sending immediately ",
+ MailSendingStatus.SUCCESS, result);
+ verify(immediateMailService, never()).createAndSendEncounter(any());
+ }
+
+ @Test
+ public void testSaveAlwaysAppliesRepeatConfiguration() {
+ when(immediateMailService.shouldSendEmailImmediately(scheduled, executor)).thenReturn(false);
+
+ encounterScheduledService.save(dto, executor);
+
+ verify(setRepeatConfiguration).apply(dto);
+ }
+
+ @Test
+ public void testSaveAlwaysMergesScheduledEncounter() {
+ when(immediateMailService.shouldSendEmailImmediately(scheduled, executor)).thenReturn(false);
+
+ encounterScheduledService.save(dto, executor);
+
+ verify(encounterScheduledDao).merge(scheduled);
+ }
+
+ @Test
+ public void testSaveWhenBundleIsNotNullMergesBundle() {
+ when(immediateMailService.shouldSendEmailImmediately(scheduled, executor)).thenReturn(false);
+
+ encounterScheduledService.save(dto, executor);
+
+ verify(bundleDao).merge(bundle);
+ }
+
+ @Test
+ public void testSaveWhenBundleIsNullDoesNotMergeBundle() {
+ when(scheduled.getBundle()).thenReturn(null);
+ when(immediateMailService.shouldSendEmailImmediately(scheduled, executor)).thenReturn(false);
+
+ encounterScheduledService.save(dto, executor);
+
+ verify(bundleDao, never()).merge(any());
+ }
+
+ @Test
+ public void testSaveAlwaysWritesAuditEntry() {
+ when(immediateMailService.shouldSendEmailImmediately(scheduled, executor)).thenReturn(false);
+
+ encounterScheduledService.save(dto, executor);
+
+ verify(auditService).writeScheduledEncounterAudit(
+ EncounterScheduledService.class, "save", scheduled);
+ }
+
+ @Test
+ public void testSaveWhenShouldNotSendImmediatelyNeverCallsCreateAndSend() {
+ when(immediateMailService.shouldSendEmailImmediately(scheduled, executor)).thenReturn(false);
+
+ encounterScheduledService.save(dto, executor);
+
+ verify(immediateMailService, never()).createAndSendEncounter(any());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/imi/mopat/service/ImmediateMailServiceTest.java b/src/test/java/de/imi/mopat/service/ImmediateMailServiceTest.java
new file mode 100644
index 00000000..8ebaf571
--- /dev/null
+++ b/src/test/java/de/imi/mopat/service/ImmediateMailServiceTest.java
@@ -0,0 +1,145 @@
+package de.imi.mopat.service;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import de.imi.mopat.cron.EncounterScheduledExecutor;
+import de.imi.mopat.model.EncounterScheduled;
+import de.imi.mopat.model.EncounterScheduledTest;
+import java.util.Calendar;
+import java.util.Date;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ImmediateMailServiceTest {
+
+ @Mock
+ private MailService mailService;
+
+ @InjectMocks
+ private ImmediateMailService immediateMailService;
+
+ @Mock
+ private EncounterScheduled scheduled;
+
+ @Mock
+ private EncounterScheduledExecutor executor;
+
+ private Date today;
+ private Date yesterday;
+ private Date tomorrow;
+
+ @Before
+ public void setUp() {
+ today = new Date();
+
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DAY_OF_MONTH, -1);
+ yesterday = cal.getTime();
+
+ cal = Calendar.getInstance();
+ cal.add(Calendar.DAY_OF_MONTH, 1);
+ tomorrow = cal.getTime();
+ }
+ /**
+ * Test of {@link ImmediateMailService#shouldSendEmailImmediately(EncounterScheduled, EncounterScheduledExecutor)}, when StartState is tomorrow
+ */
+ @Test
+ public void testShouldSendEmailImmediatelyWhenStartDateIsTomorrow() {
+ when(scheduled.getStartDate()).thenReturn(tomorrow);
+
+ assertFalse("Email does not send immediately, when start date not today",
+ immediateMailService.shouldSendEmailImmediately(scheduled, executor));
+ }
+
+ /**
+ * Test of {@link ImmediateMailService#shouldSendEmailImmediately(EncounterScheduled, EncounterScheduledExecutor)},
+ * when StartDate is today and executor never executed
+ */
+ @Test
+ public void testShouldSendEmailImmediatelyWhenStartDateTodayNeverExecuted() {
+ when(scheduled.getStartDate()).thenReturn(today);
+ when(executor.getLastExecutionTime()).thenReturn(null);
+ when(executor.getNextExecutionTime()).thenReturn(tomorrow);
+
+ assertTrue("Email sends immediately, when it start date today and executor never executed",
+ immediateMailService.shouldSendEmailImmediately(scheduled, executor));
+ }
+ /**
+ * Test of {@link ImmediateMailService#shouldSendEmailImmediately(EncounterScheduled, EncounterScheduledExecutor)},
+ * when StartDate is today but next execution time is today
+ */
+ @Test
+ public void testShouldSendEmailImmediatelyWhenStartDateTodayNeverExecutedNextExecIsToday() {
+ when(scheduled.getStartDate()).thenReturn(today);
+ when(executor.getLastExecutionTime()).thenReturn(null);
+ when(executor.getNextExecutionTime()).thenReturn(today);
+
+ assertFalse("Email does not send immediately, when start date is today, executor never executed but next execution time is today",
+ immediateMailService.shouldSendEmailImmediately(scheduled, executor));
+ }
+
+ /**
+ * Test of {@link ImmediateMailService#shouldSendEmailImmediately(EncounterScheduled, EncounterScheduledExecutor)},
+ * when StartDate is today but already executed
+ */
+ @Test
+ public void testShouldSendEmailImmediatelyWhenStartDateTodayAndAlreadyExecutedToday() {
+ when(scheduled.getStartDate()).thenReturn(today);
+ when(executor.getLastExecutionTime()).thenReturn(today);
+
+ assertTrue("Email sends immediately, when start date is today and last execution time was today",
+ immediateMailService.shouldSendEmailImmediately(scheduled, executor));
+ }
+
+ @Test
+ public void testShouldSendEmailImmediatelyWhenStartDateTodayAndLastExecutionWasYesterday() {
+ when(scheduled.getStartDate()).thenReturn(today);
+ when(executor.getLastExecutionTime()).thenReturn(yesterday);
+
+ assertFalse("Email does not send immediately, when start date is today and last execution time was yesterday",
+ immediateMailService.shouldSendEmailImmediately(scheduled, executor));
+ }
+ /**
+ * Test of {@link ImmediateMailService#createAndSendEncounter(EncounterScheduled)},
+ */
+ @Test
+ public void testCreateAndSendEncounterDelegatesToMailService() {
+ EncounterScheduled realScheduled = EncounterScheduledTest.getNewValidEncounterScheduled();
+ when(mailService.sendEncounterMail(any())).thenReturn(MailSendingStatus.SUCCESS);
+
+ MailSendingStatus result = immediateMailService.createAndSendEncounter(realScheduled);
+
+ assertEquals("returns SUCCESS", MailSendingStatus.SUCCESS, result);
+ verify(mailService).sendEncounterMail(any());
+ }
+
+ @Test
+ public void testCreateAndSendEncounterWhenMailFails() {
+ EncounterScheduled realScheduled = EncounterScheduledTest.getNewValidEncounterScheduled();
+ when(mailService.sendEncounterMail(any())).thenReturn(MailSendingStatus.FAILURE);
+
+ MailSendingStatus result = immediateMailService.createAndSendEncounter(realScheduled);
+
+ assertEquals("returns FAILURE", MailSendingStatus.FAILURE, result);
+ }
+
+ @Test
+ public void testCreateAndSendEncounterWhenAddressInvalid() {
+ EncounterScheduled realScheduled = EncounterScheduledTest.getNewValidEncounterScheduled();
+ when(mailService.sendEncounterMail(any())).thenReturn(MailSendingStatus.INVALID_ADDRESS);
+
+ MailSendingStatus result = immediateMailService.createAndSendEncounter(realScheduled);
+
+ assertEquals("returns INVALID_ADDRESS", MailSendingStatus.INVALID_ADDRESS, result);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/imi/mopat/service/MailServiceTest.java b/src/test/java/de/imi/mopat/service/MailServiceTest.java
new file mode 100644
index 00000000..11665c16
--- /dev/null
+++ b/src/test/java/de/imi/mopat/service/MailServiceTest.java
@@ -0,0 +1,95 @@
+package de.imi.mopat.service;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import de.imi.mopat.dao.ConfigurationDao;
+import de.imi.mopat.helper.controller.ApplicationMailer;
+import de.imi.mopat.model.Encounter;
+import de.imi.mopat.model.EncounterScheduled;
+import de.imi.mopat.model.enumeration.EncounterScheduledMailStatus;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.context.MessageSource;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MailServiceTest {
+
+ private static final String BASE_URL = "http://localhost/";
+
+ @Mock
+ private ApplicationMailer applicationMailer;
+
+ @Mock
+ private MessageSource messageSource;
+
+ @Mock
+ private ConfigurationDao configurationDao;
+
+ @Mock
+ private ConfigurationService configurationService;
+
+ @InjectMocks
+ private MailService mailService;
+
+ @Mock
+ private Encounter encounter;
+
+ @Mock
+ private EncounterScheduled encounterScheduled;
+
+ @Before
+ public void setUp() {
+ when(configurationDao.getBaseURL()).thenReturn(BASE_URL);
+ when(encounter.getEncounterScheduled()).thenReturn(encounterScheduled);
+ }
+
+ /**
+ * Test of {@link MailService#sendEncounterMail(Encounter)}
+ */
+
+ @Test
+ public void testSendEncounterMailWhenMailSentSuccessfully() {
+ when(encounter.sendMail(applicationMailer, messageSource, BASE_URL)).thenReturn(true);
+
+ MailSendingStatus result = mailService.sendEncounterMail(encounter);
+
+ assertEquals("should return SUCCESS", MailSendingStatus.SUCCESS, result);
+ }
+
+ @Test
+ public void testSendEncounterMailWhenMailFailsAndAddressIsRejected() {
+ when(encounter.sendMail(applicationMailer, messageSource, BASE_URL)).thenReturn(false);
+ when(encounterScheduled.getMailStatus())
+ .thenReturn(EncounterScheduledMailStatus.ADDRESS_REJECTED);
+
+ MailSendingStatus result = mailService.sendEncounterMail(encounter);
+
+ assertEquals("should return INVALID_ADDRESS", MailSendingStatus.INVALID_ADDRESS, result);
+ }
+
+ @Test
+ public void testSendEncounterMailWhenMailFailsAndStatusIsActive() {
+ when(encounter.sendMail(applicationMailer, messageSource, BASE_URL)).thenReturn(false);
+ when(encounterScheduled.getMailStatus()).thenReturn(EncounterScheduledMailStatus.ACTIVE);
+
+ MailSendingStatus result = mailService.sendEncounterMail(encounter);
+
+ assertEquals("should return FAILURE", MailSendingStatus.FAILURE, result);
+ }
+
+ @Test
+ public void testSendEncounterMailWhenMailFailsAndStatusIsDeactivatedPatient() {
+ when(encounter.sendMail(applicationMailer, messageSource, BASE_URL)).thenReturn(false);
+ when(encounterScheduled.getMailStatus())
+ .thenReturn(EncounterScheduledMailStatus.DEACTIVATED_PATIENT);
+
+ MailSendingStatus result = mailService.sendEncounterMail(encounter);
+
+ assertEquals("should return FAILURE", MailSendingStatus.FAILURE, result);
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/message/messages.properties b/src/test/resources/message/messages.properties
index 77361b44..f4ab212c 100644
--- a/src/test/resources/message/messages.properties
+++ b/src/test/resources/message/messages.properties
@@ -265,7 +265,9 @@ condition.warning.changeTrigger=If you change the condition trigger, any newly c
configuration.alert.uploadImagePath=WARNING: If you change the path, the previously saved images can no longer be used by MoPat. They are still saved in the old location and can be copied manually to the new directory to make them accessible again.
configuration.button.add=Add
configuration.button.remove=Remove
+configuration.description.apiKey=Defines the API key required to access the encounter API.
configuration.description.baseUrl=This is the base URL including the context path of the application.
+configuration.description.enableApiTokenAccess=This enables access to the encounter API via API key. Anyone in possession of the configured API key can create new encounters through the API. Enable this feature only if external access is explicitly intended.
configuration.description.exportPath=This is the path completed questionnaires will be exported to. The path has to be a directory, so it has to ends with a slash.
configuration.description.finishedEncounterMailaddressTimeWindowInMillis=Any email address of a finished encounter older than this timeframe will be deleted (default: 30 days). The value -1 deactivates the deletion.
configuration.description.finishedEncounterScheduledTimeWindowInMillis=Any scheduled encounter finished and older than this timeframe will be deleted (default: 90 days). The value -1 deactivates the deletion.
@@ -282,6 +284,8 @@ configuration.error.wrongValidationSchemaType=The file format doesn't accord wit
configuration.file.notUploaded=No file uploaded
configuration.file.path=Path to the uploaded file
configuration.file.uploaded=File uploaded
+configuration.label.apiKey=API-Key for scheduled encounters
+configuration.label.enableApiTokenAccess=Enablement of API-Key for scheduled encounters
configuration.label.FHIRsystemURI=System URI for FHIR export
configuration.label.HL7v22PatientInformationRetrieverHostname=Host for the HL7v22PatientInformationRetriever
configuration.label.HL7v22PatientInformationRetrieverPort=Port for the HL7v22PatientInformationRetriever
@@ -366,6 +370,7 @@ configuration.label.usePatientDataLookup=Look up patient data of a connected hos
configuration.label.validationSchema=Path for schema base validation
configuration.label.webappRootPath=Path for the main directory of the application. Please enter the absolute path (not the relative path).
configuration.success.changed=Configuration changed successfully.
+configuration.validate.apiKey=The value of the field is no valid API-key.
configuration.validate.boolean=The value of the field {field} has to either be 'true' or 'false'.
configuration.validate.double=The value of the field {field} is not decimal.
configuration.validate.integer=The value of the field {field} has to be between 0 and 2E31-1.
@@ -475,6 +480,14 @@ encounterScheduled.validator.repeatPeriodGreaterThanZero=Number of days must be
encounterScheduled.validator.startDateEmpty=The start date can not be empty
encounterScheduled.validator.startdateCanNotBeInThePast=Startdate can not be in the past
encounterScheduled.warning.daysShorterThanThePeriod=The given repeat period is larger than the period between start- and enddate. Thus, only one survey will be scheduled on the startdate.
+encounterScheduledApi.validate.bundleId.required= Bundle-ID can not be empty
+encounterScheduledApi.validate.caseNumber.required= Case Number can not be empty
+encounterScheduledApi.validate.clinicId.required=Clinic-ID can not be empty
+encounterScheduledApi.validate.invalidEmail=Illegal e-mail address
+encounterScheduledApi.validate.serialType.required= Serial Type can not be empty
+encounterScheduledApi.validate.startEndDate.CanNotBeInThePast=Startdate and enddate can not be in the past
+encounterScheduledApi.validate.startEndDate.enddateBiggerThanStartdate=Startdate must be greater than enddate
+encounterScheduledApi.validate.locale.required=Locale can not be null
encountermanager.welcome=Welcome to Mobile Patient Survey (MoPat)!
This is the administration interface of MoPat.
From this interface on, you are able toMoPat is developed by the Institute of Medical Informatics, M\u00fcnster, managed by Univ.-Prof. Dr. rer. nat. Dominik Heider.
You can contact us at {1} or {2}.
error.heading.denied=Access denied!
error.heading.internalservererror=Oops! There was something wrong. The support team has been informed.
@@ -1147,6 +1160,7 @@ user.type.local=Local
value=Value
valueOf=Value of question
valueOfScore=Value of score
+login.demo.infoText=Welcome to MoPat Demo! This is a test installation with unrestricted access to the application. You can try out all the functions of MoPat here. If you need help, you can consult our manual, which contains detailed information on all functions.
Please note: The data is reset regularly. Questionnaires and surveys you have created will be deleted and there is no way to restore them. Use this system for testing only. If you require further information, please contact us: mopat@uni-muenster.de
questionnaire.download.mopatComplete=MoPat with Export Templates
configuration.label.exportFHIRViaHL7v2=Send FHIR export via HL7 communication server.
configuration.label.FHIRViaHL7v2Host=Send FHIR export via HL7 communication server.
diff --git a/src/test/resources/message/messages_de_DE.properties b/src/test/resources/message/messages_de_DE.properties
index 4492a1e5..e01975b8 100644
--- a/src/test/resources/message/messages_de_DE.properties
+++ b/src/test/resources/message/messages_de_DE.properties
@@ -137,8 +137,8 @@ bundle.button.add=Fragebogenpaket hinzuf\u00fcgen
bundle.button.edit=Editieren
bundle.button.lock=Sperren
bundle.button.publish=Freigeben
-bundle.button.testExport=Zugewiesene Exporte testen
bundle.button.remove=L\u00f6schen
+bundle.button.testExport=Zugewiesene Exporte testen
bundle.error.deleteNotPossible=Das Fragebogenpaket {0} kann nicht gel\u00f6scht werden, da mindestens eine Befragung mit diesem Fragebogenpaket durchgef\u00fchrt wurde.
bundle.error.deletePossible=Das Fragebogenpaket {0} wurde gel\u00f6scht.
bundle.error.firstQuestionnaireNotActive=Der erste Fragebogen dieses Pakets muss aktiviert sein.
@@ -267,7 +267,9 @@ configuration.alert.uploadImagePath=WARNUNG: Wenn Sie den Pfad \u00e4ndern, sind
configuration.button.add=Hinzuf\u00fcgen
configuration.button.remove=Entfernen
configuration.description.baseUrl=Dies ist die Base-URL einschlie\u00dflich des Kontextpfades der Anwendung.
+configuration.description.exportFHIRViaHL7v2=F\u00fcr FHIR kann der Export mittels eines HL7v2 Kommunikationsservers eingerichtet werden. In dem Fall wird die FHIR Ressource als Blob in die HL7 Nachricht eingebettet.
configuration.description.exportPath=Hierhin werden die beantworteten Frageb\u00f6gen exportiert. Der angegenbene Pfad muss ein Verzeichnis sein und deshalb mit einem Slash enden.
+configuration.description.fileDeletionTimeWindowInMillis=Alle von MoPat exportierten Dateien, die \u00e4lter als das angegebene Zeitfenster sind, werden gel\u00f6scht (Standard: 30 Tage). Bei der Angabe von -1 wird das L\u00f6schen deaktiviert.
configuration.description.finishedEncounterMailaddressTimeWindowInMillis=Alle E-Mail Adressen von abgeschlossenen Befragungen, die \u00e4lter als das angegebene Zeitfenster sind, werden gel\u00f6scht (Standard: 30 Tage). Bei der Angabe von -1 wird das L\u00f6schen deaktiviert.
configuration.description.finishedEncounterScheduledTimeWindowInMillis=Alle abgeschlossenen Befragungsserien, die \u00e4lter als das angegebene Zeitfenster sind, werden gel\u00f6scht (Standard: 90 Tage). Bei der Angabe von -1 wird das L\u00f6schen deaktiviert.
configuration.description.finishedEncounterTimeWindowInMillis=Alle abgeschlossenen Befragungen, die \u00e4lter als das angegebene Zeitfenster sind, werden gel\u00f6scht (Standard: 30 Tage). Bei der Angabe von -1 wird das L\u00f6schen deaktiviert.
@@ -283,6 +285,8 @@ configuration.error.wrongValidationSchemaType=Das Dateiformat entspricht nicht d
configuration.file.notUploaded=Keine Datei hochgeladen
configuration.file.path=Pfad der hochgeladenen Datei
configuration.file.uploaded=Datei hochgeladen
+configuration.label.FHIRViaHL7v2Host=Host des HL7 Kommunikationsservers f\u00fcr den FHIR Export.
+configuration.label.FHIRViaHL7v2Port=Port des HL7 Kommunikationsservers f\u00fcr den FHIR Export.
configuration.label.FHIRsystemURI=System URI f\u00fcr FHIR Export
configuration.label.HL7v22PatientInformationRetrieverHostname=Host f\u00fcr den HL7v22PatientInformationRetriever
configuration.label.HL7v22PatientInformationRetrieverPort=Port f\u00fcr den HL7v22PatientInformationRetriever
@@ -305,6 +309,7 @@ configuration.label.caseNumberType=Typ der Fallnummer bei Fallnummerneingabe
configuration.label.defaultLanguage=Standardsprache f\u00fcr die Anwendung
configuration.label.enableGlobalPinAuth=Die Verwendung eines Pins erlauben, um Nutzern eine schnelle Anwendung zu ermöglichen.
configuration.label.encounter.checkTimeActivated=Abgeschlossene Befragungen nach einer vorgegebenden Zeit l\u00f6schen
+configuration.label.encounter.fileDeletionTimeWindowInMillis=Zeit nach der erstellte Dateien gel\u00f6scht werden (in ms)
configuration.label.encounter.finishedEncounterMailaddressTimeWindowInMillis=Zeit, nach der die E-Mail Adressen der Patienten nach abgeschlossenen Befragungen gel\u00f6scht werden (in ms)
configuration.label.encounter.finishedEncounterScheduledTimeWindowInMillis=Zeit, nach der abgeschlossene Befragungsserien gel\u00f6scht werden (in ms)
configuration.label.encounter.finishedEncounterTimeWindowInMillis=Zeit, nach der abgeschlossene Befragungen gel\u00f6scht werden (in ms)
@@ -315,6 +320,7 @@ configuration.label.exportFHIRInDirectory=Dateibasierten FHIR-Export nutzen.
configuration.label.exportFHIRPath=Exportpfad f\u00fcr den dateibasierten FHIR-Export. Bitte geben Sie den absoluten Pfad (nicht den relativen Pfad) an.
configuration.label.exportFHIRUrl=URL der REST-Schnittstelle f\u00fcr den FHIR-Export.
configuration.label.exportFHIRViaCommunicationServer=FHIR-Export an REST Schnittstelle senden.
+configuration.label.exportFHIRViaHL7v2=FHIR Export via HL7 Kommunikationsserver senden.
configuration.label.exportHL7ClientPKCSPassword=Password f\u00fcr den privaten Schl\u00fcssel des Clients (der in der angegebenen Datei enthalten ist).
configuration.label.exportHL7ClientPKCSPath=Client PKCS 12 Archiv (.p12 Datei), das den Client authorisiert und die Nachricht verschl\u00fcsselt. Bitte laden Sie ein valides PKCS Archiv hoch.
configuration.label.exportHL7Host=Host des HL7 Kommunikationsservers.
@@ -401,6 +407,7 @@ dateAnswer.validator.startLaterThanEnd=Das fr\u00fcheste Datum ist sp\u00e4ter a
editor.welcome=Willkommen zur Mobilen Patientenbefragung (MoPat)!
Dies ist die Administrationsoberfl\u00e4che von MoPat.
Sie k\u00f6nnen von dieser Oberfl\u00e4che ausMoPat ist eine Entwicklung des Instituts f\u00fcr Medizinische Informatik, M\u00fcnster, unter Leitung von Univ.-Prof. Dr. rer. nat. Dominik Heider.
Sie erreichen uns unter {1} oder {2}.
encounter.button.encounterName=Name
encounter.button.export=Exportieren
+encounter.error.caseNumberInvalid=Die Fallnummer ist nicht g\u00FCltig. Bitte versuchen Sie es erneut!
encounter.error.caseNumberIsEmpty=Die Fallnummer darf nicht nur aus Leerzeichen bestehen.
encounter.export.auto=Automatisch exportiert
encounter.export.conflict=Konflikt
@@ -699,10 +706,10 @@ maximum=Maximum von
minimum=Minimum von
modal.delete.cancel=Abbrechen
modal.delete.confirm=L\u00f6schen
-modal.delete.question.clinic=Sind Sie sicher, dass Sie die Klinik "{0}" l\u00f6schen m\u00f6chten?
-modal.delete.question.questionnaire=Sind Sie sicher, dass Sie den Fragebogen "{0}" l\u00f6schen m\u00f6chten?
modal.delete.question.bundle=Sind Sie sicher, dass Sie das Fragebogenpaket "{0}" l\u00f6schen m\u00f6chten?
+modal.delete.question.clinic=Sind Sie sicher, dass Sie die Klinik "{0}" l\u00f6schen m\u00f6chten?
modal.delete.question.question=Sind Sie sicher, dass Sie Frage {0} l\u00f6schen m\u00f6chten?
+modal.delete.question.questionnaire=Sind Sie sicher, dass Sie den Fragebogen "{0}" l\u00f6schen m\u00f6chten?
modal.delete.title=Best\u00e4tigung der L\u00f6schung
modal.delete.warning=Diese Aktion kann nicht r\u00fcckg\u00e4ngig gemacht werden.
numberInputAnswer.validator.differenceMaxMinNotDivisibleByStepsize=Der Abstand zwischen Minimum und Maximum ist nicht restlos durch die Schrittgr\u00f6\u00dfe teilbar
@@ -815,6 +822,7 @@ questionnaire.button.remove=L\u00f6schen
questionnaire.button.saveAndEdit=Speichern und Fragen bearbeiten
questionnaire.button.showExportTemplates=Export-Templates bearbeiten
questionnaire.displayName.notNull=Der Fragebogen ben\u00f6tigt einen lokalisierten Anzeigenamen
+questionnaire.download.mopatComplete=MoPat mit Export-Templates
questionnaire.error.deleteQuestionnaireNotPossible=Der Fragebogen {0} kann nicht gel\u00f6scht werden, da der Fragebogen w\u00e4hrend einer Befragung bereits beantwortet wurde.
questionnaire.error.deleteQuestionnairePossible=Der Fragebogen {0} wurde gel\u00f6scht.
questionnaire.error.editLastCondition=Dies ist die letzte Frage dieses Fragebogens. Daher k\u00f6nnen f\u00fcr diese Frage keine Bedingungen festgelegt werden.
@@ -1005,6 +1013,7 @@ survey.questionnaire.MinMaxAnswer=Geben Sie zwischen {min} und {max} Antworten.
survey.questionnaire.button.answerQuestionsMultiple=Fragen beantworten
survey.questionnaire.button.answerQuestionsSingle=Frage beantworten
survey.questionnaire.button.closeApplication=Anwendung beenden
+survey.questionnaire.button.completeQuestionnaireInBundle=Abschluss des Befragungsabschnittes
survey.questionnaire.button.completenessCheck=Vollst\u00e4ndigkeitspr\u00fcfung
survey.questionnaire.button.completenessCheckTitlePartMultiple=Der Fragebogen enth\u00e4lt {numberQuestions} nicht vollst\u00e4ndig beantwortete Fragen. Wollen Sie diese nachtr\u00e4glich beantworten?
survey.questionnaire.button.completenessCheckTitlePartSingle=Der Fragebogen enth\u00e4lt eine nicht vollst\u00e4ndig beantwortete Frage. Wollen Sie diese nachtr\u00e4glich beantworten?
@@ -1148,11 +1157,18 @@ user.type.local=Lokal
value=Wert
valueOf=Wert der Frage
valueOfScore=Wert des Scores
-questionnaire.download.mopatComplete=MoPat mit Export-Templates
-configuration.label.exportFHIRViaHL7v2=FHIR Export via HL7 Kommunikationsserver senden.
-configuration.label.FHIRViaHL7v2Host=Host des HL7 Kommunikationsservers f\u00fcr den FHIR Export.
-configuration.label.FHIRViaHL7v2Port=Port des HL7 Kommunikationsservers f\u00fcr den FHIR Export.
-configuration.description.exportFHIRViaHL7v2=F\u00fcr FHIR kann der Export mittels eines HL7v2 Kommunikationsservers eingerichtet werden. In dem Fall wird die FHIR Ressource als Blob in die HL7 Nachricht eingebettet.
-encounter.error.caseNumberInvalid=Die Fallnummer ist nicht g\u00FCltig. Bitte versuchen Sie es erneut!
-survey.questionnaire.button.completeQuestionnaireInBundle=Abschluss des Befragungsabschnittes
-sliderAnswer.validator.localizedMinMaxText=Der eingegebene Text hat mehr als 255 Zeichen.
\ No newline at end of file
+sliderAnswer.validator.localizedMinMaxText=Der eingegebene Text hat mehr als 255 Zeichen.
+login.demo.infoText=Willkommen zu MoPat Demo!
Dies ist eine Testinstallation mit uneingeschr\u00e4nktem Zugriff zur Anwendung. Sie k\u00f6nnen hier alle Funktionen von MoPat ausprobieren. Ben\u00f6tigen Sie Hilfe, k\u00f6nnen Sie unser Handbuch zurate ziehen, in welchem sich ausf\u00fchrliche Informationen zu allen Funktionen finden lassen.
Achtung: Die Daten werden regelm\u00e4\u00dfig zur\u00fcckgesetzt. Von Ihnen angelegte Frageb\u00f6gen und Befragungen werden demnach gel\u00f6scht und es gibt keine M\u00f6glichkeit diese wiederherzustellen. Nutzen Sie dieses System ausschlie\u00dflich f\u00fcr Tests.
Ben\u00f6tigen Sie weitere Informationen, kontaktieren Sie uns: mopat@uni-muenster.de
+configuration.label.apiKey=API-Key f\u00fcr geplante Befragungen
+configuration.label.enableApiTokenAccess=Automatisierte geplante Befragungen unter Anwendung eines API-Keys erlauben
+configuration.validate.apiKey=Der eingegebene Wert ist kein gültiger API-Key.
+encounterScheduledApi.validate.bundleId.required=Bundle-ID farf nicht leer sein
+encounterScheduledApi.validate.caseNumber.required=Fallnummer darf nicht leer sein
+encounterScheduledApi.validate.clinicId.required=Clinic-ID darf nicht leer sein
+encounterScheduledApi.validate.invalidEmail=E-Mail Adresse ung\u00fcltig
+encounterScheduledApi.validate.serialType.required=SerialType darf nicht leer sein
+encounterScheduledApi.validate.startEndDate.CanNotBeInThePast=Das Start- und Enddatum darf nicht in der Vergangenheit liegen
+encounterScheduledApi.validate.startEndDate.enddateBiggerThanStartdate=Endzeitpunkt darf nicht kleiner als Startzeitpunkt sein
+configuration.description.enableApiTokenAccess=Aktiviert den Zugriff auf die Encounter-API per API-Key. Jede Person mit Kenntnis des API-Keys kann \u00fcber die API neue Encounters anlegen. Sollte nur aktiviert werden, wenn ein externer Zugriff ausdr\u00fccklich gew\u00fcnscht ist.
+configuration.description.apiKey=Definiert den API-Key, der f\u00fcr r den Encounter-Zugriff per API vorausgesetzt wird.
+encounterScheduledApi.validate.locale.required=Locale darf nicht null sein
\ No newline at end of file
diff --git a/src/test/resources/message/messages_en_GB.properties b/src/test/resources/message/messages_en_GB.properties
index 77361b44..7bca57e6 100644
--- a/src/test/resources/message/messages_en_GB.properties
+++ b/src/test/resources/message/messages_en_GB.properties
@@ -1147,6 +1147,7 @@ user.type.local=Local
value=Value
valueOf=Value of question
valueOfScore=Value of score
+login.demo.infoText=Welcome to MoPat Demo! This is a test installation with unrestricted access to the application. You can try out all the functions of MoPat here. If you need help, you can consult our manual, which contains detailed information on all functions.
Please note: The data is reset regularly. Questionnaires and surveys you have created will be deleted and there is no way to restore them. Use this system for testing only. If you require further information, please contact us: mopat@uni-muenster.de
questionnaire.download.mopatComplete=MoPat with Export Templates
configuration.label.exportFHIRViaHL7v2=Send FHIR export via HL7 communication server.
configuration.label.FHIRViaHL7v2Host=Send FHIR export via HL7 communication server.
@@ -1154,4 +1155,16 @@ configuration.label.FHIRViaHL7v2Port=Port of the HL7 communication server for FH
configuration.label.FHIRViaHL7v2SendingFacility=
configuration.description.exportFHIRViaHL7v2=For FHIR, export can be set up using an HL7v2 communication server. In this case, the FHIR resource is embedded as a blob in the HL7 message.
encounter.error.caseNumberInvalid=The case number was invalid. Please try again!
-survey.questionnaire.button.completeQuestionnaireInBundle=Complete the survey section
\ No newline at end of file
+survey.questionnaire.button.completeQuestionnaireInBundle=Complete the survey section
+configuration.label.apiKey=API-Key for automated scheduled encounters
+configuration.label.enableApiTokenAccess=Enablement of API-Key for automated scheduled encounters
+configuration.validate.apiKey=The value of the field is no valid API-key.
+encounterScheduledApi.validate.bundleId.required=Bundle-ID can not be empty
+encounterScheduledApi.validate.caseNumber.required=Case Number can not be empty
+encounterScheduledApi.validate.clinicId.required=Clinic-ID can not be empty
+encounterScheduledApi.validate.invalidEmail=Illegal e-mail address
+encounterScheduledApi.validate.serialType.required=Serial Type can not be empty
+encounterScheduledApi.validate.startEndDate.CanNotBeInThePast=Startdate and Enddate can not be in the past
+configuration.description.enableApiTokenAccess=This enables access to the encounter API via API key. Anyone in possession of the configured API key can create new encounters through the API. Enable this feature only if external access is explicitly intended.
+configuration.description.apiKey=Defines the API key required to access the encounter API.
+encounterScheduledApi.validate.locale.required=Locale can not be null
\ No newline at end of file