From 538646a348867329de85fb2fe6e773d8e08428dc Mon Sep 17 00:00:00 2001 From: AnonimProgrammer Date: Fri, 15 May 2026 02:40:12 +0400 Subject: [PATCH] docs: README.md updated. --- README.md | 10 +- docs/dtos-and-mappers.md | 5 +- docs/foundation-flow.md | 169 ---------------- docs/system-flow.md | 191 ++++++++++++++++++ .../application/dto/AppointmentDTO.java | 12 +- .../com/ironhack/application/dto/BaseDto.java | 21 ++ .../application/dto/DoctorAppointmentDTO.java | 12 +- .../ironhack/application/dto/DoctorDTO.java | 9 +- .../dto/PatientAppointmentDTO.java | 12 +- .../ironhack/application/dto/PatientDTO.java | 9 +- .../dto/query/AppointmentQueryCriteria.java | 5 +- .../dto/request/BookAppointmentRequest.java | 4 +- .../application/dto/response/ApiResponse.java | 10 +- .../appointment/BookAppointmentUseCase.java | 8 +- .../ironhack/domain/AppointmentEntity.java | 26 +-- .../java/com/ironhack/domain/BaseEntity.java | 37 ++++ .../com/ironhack/domain/DoctorEntity.java | 20 +- .../com/ironhack/domain/PatientEntity.java | 18 +- .../adapter/input/AppointmentRestAdapter.java | 6 +- .../adapter/input/DoctorRestAdapter.java | 6 +- .../adapter/input/PatientRestAdapter.java | 6 +- .../infra/adapter/mapper/DoctorMapper.java | 2 + .../infra/adapter/mapper/PatientMapper.java | 2 + .../adapter/output/AppointmentRepository.java | 4 +- .../AppointmentSpecificationFactory.java | 13 +- .../input/AppointmentRestAdapterTest.java | 32 +-- .../adapter/input/DoctorRestAdapterTest.java | 8 +- .../adapter/input/PatientRestAdapterTest.java | 6 +- 28 files changed, 361 insertions(+), 302 deletions(-) delete mode 100644 docs/foundation-flow.md create mode 100644 docs/system-flow.md create mode 100644 src/main/java/com/ironhack/application/dto/BaseDto.java create mode 100644 src/main/java/com/ironhack/domain/BaseEntity.java diff --git a/README.md b/README.md index 3fcbceb..b218600 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,15 @@ Simple web client: [MediCare UI](https://medicare-z79v.onrender.com) ### Architecture -- [System flow](docs/foundation-flow.md) — domain model, core flows, and simple business rules. +- [System flow](docs/system-flow.md) — domain model, core flows, and simple business rules. - [DTOs and MapStruct mappers](docs/dtos-and-mappers.md) — request/read DTOs, mappers, facade, and build notes. ### API documentation -**Official reference (not tied to your laptop)** — Start with [system flow](docs/foundation-flow.md): domain model, HTTP behaviour, status codes, and business rules. That document is the main human-readable contract for the API. +- **Official reference** — [System flow](docs/system-flow.md) is the main human-readable contract: domain model, HTTP behaviour, status codes, and business rules. It is not tied to a particular environment. -**OpenAPI (machine-readable, generated)** — The running service exposes OpenAPI 3 JSON at `/v3/api-docs` (see `springdoc.api-docs.path` in `application.properties`). For the deployed API, use `https://medicare-z79v.onrender.com/v3/api-docs`. For a server you start on your machine, use `http://localhost:8080/v3/api-docs`. +- **OpenAPI** — The running app serves generated OpenAPI 3 JSON at `/v3/api-docs` (see `springdoc.api-docs.path` in `application.properties`). Use [Deployed OpenAPI](https://medicare-z79v.onrender.com/v3/api-docs) for production, or [Local OpenAPI](http://localhost:8080/v3/api-docs) when the app runs on your machine. -**Interactive Swagger UI** — Same paths as above, with `/swagger-ui.html` (see `springdoc.swagger-ui.path`). Production: `https://medicare-z79v.onrender.com/swagger-ui.html`. **Local development only:** `http://localhost:8080/swagger-ui.html` when you are running the app locally; it is not the canonical place to read “official” docs, only a convenient explorer. +- **Interactive Swagger UI** — Explorer at `/swagger-ui.html` (see `springdoc.swagger-ui.path` in `application.properties`). Use [Deployed Swagger UI](https://medicare-z79v.onrender.com/swagger-ui.html) in production, or [Local Swagger UI](http://localhost:8080/swagger-ui.html) for a quick local try-out (not a substitute for the official reference). -- [Bruno collection](bruno/) — runnable HTTP requests for the REST API, grouped by resource. Open the `bruno` folder in [Bruno](https://www.usebruno.com/), then pick an environment: **local** (`environments/local.bru`, `http://localhost:8080`) or **prod** (`environments/prod.bru`, `https://medicare-z79v.onrender.com`, same host as [MediCare UI](#live-deployment)). +- **Bruno collection** — The [Bruno](https://www.usebruno.com/) collection lives under [bruno/](bruno/): runnable requests grouped by resource. Pick an environment in Bruno: **local** uses [local.bru](bruno/environments/local.bru) with base URL [Local API](http://localhost:8080); **prod** uses [prod.bru](bruno/environments/prod.bru) with base URL [Deployed API](https://medicare-z79v.onrender.com) (same host as [MediCare UI](#live-deployment)). diff --git a/docs/dtos-and-mappers.md b/docs/dtos-and-mappers.md index 0d96c43..4690518 100644 --- a/docs/dtos-and-mappers.md +++ b/docs/dtos-and-mappers.md @@ -236,10 +236,7 @@ MapStruct and Lombok annotation processing are enabled in `pom.xml`. Unmapped ta ## Possible next steps -- REST controllers and `@ControllerAdvice` for consistent error bodies -- Map domain exceptions to `ApiResponse` error shapes - Pagination metadata on list endpoints -- Auditing fields (`createdAt`, `updatedAt`) on entities and DTOs where needed - Read-side caching where response shapes are stable -For domain rules and flows, see [System flow](foundation-flow.md). +For domain rules and flows, see [System flow](system-flow.md). diff --git a/docs/foundation-flow.md b/docs/foundation-flow.md deleted file mode 100644 index 9d7e5e8..0000000 --- a/docs/foundation-flow.md +++ /dev/null @@ -1,169 +0,0 @@ -# MediCare — Foundation Flow - -This document describes the foundation of the **MediCare** clinic appointment system: its domain, the relationships between entities, the main user flows, and the business rules that the system must enforce. - ---- - -## 1. Purpose - -MediCare is a small clinic appointment management system. It lets a clinic register **patients** and **doctors**, and book/cancel **appointments** between them, while enforcing a few core scheduling rules. - ---- - -## 2. Domain Model - -The domain has three core entities and one supporting enum. - -### 2.1 Entity-Relationship Diagram - -```mermaid -erDiagram - PATIENT ||--o{ APPOINTMENT : "books" - DOCTOR ||--o{ APPOINTMENT : "serves" - DOCTOR }o--|| SPECIALTY : "has" - - PATIENT { - UUID id - String fullName - String phoneNumber - } - - DOCTOR { - UUID id - String fullName - Specialty specialty - } - - APPOINTMENT { - UUID id - UUID patientId - UUID doctorId - LocalDateTime appointmentTime - AppointmentStatus status - } - - SPECIALTY { - enum CARDIOLOGY - enum DENTIST - enum THERAPIST - } -``` - -### 2.2 Relationships - -- One **Doctor** has exactly **one Specialty** (enum value). -- One **Patient** can have **many Appointments**. -- One **Doctor** can have **many Appointments**. -- One **Appointment** belongs to exactly **one Patient** and **one Doctor**. - -### 2.3 Appointment Status - -An appointment moves through a small state machine: - -```mermaid -stateDiagram-v2 - [*] --> SCHEDULED : book appointment - SCHEDULED --> CANCELLED : cancel - SCHEDULED --> COMPLETED : visit finished - CANCELLED --> [*] - COMPLETED --> [*] -``` - -- `SCHEDULED` — booked, not yet completed or cancelled. -- `CANCELLED` — cancelled by patient/clinic; **terminal**. -- `COMPLETED` — visit has happened; **terminal**. - ---- - -## 3. Core Flows - -### 3.1 Book an Appointment - -The most important flow. It is also where most business rules are enforced. - -```mermaid -sequenceDiagram - actor Client - participant API as Appointment API - participant Svc as Appointment Service - participant Repo as Appointment Repository - - Client->>API: POST /appointments {patientId, doctorId, time} - API->>Svc: bookAppointment(...) - Svc->>Repo: find patient & doctor - Svc->>Svc: validate time is in the future - Svc->>Repo: find doctor's appointments at requested time - Svc->>Svc: ensure no overlap - Svc->>Repo: save appointment (status = SCHEDULED) - Repo-->>Svc: saved appointment - Svc-->>API: appointment DTO - API-->>Client: 201 Created -``` - -### 3.2 Cancel an Appointment - -```mermaid -flowchart TD - A[Client requests cancel] --> B{Appointment exists?} - B -- No --> E[404 Not Found] - B -- Yes --> C{Status = SCHEDULED?} - C -- No --> F[409 Conflict: cannot cancel] - C -- Yes --> D[Set status = CANCELLED] - D --> G[200 OK] -``` - -### 3.3 Complete an Appointment - -- **Endpoint:** `POST /v1/appointments/{id}/complete` -- Only appointments in **`SCHEDULED`** may be completed; response is **409 Conflict** if the appointment is already **CANCELLED** or **COMPLETED**. -- On success the status becomes **`COMPLETED`** (**terminal**). - -### 3.4 List Appointments by Date / Doctor Schedule - -- **By date** — query all appointments where `appointmentTime` falls within the given calendar day (in the JVM default time zone). -- **Doctor schedule** — query all appointments for a given `doctorId`, optionally filtered by date range, ordered by `appointmentTime`. -- **Patient appointments** — same optional filters on `GET /v1/patients/{id}/appointments`. -- **Clinic-wide list** — `GET /v1/appointments` returns full `AppointmentDTO` rows (patient and doctor embedded), ordered by `appointmentTime` ascending. - -**Query parameters** (all optional; filters are combined with **AND**): - -| Parameter | Meaning | -|-----------|---------| -| `status` | Repeat the parameter for multiple values, e.g. `status=SCHEDULED&status=COMPLETED`. Omit entirely to ignore status. | -| `date` | ISO calendar date (`YYYY-MM-DD`). Matches appointments from start-of-day through end-of-day in the JVM default zone. | -| `from` | Inclusive lower bound on `appointmentTime` as **UTC** `Instant` (ISO-8601, e.g. `2026-05-14T00:00:00Z`). Converted to `LocalDateTime` using the JVM default zone for comparison with stored times. | -| `to` | Inclusive upper bound on `appointmentTime`, same format and zone rules as `from`. | - -`from` must not be after `to` when both are present (otherwise the API returns **400 Bad Request**). - -### 3.5 View Patient Appointments - -Query all appointments for a given `patientId`, ordered by `appointmentTime` ascending (upcoming first). The same optional query parameters as in §3.4 apply on `GET /v1/patients/{id}/appointments`. - -### 3.6 Delete Patient or Doctor - -| Method | Path | Success | Missing entity | -|--------|------|---------|----------------| -| `DELETE` | `/v1/patients/{id}` | **200 OK** with envelope message `Patient deleted successfully.` | **404** `Patient not found.` | -| `DELETE` | `/v1/doctors/{id}` | **200 OK** with envelope message `Doctor deleted successfully.` | **404** `Doctor not found.` | - -**Related appointments (product rule):** deleting a patient or doctor performs a **hard delete** of that entity and **cascade-deletes every appointment** that referenced them (all statuses). The API does not orphan appointment rows. - ---- - -## 4. Business Rules - -These are invariants the service layer must enforce. They are independent of the storage technology. - -| # | Rule | Where enforced | -|---|------|----------------| -| 1 | A doctor cannot have **overlapping appointments** | `AppointmentService.book(...)` | -| 2 | An appointment time **cannot be in the past** | `AppointmentService.book(...)` | -| 3 | A **cancelled appointment cannot be completed** | `CompleteAppointmentUseCase` | -| 4 | A **cancelled or completed appointment cannot be cancelled again** | `AppointmentService.cancel(...)` | -| 5 | A doctor must have a **specialty** assigned | `DoctorService.create(...)` | -| 6 | Deleting a **patient** or **doctor** **cascade-deletes** all appointments that reference them | `DeletePatientUseCase`, `DeleteDoctorUseCase` | - -> Rule #1 currently treats "overlap" as **same `appointmentTime` for the same doctor**. If appointment durations are introduced later, this rule should evolve into a real time-range overlap check. - ---- diff --git a/docs/system-flow.md b/docs/system-flow.md new file mode 100644 index 0000000..d1f93a3 --- /dev/null +++ b/docs/system-flow.md @@ -0,0 +1,191 @@ +# MediCare — System Flow + +This document is the human-readable contract for **MediCare**: domain model, HTTP behaviour, main flows, and business rules the application enforces today. + +--- + +## 1. Purpose + +MediCare is a small clinic appointment system. A clinic registers **patients** and **doctors**, books **appointments** between them, and can cancel or complete visits while a few scheduling rules are enforced. + +--- + +## 2. Domain model + +The domain has three JPA entities, one supporting enum, and a shared persistence base. + +### 2.1 Auditing and identifiers + +All entities extend **`BaseEntity`** (`@MappedSuperclass`): **`id`** (UUID, JPA-generated), **`createdAt`** / **`updatedAt`** (`OffsetDateTime`, Hibernate `@CreationTimestamp` / `@UpdateTimestamp`). Read-side DTOs expose the same audit fields via **`BaseDto`**. + +### 2.2 Entity–relationship diagram + +```mermaid +erDiagram + PATIENT ||--o{ APPOINTMENT : "books" + DOCTOR ||--o{ APPOINTMENT : "serves" + DOCTOR }o--|| SPECIALTY : "has" + + PATIENT { + UUID id + OffsetDateTime createdAt + OffsetDateTime updatedAt + String fullName + String phoneNumber + } + + DOCTOR { + UUID id + OffsetDateTime createdAt + OffsetDateTime updatedAt + String fullName + Specialty specialty + } + + APPOINTMENT { + UUID id + OffsetDateTime createdAt + OffsetDateTime updatedAt + OffsetDateTime appointmentTime + AppointmentStatus status + } + + SPECIALTY { + enum CARDIOLOGY + enum DENTIST + enum THERAPIST + } +``` + +### 2.3 Relationships + +- One **Doctor** has at most **one Specialty** (enum); it may be unset until assigned via the API. +- One **Patient** can have **many Appointments**. +- One **Doctor** can have **many Appointments**. +- One **Appointment** belongs to exactly **one Patient** and **one Doctor**. + +### 2.4 Appointment status + +```mermaid +stateDiagram-v2 + [*] --> SCHEDULED : book appointment + SCHEDULED --> CANCELLED : cancel + SCHEDULED --> COMPLETED : visit finished + CANCELLED --> [*] + COMPLETED --> [*] +``` + +- `SCHEDULED` — booked, not yet completed or cancelled. +- `CANCELLED` — cancelled; **terminal**. +- `COMPLETED` — visit finished; **terminal**. + +--- + +## 3. HTTP surface (summary) + +All REST paths are under **`/v1`**. Successful and error bodies use the **`ApiResponse`** envelope (status, message, optional `data`, optional `errorCode`, **`OffsetDateTime` timestamp**). + +| Concern | Method | Path | +|--------|--------|------| +| List patients | `GET` | `/v1/patients` | +| Create patient | `POST` | `/v1/patients` | +| Delete patient | `DELETE` | `/v1/patients/{id}` | +| Patient’s appointments | `GET` | `/v1/patients/{id}/appointments` | +| List doctors | `GET` | `/v1/doctors` | +| Create doctor | `POST` | `/v1/doctors` | +| Assign specialty | `PATCH` | `/v1/doctors/{id}/specialty` | +| Delete doctor | `DELETE` | `/v1/doctors/{id}` | +| Doctor’s appointments | `GET` | `/v1/doctors/{id}/appointments` | +| List appointments (clinic-wide) | `GET` | `/v1/appointments` | +| Book appointment | `POST` | `/v1/appointments` | +| Cancel appointment | `POST` | `/v1/appointments/{id}/cancel` | +| Complete appointment | `POST` | `/v1/appointments/{id}/complete` | + +--- + +## 4. Core flows + +### 4.1 Book an appointment + +Most rules are enforced here. Request body includes **`patientId`**, **`doctorId`**, and **`appointmentTime`** as **`OffsetDateTime`**. + +```mermaid +sequenceDiagram + actor Client + participant API as AppointmentRestAdapter + participant UC as BookAppointmentUseCase + participant Repo as Repositories + + Client->>API: POST /v1/appointments + API->>UC: invoke request + UC->>Repo: load patient and doctor + UC->>UC: validate future time and doctor specialty + UC->>Repo: check duplicate scheduled slot + UC->>Repo: save appointment SCHEDULED + UC-->>API: ApiResponse 201 with AppointmentDTO + API-->>Client: 201 Created +``` + +### 4.2 Cancel an appointment + +```mermaid +flowchart TD + A[Client POST cancel] --> B{Appointment exists?} + B -- No --> E[404 Not Found] + B -- Yes --> C{Status = SCHEDULED?} + C -- No --> F[409 Conflict] + C -- Yes --> D[Set CANCELLED] + D --> G[200 OK] +``` + +### 4.3 Complete an appointment + +- **Endpoint:** `POST /v1/appointments/{id}/complete` +- Only **`SCHEDULED`** appointments may be completed; **`CANCELLED`** or **`COMPLETED`** yields **409 Conflict**. +- Success sets status to **`COMPLETED`**. + +### 4.4 List and filter appointments + +Optional query parameters (combined with **AND**): `status` (repeatable), `date` (`YYYY-MM-DD`, full calendar day in the **JVM default zone**), `from` / `to` (inclusive bounds on **`appointmentTime`** as **`OffsetDateTime`**). + +| Parameter | Meaning | +|-----------|---------| +| `status` | Repeat for multiple values, e.g. `status=SCHEDULED&status=COMPLETED`. Omit to ignore. | +| `date` | Calendar day; matches `[startOfDay, nextDay)` in the default zone. | +| `from` | Inclusive lower bound, e.g. `2026-05-14T00:00:00Z`. | +| `to` | Inclusive upper bound; same style as `from`. | + +If both `from` and `to` are present, `from` must not be after `to` (**400 Bad Request**). + +- **`GET /v1/appointments`** — full **`AppointmentDTO`** (patient and doctor embedded), sorted by **`appointmentTime`** ascending. +- **`GET /v1/doctors/{id}/appointments`** — **`DoctorAppointmentDTO`** list (no nested doctor), same filters and sort. +- **`GET /v1/patients/{id}/appointments`** — **`PatientAppointmentDTO`** list (no nested patient), same filters and sort. + +### 4.5 Delete patient or doctor + +| Method | Path | Success | Missing entity | +|--------|------|---------|----------------| +| `DELETE` | `/v1/patients/{id}` | **200 OK**, message `Patient deleted successfully.` | **404** `Patient not found.` | +| `DELETE` | `/v1/doctors/{id}` | **200 OK**, message `Doctor deleted successfully.` | **404** `Doctor not found.` | + +**Appointments:** deletes call **`AppointmentRepository.deleteByPatient_Id`** / **`deleteByDoctor_Id`** before removing the parent row so no orphan appointments remain. + +--- + +## 5. Business rules + +Invariants enforced in the application layer (storage-agnostic wording): + +| # | Rule | Where enforced | +|---|------|----------------| +| 1 | A doctor cannot have another **SCHEDULED** appointment at the **same** **`appointmentTime`** | `BookAppointmentUseCase` | +| 2 | **`appointmentTime`** must be **strictly in the future** at booking (using the injected **`Clock`**) | `BookAppointmentUseCase` | +| 3 | Only **`SCHEDULED`** appointments may be **completed** | `CompleteAppointmentUseCase` | +| 4 | Only **`SCHEDULED`** appointments may be **cancelled** | `CancelAppointmentUseCase` | +| 5 | Booking requires the doctor to have a **non-null specialty** | `BookAppointmentUseCase` | +| 6 | Deleting a **patient** or **doctor** removes **all** of their appointments first | `DeletePatientUseCase`, `DeleteDoctorUseCase` | +| 7 | **Duplicate doctor full name** is rejected on create | `CreateDoctorUseCase` | + +> Rule **#1** is “same instant for same doctor,” not a duration overlap. If visit lengths are introduced later, this should become a real interval overlap check. + +--- diff --git a/src/main/java/com/ironhack/application/dto/AppointmentDTO.java b/src/main/java/com/ironhack/application/dto/AppointmentDTO.java index 8612df8..3423aed 100644 --- a/src/main/java/com/ironhack/application/dto/AppointmentDTO.java +++ b/src/main/java/com/ironhack/application/dto/AppointmentDTO.java @@ -1,23 +1,21 @@ package com.ironhack.application.dto; -import java.time.LocalDateTime; -import java.util.UUID; +import java.time.OffsetDateTime; import com.ironhack.domain.AppointmentStatus; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; @Getter @Setter @NoArgsConstructor @AllArgsConstructor -@Builder -public class AppointmentDTO { - private UUID id; - private LocalDateTime appointmentTime; +@SuperBuilder +public class AppointmentDTO extends BaseDto { + private OffsetDateTime appointmentTime; private AppointmentStatus status; private DoctorDTO doctor; private PatientDTO patient; diff --git a/src/main/java/com/ironhack/application/dto/BaseDto.java b/src/main/java/com/ironhack/application/dto/BaseDto.java new file mode 100644 index 0000000..ed947f7 --- /dev/null +++ b/src/main/java/com/ironhack/application/dto/BaseDto.java @@ -0,0 +1,21 @@ +package com.ironhack.application.dto; + +import java.time.OffsetDateTime; +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public abstract class BaseDto { + private UUID id; + private OffsetDateTime createdAt; + private OffsetDateTime updatedAt; +} diff --git a/src/main/java/com/ironhack/application/dto/DoctorAppointmentDTO.java b/src/main/java/com/ironhack/application/dto/DoctorAppointmentDTO.java index 9c0fd39..4393826 100644 --- a/src/main/java/com/ironhack/application/dto/DoctorAppointmentDTO.java +++ b/src/main/java/com/ironhack/application/dto/DoctorAppointmentDTO.java @@ -1,23 +1,21 @@ package com.ironhack.application.dto; -import java.time.LocalDateTime; -import java.util.UUID; +import java.time.OffsetDateTime; import com.ironhack.domain.AppointmentStatus; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; @Getter @Setter @NoArgsConstructor @AllArgsConstructor -@Builder -public class DoctorAppointmentDTO { - private UUID id; - private LocalDateTime appointmentTime; +@SuperBuilder +public class DoctorAppointmentDTO extends BaseDto { + private OffsetDateTime appointmentTime; private AppointmentStatus status; private PatientDTO patient; } diff --git a/src/main/java/com/ironhack/application/dto/DoctorDTO.java b/src/main/java/com/ironhack/application/dto/DoctorDTO.java index a08e6ed..593de8e 100644 --- a/src/main/java/com/ironhack/application/dto/DoctorDTO.java +++ b/src/main/java/com/ironhack/application/dto/DoctorDTO.java @@ -1,21 +1,18 @@ package com.ironhack.application.dto; -import java.util.UUID; - import com.ironhack.domain.Specialty; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; @Getter @Setter @NoArgsConstructor @AllArgsConstructor -@Builder -public class DoctorDTO { - private UUID id; +@SuperBuilder +public class DoctorDTO extends BaseDto { private String fullName; private Specialty specialty; } diff --git a/src/main/java/com/ironhack/application/dto/PatientAppointmentDTO.java b/src/main/java/com/ironhack/application/dto/PatientAppointmentDTO.java index cc528ce..9bf322f 100644 --- a/src/main/java/com/ironhack/application/dto/PatientAppointmentDTO.java +++ b/src/main/java/com/ironhack/application/dto/PatientAppointmentDTO.java @@ -1,23 +1,21 @@ package com.ironhack.application.dto; -import java.time.LocalDateTime; -import java.util.UUID; +import java.time.OffsetDateTime; import com.ironhack.domain.AppointmentStatus; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; @Getter @Setter @NoArgsConstructor @AllArgsConstructor -@Builder -public class PatientAppointmentDTO { - private UUID id; - private LocalDateTime appointmentTime; +@SuperBuilder +public class PatientAppointmentDTO extends BaseDto { + private OffsetDateTime appointmentTime; private AppointmentStatus status; private DoctorDTO doctor; } diff --git a/src/main/java/com/ironhack/application/dto/PatientDTO.java b/src/main/java/com/ironhack/application/dto/PatientDTO.java index ea51f68..d9fe7cb 100644 --- a/src/main/java/com/ironhack/application/dto/PatientDTO.java +++ b/src/main/java/com/ironhack/application/dto/PatientDTO.java @@ -1,20 +1,17 @@ package com.ironhack.application.dto; -import java.util.UUID; - import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; @Getter @Setter @NoArgsConstructor @AllArgsConstructor -@Builder -public class PatientDTO { - private UUID id; +@SuperBuilder +public class PatientDTO extends BaseDto { private String fullName; private String phoneNumber; } diff --git a/src/main/java/com/ironhack/application/dto/query/AppointmentQueryCriteria.java b/src/main/java/com/ironhack/application/dto/query/AppointmentQueryCriteria.java index 3160643..971813f 100644 --- a/src/main/java/com/ironhack/application/dto/query/AppointmentQueryCriteria.java +++ b/src/main/java/com/ironhack/application/dto/query/AppointmentQueryCriteria.java @@ -1,12 +1,13 @@ package com.ironhack.application.dto.query; -import java.time.Instant; import java.time.LocalDate; +import java.time.OffsetDateTime; import java.util.List; import com.ironhack.domain.AppointmentStatus; -public record AppointmentQueryCriteria(List statuses, LocalDate date, Instant from, Instant to) { +public record AppointmentQueryCriteria( + List statuses, LocalDate date, OffsetDateTime from, OffsetDateTime to) { public AppointmentQueryCriteria { statuses = statuses == null ? List.of() : List.copyOf(statuses); diff --git a/src/main/java/com/ironhack/application/dto/request/BookAppointmentRequest.java b/src/main/java/com/ironhack/application/dto/request/BookAppointmentRequest.java index 1d728d2..0dda0eb 100644 --- a/src/main/java/com/ironhack/application/dto/request/BookAppointmentRequest.java +++ b/src/main/java/com/ironhack/application/dto/request/BookAppointmentRequest.java @@ -1,6 +1,6 @@ package com.ironhack.application.dto.request; -import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.UUID; import jakarta.validation.constraints.NotNull; @@ -8,4 +8,4 @@ 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") LocalDateTime appointmentTime) {} + @NotNull(message = "Appointment time is required") OffsetDateTime appointmentTime) {} diff --git a/src/main/java/com/ironhack/application/dto/response/ApiResponse.java b/src/main/java/com/ironhack/application/dto/response/ApiResponse.java index 761f045..f2a1b4b 100644 --- a/src/main/java/com/ironhack/application/dto/response/ApiResponse.java +++ b/src/main/java/com/ironhack/application/dto/response/ApiResponse.java @@ -1,6 +1,6 @@ package com.ironhack.application.dto.response; -import java.time.Instant; +import java.time.OffsetDateTime; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.*; @@ -16,14 +16,14 @@ public class ApiResponse { private String message; private T data; private String errorCode; - private Instant timestamp; + private OffsetDateTime timestamp; public static ApiResponse success(T data, String message) { return ApiResponse.builder() .status(200) .message(message) .data(data) - .timestamp(Instant.now()) + .timestamp(OffsetDateTime.now()) .build(); } @@ -32,7 +32,7 @@ public static ApiResponse created(T data, String message) { .status(201) .message(message) .data(data) - .timestamp(Instant.now()) + .timestamp(OffsetDateTime.now()) .build(); } @@ -46,7 +46,7 @@ public static ApiResponse error(int status, String message, String errorC .message(message) .errorCode(errorCode) .data(data) - .timestamp(Instant.now()) + .timestamp(OffsetDateTime.now()) .build(); } } diff --git a/src/main/java/com/ironhack/application/usecase/appointment/BookAppointmentUseCase.java b/src/main/java/com/ironhack/application/usecase/appointment/BookAppointmentUseCase.java index 929d60e..161efe4 100644 --- a/src/main/java/com/ironhack/application/usecase/appointment/BookAppointmentUseCase.java +++ b/src/main/java/com/ironhack/application/usecase/appointment/BookAppointmentUseCase.java @@ -1,7 +1,7 @@ package com.ironhack.application.usecase.appointment; import java.time.Clock; -import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.UUID; import org.springframework.stereotype.Service; @@ -51,8 +51,8 @@ public ApiResponse invoke(BookAppointmentRequest request) { return ApiResponse.created(dto, "Appointment booked successfully."); } - private void ensureAppointmentTimeInFuture(LocalDateTime appointmentTime) { - if (appointmentTime.isBefore(LocalDateTime.now(clock))) { + private void ensureAppointmentTimeInFuture(OffsetDateTime appointmentTime) { + if (appointmentTime.isBefore(OffsetDateTime.now(clock))) { throw new IllegalArgumentException("Appointment time must not be in the past."); } } @@ -70,7 +70,7 @@ private DoctorEntity requireDoctorWithSpecialty(UUID doctorId) { return doctor; } - private void ensureDoctorHasNoSchedulingConflict(UUID doctorId, LocalDateTime appointmentTime) { + private void ensureDoctorHasNoSchedulingConflict(UUID doctorId, OffsetDateTime appointmentTime) { if (appointmentRepository.existsByDoctor_IdAndAppointmentTimeAndStatus( doctorId, appointmentTime, AppointmentStatus.SCHEDULED)) { throw new ConflictException("The doctor already has a scheduled appointment at this time."); diff --git a/src/main/java/com/ironhack/domain/AppointmentEntity.java b/src/main/java/com/ironhack/domain/AppointmentEntity.java index 9eae0dc..4ce1708 100644 --- a/src/main/java/com/ironhack/domain/AppointmentEntity.java +++ b/src/main/java/com/ironhack/domain/AppointmentEntity.java @@ -1,27 +1,27 @@ package com.ironhack.domain; -import java.time.LocalDateTime; -import java.util.UUID; +import java.time.OffsetDateTime; -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; @Entity @Table(name = "appointments") @Getter @Setter @NoArgsConstructor -@AllArgsConstructor -@Builder -public class AppointmentEntity { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - +@SuperBuilder +public class AppointmentEntity extends BaseEntity { @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "patient_id", nullable = false) private PatientEntity patient; @@ -31,7 +31,7 @@ public class AppointmentEntity { private DoctorEntity doctor; @Column(name = "appointment_time", nullable = false) - private LocalDateTime appointmentTime; + private OffsetDateTime appointmentTime; @Enumerated(EnumType.STRING) @Column(nullable = false) diff --git a/src/main/java/com/ironhack/domain/BaseEntity.java b/src/main/java/com/ironhack/domain/BaseEntity.java new file mode 100644 index 0000000..de3451f --- /dev/null +++ b/src/main/java/com/ironhack/domain/BaseEntity.java @@ -0,0 +1,37 @@ +package com.ironhack.domain; + +import java.time.OffsetDateTime; +import java.util.UUID; + +import jakarta.persistence.Column; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +@MappedSuperclass +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public abstract class BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private OffsetDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private OffsetDateTime updatedAt; +} diff --git a/src/main/java/com/ironhack/domain/DoctorEntity.java b/src/main/java/com/ironhack/domain/DoctorEntity.java index c6f4b53..5fdbcbe 100644 --- a/src/main/java/com/ironhack/domain/DoctorEntity.java +++ b/src/main/java/com/ironhack/domain/DoctorEntity.java @@ -1,26 +1,22 @@ package com.ironhack.domain; -import java.util.UUID; - -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; @Entity @Table(name = "doctors") @Getter @Setter @NoArgsConstructor -@AllArgsConstructor -@Builder -public class DoctorEntity { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - +@SuperBuilder +public class DoctorEntity extends BaseEntity { @Column(name = "full_name", nullable = false) private String fullName; diff --git a/src/main/java/com/ironhack/domain/PatientEntity.java b/src/main/java/com/ironhack/domain/PatientEntity.java index 2bbb014..51314ad 100644 --- a/src/main/java/com/ironhack/domain/PatientEntity.java +++ b/src/main/java/com/ironhack/domain/PatientEntity.java @@ -1,26 +1,20 @@ package com.ironhack.domain; -import java.util.UUID; - -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; @Entity @Table(name = "patients") @Getter @Setter @NoArgsConstructor -@AllArgsConstructor -@Builder -public class PatientEntity { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - +@SuperBuilder +public class PatientEntity extends BaseEntity { @Column(name = "full_name", nullable = false) private String fullName; diff --git a/src/main/java/com/ironhack/infra/adapter/input/AppointmentRestAdapter.java b/src/main/java/com/ironhack/infra/adapter/input/AppointmentRestAdapter.java index 2c4fa7a..3fcd539 100644 --- a/src/main/java/com/ironhack/infra/adapter/input/AppointmentRestAdapter.java +++ b/src/main/java/com/ironhack/infra/adapter/input/AppointmentRestAdapter.java @@ -1,7 +1,7 @@ package com.ironhack.infra.adapter.input; -import java.time.Instant; import java.time.LocalDate; +import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -40,8 +40,8 @@ public class AppointmentRestAdapter { public ResponseEntity>> listAppointments( @RequestParam(name = "status", required = false) List status, @RequestParam(required = false) LocalDate date, - @RequestParam(required = false) Instant from, - @RequestParam(required = false) Instant to) { + @RequestParam(required = false) OffsetDateTime from, + @RequestParam(required = false) OffsetDateTime to) { var criteria = new AppointmentQueryCriteria(status, date, from, to); ApiResponse> body = searchAppointmentsUseCase.invoke(criteria); return ResponseEntity.ok(body); diff --git a/src/main/java/com/ironhack/infra/adapter/input/DoctorRestAdapter.java b/src/main/java/com/ironhack/infra/adapter/input/DoctorRestAdapter.java index 90e3331..9488e4c 100644 --- a/src/main/java/com/ironhack/infra/adapter/input/DoctorRestAdapter.java +++ b/src/main/java/com/ironhack/infra/adapter/input/DoctorRestAdapter.java @@ -1,7 +1,7 @@ package com.ironhack.infra.adapter.input; -import java.time.Instant; import java.time.LocalDate; +import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -64,8 +64,8 @@ public ResponseEntity>> listDoctorAppoint @PathVariable UUID id, @RequestParam(name = "status", required = false) List status, @RequestParam(required = false) LocalDate date, - @RequestParam(required = false) Instant from, - @RequestParam(required = false) Instant to) { + @RequestParam(required = false) OffsetDateTime from, + @RequestParam(required = false) OffsetDateTime to) { var criteria = new AppointmentQueryCriteria(status, date, from, to); ApiResponse> body = listDoctorAppointmentsUseCase.invoke(id, criteria); return ResponseEntity.ok(body); diff --git a/src/main/java/com/ironhack/infra/adapter/input/PatientRestAdapter.java b/src/main/java/com/ironhack/infra/adapter/input/PatientRestAdapter.java index cf8f165..3f2d9ab 100644 --- a/src/main/java/com/ironhack/infra/adapter/input/PatientRestAdapter.java +++ b/src/main/java/com/ironhack/infra/adapter/input/PatientRestAdapter.java @@ -1,7 +1,7 @@ package com.ironhack.infra.adapter.input; -import java.time.Instant; import java.time.LocalDate; +import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -61,8 +61,8 @@ public ResponseEntity>> listPatientAppoi @PathVariable UUID id, @RequestParam(name = "status", required = false) List status, @RequestParam(required = false) LocalDate date, - @RequestParam(required = false) Instant from, - @RequestParam(required = false) Instant to) { + @RequestParam(required = false) OffsetDateTime from, + @RequestParam(required = false) OffsetDateTime to) { var criteria = new AppointmentQueryCriteria(status, date, from, to); ApiResponse> body = listPatientAppointmentsUseCase.invoke(id, criteria); return ResponseEntity.ok(body); diff --git a/src/main/java/com/ironhack/infra/adapter/mapper/DoctorMapper.java b/src/main/java/com/ironhack/infra/adapter/mapper/DoctorMapper.java index 95baf72..8be70d0 100644 --- a/src/main/java/com/ironhack/infra/adapter/mapper/DoctorMapper.java +++ b/src/main/java/com/ironhack/infra/adapter/mapper/DoctorMapper.java @@ -16,5 +16,7 @@ public interface DoctorMapper { List toDoctorDTOList(List doctorEntities); @Mapping(target = "id", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) DoctorEntity toDoctorEntity(CreateDoctorRequest createDoctorRequest); } diff --git a/src/main/java/com/ironhack/infra/adapter/mapper/PatientMapper.java b/src/main/java/com/ironhack/infra/adapter/mapper/PatientMapper.java index 036a79d..5fc96c3 100644 --- a/src/main/java/com/ironhack/infra/adapter/mapper/PatientMapper.java +++ b/src/main/java/com/ironhack/infra/adapter/mapper/PatientMapper.java @@ -16,5 +16,7 @@ public interface PatientMapper { List toPatientDTOList(List patientEntities); @Mapping(target = "id", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) PatientEntity toPatientEntity(CreatePatientRequest createPatientRequest); } diff --git a/src/main/java/com/ironhack/infra/adapter/output/AppointmentRepository.java b/src/main/java/com/ironhack/infra/adapter/output/AppointmentRepository.java index 99222ed..dbb56d1 100644 --- a/src/main/java/com/ironhack/infra/adapter/output/AppointmentRepository.java +++ b/src/main/java/com/ironhack/infra/adapter/output/AppointmentRepository.java @@ -1,6 +1,6 @@ package com.ironhack.infra.adapter.output; -import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.Optional; import java.util.UUID; @@ -17,7 +17,7 @@ public interface AppointmentRepository extends JpaRepository, JpaSpecificationExecutor { boolean existsByDoctor_IdAndAppointmentTimeAndStatus( - UUID doctorId, LocalDateTime appointmentTime, AppointmentStatus status); + UUID doctorId, OffsetDateTime appointmentTime, AppointmentStatus status); @Modifying void deleteByPatient_Id(UUID patientId); diff --git a/src/main/java/com/ironhack/infra/adapter/output/specification/AppointmentSpecificationFactory.java b/src/main/java/com/ironhack/infra/adapter/output/specification/AppointmentSpecificationFactory.java index 2d89191..4637027 100644 --- a/src/main/java/com/ironhack/infra/adapter/output/specification/AppointmentSpecificationFactory.java +++ b/src/main/java/com/ironhack/infra/adapter/output/specification/AppointmentSpecificationFactory.java @@ -1,6 +1,6 @@ package com.ironhack.infra.adapter.output.specification; -import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; @@ -39,20 +39,19 @@ public Specification forFilteredList( } if (criteria.date() != null) { - LocalDateTime dayStart = criteria.date().atStartOfDay(); - LocalDateTime dayEndExclusive = criteria.date().plusDays(1).atStartOfDay(); + OffsetDateTime dayStart = criteria.date().atStartOfDay(ZONE).toOffsetDateTime(); + OffsetDateTime dayEndExclusive = + criteria.date().plusDays(1).atStartOfDay(ZONE).toOffsetDateTime(); predicates.add(cb.greaterThanOrEqualTo(root.get("appointmentTime"), dayStart)); predicates.add(cb.lessThan(root.get("appointmentTime"), dayEndExclusive)); } if (criteria.from() != null) { - LocalDateTime fromLdt = LocalDateTime.ofInstant(criteria.from(), ZONE); - predicates.add(cb.greaterThanOrEqualTo(root.get("appointmentTime"), fromLdt)); + predicates.add(cb.greaterThanOrEqualTo(root.get("appointmentTime"), criteria.from())); } if (criteria.to() != null) { - LocalDateTime toLdt = LocalDateTime.ofInstant(criteria.to(), ZONE); - predicates.add(cb.lessThanOrEqualTo(root.get("appointmentTime"), toLdt)); + predicates.add(cb.lessThanOrEqualTo(root.get("appointmentTime"), criteria.to())); } if (predicates.isEmpty()) { diff --git a/src/test/java/com/ironhack/infra/adapter/input/AppointmentRestAdapterTest.java b/src/test/java/com/ironhack/infra/adapter/input/AppointmentRestAdapterTest.java index 8cce6e7..8ded3ee 100644 --- a/src/test/java/com/ironhack/infra/adapter/input/AppointmentRestAdapterTest.java +++ b/src/test/java/com/ironhack/infra/adapter/input/AppointmentRestAdapterTest.java @@ -1,6 +1,6 @@ package com.ironhack.infra.adapter.input; -import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; @@ -73,7 +73,7 @@ void setup() { @Test @DisplayName("Should successfully book appointment with valid data") void shouldBookAppointmentSuccessfully() throws Exception { - LocalDateTime futureTime = LocalDateTime.now().plusDays(7); + OffsetDateTime futureTime = OffsetDateTime.now().plusDays(7); BookAppointmentRequest request = new BookAppointmentRequest(patientId, doctorId, futureTime); mockMvc.perform(post("/v1/appointments") @@ -88,7 +88,7 @@ void shouldBookAppointmentSuccessfully() throws Exception { @Test @DisplayName("Should return 400 when booking appointment with past time") void shouldReturnBadRequestWhenAppointmentTimeInPast() throws Exception { - LocalDateTime pastTime = LocalDateTime.now().minusDays(1); + OffsetDateTime pastTime = OffsetDateTime.now().minusDays(1); BookAppointmentRequest request = new BookAppointmentRequest(patientId, doctorId, pastTime); mockMvc.perform(post("/v1/appointments") @@ -100,7 +100,7 @@ void shouldReturnBadRequestWhenAppointmentTimeInPast() throws Exception { @Test @DisplayName("Should return 404 when patient does not exist") void shouldReturnNotFoundWhenPatientDoesNotExist() throws Exception { - LocalDateTime futureTime = LocalDateTime.now().plusDays(7); + OffsetDateTime futureTime = OffsetDateTime.now().plusDays(7); BookAppointmentRequest request = new BookAppointmentRequest(UUID.randomUUID(), doctorId, futureTime); mockMvc.perform(post("/v1/appointments") @@ -112,7 +112,7 @@ void shouldReturnNotFoundWhenPatientDoesNotExist() throws Exception { @Test @DisplayName("Should return 404 when doctor does not exist") void shouldReturnNotFoundWhenDoctorDoesNotExist() throws Exception { - LocalDateTime futureTime = LocalDateTime.now().plusDays(7); + OffsetDateTime futureTime = OffsetDateTime.now().plusDays(7); BookAppointmentRequest request = new BookAppointmentRequest(patientId, UUID.randomUUID(), futureTime); mockMvc.perform(post("/v1/appointments") @@ -126,7 +126,7 @@ void shouldReturnNotFoundWhenDoctorDoesNotExist() throws Exception { @Test @DisplayName("Should successfully cancel scheduled appointment") void shouldCancelAppointmentSuccessfully() throws Exception { - LocalDateTime futureTime = LocalDateTime.now().plusDays(7); + OffsetDateTime futureTime = OffsetDateTime.now().plusDays(7); BookAppointmentRequest bookRequest = new BookAppointmentRequest(patientId, doctorId, futureTime); // First, book an appointment @@ -159,7 +159,7 @@ void shouldReturnNotFoundWhenAppointmentDoesNotExist() throws Exception { @Test @DisplayName("Should return 409 when trying to cancel already cancelled appointment") void shouldReturnConflictWhenAppointmentAlreadyCancelled() throws Exception { - LocalDateTime futureTime = LocalDateTime.now().plusDays(7); + OffsetDateTime futureTime = OffsetDateTime.now().plusDays(7); BookAppointmentRequest bookRequest = new BookAppointmentRequest(patientId, doctorId, futureTime); // Book an appointment @@ -184,7 +184,7 @@ void shouldReturnConflictWhenAppointmentAlreadyCancelled() throws Exception { @Test @DisplayName("Should complete a scheduled appointment") void shouldCompleteScheduledAppointment() throws Exception { - LocalDateTime futureTime = LocalDateTime.now().plusDays(7); + OffsetDateTime futureTime = OffsetDateTime.now().plusDays(7); BookAppointmentRequest bookRequest = new BookAppointmentRequest(patientId, doctorId, futureTime); String response = mockMvc.perform(post("/v1/appointments") @@ -215,7 +215,7 @@ void shouldReturnNotFoundWhenCompletingNonExistentAppointment() throws Exception @Test @DisplayName("Should return 409 when trying to complete already completed appointment") void shouldReturnConflictWhenCompletingAlreadyCompletedAppointment() throws Exception { - LocalDateTime futureTime = LocalDateTime.now().plusDays(7); + OffsetDateTime futureTime = OffsetDateTime.now().plusDays(7); BookAppointmentRequest bookRequest = new BookAppointmentRequest(patientId, doctorId, futureTime); String response = mockMvc.perform(post("/v1/appointments") @@ -239,8 +239,8 @@ void shouldReturnConflictWhenCompletingAlreadyCompletedAppointment() throws Exce @Test @DisplayName("Should return patient appointments ordered by time, without nested patient field") void shouldListPatientAppointmentsOrderedByTime() throws Exception { - LocalDateTime later = LocalDateTime.now().plusDays(14); - LocalDateTime earlier = LocalDateTime.now().plusDays(7); + OffsetDateTime later = OffsetDateTime.now().plusDays(14); + OffsetDateTime earlier = OffsetDateTime.now().plusDays(7); mockMvc.perform(post("/v1/appointments") .contentType(MediaType.APPLICATION_JSON) @@ -286,8 +286,8 @@ void shouldReturnNotFoundWhenListingAppointmentsForUnknownPatient() throws Excep @Test @DisplayName("Should list appointments globally with patient and doctor populated") void shouldListAppointmentsGloballyOrderedByTime() throws Exception { - LocalDateTime later = LocalDateTime.now().plusDays(14); - LocalDateTime earlier = LocalDateTime.now().plusDays(7); + OffsetDateTime later = OffsetDateTime.now().plusDays(14); + OffsetDateTime earlier = OffsetDateTime.now().plusDays(7); mockMvc.perform(post("/v1/appointments") .contentType(MediaType.APPLICATION_JSON) @@ -323,8 +323,8 @@ void shouldReturnEmptyListWhenNoAppointmentsGlobally() throws Exception { @Test @DisplayName("Should filter global list by repeated status query params") void shouldFilterGlobalAppointmentsByStatus() throws Exception { - LocalDateTime t1 = LocalDateTime.now().plusDays(5); - LocalDateTime t2 = LocalDateTime.now().plusDays(6); + OffsetDateTime t1 = OffsetDateTime.now().plusDays(5); + OffsetDateTime t2 = OffsetDateTime.now().plusDays(6); mockMvc.perform(post("/v1/appointments") .contentType(MediaType.APPLICATION_JSON) @@ -353,7 +353,7 @@ void shouldFilterGlobalAppointmentsByStatus() throws Exception { } @Test - @DisplayName("Should return 400 when from instant is after to instant") + @DisplayName("Should return 400 when from is after to") void shouldReturnBadRequestWhenFromAfterTo() throws Exception { mockMvc.perform(get("/v1/appointments") .param("from", "2026-06-01T12:00:00Z") diff --git a/src/test/java/com/ironhack/infra/adapter/input/DoctorRestAdapterTest.java b/src/test/java/com/ironhack/infra/adapter/input/DoctorRestAdapterTest.java index e51f622..14aabc7 100644 --- a/src/test/java/com/ironhack/infra/adapter/input/DoctorRestAdapterTest.java +++ b/src/test/java/com/ironhack/infra/adapter/input/DoctorRestAdapterTest.java @@ -1,6 +1,6 @@ package com.ironhack.infra.adapter.input; -import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; @@ -130,8 +130,8 @@ void shouldListDoctorAppointmentsOrderedByTime() throws Exception { .phoneNumber("201234568") .build()); - LocalDateTime later = LocalDateTime.now().plusDays(14); - LocalDateTime earlier = LocalDateTime.now().plusDays(7); + OffsetDateTime later = OffsetDateTime.now().plusDays(14); + OffsetDateTime earlier = OffsetDateTime.now().plusDays(7); mockMvc.perform(post("/v1/appointments") .contentType(MediaType.APPLICATION_JSON) @@ -213,7 +213,7 @@ void shouldDeleteDoctorAndCascadeAppointments() throws Exception { appointmentRepository.save(AppointmentEntity.builder() .patient(patient) .doctor(doctor) - .appointmentTime(LocalDateTime.now().plusDays(2)) + .appointmentTime(OffsetDateTime.now().plusDays(2)) .status(AppointmentStatus.COMPLETED) .build()); diff --git a/src/test/java/com/ironhack/infra/adapter/input/PatientRestAdapterTest.java b/src/test/java/com/ironhack/infra/adapter/input/PatientRestAdapterTest.java index 0f46286..70ce45a 100644 --- a/src/test/java/com/ironhack/infra/adapter/input/PatientRestAdapterTest.java +++ b/src/test/java/com/ironhack/infra/adapter/input/PatientRestAdapterTest.java @@ -1,6 +1,6 @@ package com.ironhack.infra.adapter.input; -import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; @@ -117,7 +117,7 @@ void shouldListPatientAppointmentsSuccessfully() throws Exception { .specialty(Specialty.CARDIOLOGY) .build()); - LocalDateTime appointmentTime = LocalDateTime.now().plusDays(5); + OffsetDateTime appointmentTime = OffsetDateTime.now().plusDays(5); mockMvc.perform(post("/v1/appointments") .contentType(MediaType.APPLICATION_JSON) @@ -189,7 +189,7 @@ void shouldDeletePatientAndCascadeAppointments() throws Exception { appointmentRepository.save(AppointmentEntity.builder() .patient(patient) .doctor(doctor) - .appointmentTime(LocalDateTime.now().plusDays(1)) + .appointmentTime(OffsetDateTime.now().plusDays(1)) .status(AppointmentStatus.CANCELLED) .build());