diff --git a/api/openapi.yml b/api/openapi.yml new file mode 100644 index 0000000..5899a97 --- /dev/null +++ b/api/openapi.yml @@ -0,0 +1,266 @@ +openapi: 3.0.3 +info: + title: Appointment Booking API + description: API for managing appointments, treatments, clients, and specialists. + version: 1.0.0 + +servers: + - url: http://localhost:8080 + description: Local development server + +paths: + /treatments: + get: + summary: Get a list of available treatments + operationId: getTreatments + tags: + - Treatments + responses: + "200": + description: List of treatments + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Treatment" + + post: + summary: Create a new treatment + operationId: createTreatment + tags: + - Treatments + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TreatmentRequest" + responses: + "201": + description: Treatment successfully created + content: + application/json: + schema: + $ref: "#/components/schemas/Treatment" + "400": + description: Invalid request data + "403": + description: Only specialists can create treatments + + /treatments/{treatmentId}: + get: + summary: Get treatment details (with specialist info) + operationId: getTreatmentDetails + tags: + - Treatments + parameters: + - name: treatmentId + in: path + required: true + schema: + type: string + responses: + "200": + description: Treatment details including assigned specialist + content: + application/json: + schema: + $ref: "#/components/schemas/TreatmentDetails" + "404": + description: Treatment not found + + /appointments: + get: + summary: Get all appointments with filtering options + operationId: getAppointments + tags: + - Appointments + parameters: + - name: clientId + in: query + schema: + type: string + - name: specialistId + in: query + schema: + type: string + - name: status + in: query + schema: + type: string + enum: [SCHEDULED, CANCELLED, COMPLETED] + responses: + "200": + description: List of appointments + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Appointment" + + post: + summary: Schedule an appointment + operationId: createAppointment + tags: + - Appointments + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppointmentRequest" + responses: + "201": + description: Appointment successfully created + content: + application/json: + schema: + $ref: "#/components/schemas/Appointment" + "400": + description: Invalid request data + "409": + description: Appointment slot conflict + + /appointments/{appointmentId}: + patch: + summary: Update appointment status + operationId: updateAppointmentStatus + tags: + - Appointments + parameters: + - name: appointmentId + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppointmentStatusUpdate" + responses: + "200": + description: Appointment status updated + "400": + description: Invalid status + "404": + description: Appointment not found + + /availability: + get: + summary: Check appointment slot availability + operationId: checkAvailability + tags: + - Appointments + parameters: + - name: specialistId + in: query + required: true + schema: + type: string + - name: dateTime + in: query + required: true + schema: + type: string + format: date-time + responses: + "200": + description: Slot availability result + content: + application/json: + schema: + type: object + properties: + available: + type: boolean + +components: + schemas: + Treatment: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + duration: + type: integer + description: Duration of the treatment in minutes + specialistId: + type: string + format: uuid + + TreatmentRequest: + type: object + properties: + name: + type: string + duration: + type: integer + specialistId: + type: string + format: uuid + + TreatmentDetails: + allOf: + - $ref: "#/components/schemas/Treatment" + - type: object + properties: + specialist: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + + AppointmentRequest: + type: object + required: + - clientId + - treatmentId + - dateTime + properties: + clientId: + type: string + format: uuid + treatmentId: + type: string + format: uuid + dateTime: + type: string + format: date-time + + Appointment: + type: object + properties: + id: + type: string + format: uuid + clientId: + type: string + format: uuid + treatmentId: + type: string + format: uuid + dateTime: + type: string + format: date-time + status: + type: string + enum: [SCHEDULED, CANCELLED, COMPLETED] + + AppointmentStatusUpdate: + type: object + required: + - status + properties: + status: + type: string + enum: [CANCELLED, COMPLETED] diff --git a/pom.xml b/pom.xml index dbec20a..715621d 100644 --- a/pom.xml +++ b/pom.xml @@ -92,6 +92,25 @@ org.springframework.boot spring-boot-starter-validation + + + + io.swagger.core.v3 + swagger-annotations + 2.2.28 + + + org.openapitools + jackson-databind-nullable + 0.2.6 + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.5 + @@ -168,6 +187,49 @@ + + org.openapitools + openapi-generator-maven-plugin + 7.11.0 + + + appointmentBooking + + generate + + + ${project.basedir}/api/openapi.yml + ${project.build.directory}/generated-sources/openapi + true + spring + com.capgemini.training.appointmentbooking.service.api + com.capgemini.training.appointmentbooking.service.model + + false + false + + + true + spring-boot + true + true + true + true + true + java-time + false + true + + @lombok.Generated + @lombok.ToString + + source + swagger2 + + + + + diff --git a/src/main/java/com/capgemini/training/appointmentbooking/service/config/ServiceMappingConfiguration.java b/src/main/java/com/capgemini/training/appointmentbooking/service/config/ServiceMappingConfiguration.java new file mode 100644 index 0000000..586486a --- /dev/null +++ b/src/main/java/com/capgemini/training/appointmentbooking/service/config/ServiceMappingConfiguration.java @@ -0,0 +1,20 @@ +package com.capgemini.training.appointmentbooking.service.config; + +import com.capgemini.training.appointmentbooking.service.mapper.AppointmentApiMapper; +import com.capgemini.training.appointmentbooking.service.mapper.TreatmentApiMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ServiceMappingConfiguration { + + @Bean + AppointmentApiMapper getAppointmentApiMapper() { + return new AppointmentApiMapper(); + } + + @Bean + TreatmentApiMapper getTreatmentApiMapper() { + return new TreatmentApiMapper(); + } +} diff --git a/src/main/java/com/capgemini/training/appointmentbooking/service/impl/AppointmentsApiController.java b/src/main/java/com/capgemini/training/appointmentbooking/service/impl/AppointmentsApiController.java new file mode 100644 index 0000000..eabfd28 --- /dev/null +++ b/src/main/java/com/capgemini/training/appointmentbooking/service/impl/AppointmentsApiController.java @@ -0,0 +1,78 @@ +package com.capgemini.training.appointmentbooking.service.impl; + +import com.capgemini.training.appointmentbooking.common.datatype.AppointmentStatus; +import com.capgemini.training.appointmentbooking.common.to.*; +import com.capgemini.training.appointmentbooking.dataaccess.repository.criteria.AppointmentCriteria; +import com.capgemini.training.appointmentbooking.logic.FindAppointmentUc; +import com.capgemini.training.appointmentbooking.logic.ManageAppointmentUc; +import com.capgemini.training.appointmentbooking.service.api.AppointmentsApi; +import com.capgemini.training.appointmentbooking.service.model.*; +import com.capgemini.training.appointmentbooking.service.mapper.AppointmentApiMapper; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class AppointmentsApiController implements AppointmentsApi { + + private final FindAppointmentUc findAppointmentUc; + private final ManageAppointmentUc manageAppointmentUc; + private final AppointmentApiMapper appointmentMapper; + + @Override + public ResponseEntity createAppointment( + @Valid AppointmentRequest appointmentRequest) { + AppointmentBookingEto bookingEto = appointmentMapper.toBookingEto(appointmentRequest); + AppointmentCto created = manageAppointmentUc.bookAppointment(bookingEto); + return ResponseEntity.status(201).body(appointmentMapper.toApiAppointment(created)); + } + + @Override + public ResponseEntity> getAppointments( + @Valid Optional clientId, + @Valid Optional specialistId, + @Valid Optional status) { + + AppointmentCriteria.AppointmentCriteriaBuilder criteria = AppointmentCriteria.builder(); + status.ifPresent(t -> criteria.status(AppointmentStatus.valueOf(t))); + clientId.ifPresent(t -> criteria.clientId(UUID.fromString(t).getMostSignificantBits())); + specialistId.ifPresent(t -> criteria.specialistId(UUID.fromString(t).getMostSignificantBits())); + + List list = findAppointmentUc.findByCriteria(criteria.build()); + List result = + list.stream().map(appointmentMapper::toApiAppointment).collect(Collectors.toList()); + return ResponseEntity.ok(result); + } + + @Override + public ResponseEntity updateAppointmentStatus( + String appointmentId, @Valid AppointmentStatusUpdate appointmentStatusUpdate) { + Long id = UUID.fromString(appointmentId).getMostSignificantBits(); + AppointmentStatus status = + AppointmentStatus.valueOf(appointmentStatusUpdate.getStatus().name()); + manageAppointmentUc.updateAppointmentStatus(id, status); + return ResponseEntity.ok().build(); + } + + @Override + public ResponseEntity checkAvailability( + @NotNull @Valid String specialistId, + @NotNull @Valid @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date dateTime) { + Long specialistLongId = UUID.fromString(specialistId).getMostSignificantBits(); + boolean available = + !findAppointmentUc.hasConflictingAppointment(specialistLongId, dateTime.toInstant()); + + CheckAvailability200Response response = new CheckAvailability200Response(); + response.setAvailable(Optional.of(available)); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/capgemini/training/appointmentbooking/service/impl/TreatmentsApiController.java b/src/main/java/com/capgemini/training/appointmentbooking/service/impl/TreatmentsApiController.java new file mode 100644 index 0000000..8210b3a --- /dev/null +++ b/src/main/java/com/capgemini/training/appointmentbooking/service/impl/TreatmentsApiController.java @@ -0,0 +1,57 @@ +package com.capgemini.training.appointmentbooking.service.impl; + +import com.capgemini.training.appointmentbooking.common.to.TreatmentCreationTo; +import com.capgemini.training.appointmentbooking.common.to.TreatmentCto; +import com.capgemini.training.appointmentbooking.logic.FindTreatmentUc; +import com.capgemini.training.appointmentbooking.logic.ManageTreatmentUc; +import com.capgemini.training.appointmentbooking.service.api.TreatmentsApi; +import com.capgemini.training.appointmentbooking.service.model.Treatment; +import com.capgemini.training.appointmentbooking.service.model.TreatmentDetails; +import com.capgemini.training.appointmentbooking.service.model.TreatmentRequest; +import com.capgemini.training.appointmentbooking.service.mapper.TreatmentApiMapper; +import jakarta.validation.Valid; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/") +@RequiredArgsConstructor +public class TreatmentsApiController implements TreatmentsApi { + + private final FindTreatmentUc findTreatmentUc; + private final ManageTreatmentUc manageTreatmentUc; + private final TreatmentApiMapper treatmentMapper; + + @Override + public ResponseEntity createTreatment(@Valid TreatmentRequest treatmentRequest) { + TreatmentCreationTo to = treatmentMapper.toCreationTo(treatmentRequest); + TreatmentCto created = manageTreatmentUc.createTreatment(to); + return ResponseEntity.status(HttpStatus.CREATED).body(treatmentMapper.toApiTreatment(created)); + } + + @Override + public ResponseEntity getTreatmentDetails(String treatmentId) { + Long id = UUID.fromString(treatmentId).getMostSignificantBits(); + Optional optional = findTreatmentUc.findById(id); + + return optional + .map(treatmentMapper::toApiTreatmentDetails) + .map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND).build()); + } + + @Override + public ResponseEntity> getTreatments() { + List list = findTreatmentUc.findAll(); + List result = + list.stream().map(treatmentMapper::toApiTreatment).collect(Collectors.toList()); + return ResponseEntity.ok(result); + } +} diff --git a/src/main/java/com/capgemini/training/appointmentbooking/service/mapper/AppointmentApiMapper.java b/src/main/java/com/capgemini/training/appointmentbooking/service/mapper/AppointmentApiMapper.java new file mode 100644 index 0000000..c54642d --- /dev/null +++ b/src/main/java/com/capgemini/training/appointmentbooking/service/mapper/AppointmentApiMapper.java @@ -0,0 +1,43 @@ +package com.capgemini.training.appointmentbooking.service.mapper; + +import java.util.Optional; +import java.util.UUID; + +import com.capgemini.training.appointmentbooking.common.to.AppointmentBookingEto; +import com.capgemini.training.appointmentbooking.common.to.AppointmentCto; +import com.capgemini.training.appointmentbooking.common.to.AppointmentEto; +import com.capgemini.training.appointmentbooking.service.model.Appointment; +import com.capgemini.training.appointmentbooking.service.model.AppointmentRequest; + +public class AppointmentApiMapper { + + public AppointmentBookingEto toBookingEto(AppointmentRequest request) { + return AppointmentBookingEto.builder() + .clientId(toLong(request.getClientId())) + .treatmentId(toLong(request.getTreatmentId())) + .specialistId( + 0L) // specjalista nie jest częścią requestu – może być wyciągany przez treatment + .dateTime(request.getDateTime().toInstant()) + .build(); + } + + public Appointment toApiAppointment(AppointmentCto cto) { + AppointmentEto appointmentEto = cto.appointmentEto(); + + Appointment result = new Appointment(); + result.setId(Optional.of(toUuid(appointmentEto.id()))); + result.setClientId(Optional.of(toUuid(cto.clientEto().id()))); + result.setTreatmentId(Optional.of(toUuid(cto.treatmentCto().treatmentEto().id()))); + result.setDateTime(Optional.of(java.util.Date.from(appointmentEto.dateTime()))); + result.setStatus(Optional.of(Appointment.StatusEnum.valueOf(appointmentEto.status().name()))); + return result; + } + + private Long toLong(UUID uuid) { + return uuid.getMostSignificantBits(); + } + + private UUID toUuid(Long id) { + return new UUID(id, 0L); + } +} diff --git a/src/main/java/com/capgemini/training/appointmentbooking/service/mapper/TreatmentApiMapper.java b/src/main/java/com/capgemini/training/appointmentbooking/service/mapper/TreatmentApiMapper.java new file mode 100644 index 0000000..8ea6c56 --- /dev/null +++ b/src/main/java/com/capgemini/training/appointmentbooking/service/mapper/TreatmentApiMapper.java @@ -0,0 +1,59 @@ +package com.capgemini.training.appointmentbooking.service.mapper; + +import java.util.Optional; +import java.util.UUID; + +import com.capgemini.training.appointmentbooking.common.to.SpecialistEto; +import com.capgemini.training.appointmentbooking.common.to.TreatmentCreationTo; +import com.capgemini.training.appointmentbooking.common.to.TreatmentCto; +import com.capgemini.training.appointmentbooking.common.to.TreatmentEto; +import com.capgemini.training.appointmentbooking.service.model.Treatment; +import com.capgemini.training.appointmentbooking.service.model.TreatmentDetails; +import com.capgemini.training.appointmentbooking.service.model.TreatmentDetailsAllOfSpecialist; +import com.capgemini.training.appointmentbooking.service.model.TreatmentRequest; + +public class TreatmentApiMapper { + + public TreatmentCreationTo toCreationTo(TreatmentRequest request) { + return TreatmentCreationTo.builder() + .name(request.getName().orElse(null)) + .durationMinutes(request.getDuration().orElse(0)) + .specialistId(request.getSpecialistId().map(UUID::getMostSignificantBits).orElse(null)) + .description("Default description") + .build(); + } + + public Treatment toApiTreatment(TreatmentCto cto) { + TreatmentEto eto = cto.treatmentEto(); + SpecialistEto specialist = cto.specialistEto(); + + Treatment result = new Treatment(); + result.setId(Optional.ofNullable(eto.id()).map(this::toUuid)); + result.setName(Optional.ofNullable(eto.name())); + result.setDuration(Optional.of(eto.durationMinutes())); + result.setSpecialistId(Optional.ofNullable(specialist.id()).map(this::toUuid)); + return result; + } + + public TreatmentDetails toApiTreatmentDetails(TreatmentCto cto) { + TreatmentEto eto = cto.treatmentEto(); + SpecialistEto specialist = cto.specialistEto(); + + TreatmentDetails result = new TreatmentDetails(); + result.setId(Optional.ofNullable(eto.id()).map(this::toUuid)); + result.setName(Optional.ofNullable(eto.name())); + result.setDuration(Optional.of(eto.durationMinutes())); + result.setSpecialistId(Optional.ofNullable(specialist.id()).map(this::toUuid)); + + TreatmentDetailsAllOfSpecialist specialistDto = new TreatmentDetailsAllOfSpecialist(); + specialistDto.setId(Optional.ofNullable(specialist.id()).map(this::toUuid)); + specialistDto.setName(Optional.ofNullable(specialist.specialization().name())); + + result.setSpecialist(Optional.of(specialistDto)); + return result; + } + + private UUID toUuid(Long id) { + return new UUID(id, 0L); + } +}