Skip to content

Commit 1b9934e

Browse files
Local datetime support (#512)
* feat: add local date parameter support * correct test * correct test * add tests
1 parent 5d5ecdd commit 1b9934e

8 files changed

Lines changed: 279 additions & 20 deletions

File tree

src/main/java/fr/insee/genesis/controller/rest/responses/InterrogationController.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import fr.insee.genesis.controller.dto.InterrogationBatchResponse;
44
import fr.insee.genesis.controller.rest.CommonApiResponse;
5+
import fr.insee.genesis.controller.utils.DateTimeUtils;
56
import fr.insee.genesis.domain.model.surveyunit.InterrogationId;
67
import fr.insee.genesis.domain.model.surveyunit.InterrogationInfo;
78
import fr.insee.genesis.domain.ports.api.SurveyUnitApiPort;
@@ -61,14 +62,27 @@ public ResponseEntity<InterrogationBatchResponse> getAllInterrogationIdsByQuesti
6162
)
6263
@RequestParam(value = "since", required = false)
6364
Instant since,
65+
@RequestParam(value = "localSinceDate", required = false)
66+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
67+
@Parameter(description = "Filter interrogations to those recorded strictly after the given timestamp (Europe/Paris timezone)",
68+
schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T01:00:00"))
69+
LocalDateTime localSinceDate,
6470
@RequestParam(value = "until", required = false)
6571
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
6672
@Parameter(
6773
description = "Filter interrogations to those recorded before the given timestamp or at the same time (ISO-8601 UTC format).",
6874
schema = @Schema(type = "string", format = "date-time", example = "2026-01-31T23:59:59Z")
6975
)
70-
Instant until) {
71-
List<InterrogationInfo> idsInfo = surveyUnitService.searchInterrogations(collectionInstrumentId, since, until);
76+
Instant until,
77+
@RequestParam(value = "localUntilDate", required = false)
78+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
79+
@Parameter(description = "Filter interrogations to those recorded before the given timestamp or at the same time (Europe/Paris timezone)",
80+
schema = @Schema(type = "string", format = "date-time", example = "2026-04-02T01:00:00"))
81+
LocalDateTime localUntilDate) {
82+
83+
Instant resolvedSinceDate = DateTimeUtils.resolveInstant(since, localSinceDate);
84+
Instant resolvedEndDate = DateTimeUtils.resolveInstant(until, localUntilDate);
85+
List<InterrogationInfo> idsInfo = surveyUnitService.searchInterrogations(collectionInstrumentId, resolvedSinceDate, resolvedEndDate);
7286
InterrogationBatchResponse response = buildInterrogationBatchResponse(idsInfo);
7387
return ResponseEntity.ok(response);
7488
}

src/main/java/fr/insee/genesis/controller/rest/responses/RawResponseController.java

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fr.insee.genesis.controller.rest.responses;
22

