Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
import java.time.OffsetDateTime;
import java.util.UUID;

import com.ironhack.infra.config.jackson.FlexibleOffsetDateTimeDeserializer;
import jakarta.validation.constraints.NotNull;
import tools.jackson.databind.annotation.JsonDeserialize;

public record BookAppointmentRequest(
@NotNull(message = "Patient ID is required") UUID patientId,
@NotNull(message = "Doctor ID is required") UUID doctorId,
@NotNull(message = "Appointment time is required") OffsetDateTime appointmentTime) {}

@NotNull(message = "Appointment time is required")
@JsonDeserialize(using = FlexibleOffsetDateTimeDeserializer.class)
OffsetDateTime appointmentTime) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.ironhack.infra.config.jackson;

import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.ValueDeserializer;

/**
* Accepts standard {@link OffsetDateTime} strings, or a local ISO-8601 datetime
* (e.g. {@code 2026-05-21T13:00}) using the JVM default zone.
*/
public class FlexibleOffsetDateTimeDeserializer extends ValueDeserializer<OffsetDateTime> {
@Override
public OffsetDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
String text = p.getValueAsString();
if (text == null || text.isBlank()) {
return null;
}
text = text.trim();
try {
return OffsetDateTime.parse(text);
} catch (DateTimeParseException ignored) {
// fall through
}
try {
LocalDateTime local = LocalDateTime.parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
return local.atZone(ZoneId.systemDefault()).toOffsetDateTime();
} catch (DateTimeException e) {
throw ctxt.weirdStringException(text, OffsetDateTime.class, e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ void shouldBookAppointmentSuccessfully() throws Exception {
.andExpect(jsonPath("$.data.status").value("SCHEDULED"));
}

@Test
@DisplayName("Should book appointment when appointment time is local ISO-8601 without offset")
void shouldBookAppointmentWithLocalIsoDateTime() throws Exception {
String body = """
{
"patientId": "%s",
"doctorId": "%s",
"appointmentTime": "2099-01-15T14:30"
}
""".formatted(patientId, doctorId);

mockMvc.perform(post("/v1/appointments")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.data.status").value("SCHEDULED"));
}

@Test
@DisplayName("Should return 400 when booking appointment with past time")
void shouldReturnBadRequestWhenAppointmentTimeInPast() throws Exception {
Expand Down
Loading