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
15 changes: 15 additions & 0 deletions bruno/appointments/Complete Appointment.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
meta {
name: Complete Appointment
type: http
seq: 5
}

post {
url: {{base_url}}/v1/appointments/{{id}}/complete
body: none
auth: none
}

vars:pre-request {
id: 00000000-0000-4000-8000-000000000003
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.ironhack.application.usecase.appointment;

import java.util.UUID;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.ironhack.application.dto.AppointmentDTO;
import com.ironhack.application.dto.response.ApiResponse;
import com.ironhack.application.exception.ConflictException;
import com.ironhack.application.exception.NotFoundException;
import com.ironhack.domain.AppointmentEntity;
import com.ironhack.domain.AppointmentStatus;
import com.ironhack.infra.adapter.mapper.MappingFacade;
import com.ironhack.infra.adapter.output.AppointmentRepository;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class CompleteAppointmentUseCase {
private final AppointmentRepository appointmentRepository;
private final MappingFacade mappingFacade;

@Transactional
public ApiResponse<AppointmentDTO> invoke(UUID appointmentId) {
AppointmentEntity appointment = requireScheduledAppointment(appointmentId);

appointment.setStatus(AppointmentStatus.COMPLETED);
AppointmentEntity saved = appointmentRepository.save(appointment);
AppointmentDTO dto = mappingFacade.toAppointmentDTO(saved);

return ApiResponse.success(dto, "Appointment completed successfully.");
}

private AppointmentEntity requireScheduledAppointment(UUID appointmentId) {
AppointmentEntity appointment = appointmentRepository
.findWithAssociationsById(appointmentId)
.orElseThrow(() -> new NotFoundException("Appointment not found."));
ensureAppointmentIsScheduled(appointment);
return appointment;
}

private void ensureAppointmentIsScheduled(AppointmentEntity appointment) {
if (appointment.getStatus() != AppointmentStatus.SCHEDULED) {
throw new ConflictException("Cannot complete appointment: only scheduled appointments may be completed.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.ironhack.application.dto.response.ApiResponse;
import com.ironhack.application.usecase.appointment.BookAppointmentUseCase;
import com.ironhack.application.usecase.appointment.CancelAppointmentUseCase;
import com.ironhack.application.usecase.appointment.CompleteAppointmentUseCase;
import com.ironhack.application.usecase.appointment.SearchAppointmentsUseCase;
import com.ironhack.domain.AppointmentStatus;
import jakarta.validation.Valid;
Expand All @@ -32,6 +33,7 @@
public class AppointmentRestAdapter {
private final BookAppointmentUseCase bookAppointmentUseCase;
private final CancelAppointmentUseCase cancelAppointmentUseCase;
private final CompleteAppointmentUseCase completeAppointmentUseCase;
private final SearchAppointmentsUseCase searchAppointmentsUseCase;

@GetMapping
Expand All @@ -57,4 +59,10 @@ public ResponseEntity<ApiResponse<AppointmentDTO>> cancelAppointment(@PathVariab
ApiResponse<AppointmentDTO> body = cancelAppointmentUseCase.invoke(id);
return ResponseEntity.ok(body);
}

@PostMapping("/{id}/complete")
public ResponseEntity<ApiResponse<AppointmentDTO>> completeAppointment(@PathVariable UUID id) {
ApiResponse<AppointmentDTO> body = completeAppointmentUseCase.invoke(id);
return ResponseEntity.ok(body);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,83 @@ void shouldReturnConflictWhenAppointmentAlreadyCancelled() throws Exception {
mockMvc.perform(post("/v1/appointments/{id}/cancel", appointmentId)).andExpect(status().isConflict());
}

// ==================== Complete Appointment Tests ====================

@Test
@DisplayName("Should successfully complete scheduled appointment")
void shouldCompleteAppointmentSuccessfully() throws Exception {
LocalDateTime futureTime = LocalDateTime.now().plusDays(7);
BookAppointmentRequest bookRequest = new BookAppointmentRequest(patientId, doctorId, futureTime);

String response = mockMvc.perform(post("/v1/appointments")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(bookRequest)))
.andExpect(status().isCreated())
.andReturn()
.getResponse()
.getContentAsString();

String appointmentId =
objectMapper.readTree(response).get("data").get("id").asString();

mockMvc.perform(post("/v1/appointments/{id}/complete", appointmentId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(200))
.andExpect(jsonPath("$.message").value("Appointment completed successfully."))
.andExpect(jsonPath("$.data.status").value("COMPLETED"));
}

@Test
@DisplayName("Should return 404 when trying to complete non-existent appointment")
void shouldReturnNotFoundWhenCompletingAppointmentDoesNotExist() throws Exception {
mockMvc.perform(post("/v1/appointments/{id}/complete", UUID.randomUUID()))
.andExpect(status().isNotFound());
}

@Test
@DisplayName("Should return 409 when trying to complete cancelled appointment")
void shouldReturnConflictWhenCompletingCancelledAppointment() throws Exception {
LocalDateTime futureTime = LocalDateTime.now().plusDays(7);
BookAppointmentRequest bookRequest = new BookAppointmentRequest(patientId, doctorId, futureTime);

String response = mockMvc.perform(post("/v1/appointments")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(bookRequest)))
.andExpect(status().isCreated())
.andReturn()
.getResponse()
.getContentAsString();

String appointmentId =
objectMapper.readTree(response).get("data").get("id").asString();

mockMvc.perform(post("/v1/appointments/{id}/cancel", appointmentId)).andExpect(status().isOk());

mockMvc.perform(post("/v1/appointments/{id}/complete", appointmentId)).andExpect(status().isConflict());
}

@Test
@DisplayName("Should return 409 when trying to complete already completed appointment")
void shouldReturnConflictWhenAppointmentAlreadyCompleted() throws Exception {
LocalDateTime futureTime = LocalDateTime.now().plusDays(7);
BookAppointmentRequest bookRequest = new BookAppointmentRequest(patientId, doctorId, futureTime);

String response = mockMvc.perform(post("/v1/appointments")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(bookRequest)))
.andExpect(status().isCreated())
.andReturn()
.getResponse()
.getContentAsString();

String appointmentId =
objectMapper.readTree(response).get("data").get("id").asString();

mockMvc.perform(post("/v1/appointments/{id}/complete", appointmentId)).andExpect(status().isOk());

mockMvc.perform(post("/v1/appointments/{id}/complete", appointmentId)).andExpect(status().isConflict());
}

// ==================== List Patient Appointments Tests ====================

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
package com.ironhack.infra.adapter.input;

import java.time.LocalDateTime;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

import com.ironhack.application.dto.request.BookAppointmentRequest;
import com.ironhack.application.dto.request.CreatePatientRequest;
import com.ironhack.domain.AppointmentStatus;
import com.ironhack.domain.DoctorEntity;
import com.ironhack.domain.PatientEntity;
import com.ironhack.domain.Specialty;
import com.ironhack.infra.adapter.output.AppointmentRepository;
import com.ironhack.infra.adapter.output.DoctorRepository;
import tools.jackson.databind.ObjectMapper;
import com.ironhack.application.dto.request.CreatePatientRequest;
import com.ironhack.infra.adapter.output.PatientRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

import java.time.LocalDateTime;
import java.util.UUID;
import tools.jackson.databind.ObjectMapper;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
Expand Down Expand Up @@ -56,7 +57,7 @@ void setUp() {
@Test
@DisplayName("Should successfully create patient with valid data")
void shouldSuccessfullyCreatePatientWithValidData() throws Exception {
CreatePatientRequest request = new CreatePatientRequest("Cafar Babayev","+994501234567");
CreatePatientRequest request = new CreatePatientRequest("Cafar Babayev", "+994501234567");

mockMvc.perform(post("/v1/patients")
.contentType(MediaType.APPLICATION_JSON)
Expand All @@ -82,11 +83,9 @@ void shouldReturnValidationErrorWhenFullNameIsBlank() throws Exception {
@DisplayName("Should return 409 when patient with same phone number already exists")
void shouldReturnConflictWhenPatientPhoneNumberAlreadyExists() throws Exception {

CreatePatientRequest request1 =
new CreatePatientRequest("Cafar Babayev", "+994501234567");
CreatePatientRequest request1 = new CreatePatientRequest("Cafar Babayev", "+994501234567");

CreatePatientRequest request2 =
new CreatePatientRequest("Ali Hasanov", "+994501234567");
CreatePatientRequest request2 = new CreatePatientRequest("Ali Hasanov", "+994501234567");

// Create first patient
mockMvc.perform(post("/v1/patients")
Expand All @@ -105,51 +104,39 @@ void shouldReturnConflictWhenPatientPhoneNumberAlreadyExists() throws Exception
@DisplayName("Should list patient appointments successfully")
void shouldListPatientAppointmentsSuccessfully() throws Exception {

PatientEntity patient = patientRepository.save(
PatientEntity.builder()
.fullName("Cafar Babayev")
.phoneNumber("+994501234567")
.build()
);
PatientEntity patient = patientRepository.save(PatientEntity.builder()
.fullName("Cafar Babayev")
.phoneNumber("+994501234567")
.build());

DoctorEntity doctor = doctorRepository.save(
DoctorEntity.builder()
.fullName("Dr. Ali")
.specialty(Specialty.CARDIOLOGY)
.build()
);
DoctorEntity doctor = doctorRepository.save(DoctorEntity.builder()
.fullName("Dr. Ali")
.specialty(Specialty.CARDIOLOGY)
.build());

LocalDateTime appointmentTime =
LocalDateTime.now().plusDays(5);
LocalDateTime appointmentTime = LocalDateTime.now().plusDays(5);

mockMvc.perform(post("/v1/appointments")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(
new BookAppointmentRequest(
patient.getId(),
doctor.getId(),
appointmentTime
))))
new BookAppointmentRequest(patient.getId(), doctor.getId(), appointmentTime))))
.andExpect(status().isCreated());

mockMvc.perform(get("/v1/patients/{id}/appointments", patient.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(200))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data[0].status")
.value(AppointmentStatus.SCHEDULED.name()));
.andExpect(jsonPath("$.data[0].status").value(AppointmentStatus.SCHEDULED.name()));
}

@Test
@DisplayName("Should return empty list when patient has no appointments")
void shouldReturnEmptyListWhenPatientHasNoAppointments() throws Exception {

PatientEntity patient = patientRepository.save(
PatientEntity.builder()
.fullName("Cafar Babayev")
.phoneNumber("+994501234567")
.build()
);
PatientEntity patient = patientRepository.save(PatientEntity.builder()
.fullName("Cafar Babayev")
.phoneNumber("+994501234567")
.build());

mockMvc.perform(get("/v1/patients/{id}/appointments", patient.getId()))
.andExpect(status().isOk())
Expand All @@ -159,7 +146,6 @@ void shouldReturnEmptyListWhenPatientHasNoAppointments() throws Exception {
@Test
@DisplayName("Should return 404 when patient does not exist")
void shouldReturnNotFoundWhenPatientDoesNotExist() throws Exception {

mockMvc.perform(get("/v1/patients/{id}/appointments", UUID.randomUUID()))
.andExpect(status().isNotFound());
}
Expand Down
Loading