33
import fr.insee.genesis.controller.dto.rawdata.LunaticJsonRawDataUnprocessedDto;
4+
import fr.insee.genesis.controller.utils.DateTimeUtils;
45
import fr.insee.genesis.domain.model.surveyunit.Mode;
56
import fr.insee.genesis.domain.model.surveyunit.rawdata.DataProcessResult;
67
import fr.insee.genesis.domain.model.surveyunit.rawdata.LunaticJsonRawDataModel;
@@ -14,6 +15,7 @@
1415
import fr.insee.modelefiliere.RawResponseDto;
1516
import io.swagger.v3.oas.annotations.Operation;
1617
import io.swagger.v3.oas.annotations.Parameter;
18+
import io.swagger.v3.oas.annotations.media.Schema;
1719
import jakarta.validation.Valid;
1820
import lombok.RequiredArgsConstructor;
1921
import lombok.extern.slf4j.Slf4j;
@@ -199,15 +201,44 @@ public ResponseEntity<Map<String, List<String>>> getProcessedDataIdsSinceHours(
199201
@PreAuthorize("hasRole('USER_BATCH_GENERIC')")
200202
public ResponseEntity<PagedModel<LunaticJsonRawDataModel>> getLunaticJsonRawDataModelFromJsonBody(
201203
@PathVariable String campaignId,
202-
@RequestParam(value = "startDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant startDate,
203-
@RequestParam(value = "endDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant endDate,
204+
@RequestParam(value = "startDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
205+
@Parameter(description = "Start date in UTC", example = "2026-02-02T00:00:00Z")
206+
Instant startDate,
207+
@RequestParam(value = "localStartDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
208+
@Parameter(description = "Extract since in Europe/Paris timezone", schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T01:00:00"))
209+
LocalDateTime localStartDate,
210+
@RequestParam(value = "endDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
211+
@Parameter(description = "End date in UTC", example = "2026-02-02T00:00:00Z")
212+
Instant endDate,
213+
@RequestParam(value = "localEndDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
214+
@Parameter(description = "Extract until in Europe/Paris timezone", schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T01:00:00"))
215+
LocalDateTime localEndDate,
204216
@RequestParam(value = "page", defaultValue = "0") int page,
205217
@RequestParam(value = "size", defaultValue = "1000") int size
206218
) {
207-
log.info("Try to read raw JSONs for campaign {}, with startDate={} and endDate={} - page={} - size={}", campaignId, startDate, endDate,page,size);
219+
Instant resolvedStartDate = DateTimeUtils.resolveInstant(startDate, localStartDate);
220+
Instant resolvedEndDate = DateTimeUtils.resolveInstant(endDate, localEndDate);
221+
log.info(
222+
"Try to read raw JSONs for campaign {} with startDateUtc={} startDateLocal={} endDateUtc={} endDateLocal={} - page={} - size={}",
223+
campaignId,
224+
resolvedStartDate,
225+
DateTimeUtils.toFranceDateTime(resolvedStartDate),
226+
resolvedEndDate,
227+
DateTimeUtils.toFranceDateTime(resolvedEndDate),
228+
page,
229+
size
230+
);
208231
Pageable pageable = PageRequest.of(page, size);
209-
Page<LunaticJsonRawDataModel> rawResponses = lunaticJsonRawDataApiPort.findRawDataByCampaignIdAndDate(campaignId, startDate, endDate, pageable);
210-
log.info("rawResponses, lunatic-json for campaign {}, with startDate={} and endDate={} ={}", campaignId, startDate, endDate,rawResponses.getContent().size());
232+
Page<LunaticJsonRawDataModel> rawResponses = lunaticJsonRawDataApiPort.findRawDataByCampaignIdAndDate(campaignId, resolvedStartDate, resolvedEndDate, pageable);
233+
log.info(
234+
"rawResponses, lunatic-json for campaign {} with startDateUtc={} startDateLocal={} endDateUtc={} endDateLocal={} count={}",
235+
campaignId,
236+
resolvedStartDate,
237+
DateTimeUtils.toFranceDateTime(resolvedStartDate),
238+
resolvedEndDate,
239+
DateTimeUtils.toFranceDateTime(resolvedEndDate),
240+
rawResponses.getContent().size()
241+
);
211242
return ResponseEntity.status(HttpStatus.OK).body(new PagedModel<>(rawResponses));
212243
}
213244

@@ -256,15 +287,41 @@ public ResponseEntity<Void> existsLunaticJsonByInterrogationId(@PathVariable Str
256287
@PreAuthorize("hasRole('USER_BATCH_GENERIC')")
257288
public ResponseEntity<PagedModel<RawResponseModel>> getRawResponsesFromJsonBody(
258289
@PathVariable String campaignId,
259-
@RequestParam(value = "startDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant startDate,
260-
@RequestParam(value = "endDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant endDate,
290+
@RequestParam(value = "startDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
291+
@Parameter(description = "Start date in UTC", example = "2026-02-02T00:00:00Z")
292+
Instant startDate,
293+
@RequestParam(value = "localStartDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
294+
@Parameter(description = "Extract since in Europe/Paris timezone", schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T01:00:00"))
295+
LocalDateTime localStartDate,
296+
@RequestParam(value = "endDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
297+
@Parameter(description = "End date in UTC", example = "2026-04-02T00:00:00Z")
298+
Instant endDate,
299+
@RequestParam(value = "localEndDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
300+
@Parameter(description = "Extract until in Europe/Paris timezone", schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T01:00:00"))
301+
LocalDateTime localEndDate,
261302
@RequestParam(value = "page", defaultValue = "0") int page,
262303
@RequestParam(value = "size", defaultValue = "1000") int size
263304
) {
264-
log.info("Try to read raw lunatic JSONs for campaign {}, with startDate={} and endDate={} - page={} - size={}", campaignId, startDate, endDate,page,size);
305+
Instant resolvedStartDate = DateTimeUtils.resolveInstant(startDate, localStartDate);
306+
Instant resolvedEndDate = DateTimeUtils.resolveInstant(endDate, localEndDate);
307+
log.info("Try to read raw JSONs for campaign {} with startDateUtc={} startDateLocal={} endDateUtc={} endDateLocal={} - page={} - size={}",
308+
campaignId,
309+
resolvedStartDate,
310+
DateTimeUtils.toFranceDateTime(resolvedStartDate),
311+
resolvedEndDate,
312+
DateTimeUtils.toFranceDateTime(resolvedEndDate),
313+
page,
314+
size
315+
);
265316
Pageable pageable = PageRequest.of(page, size);
266-
Page<RawResponseModel> rawResponses = rawResponseApiPort.findRawResponseDataByCampaignIdAndDate(campaignId, startDate, endDate, pageable);
267-
log.info("rawResponses for campaign {}, with startDate={} and endDate={} ={}",campaignId, startDate, endDate, rawResponses.getContent().size());
317+
Page<RawResponseModel> rawResponses = rawResponseApiPort.findRawResponseDataByCampaignIdAndDate(campaignId, resolvedStartDate, resolvedEndDate, pageable);
318+
log.info("rawResponses for campaign {},with startDateUtc={} startDateLocal={} endDateUtc={} endDateLocal={} count={}",
319+
campaignId,
320+
resolvedStartDate,
321+
DateTimeUtils.toFranceDateTime(resolvedStartDate),
322+
resolvedEndDate,
323+
DateTimeUtils.toFranceDateTime(resolvedEndDate),
324+
rawResponses.getContent().size());
268325
return ResponseEntity.status(HttpStatus.OK).body(new PagedModel<>(rawResponses));
269326
}
270327

src/main/java/fr/insee/genesis/controller/rest/responses/RawResponseReprocessController.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package fr.insee.genesis.controller.rest.responses;
22

3+
import fr.insee.genesis.controller.utils.DateTimeUtils;
34
import fr.insee.genesis.domain.model.surveyunit.rawdata.DataProcessResult;
45
import fr.insee.genesis.domain.model.surveyunit.rawdata.RawDataModelType;
56
import fr.insee.genesis.domain.ports.api.ReprocessRawResponseApiPort;
@@ -18,6 +19,7 @@
1819
import org.springframework.web.bind.annotation.RequestParam;
1920

2021
import java.time.Instant;
22+
import java.time.LocalDateTime;
2123

2224
@Controller
2325
@RequiredArgsConstructor
@@ -44,20 +46,33 @@ public ResponseEntity<String> reProcessRawResponsesByCollectionInstrumentId(
4446
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
4547
Instant sinceDate,
4648

49+
@RequestParam(value = "localSinceDate", required = false)
50+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
51+
@Parameter(description = "Extract since in Europe/Paris timezone", schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T01:00:00"))
52+
LocalDateTime localSinceDate,
53+
4754
@Parameter(
4855
description = "Extract until",
4956
schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T00:00:00Z")
5057
)
5158
@RequestParam(value = "endDate", required = false)
5259
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
53-
Instant endDate
60+
Instant endDate,
61+
62+
@RequestParam(value = "localEndDate", required = false)
63+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
64+
@Parameter(description = "Extract until in Europe/Paris timezone", schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T01:00:00"))
65+
LocalDateTime localEndDate
5466
) throws GenesisException {
5567

68+
Instant resolvedSinceDate = DateTimeUtils.resolveInstant(sinceDate, localSinceDate);
69+
Instant resolvedEndDate = DateTimeUtils.resolveInstant(endDate, localEndDate);
70+
5671
DataProcessResult result = reprocessRawResponseApiPort.reprocessRawResponses(
5772
RawDataModelType.FILIERE,
5873
collectionInstrumentId,
59-
sinceDate,
60-
endDate);
74+
resolvedSinceDate,
75+
resolvedEndDate);
6176

6277
return ResponseEntity.ok(result.message(collectionInstrumentId));
6378
}
@@ -81,20 +96,33 @@ public ResponseEntity<String> reProcessJsonRawDataByQuestionnaireId(
8196
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
8297
Instant sinceDate,
8398

99+
@RequestParam(value = "localSinceDate", required = false)
100+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
101+
@Parameter(description = "Extract since in Europe/Paris timezone", schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T01:00:00"))
102+
LocalDateTime localSinceDate,
103+
84104
@Parameter(
85105
description = "Extract until",
86106
schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T00:00:00Z")
87107
)
88108
@RequestParam(value = "endDate", required = false)
89109
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
90-
Instant endDate
110+
Instant endDate,
111+
112+
@RequestParam(value = "localEndDate", required = false)
113+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
114+
@Parameter(description = "Extract until in Europe/Paris timezone", schema = @Schema(type = "string", format = "date-time", example = "2026-02-02T01:00:00"))
115+
LocalDateTime localEndDate
91116
) throws GenesisException {
92117

118+
Instant resolvedSinceDate = DateTimeUtils.resolveInstant(sinceDate, localSinceDate);
119+
Instant resolvedEndDate = DateTimeUtils.resolveInstant(endDate, localEndDate);
120+
93121
DataProcessResult result = reprocessRawResponseApiPort.reprocessRawResponses(
94122
RawDataModelType.LEGACY,
95123
collectionInstrumentId,
96-
sinceDate,
97-
endDate);
124+
resolvedSinceDate,
125+
resolvedEndDate);
98126

99127
return ResponseEntity.ok(result.message(collectionInstrumentId));
100128
}

src/main/java/fr/insee/genesis/controller/rest/responses/ResponseController.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import fr.insee.genesis.controller.utils.AuthUtils;
2020
import fr.insee.genesis.controller.utils.ControllerUtils;
2121
import fr.insee.genesis.controller.utils.DataTransformer;
22+
import fr.insee.genesis.controller.utils.DateTimeUtils;
2223
import fr.insee.genesis.domain.model.context.DataProcessingContextModel;
2324
import fr.insee.genesis.domain.model.surveyunit.InterrogationId;
2425
import fr.insee.genesis.domain.model.surveyunit.Mode;
@@ -39,6 +40,7 @@
3940
import io.swagger.v3.oas.annotations.media.Schema;
4041
import io.swagger.v3.oas.annotations.tags.Tag;
4142
import lombok.extern.slf4j.Slf4j;
43+
import org.springframework.format.annotation.DateTimeFormat;
4244
import org.springframework.http.HttpStatus;
4345
import org.springframework.http.MediaType;
4446
import org.springframework.http.ResponseEntity;
@@ -420,13 +422,27 @@ public ResponseEntity<List<SurveyUnitSimplifiedDto>> searchResponses(
420422
schema = @Schema(type = "string", format = "date-time", example = "2026-01-01T00:00:00Z")
421423
)
422424
@RequestParam(value = "recordedBefore", required = false) Instant recordedBefore,
425+
@Parameter(
426+
description = "Filter responses recorded before or at the same time of the given timestamp in Europe/Paris local time",
427+
schema = @Schema(
428+
type = "string",
429+
format = "date-time",
430+
example = "2026-01-01T01:00:00"
431+
)
432+
)
433+
@RequestParam(value = "localRecordedBefore", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
434+
LocalDateTime localRecordedBefore,
423435
@RequestBody List<InterrogationId> interrogationIds)
424436
{
437+
Instant resolvedRecordedBefore = DateTimeUtils.resolveInstant(
438+
recordedBefore,
439+
localRecordedBefore
440+
);
425441
return ResponseEntity.ok(
426442
surveyUnitService.findSimplifiedList(
427443
collectionInstrumentId,
428444
interrogationIds,
429-
recordedBefore
445+
resolvedRecordedBefore
430446
)
431447
);
432448
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package fr.insee.genesis.controller.utils;
2+
3+
import fr.insee.genesis.exceptions.InvalidDateIntervalException;
4+
5+
import java.time.Instant;
6+
import java.time.LocalDateTime;
7+
import java.time.ZoneId;
8+
import java.time.ZonedDateTime;
9+
10+
public final class DateTimeUtils {
11+
12+
private static final ZoneId FRANCE_ZONE = ZoneId.of("Europe/Paris");
13+
14+
private DateTimeUtils() {
15+
}
16+
17+
public static Instant resolveInstant(
18+
Instant utcDate,
19+
LocalDateTime localDate
20+
) {
21+
if (utcDate != null && localDate != null) {
22+
throw new InvalidDateIntervalException(
23+
"Use either UTC date or local date, not both"
24+
);
25+
}
26+
27+
if (localDate != null) {
28+
return localDate
29+
.atZone(FRANCE_ZONE)
30+
.toInstant();
31+
}
32+
33+
return utcDate;
34+
}
35+
36+
public static ZonedDateTime toFranceDateTime(Instant instant) {
37+
if (instant == null) {
38+
return null;
39+
}
40+
41+
return instant.atZone(FRANCE_ZONE);
42+
}
43+
}

src/test/java/fr/insee/genesis/controller/rest/responses/InterrogationControllerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ void getAllInterrogationIdsByQuestionnaire_date_test() {
6767
interrogationController.getAllInterrogationIdsByQuestionnaire(
6868
TestConstants.DEFAULT_COLLECTION_INSTRUMENT_ID,
6969
since,
70-
null
70+
null,
71+
null,null
7172
);
7273

7374
//THEN

0 commit comments

Comments
 (0)