From ea664e6ccc82f50a5384edfc65c12b47b01396f0 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Wed, 8 Jul 2026 15:05:50 -0700 Subject: [PATCH 1/5] Added integration test for storing sub-minute levels. Added check for sub-minute level dates. --- .../java/cwms/cda/api/LevelsController.java | 8 +- .../cwms/cda/api/LevelsControllerTestIT.java | 106 +++++++++++++----- 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java index a50fb151a..5b014c5e4 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java @@ -75,7 +75,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -124,6 +124,12 @@ public void create(@NotNull Context ctx) { LocationLevel level = deserializeLocationLevel(ctx); level.validate(); + if (!level.getLevelDate().truncatedTo(ChronoUnit.MINUTES).equals(level.getLevelDate())) { + Map errorDetails = new HashMap<>(); + errorDetails.put("message", "Level effective date cannot have seconds"); + throw new BadRequestResponse("", errorDetails); + } + DSLContext dsl = getDslContext(ctx); LocationLevelsDao levelsDao = getLevelsDao(dsl); levelsDao.storeLocationLevel(level); diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java index 2a967a7f1..8df8ede09 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java @@ -24,6 +24,33 @@ package cwms.cda.api; +import static cwms.cda.api.Controllers.BEGIN; +import static cwms.cda.api.Controllers.EFFECTIVE_DATE; +import static cwms.cda.api.Controllers.EFFECTIVE_DATE_EXACT; +import static cwms.cda.api.Controllers.END; +import static cwms.cda.api.Controllers.FORMAT; +import static cwms.cda.api.Controllers.INCLUDE_ALIASES; +import static cwms.cda.api.Controllers.INTERVAL; +import static cwms.cda.api.Controllers.LEVEL_ID_MASK; +import static cwms.cda.api.Controllers.PAGE; +import static cwms.cda.api.Controllers.PAGE_SIZE; +import static cwms.cda.api.Controllers.START; +import static cwms.cda.api.Controllers.UNIT; +import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER; +import static helpers.FloatCloseTo.floatCloseTo; +import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isOneOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + import cwms.cda.ApiServlet; import cwms.cda.data.dao.LocationCategoryDao; import cwms.cda.data.dao.LocationGroupDao; @@ -33,9 +60,9 @@ import cwms.cda.data.dto.AssignedLocation; import cwms.cda.data.dto.LocationCategory; import cwms.cda.data.dto.LocationGroup; +import cwms.cda.data.dto.TimeSeries; import cwms.cda.data.dto.locationlevel.ConstantLocationLevel; import cwms.cda.data.dto.locationlevel.LocationLevel; -import cwms.cda.data.dto.TimeSeries; import cwms.cda.data.dto.locationlevel.SeasonalLocationLevel; import cwms.cda.data.dto.locationlevel.SeasonalValueBean; import cwms.cda.data.dto.locationlevel.TimeSeriesLocationLevel; @@ -44,11 +71,24 @@ import fixtures.CwmsDataApiSetupCallback; import fixtures.MinimumSchema; import fixtures.TestAccounts; +import hec.data.RatingException; +import hec.data.cwmsRating.io.RatingSetContainer; +import hec.data.cwmsRating.io.RatingSpecContainer; import io.restassured.filter.log.LogDetail; - import io.restassured.path.json.JsonPath; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.io.IOException; +import java.sql.SQLException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import javax.servlet.http.HttpServletResponse; import mil.army.usace.hec.cwms.rating.io.xml.RatingContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSpecXmlFactory; @@ -61,32 +101,6 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.sql.SQLException; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.TreeMap; - -import hec.data.RatingException; -import hec.data.cwmsRating.io.RatingSetContainer; -import hec.data.cwmsRating.io.RatingSpecContainer; - -import static cwms.cda.api.Controllers.*; -import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER; -import static helpers.FloatCloseTo.floatCloseTo; -import static io.restassured.RestAssured.given; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - @Tag("integration") public class LevelsControllerTestIT extends DataApiTestIT { @@ -2414,6 +2428,42 @@ void test_get_aliases() throws Exception { ; } + @Test + void test_create_subminute_level() throws Exception { + String locName = "subminuteloc"; + createLocation(locName, true, OFFICE); + String levelId = String.format("%s.Elev.Ave.1Day.Top of Inlet", locName); + ZonedDateTime time = ZonedDateTime.ofInstant(Instant.parse("2024-01-01T00:00:25Z"), ZoneId.of("UTC")); + ConstantLocationLevel level = new ConstantLocationLevel.Builder(levelId, time.toInstant()) + .withOfficeId(OFFICE) + .withLevelUnitsId("ft") + .withConstantValue(8675.309) + .withExpirationDate(time.toInstant()) + .build(); + + String levelJson = Formats.format(new ContentType(Formats.JSONV2), level); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, OFFICE) + .header("Authorization", TestAccounts.KeyUser.SPK_NORMAL.toHeaderValue()) + .body(levelJson) + .contentType(Formats.JSONV2) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .post("/levels/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_BAD_REQUEST)) + .body("details.message", is("Level effective date cannot have seconds")) + .body("message", is("Bad Request")) + .body("source", is("User Input")); + } + enum GetAllTestNewAliases { DEFAULT(Formats.DEFAULT, Formats.JSONV2), JSON(Formats.JSON, Formats.JSONV2), From 520783e7a95e2cce62802fe3ee9e7b5f0c440185 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Fri, 17 Jul 2026 13:57:16 -0700 Subject: [PATCH 2/5] Added integration test for subminute level date. Updated Swagger UI examples for levels. --- .../data/levels/levels_constant_create.json | 2 +- .../data/levels/levels_seasonal_create.json | 4 +-- .../data/levels/levels_timeseries_create.json | 2 +- .../cwms/cda/api/LevelsControllerTestIT.java | 36 +++++++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_constant_create.json b/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_constant_create.json index c93cca2a0..f2b2f8c50 100644 --- a/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_constant_create.json +++ b/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_constant_create.json @@ -1,7 +1,7 @@ { "constant-value": 10, "level-units-id": "ft", - "level-date": "2008-12-03T10:15:30+01:00[UTC]", + "level-date": "2008-12-03T10:15:00+01:00[UTC]", "level-comment": "Lowest Point", "interpolate-string": "F", "location-level-id": "Sacramento.Elev.Inst.0.Bottom of Inlet", diff --git a/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_seasonal_create.json b/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_seasonal_create.json index fb2588a89..b3fcc8594 100644 --- a/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_seasonal_create.json +++ b/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_seasonal_create.json @@ -11,9 +11,9 @@ } ], "level-units-id": "ft", - "level-date": "2008-12-03T10:15:30+01:00[UTC]", + "level-date": "2008-12-03T10:15:00+01:00[UTC]", "level-comment": "Lowest Point", - "interval-origin": "2008-12-03T10:15:30+01:00[UTC]", + "interval-origin": "2008-12-03T10:15:00+01:00[UTC]", "interval-months": 1, "interpolate-string": "F", "location-level-id": "Sacramento.Elev.Inst.0.Bottom of Inlet", diff --git a/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_timeseries_create.json b/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_timeseries_create.json index a2a84c832..ec4c1205a 100644 --- a/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_timeseries_create.json +++ b/cwms-data-api/src/main/resources/cwms/cda/data/levels/levels_timeseries_create.json @@ -1,7 +1,7 @@ { "seasonal-time-series-id": "Sacramento.Stage.Inst.5Minutes.0.Bottom of Inlet", "level-units-id": "ft", - "level-date": "2008-12-03T10:15:30+01:00[UTC]", + "level-date": "2008-12-03T10:15:00+01:00[UTC]", "level-comment": "Bottom of Inlet, lowest level", "interpolate-string": "F", "location-level-id": "Sacramento.Elev.Inst.0.Bottom of Inlet", diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java index 8df8ede09..22e7bc0c9 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java @@ -2095,6 +2095,42 @@ void testStoreConstantLevel() throws Exception { .body("constant-value", equalTo(8675.309f)); } + @Test + void testStoreTimeRestrictedLevel() throws Exception { + String locName = "restrictedLoc123"; + createLocation(locName, true, OFFICE); + String levelId = String.format("%s.Elev.Ave.1Day.Regulating", locName); + ZonedDateTime time = ZonedDateTime.ofInstant(Instant.parse("2024-01-01T00:00:12Z"), ZoneId.of("UTC")); + ConstantLocationLevel level = new ConstantLocationLevel.Builder(levelId, time.toInstant()) + .withOfficeId(OFFICE) + .withLevelUnitsId("ft") + .withConstantValue(8675.309) + .withExpirationDate(time.plusYears(50).toInstant()) + .build(); + + String levelJson = Formats.format(new ContentType(Formats.JSONV2), level); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, OFFICE) + .header("Authorization", TestAccounts.KeyUser.SPK_NORMAL.toHeaderValue()) + .body(levelJson) + .contentType(Formats.JSONV2) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .post("/levels/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_BAD_REQUEST)) + .body("details.message", equalTo("Level effective date cannot have seconds")) + .body("source", equalTo("User Input")) + .body(MESSAGE, equalTo("Bad Request")); + } + enum GetAllTestLegacy { JSON(Formats.JSON_LEGACY, Formats.JSON), XML(Formats.XML_LEGACY, Formats.XML), From c4b7befe8265afe0a50c968fe20976f24f9b9536 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Fri, 17 Jul 2026 10:15:12 -0700 Subject: [PATCH 3/5] Resolve failing lock test --- .../data/dto/locationlevel/LocationLevel.java | 9 ++++++++- .../test/java/cwms/cda/api/LockControllerIT.java | 16 ++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java index 8ab3d09df..76262673e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java @@ -29,6 +29,8 @@ import cwms.cda.data.dto.catalog.LocationAlias; import java.math.BigDecimal; import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -95,7 +97,8 @@ public abstract class LocationLevel extends CwmsDTO { @Schema(description = "Units the provided levels are in") private final String levelUnitsId; - @Schema(description = "The date/time at which this location level configuration takes effect.") + @Schema(description = "The date/time at which this location level configuration takes effect. " + + "Must be limited to minute accuracy.") @JsonFormat(shape = JsonFormat.Shape.STRING) private final Instant levelDate; @@ -501,4 +504,8 @@ protected void validateInternal(CwmsDTOValidator validator) { validator.required(getLocationLevelId(), "location-level-id"); validator.required(getLevelDate(), "level-date"); } + + public static ZonedDateTime truncateDate(ZonedDateTime date) { + return date.truncatedTo(ChronoUnit.MINUTES); + } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LockControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LockControllerIT.java index cf0204d03..1303f1235 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LockControllerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LockControllerIT.java @@ -35,6 +35,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; +import com.google.common.flogger.FluentLogger; import cwms.cda.api.enums.Nation; import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dao.DeleteRule; @@ -45,10 +46,10 @@ import cwms.cda.data.dao.location.kind.LockDao; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.Location; -import cwms.cda.data.dto.locationlevel.ConstantLocationLevel; -import cwms.cda.data.dto.locationlevel.LocationLevel; import cwms.cda.data.dto.location.kind.Lock; import cwms.cda.data.dto.location.kind.LockLocationLevelRef; +import cwms.cda.data.dto.locationlevel.ConstantLocationLevel; +import cwms.cda.data.dto.locationlevel.LocationLevel; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import fixtures.CwmsDataApiSetupCallback; @@ -65,7 +66,6 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; -import com.google.common.flogger.FluentLogger; import javax.servlet.http.HttpServletResponse; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; import org.apache.commons.io.IOUtils; @@ -1049,28 +1049,28 @@ private static PROJECT_OBJ_T buildProject(Location projectLocation) { private List createLocationLevelList(Lock lock) { List retVal = new ArrayList<>(); - var lowLowerLevel = new ConstantLocationLevel.Builder(lock.getLowWaterLowerPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var lowLowerLevel = new ConstantLocationLevel.Builder(lock.getLowWaterLowerPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getLowWaterLowerPoolLocationLevel().getOfficeId()) .withSpecifiedLevelId(lock.getLowWaterLowerPoolLocationLevel().getSpecifiedLevelId()) .withConstantValue(lock.getLowWaterLowerPoolLocationLevel().getLevelValue()) .build(); retVal.add(lowLowerLevel); - var lowUpperLevel = new ConstantLocationLevel.Builder(lock.getLowWaterUpperPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var lowUpperLevel = new ConstantLocationLevel.Builder(lock.getLowWaterUpperPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getLowWaterUpperPoolLocationLevel().getOfficeId()) .withSpecifiedLevelId(lock.getLowWaterUpperPoolLocationLevel().getSpecifiedLevelId()) .withConstantValue(lock.getLowWaterUpperPoolLocationLevel().getLevelValue()) .build(); retVal.add(lowUpperLevel); - var highLowerLevel = new ConstantLocationLevel.Builder(lock.getHighWaterLowerPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var highLowerLevel = new ConstantLocationLevel.Builder(lock.getHighWaterLowerPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getHighWaterLowerPoolLocationLevel().getOfficeId()) .withSpecifiedLevelId(lock.getHighWaterLowerPoolLocationLevel().getSpecifiedLevelId()) .withConstantValue(lock.getHighWaterLowerPoolLocationLevel().getLevelValue()) .build(); retVal.add(highLowerLevel); - var highUpperLevel = new ConstantLocationLevel.Builder(lock.getHighWaterUpperPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var highUpperLevel = new ConstantLocationLevel.Builder(lock.getHighWaterUpperPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getHighWaterUpperPoolLocationLevel().getOfficeId()) @@ -1078,7 +1078,7 @@ private List createLocationLevelList(Lock lock) { .withConstantValue(lock.getHighWaterUpperPoolLocationLevel().getLevelValue()) .build(); retVal.add(highUpperLevel); - var warningBuffer = new ConstantLocationLevel.Builder(String.format("%s.Elev-Closure.Inst.0.Warning Buffer", lock.getLocation().getName()), ZonedDateTime.now().toInstant()) + var warningBuffer = new ConstantLocationLevel.Builder(String.format("%s.Elev-Closure.Inst.0.Warning Buffer", lock.getLocation().getName()), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getLocation().getOfficeId()) .withSpecifiedLevelId("Warning Buffer") From d0acd6d9671db3ecea2de3488ba0aeaeb48e8b41 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Fri, 17 Jul 2026 16:11:12 -0700 Subject: [PATCH 4/5] Fixed broken test --- .../cwms/cda/data/dao/location/kind/LockDaoIT.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/LockDaoIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/LockDaoIT.java index 485771c9c..4d0f11fab 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/LockDaoIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/LockDaoIT.java @@ -330,35 +330,35 @@ private Lock storeLocLevelsAndBuildStorableLock(Lock lock) throws SQLException { private List createLocationLevelList(Lock lock) { List retVal = new ArrayList<>(); - var lowLowerLevel = new ConstantLocationLevel.Builder(lock.getLowWaterLowerPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var lowLowerLevel = new ConstantLocationLevel.Builder(lock.getLowWaterLowerPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getLowWaterLowerPoolLocationLevel().getOfficeId()) .withSpecifiedLevelId(lock.getLowWaterLowerPoolLocationLevel().getSpecifiedLevelId()) .withConstantValue(lock.getLowWaterLowerPoolLocationLevel().getLevelValue()) .build(); retVal.add(lowLowerLevel); - var lowUpperLevel = new ConstantLocationLevel.Builder(lock.getLowWaterUpperPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var lowUpperLevel = new ConstantLocationLevel.Builder(lock.getLowWaterUpperPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getLowWaterUpperPoolLocationLevel().getOfficeId()) .withSpecifiedLevelId(lock.getLowWaterUpperPoolLocationLevel().getSpecifiedLevelId()) .withConstantValue(lock.getLowWaterUpperPoolLocationLevel().getLevelValue()) .build(); retVal.add(lowUpperLevel); - var highLowerLevel = new ConstantLocationLevel.Builder(lock.getHighWaterLowerPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var highLowerLevel = new ConstantLocationLevel.Builder(lock.getHighWaterLowerPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getHighWaterLowerPoolLocationLevel().getOfficeId()) .withSpecifiedLevelId(lock.getHighWaterLowerPoolLocationLevel().getSpecifiedLevelId()) .withConstantValue(lock.getHighWaterLowerPoolLocationLevel().getLevelValue()) .build(); retVal.add(highLowerLevel); - var highUpperLevel = new ConstantLocationLevel.Builder(lock.getHighWaterUpperPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var highUpperLevel = new ConstantLocationLevel.Builder(lock.getHighWaterUpperPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getHighWaterUpperPoolLocationLevel().getOfficeId()) .withSpecifiedLevelId(lock.getHighWaterUpperPoolLocationLevel().getSpecifiedLevelId()) .withConstantValue(lock.getHighWaterUpperPoolLocationLevel().getLevelValue()) .build(); retVal.add(highUpperLevel); - var warningBuffer = new ConstantLocationLevel.Builder(String.format("%s.Elev-Closure.Inst.0.Warning Buffer", lock.getLocation().getName()), ZonedDateTime.now().toInstant()) + var warningBuffer = new ConstantLocationLevel.Builder(String.format("%s.Elev-Closure.Inst.0.Warning Buffer", lock.getLocation().getName()), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getLocation().getOfficeId()) .withSpecifiedLevelId("Warning Buffer") From 840c76ae7f4bab029d4375f1c7dfd60bbc28e4eb Mon Sep 17 00:00:00 2001 From: zack-rma Date: Mon, 20 Jul 2026 09:27:29 -0700 Subject: [PATCH 5/5] Cleanup of date truncation --- .../java/cwms/cda/api/LevelsController.java | 2 +- .../data/dto/locationlevel/LocationLevel.java | 24 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java index 5b014c5e4..557dbb4bd 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java @@ -124,7 +124,7 @@ public void create(@NotNull Context ctx) { LocationLevel level = deserializeLocationLevel(ctx); level.validate(); - if (!level.getLevelDate().truncatedTo(ChronoUnit.MINUTES).equals(level.getLevelDate())) { + if (!LocationLevel.truncateDate(level.getLevelDate()).equals(level.getLevelDate())) { Map errorDetails = new HashMap<>(); errorDetails.put("message", "Level effective date cannot have seconds"); throw new BadRequestResponse("", errorDetails); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java index 76262673e..8f6b15951 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java @@ -26,16 +26,6 @@ package cwms.cda.data.dto.locationlevel; -import cwms.cda.data.dto.catalog.LocationAlias; -import java.math.BigDecimal; -import java.time.Instant; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -49,6 +39,7 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import cwms.cda.data.dto.CwmsDTO; import cwms.cda.data.dto.CwmsDTOValidator; +import cwms.cda.data.dto.catalog.LocationAlias; import cwms.cda.formatters.Formats; import cwms.cda.formatters.UnsupportedFormatException; import cwms.cda.formatters.annotations.FormattableWith; @@ -56,6 +47,15 @@ import cwms.cda.formatters.json.JsonV2; import cwms.cda.formatters.xml.XMLv2; import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; @JsonRootName("LocationLevel") @JsonDeserialize(builder = LocationLevel.Builder.class) @@ -508,4 +508,8 @@ protected void validateInternal(CwmsDTOValidator validator) { public static ZonedDateTime truncateDate(ZonedDateTime date) { return date.truncatedTo(ChronoUnit.MINUTES); } + + public static Instant truncateDate(Instant date) { + return date.truncatedTo(ChronoUnit.MINUTES); + } }