OBPIH-7559 allow APIs to accept date-only strings when binding Instants#5598
OBPIH-7559 allow APIs to accept date-only strings when binding Instants#5598
Conversation
ewaterman
left a comment
There was a problem hiding this comment.
I'm marking this PR as do not merge until we've had a chance to discuss. If you guys decide you want to merge it so QA can get a jump on testing it, I'm okay with that.
|
|
||
| @Autowired | ||
| DateFormatterManager dateFormatter | ||
| DateFormatterManager dateFormatterManager |
There was a problem hiding this comment.
unrelated. I just realized that we can do away with the @Autowired if we make the variable the same as the class name
| // 2) Modify the date picker on the react side to send a full date + time + zone string | ||
| ZoneId zone = sessionManager.timezone?.toZoneId() | ||
| Instant dateCreatedAfter = params.createdAfter ? DateUtil.asInstant(params.createdAfter, zone) : null | ||
| Instant dateCreatedBefore = params.createdBefore ? DateUtil.asInstant(params.createdBefore, zone) : null |
There was a problem hiding this comment.
I don't like this solution long term, hence the TODO
There was a problem hiding this comment.
Also, just to note that I could have created an "asInstantInUserTimezone(String)" helper method somewhere but I chose not to because I don't want to make it easier to implement this in an "improper" way. When we do the date refactor, we should try to always do one or both of the steps mentioned in the TODO since that is the preferred approach.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #5598 +/- ##
============================================
- Coverage 9.12% 8.52% -0.60%
+ Complexity 1170 1127 -43
============================================
Files 701 712 +11
Lines 45281 45615 +334
Branches 10851 10914 +63
============================================
- Hits 4131 3890 -241
- Misses 40497 41146 +649
+ Partials 653 579 -74 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
kchelstowski
left a comment
There was a problem hiding this comment.
the tests were really helpful in understanding the context, thanks
There was a problem hiding this comment.
Pull Request Overview
This PR enables APIs to accept date-only strings (e.g., "2000-01-01") when binding to Instant fields by defaulting to midnight in the user's timezone. This change simplifies the migration from java.util.Date to java.time.Instant and fixes issues with the product list API filters.
Key changes:
- Modified
InstantValueConverterto accept date-only strings and default to midnight in user's timezone when timezone information is missing - Added
DateUtil.asInstant(String, ZoneId)method to handle string-to-Instant conversion with fallback timezone support - Updated
ProductServiceto properly handleInstantdate filters using the user's timezone
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
InstantValueConverter.groovy |
Delegates to DateUtil.asInstant() with user's timezone, enabling date-only string parsing |
InstantValueConverterSpec.groovy |
Adds comprehensive test coverage for date-only string parsing with and without default timezone |
DateUtil.groovy |
Implements new asInstant(String, ZoneId) and asLocalDate(String) methods, plus helper methods for timezone conversions |
LocalDateValueConverter.groovy |
Refactored to delegate to DateUtil.asLocalDate() for consistency |
DataBindingConstants.groovy |
Minor code style improvements (removed unnecessary class name prefix) |
ProductService.groovy |
Fixed product list date filters to work with Instant fields and corrected field name from dateFormatter to dateFormatterManager |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| LocalDate localDate = asLocalDate(date) | ||
| return asInstant(localDate, defaultZone) |
There was a problem hiding this comment.
The error handling in asInstant(String, ZoneId) has a potential bug. If the initial parse fails with a DateTimeException and defaultZone is provided, the code attempts to parse the string as a LocalDate at line 136. However, if this second parse also fails (e.g., for an invalid date string like "2000-01-32"), the exception from asLocalDate() will be thrown without context about the original error.
Consider wrapping the fallback parsing in a try-catch block and providing a more informative error message that indicates both parsing attempts failed:
try {
LocalDate localDate = asLocalDate(date)
return asInstant(localDate, defaultZone)
} catch (DateTimeException e2) {
// If both parsing attempts failed, throw the original exception with additional context
throw new DateTimeException("Failed to parse '${date}' as either a full datetime or a date-only string", e)
}| LocalDate localDate = asLocalDate(date) | |
| return asInstant(localDate, defaultZone) | |
| try { | |
| LocalDate localDate = asLocalDate(date) | |
| return asInstant(localDate, defaultZone) | |
| } catch (DateTimeException e2) { | |
| throw new DateTimeException("Failed to parse '${date}' as either a full datetime or a date-only string", e) | |
| } |
| Date dateCreatedAfter = params.createdAfter ? Date.parse("MM/dd/yyyy", params.createdAfter) : null | ||
| Date dateCreatedBefore = params.createdBefore ? Date.parse("MM/dd/yyyy", params.createdBefore) : null | ||
|
|
||
| // TODO: The date picker on the React size only sends up date information, but Instants need a time and zone. |
There was a problem hiding this comment.
The comment contains a typo: "React size" should be "React side".
Corrected text: "TODO: The date picker on the React side only sends up date information..."
| // TODO: The date picker on the React size only sends up date information, but Instants need a time and zone. | |
| // TODO: The date picker on the React side only sends up date information, but Instants need a time and zone. |
| * Parse a given String into a LocalDate of the given format. | ||
| * | ||
| * Because LocalDate is time and timezone agnostic, the String only expects day, month, and year elements. To avoid | ||
| * any timezone related confusion, Any additional data provided (such as time and timezone) will trigger an error. |
There was a problem hiding this comment.
The documentation comment has a capitalization error. "Any additional data provided" should not start with a capital 'A' after the comma in the previous sentence.
The sentence should read: "Because LocalDate is time and timezone agnostic, the String only expects day, month, and year elements. To avoid any timezone related confusion, any additional data provided (such as time and timezone) will trigger an error."
| * any timezone related confusion, Any additional data provided (such as time and timezone) will trigger an error. | |
| * any timezone related confusion, any additional data provided (such as time and timezone) will trigger an error. |
| * @param zone "Z", or "UTC" or "+01:00" or "America/Anchorage" for example. | ||
| */ | ||
| static Instant asInstant(LocalDate localDate, ZoneId zone) { | ||
| return localDate ? asInstant(asZonedDateTime(localDate, zone)) : null |
There was a problem hiding this comment.
The asInstant(LocalDate, ZoneId) method is missing null safety for the zone parameter. Unlike other methods in this class that have default zone handling (e.g., asZonedDateTime(Instant, ZoneId) which defaults to system zone), this method will throw a NullPointerException if zone is null and localDate is not null.
Consider adding a null check or default value for the zone parameter:
static Instant asInstant(LocalDate localDate, ZoneId zone) {
if (!zone) {
throw new IllegalArgumentException("Zone cannot be null when converting LocalDate to Instant")
}
return localDate ? asInstant(asZonedDateTime(localDate, zone)) : null
}Alternatively, provide a default zone like other methods in this class do.
| return localDate ? asInstant(asZonedDateTime(localDate, zone)) : null | |
| ZoneId zoneToUse = zone ?: getSystemZoneId() | |
| return localDate ? asInstant(asZonedDateTime(localDate, zoneToUse)) : null |
| static ZonedDateTime asZonedDateTime(LocalDate localDate, ZoneId zone) { | ||
| return localDate ? localDate.atStartOfDay(zone) : null | ||
| } |
There was a problem hiding this comment.
Similar to asInstant(LocalDate, ZoneId), the asZonedDateTime(LocalDate, ZoneId) method lacks null safety for the zone parameter. If zone is null and localDate is not null, calling localDate.atStartOfDay(zone) will throw a NullPointerException.
Consider adding the same protection as suggested for asInstant(LocalDate, ZoneId) or provide a default zone value like zone=null with null-coalescing logic.
✨ Description of Change
Link to GitHub issue or Jira ticket: https://pihemr.atlassian.net/browse/OBPIH-7559
Description: Two fixes:
a) Modify the API to default to the user’s timezone at midnight.
b) Modify the date picker to send up a full ISO date (while still only displaying date?)
I chose to implement A because that's what I was familiar with, but I'm not certain that's the best solution. I don't like the idea of allowing the frontend to provide a date-only string to an Instant field because it requires the frontend to make assumptions about how the backend will handle that data (it has to assume that the backend will parse that as midnight in the user's time). I'd rather continue throwing an error in that scenario and require the frontend to pass the full ISO date (ie solution B) but I didn't feel confident enough to implement that into the date filter on the react side this close to release.
We should discuss this as a team and decide what we want to do
📷 Screenshots & Recordings (optional)
Before the fix:
2025-11-03.15-50-56.mp4
After: