Skip to content

Commit 9d5d562

Browse files
authored
šŸ›: FIX: 14130/14136: ngSurvey: Missing facilities, Enhacements Hospitalization handling, customizable fields (#14138)
* šŸ›: FIX: Only facilities without regions/districts can be patched. - Reversed condition for cron activation - copy-paste issue in log - NPE exception for some occurrences * ✨ Handling partial dates in a better manner: also supporting month only and different format * ✨ Made survey fetch operation transactional * šŸ› Manually handling Epidata that is alone without any normal
1 parent 7030c55 commit 9d5d562

9 files changed

Lines changed: 174 additions & 63 deletions

File tree

ā€Žsormas-backend/src/main/java/de/symeda/sormas/backend/ReferenceDataValueInstanceProviderImpl.javaā€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private void init() {
5757
.collect(Collectors.toList())),
5858
Map.entry(
5959
FacilityReferenceDto.class,
60-
() -> getInstance(FacilityFacade.class).getAllWithoutRegionAfter(DATE_ALL_VALUES)
60+
() -> getInstance(FacilityFacade.class).getAllAfter(DATE_ALL_VALUES)
6161
.stream()
6262
.map(FacilityDto::toReference)
6363
.collect(Collectors.toList())),

ā€Žsormas-backend/src/main/java/de/symeda/sormas/backend/common/CronService.javaā€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ public void fetchExternalMessages() {
235235
@Schedule(hour = "*", persistent = false)
236236
public void fetchSurveyResponses() {
237237
if (!featureConfigurationFacade.isFeatureEnabled(FeatureType.EXTERNAL_MESSAGES)
238-
|| featureConfigurationFacade.isPropertyValueTrue(FeatureType.EXTERNAL_MESSAGES, FeatureTypeProperty.SURVEY_FETCH_ENABLED)) {
238+
|| !featureConfigurationFacade.isPropertyValueTrue(FeatureType.EXTERNAL_MESSAGES, FeatureTypeProperty.SURVEY_FETCH_ENABLED)) {
239239
logger.info("External messages are disabled, survey responses will not be fetched");
240240
return;
241241
}

ā€Žsormas-backend/src/main/java/de/symeda/sormas/backend/externalmessage/ExternalMessageFacadeEjb.javaā€Ž

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import javax.persistence.criteria.Predicate;
5151
import javax.persistence.criteria.Root;
5252
import javax.persistence.criteria.Selection;
53+
import javax.transaction.Transactional;
5354
import javax.validation.Valid;
5455
import javax.validation.constraints.NotNull;
5556

@@ -329,6 +330,9 @@ private ExternalMessageDto saveWithFallback(ExternalMessageDto dto) {
329330

330331
@Override
331332
@RightsAllowed(UserRight._EXTERNAL_MESSAGE_SURVEY_RESPONSE_PROCESS)
333+
@Transactional(value = Transactional.TxType.REQUIRES_NEW,
334+
rollbackOn = {
335+
Exception.class })
332336
public List<ExternalMessageDto> saveAndProcessSurveyResponses(Date since) {
333337

334338
if (since == null) {
@@ -388,12 +392,12 @@ public List<ExternalMessageDto> saveAndProcessSurveyResponses(Date since) {
388392

389393
processingResults.forEach(wrapper -> {
390394
ProcessingResultStatus result = wrapper.getResultStatus();
391-
if (result.isCanceled()) {
395+
if (result == null || result.isCanceled()) {
392396
logger.error("Processing of surveyResponse with UUID {} has been canceled", wrapper.getExternalMessage().getUuid());
393397
}
394398
});
395399
} catch (InterruptedException e) {
396-
logger.error("Could not process lab message with UUID [{}]", extractUuids(filteredSurveyResponses), e);
400+
logger.error("Could not process survey responses with UUID [{}]", extractUuids(filteredSurveyResponses), e);
397401
Thread.currentThread().interrupt();
398402
} catch (ExecutionException e) {
399403
logger.error("Could not process survey responses with UUID [{}]", extractUuids(filteredSurveyResponses), e);

ā€Žsormas-backend/src/main/java/de/symeda/sormas/backend/externalmessage/survey/AutomaticSurveyResponseProcessor.javaā€Ž

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package de.symeda.sormas.backend.externalmessage.survey;
22

3-
import java.util.*;
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Optional;
48
import java.util.concurrent.ExecutionException;
59
import java.util.stream.Collectors;
610

@@ -9,8 +13,6 @@
913
import javax.inject.Inject;
1014
import javax.transaction.Transactional;
1115

12-
import de.symeda.sormas.backend.survey.SurveyFacadeEjb;
13-
import de.symeda.sormas.backend.survey.SurveyTokenFacadeEjb;
1416
import org.apache.commons.collections4.CollectionUtils;
1517
import org.jetbrains.annotations.NotNull;
1618
import org.slf4j.Logger;
@@ -24,12 +26,12 @@
2426
import de.symeda.sormas.api.patch.CaseDataPatchRequest;
2527
import de.symeda.sormas.api.patch.DataPatchResponse;
2628
import de.symeda.sormas.api.patch.DataPatcher;
27-
import de.symeda.sormas.api.survey.SurveyFacade;
2829
import de.symeda.sormas.api.survey.SurveyReferenceDto;
2930
import de.symeda.sormas.api.survey.SurveyTokenDto;
30-
import de.symeda.sormas.api.survey.SurveyTokenFacade;
3131
import de.symeda.sormas.api.utils.Tuple;
3232
import de.symeda.sormas.api.utils.dataprocessing.ProcessingResultStatus;
33+
import de.symeda.sormas.backend.survey.SurveyFacadeEjb;
34+
import de.symeda.sormas.backend.survey.SurveyTokenFacadeEjb;
3335
import de.symeda.sormas.backend.util.CollectorUtils;
3436

3537
/**
@@ -146,6 +148,10 @@ public List<SurveyResponseProcessingResult> processSurveyResponses(List<External
146148
"Exception while patching survey response for external message: [{}]. Processing will continue for other messages",
147149
externalMessage.getUuid(),
148150
e);
151+
152+
// in case of failure status must be changed to unprocessed
153+
externalMessage.setStatus(ExternalMessageStatus.UNPROCESSED);
154+
149155
surveyResponseProcessingResult.setRuntimeException(e);
150156
}
151157

ā€Žsormas-backend/src/main/java/de/symeda/sormas/backend/patch/DataPatcherImpl.javaā€Ž

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import javax.ejb.EJB;
1717
import javax.enterprise.context.ApplicationScoped;
1818
import javax.inject.Inject;
19+
import javax.transaction.Transactional;
1920

2021
import org.apache.commons.lang3.StringUtils;
2122
import org.jetbrains.annotations.NotNull;
@@ -25,6 +26,7 @@
2526
import de.symeda.sormas.api.Disease;
2627
import de.symeda.sormas.api.EntityDto;
2728
import de.symeda.sormas.api.caze.CaseDataDto;
29+
import de.symeda.sormas.api.customizablefield.CustomizableFieldContext;
2830
import de.symeda.sormas.api.customizablefield.CustomizableFieldValueDto;
2931
import de.symeda.sormas.api.externalmessage.survey.PatchDictionary;
3032
import de.symeda.sormas.api.externalmessage.survey.PatchField;
@@ -109,6 +111,9 @@ public DataPatcherImpl(
109111
this.configFacade = configFacade;
110112
}
111113

114+
@Transactional(value = Transactional.TxType.REQUIRES_NEW,
115+
rollbackOn = {
116+
Exception.class })
112117
@Override
113118
public DataPatchResponse patch(CaseDataPatchRequest request) {
114119
logger.debug("patch: [{}]", request);
@@ -154,7 +159,7 @@ public DataPatchResponse patch(CaseDataPatchRequest request) {
154159
List<CustomizableFieldSinglePatchingResult> customizableResults = customizableFieldDataPatcher.patch(
155160
new CustomizableFieldDataPatchRequest().setCaseDataPatchRequest(request)
156161
.setPatchingTuples(patchingTuples.stream().filter(customizableFieldsPredicate).collect(Collectors.toList()))
157-
.setEntityUuidDictionary(buildEntityUuidDictionaryFrom(entityCache))
162+
.setEntityUuidDictionary(buildEntityUuidDictionaryFrom(entityCache, caseData))
158163
.setCaseDataDto(caseData));
159164

160165
List<SinglePatchResult> aggregatedResults = Stream.concat(plainResults.stream(), customizableResults.stream()).collect(Collectors.toList());
@@ -186,8 +191,9 @@ public DataPatchResponse patch(CaseDataPatchRequest request) {
186191
}
187192

188193
private static @NotNull Map<CustomizableContextIndexKey, String> buildEntityUuidDictionaryFrom(
189-
Map<Tuple<String, Integer>, AttachedEntityWrapper> entityCache) {
190-
return entityCache.entrySet()
194+
Map<Tuple<String, Integer>, AttachedEntityWrapper> entityCache,
195+
CaseDataDto caseData) {
196+
Map<CustomizableContextIndexKey, String> temporaryResult = entityCache.entrySet()
191197
.stream()
192198
.map(
193199
entry -> CustomizableFieldContextPatchMapping.fromI18nName(entry.getKey().getFirst())
@@ -197,6 +203,19 @@ public DataPatchResponse patch(CaseDataPatchRequest request) {
197203
entry.getValue().getEntityDto().getUuid())))
198204
.flatMap(Optional::stream)
199205
.collect(Collectors.toMap(Tuple::getFirst, Tuple::getSecond));
206+
207+
if (temporaryResult.keySet().stream().anyMatch(key -> key.getContext() == CustomizableFieldContext.EPIDATA)) {
208+
return temporaryResult;
209+
}
210+
211+
// manually adding epiData: covers edge case were only a customizable field in set in epiData.
212+
return Stream.concat(
213+
temporaryResult.entrySet().stream(),
214+
Stream.of(
215+
new AbstractMap.SimpleEntry<>(
216+
new CustomizableContextIndexKey().setContext(CustomizableFieldContext.EPIDATA),
217+
caseData.getEpiData().getUuid())))
218+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
200219
}
201220

202221
private void saveCustomizableFieldsIfAppropriate(List<CustomizableFieldSinglePatchingResult> customizableResults) {

ā€Žsormas-backend/src/main/java/de/symeda/sormas/backend/patch/customizablefield/CustomizableFieldDataPatcher.javaā€Ž

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package de.symeda.sormas.backend.patch.customizablefield;
22

3-
import java.util.*;
3+
import java.util.HashMap;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Objects;
7+
import java.util.Optional;
8+
import java.util.Set;
49
import java.util.function.Function;
510
import java.util.stream.Collectors;
611

ā€Žsormas-backend/src/main/java/de/symeda/sormas/backend/patch/mapping/impl/valuemapper/DatePatchMapper.javaā€Ž

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import java.text.SimpleDateFormat;
55
import java.time.LocalDate;
66
import java.time.LocalDateTime;
7+
import java.time.YearMonth;
78
import java.time.ZoneId;
9+
import java.time.format.DateTimeFormatter;
10+
import java.time.format.DateTimeParseException;
811
import java.util.Arrays;
912
import java.util.Date;
1013
import java.util.List;
@@ -32,9 +35,14 @@ public class DatePatchMapper implements ValuePatchMapper {
3235
"yyyy-MM-dd'T'HH:mm:ssZ", // 2025-12-17T14:30:00+0100
3336
"yyyy-MM-dd'T'HH:mm:ss", // 2025-12-17T14:30:00
3437
"yyyy-MM-dd'T'HH:mm", // 2025-12-17T14:30
35-
"yyyy-MM-dd" // 2025-12-17
38+
"yyyy-MM-dd", // 2025-12-17,
39+
"dd/MM/yyyy" // 17-12-2025,
3640
);
3741

42+
private static final String YEAR_ONLY_REGEX = "\\d{4}";
43+
44+
private static final String MONTH_YEAR_ONLY_REGEX = "\\d{2}/\\d{4}";
45+
3846
@Override
3947
public Set<Class<?>> getSupportedTypes() {
4048
return SUPPORTED_TYPES;
@@ -44,9 +52,6 @@ public Set<Class<?>> getSupportedTypes() {
4452
@SuppressWarnings("unchecked")
4553
public <T> ValueMappingResult<T> map(ValuePatchRequest<T> request) {
4654
Object value = request.getValue();
47-
if (value == null) {
48-
return ValueMappingResult.withData(null);
49-
}
5055

5156
String str = value.toString().trim();
5257

@@ -65,9 +70,9 @@ public <T> ValueMappingResult<T> map(ValuePatchRequest<T> request) {
6570

6671
if (request.isAllowFallbackValues()) {
6772
logger.debug("Edge case to allow years only in lenient manner");
68-
ValueMappingResult<T> yearOnlyResult = tryParseYearOnly(str, targetType);
69-
if (yearOnlyResult != null) {
70-
return yearOnlyResult;
73+
ValueMappingResult<T> partialDate = tryParsePartialDate(str, targetType);
74+
if (partialDate != null) {
75+
return partialDate;
7176
}
7277
}
7378

@@ -81,18 +86,31 @@ public <T> ValueMappingResult<T> map(ValuePatchRequest<T> request) {
8186
* Returns {@code null} when the input is not a 4-digit year pattern.
8287
*/
8388
@SuppressWarnings("unchecked")
84-
private <T> ValueMappingResult<T> tryParseYearOnly(String str, Class<?> targetType) {
85-
if (!str.matches("\\d{4}")) {
86-
return null;
87-
}
88-
try {
89-
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
90-
sdf.setLenient(false);
91-
Date january1st = sdf.parse(str + "-01-01");
92-
return ValueMappingResult.withData((T) toTargetType(january1st, targetType));
93-
} catch (ParseException e) {
89+
private <T> ValueMappingResult<T> tryParsePartialDate(String str, Class<?> targetType) {
90+
if (str.matches(YEAR_ONLY_REGEX)) {
91+
try {
92+
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
93+
sdf.setLenient(false);
94+
Date january1st = sdf.parse(str + "-01-01");
95+
return ValueMappingResult.withData((T) toTargetType(january1st, targetType));
96+
} catch (ParseException e) {
97+
return null;
98+
}
99+
} else if (str.matches(MONTH_YEAR_ONLY_REGEX)) {
100+
try {
101+
YearMonth ym = YearMonth.parse(str, DateTimeFormatter.ofPattern("MM/uuuu"));
102+
LocalDate ld = ym.atDay(1);
103+
104+
Date legacyDate = Date.from(ld.atStartOfDay(ZoneId.systemDefault()).toInstant());
105+
106+
return ValueMappingResult.withData((T) toTargetType(legacyDate, targetType));
107+
} catch (DateTimeParseException e) {
108+
return null;
109+
}
110+
} else {
94111
return null;
95112
}
113+
96114
}
97115

98116
private Object toTargetType(Date parsed, Class<?> targetType) {

ā€Žsormas-backend/src/test/java/de/symeda/sormas/backend/patch/mapping/impl/valuemapper/DateMapperTest.javaā€Ž

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,6 @@ void map_valueAsNonStringObject_usesToString() throws Exception {
8383
assertEquals(expected, actual);
8484
}
8585

86-
@Test
87-
void map_invalidFormat_throwsIllegalArgumentException() {
88-
// EXECUTE & CHECK
89-
assertEquals(DataPatchFailureCause.INVALID_VALUE_TYPE, victim.map("15/06/2024", Date.class).getDataPatchFailureCause());
90-
}
91-
9286
@Test
9387
void map_lenientOff_invalidDay_throwsIllegalArgumentException() {
9488
// EXECUTE & CHECK
@@ -127,12 +121,6 @@ void map_localDate_fromDateTimeString_returnsDatePart() {
127121
assertEquals(LocalDate.of(2024, 6, 15), victim.map("2024-06-15T14:30:00", LocalDate.class).getData());
128122
}
129123

130-
@Test
131-
void map_localDate_invalidFormat() {
132-
// EXECUTE & CHECK
133-
assertEquals(DataPatchFailureCause.INVALID_VALUE_TYPE, victim.map("15/06/2024", LocalDate.class).getDataPatchFailureCause());
134-
}
135-
136124
@Test
137125
void map_localDate_invalidDay() {
138126
// EXECUTE & CHECK
@@ -159,12 +147,6 @@ void map_localDateTime_fromDateOnlyString_returnsMidnight() {
159147
assertEquals(LocalDateTime.of(2024, 6, 15, 0, 0, 0), victim.map("2024-06-15", LocalDateTime.class).getData());
160148
}
161149

162-
@Test
163-
void map_localDateTime_invalidFormat() {
164-
// EXECUTE & CHECK
165-
assertEquals(DataPatchFailureCause.INVALID_VALUE_TYPE, victim.map("15/06/2024", LocalDateTime.class).getDataPatchFailureCause());
166-
}
167-
168150
@Test
169151
void map_localDateTime_invalidDay() {
170152
// EXECUTE & CHECK
@@ -217,4 +199,47 @@ void map_partialYear_fiveDigits_returnsInvalidValueType() {
217199
// EXECUTE & CHECK — 5-digit strings are not valid year-only input
218200
assertEquals(DataPatchFailureCause.INVALID_VALUE_TYPE, victim.map("20241", Date.class).getDataPatchFailureCause());
219201
}
202+
203+
@Test
204+
void map_dayMonthYear_dayWithinJanuary_parsesAsDayOfYear() throws Exception {
205+
// PREPARE
206+
String input = "15/01/2024";
207+
Date expected = new SimpleDateFormat("yyyy-MM-dd").parse("2024-01-15");
208+
209+
// EXECUTE
210+
Date actual = victim.map(input, Date.class).getData();
211+
212+
// CHECK
213+
assertEquals(expected, actual);
214+
}
215+
216+
@Test
217+
void map_monthYear_validDate_returnsFirstDayOfMonth() throws Exception {
218+
// PREPARE — no day field is set, so Calendar defaults it to the 1st
219+
Date expected = new SimpleDateFormat("yyyy-MM-dd").parse("2024-06-01");
220+
221+
// EXECUTE
222+
Date actual = victim.map("06/2024", Date.class).getData();
223+
224+
// CHECK
225+
assertEquals(expected, actual);
226+
}
227+
228+
@Test
229+
void map_monthYear_localDate_returnsFirstDayOfMonth() {
230+
// EXECUTE & CHECK
231+
assertEquals(LocalDate.of(2024, 6, 1), victim.map("06/2024", LocalDate.class).getData());
232+
}
233+
234+
@Test
235+
void map_monthYear_localDateTime_returnsMidnightFirstDayOfMonth() {
236+
// EXECUTE & CHECK
237+
assertEquals(LocalDateTime.of(2024, 6, 1, 0, 0, 0), victim.map("06/2024", LocalDateTime.class).getData());
238+
}
239+
240+
@Test
241+
void map_monthYear_invalidMonth_returnsInvalidValueType() {
242+
// EXECUTE & CHECK — month 13 does not exist
243+
assertEquals(DataPatchFailureCause.INVALID_VALUE_TYPE, victim.map("13/2024", Date.class).getDataPatchFailureCause());
244+
}
220245
}

0 commit comments

Comments
Ā (0)