From 28c45fc0d18516f3601fc0516dbd4fc50ef7d252 Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Sat, 9 May 2026 20:16:06 +0200 Subject: [PATCH 1/4] [SKY-26]: Add API GET endpoint for all pilots --- .../application/pilot/GetPilotUseCase.java | 59 +++++++ .../skyroster_backend/domain/model/Pilot.java | 6 +- .../domain/model/Qualification.java | 2 + .../domain/port/PilotRepository.java | 17 ++ .../adapter/in/web/PilotController.java | 23 +++ .../out/persistence/JpaPilotRepository.java | 10 ++ .../infrastructure/mappers/PilotMapper.java | 33 ++++ .../mappers/PilotQualificationMapper.java | 13 ++ .../src/main/resources/api/openapi.yaml | 154 ++++++++++++++++-- 9 files changed, 304 insertions(+), 13 deletions(-) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java new file mode 100644 index 0000000..a42dc85 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java @@ -0,0 +1,59 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; +import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotMapper; + +@Service +@RequiredArgsConstructor +public class GetPilotUseCase { + + private final PilotRepository pilotRepository; + + public PagedPilotResponse getPilots(Integer page, Integer size, String sort) { + Pageable pageable = createPageable(page, size, sort); + + Page pilotsPage = pilotRepository.findAllWithRelations(pageable); + + return new PagedPilotResponse() + .content( + pilotsPage.getContent() + .stream() + .map(PilotMapper::toResponse) + .toList() + ) + .page(pilotsPage.getNumber()) + .size(pilotsPage.getSize()) + .totalElements(pilotsPage.getTotalElements()) + .totalPages(pilotsPage.getTotalPages()) + .first(pilotsPage.isFirst()) + .last(pilotsPage.isLast()); + } + + private Pageable createPageable(Integer page, Integer size, String sort) { + int resolvedPage = page == null ? 0 : page; + int resolvedSize = size == null ? 20 : Math.min(size, 100); + + Sort resolvedSort = Sort.by(Sort.Direction.ASC, "lastName"); + + if (sort != null && !sort.isBlank()) { + String[] parts = sort.split(","); + String property = parts[0]; + + Sort.Direction direction = parts.length > 1 + ? Sort.Direction.fromOptionalString(parts[1]).orElse(Sort.Direction.ASC) + : Sort.Direction.ASC; + + resolvedSort = Sort.by(direction, property); + } + + return PageRequest.of(resolvedPage, resolvedSize, resolvedSort); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java index efe885a..a8f20c6 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java @@ -1,11 +1,13 @@ package pl.skyroster.skyroster_backend.domain.model; +import jakarta.persistence.*; +import lombok.Getter; + import java.util.Set; import java.util.UUID; -import jakarta.persistence.*; - @Entity +@Getter public class Pilot { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java index 750a24a..b43b9a0 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java @@ -3,10 +3,12 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import lombok.Getter; import java.util.UUID; @Entity +@Getter public class Qualification { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java new file mode 100644 index 0000000..f2a8e56 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java @@ -0,0 +1,17 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.Query; +import pl.skyroster.skyroster_backend.domain.model.Pilot; + +public interface PilotRepository { + @EntityGraph(attributePaths = { + "qualifications", + "aircraftTypes", + "operationalBase" + }) + @Query("SELECT p FROM Pilot p") + Page findAllWithRelations(Pageable pageable); +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java new file mode 100644 index 0000000..d71d320 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java @@ -0,0 +1,23 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.Nullable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import pl.skyroster.skyroster_backend.application.pilot.GetPilotUseCase; +import pl.skyroster.skyroster_backend.generated.api.ApiApi; +import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; + +@RequiredArgsConstructor +@RestController +public class PilotController { + + private final GetPilotUseCase getPilotUseCase; + + @GetMapping(ApiApi.PATH_GET_PILOTS) + public ResponseEntity getPilots(@RequestParam Integer page, @RequestParam Integer size, @RequestParam @Nullable String sort) { + return ResponseEntity.ok(getPilotUseCase.getPilots(page, size, sort)); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java new file mode 100644 index 0000000..e659210 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java @@ -0,0 +1,10 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; + +import java.util.UUID; + +public interface JpaPilotRepository extends JpaRepository, PilotRepository { +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java new file mode 100644 index 0000000..f81572c --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java @@ -0,0 +1,33 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import org.springframework.stereotype.Component; +import pl.skyroster.skyroster_backend.domain.model.AircraftType; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.model.Qualification; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; + +import java.util.Comparator; + +@Component +public class PilotMapper { + public static PilotResponse toResponse(Pilot pilot) { + return new PilotResponse() + .id(pilot.getId()) + .firstName(pilot.getName()) + .surname(pilot.getSurname()) + .licence(pilot.getLicence()) + .homeBase(OperationalBaseInfoMapper.map(pilot.getOperationalBase())) + .qualifications( + pilot.getQualifications() + .stream() + .sorted(Comparator.comparing(Qualification::getLabel)) + .map(PilotQualificationMapper::toPilotQualificationInfo) + .toList()) + .aircraftTypes( + pilot.getAircraftTypes() + .stream() + .sorted(Comparator.comparing(AircraftType::getName)) + .map(AircraftTypeInfoMapper::map) + .toList()); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java new file mode 100644 index 0000000..a26533a --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java @@ -0,0 +1,13 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import pl.skyroster.skyroster_backend.domain.model.Qualification; +import pl.skyroster.skyroster_backend.generated.model.PilotQualificationInfo; + +public class PilotQualificationMapper { + public static PilotQualificationInfo toPilotQualificationInfo(Qualification qualification) { + return new PilotQualificationInfo() + .id(qualification.getId()) + .values(qualification.getValue()) + .label(qualification.getLabel()); + } +} diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index facf0e1..64e077f 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -5,15 +5,15 @@ info: version: 0.1.0 security: - - bearerAuth: [] + - bearerAuth: [ ] paths: /api/health: get: operationId: getHealth - tags: [Health] + tags: [ Health ] summary: Health check - security: [] + security: [ ] responses: '200': description: Service is healthy @@ -25,7 +25,7 @@ paths: /api/me: get: operationId: getCurrentUser - tags: [User] + tags: [ User ] summary: Get current authenticated user info responses: '200': @@ -44,7 +44,7 @@ paths: /api/compliance/reports: get: operationId: getComplianceReports - tags: [Compliance] + tags: [ Compliance ] summary: List compliance reports responses: '200': @@ -71,7 +71,7 @@ paths: /api/planning/schedules: get: operationId: getPlanningSchedules - tags: [Planning] + tags: [ Planning ] summary: List schedules responses: '200': @@ -98,7 +98,7 @@ paths: /api/aircraft: post: operationId: addAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Add a new aircraft to the fleet requestBody: required: true @@ -139,7 +139,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' get: operationId: getAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: List all aircraft in the fleet responses: '200': @@ -156,6 +156,53 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + /api/pilots: + get: + tags: + - Pilots + operationId: getPilots + summary: Get paginated list of pilots + parameters: + - name: page + in: query + required: false + schema: + type: integer + minimum: 0 + default: 0 + - name: size + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: sort + in: query + required: false + schema: + type: string + example: surname,asc + responses: + '200': + description: Paginated list of pilots + content: + application/json: + schema: + $ref: '#/components/schemas/PagedPilotResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /api/flights: get: @@ -187,7 +234,7 @@ paths: /api/aircraft/{id}: put: operationId: updateAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Update an aircraft in the fleet parameters: - in: path @@ -241,7 +288,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' delete: operationId: deleteAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Delete an aircraft from the fleet parameters: - in: path @@ -336,7 +383,7 @@ components: description: List of realm roles assigned to the user items: type: string - example: ["operations_administrator"] + example: [ "operations_administrator" ] HealthStatus: type: object @@ -388,6 +435,91 @@ components: format: date-time example: "2026-03-20T11:00:00Z" + PagedPilotResponse: + type: object + required: + - content + - page + - size + - totalElements + - totalPages + - first + - last + properties: + content: + type: array + items: + $ref: '#/components/schemas/PilotResponse' + page: + type: integer + example: 0 + size: + type: integer + example: 20 + totalElements: + type: integer + format: int64 + example: 42 + totalPages: + type: integer + example: 3 + first: + type: boolean + example: true + last: + type: boolean + example: false + + PilotResponse: + type: object + required: + - id + - firstName + - surname + - homeBase + - qualifications + properties: + id: + type: string + format: uuid + licence: + type: string + example: PIL-001 + firstName: + type: string + example: Jan + surname: + type: string + example: Kowalski + homeBase: + $ref: '#/components/schemas/OperationalBaseInfo' + qualifications: + type: array + items: + $ref: '#/components/schemas/PilotQualificationInfo' + aircraftTypes: + type: array + items: + $ref: '#/components/schemas/AircraftTypeInfo' + + + PilotQualificationInfo: + type: object + required: + - id + - aircraftType + - licenseValidUntil + properties: + id: + type: string + format: uuid + values: + type: string + label: + type: string + + + AddAircraftRequest: type: object required: From 1eee3c638bce3b0d877660d0a5a6a55f052a4da7 Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Sun, 10 May 2026 21:28:34 +0200 Subject: [PATCH 2/4] [SKY-28]: Add API DELETE endpoint for pilot --- .../application/pilot/DeletePilotUseCase.java | 35 +++++++++++++++++++ .../exception/PilotNotFoundException.java | 9 +++++ .../domain/port/FlightRepository.java | 5 +++ .../domain/port/PilotRepository.java | 4 +++ .../adapter/in/web/PilotController.java | 14 ++++++-- .../config/GlobalExceptionHandler.java | 11 ++++++ .../config/security/SecurityConfig.java | 1 + .../src/main/resources/api/openapi.yaml | 19 ++++++++++ 8 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java new file mode 100644 index 0000000..6acb599 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java @@ -0,0 +1,35 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.exception.PilotNotFoundException; +import pl.skyroster.skyroster_backend.domain.port.FlightRepository; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; + +import java.time.OffsetDateTime; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class DeletePilotUseCase { + + private final PilotRepository pilotRepository; + private final FlightRepository flightRepository; + + @Transactional + public void deletePilotById(UUID pilotId) { + if (!pilotRepository.existsById(pilotId)) { + throw new PilotNotFoundException(pilotId); + } + + if(pilotHasScheduledFlights(pilotId)) + throw new IllegalStateException("Pilot has ongoing or incoming flights"); + + pilotRepository.deleteById(pilotId); + } + + private boolean pilotHasScheduledFlights(UUID pilotId){ + return flightRepository.existsByPilotIdAndFlightEndAfter(pilotId, OffsetDateTime.now()); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java new file mode 100644 index 0000000..fa5720e --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java @@ -0,0 +1,9 @@ +package pl.skyroster.skyroster_backend.domain.exception; + +import java.util.UUID; + +public class PilotNotFoundException extends RuntimeException { + public PilotNotFoundException(UUID pilotId) { + super("Pilot with id " + pilotId + " not found"); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java index 555e749..e335ab3 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java @@ -3,10 +3,15 @@ import org.springframework.data.jpa.repository.JpaRepository; import pl.skyroster.skyroster_backend.domain.model.Flight; +import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; public interface FlightRepository extends JpaRepository { List findAll(); boolean existsByAircraftId(UUID aircraftId); + boolean existsByPilotIdAndFlightEndAfter( + UUID pilotId, + OffsetDateTime now + ); } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java index f2a8e56..84f5900 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java @@ -6,6 +6,8 @@ import org.springframework.data.jpa.repository.Query; import pl.skyroster.skyroster_backend.domain.model.Pilot; +import java.util.UUID; + public interface PilotRepository { @EntityGraph(attributePaths = { "qualifications", @@ -14,4 +16,6 @@ public interface PilotRepository { }) @Query("SELECT p FROM Pilot p") Page findAllWithRelations(Pageable pageable); + boolean existsById(UUID id); + void deleteById(UUID id); } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java index d71d320..b2aa8d5 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java @@ -3,21 +3,29 @@ import lombok.RequiredArgsConstructor; import org.jspecify.annotations.Nullable; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import pl.skyroster.skyroster_backend.application.pilot.DeletePilotUseCase; import pl.skyroster.skyroster_backend.application.pilot.GetPilotUseCase; import pl.skyroster.skyroster_backend.generated.api.ApiApi; import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; +import java.util.UUID; + @RequiredArgsConstructor @RestController public class PilotController { private final GetPilotUseCase getPilotUseCase; + private final DeletePilotUseCase deletePilotUseCase; @GetMapping(ApiApi.PATH_GET_PILOTS) public ResponseEntity getPilots(@RequestParam Integer page, @RequestParam Integer size, @RequestParam @Nullable String sort) { return ResponseEntity.ok(getPilotUseCase.getPilots(page, size, sort)); } + + @DeleteMapping(ApiApi.PATH_DELETE_PILOT_BY_ID) + public ResponseEntity deletePilotById(@PathVariable UUID pilotId) { + deletePilotUseCase.deletePilotById(pilotId); + return ResponseEntity.noContent().build(); + } } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java index 6123af3..532cf70 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java @@ -11,6 +11,7 @@ import pl.skyroster.skyroster_backend.domain.exception.AircraftNotFoundException; import pl.skyroster.skyroster_backend.domain.exception.AircraftTypeNotFoundException; import pl.skyroster.skyroster_backend.domain.exception.OperationalBaseNotFoundException; +import pl.skyroster.skyroster_backend.domain.exception.PilotNotFoundException; import pl.skyroster.skyroster_backend.generated.model.ErrorResponse; import java.time.OffsetDateTime; @@ -50,6 +51,11 @@ public ResponseEntity handleAircraftTypeNotFound(AircraftTypeNotF return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); } + @ExceptionHandler(PilotNotFoundException.class) + public ResponseEntity handlePilotNotFound(PilotNotFoundException ex,HttpServletRequest request) { + return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage(), request); + } + @ExceptionHandler(OperationalBaseNotFoundException.class) public ResponseEntity handleOperationalBaseNotFound(OperationalBaseNotFoundException ex, HttpServletRequest request) { @@ -62,6 +68,11 @@ public ResponseEntity handleIllegalArgument(IllegalArgumentExcept return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); } + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleIllegalState(Exception ex, HttpServletRequest request) { + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); + } + private ResponseEntity buildResponse(HttpStatus status, String message, HttpServletRequest request) { ErrorResponse error = new ErrorResponse() diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java index 2df005b..8cbb358 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java @@ -48,6 +48,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers("/api/planning/**").hasRole("schedule_planner") .requestMatchers(HttpMethod.POST, "/api/aircraft/**").hasRole("operations_administrator") .requestMatchers(HttpMethod.GET, "/api/aircraft/**").authenticated() + .requestMatchers(HttpMethod.DELETE, "/api/pilots/**").hasRole("operations_administrator") .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 64e077f..21c3e56 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -325,6 +325,25 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /api/pilots/{pilotId}: + delete: + tags: + - AdminPilots + operationId: deletePilotById + summary: Delete pilot by id + parameters: + - name: pilotId + in: path + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Pilot deleted successfully + '404': + description: Pilot not found + components: securitySchemes: bearerAuth: From 87a45c65f57425b00274898cbc724704b4bb36b5 Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Thu, 14 May 2026 22:41:59 +0200 Subject: [PATCH 3/4] [SKY-29]: Add API PATCH endpoint for pilots --- .../application/pilot/PatchPilotUseCase.java | 55 ++++++++++++ .../skyroster_backend/domain/model/Pilot.java | 2 + .../domain/model/Qualification.java | 4 + .../domain/port/PilotRepository.java | 3 + .../adapter/in/web/PilotController.java | 9 ++ .../config/security/SecurityConfig.java | 3 +- .../mappers/AircraftResponseMapper.java | 2 +- .../mappers/AircraftTypeInfoMapper.java | 9 +- .../infrastructure/mappers/PilotMapper.java | 2 +- .../mappers/PilotQualificationMapper.java | 4 + .../src/main/resources/api/openapi.yaml | 89 +++++++++++++++++++ 11 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java new file mode 100644 index 0000000..5e8b88c --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java @@ -0,0 +1,55 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.model.AircraftType; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.model.Qualification; +import pl.skyroster.skyroster_backend.domain.port.OperationalBaseRepository; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; +import pl.skyroster.skyroster_backend.generated.model.PilotPatchRequest; +import pl.skyroster.skyroster_backend.infrastructure.mappers.AircraftTypeInfoMapper; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotMapper; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotQualificationMapper; + +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PatchPilotUseCase { + private final PilotRepository pilotRepository; + private final OperationalBaseRepository operationalBaseRepository; + + @Transactional + public PilotResponse patchPilot(UUID id, PilotPatchRequest request) { + Pilot pilot = pilotRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Pilot not found: " + id)); + + String name = request.getName(); + if (name != null) { + pilot.setName(name); + } + String surname = request.getSurname(); + if (surname != null) { + pilot.setSurname(surname); + } + String icaoCode = request.getOperationalBaseIcaoCode(); + if (icaoCode != null) { + pilot.setOperationalBase( + operationalBaseRepository.findByIcaoCode(icaoCode) + .orElseThrow(() -> new IllegalArgumentException("Operational base not found: " + icaoCode)) + ); + } + Set qualifications = request.getQualifications().stream().map(PilotQualificationMapper::fromPilotQualificationInfo).collect(Collectors.toSet()); + pilot.setQualifications(qualifications); + + Set aircraftTypes = request.getAircraftTypes().stream().map(AircraftTypeInfoMapper::toAircraftTypeInfo).collect(Collectors.toSet()); + pilot.setAircraftTypes(aircraftTypes); + + return PilotMapper.toResponse(pilotRepository.save(pilot)); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java index a8f20c6..692eb05 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java @@ -2,12 +2,14 @@ import jakarta.persistence.*; import lombok.Getter; +import lombok.Setter; import java.util.Set; import java.util.UUID; @Entity @Getter +@Setter public class Pilot { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java index b43b9a0..7cdb3d9 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java @@ -3,12 +3,16 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.UUID; @Entity @Getter +@AllArgsConstructor +@NoArgsConstructor public class Qualification { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java index 84f5900..62dc727 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import pl.skyroster.skyroster_backend.domain.model.Pilot; +import java.util.Optional; import java.util.UUID; public interface PilotRepository { @@ -16,6 +17,8 @@ public interface PilotRepository { }) @Query("SELECT p FROM Pilot p") Page findAllWithRelations(Pageable pageable); + Optional findById(UUID id); boolean existsById(UUID id); void deleteById(UUID id); + Pilot save(Pilot pilot); } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java index b2aa8d5..3d66bee 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java @@ -6,8 +6,11 @@ import org.springframework.web.bind.annotation.*; import pl.skyroster.skyroster_backend.application.pilot.DeletePilotUseCase; import pl.skyroster.skyroster_backend.application.pilot.GetPilotUseCase; +import pl.skyroster.skyroster_backend.application.pilot.PatchPilotUseCase; import pl.skyroster.skyroster_backend.generated.api.ApiApi; import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; +import pl.skyroster.skyroster_backend.generated.model.PilotPatchRequest; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; import java.util.UUID; @@ -17,6 +20,7 @@ public class PilotController { private final GetPilotUseCase getPilotUseCase; private final DeletePilotUseCase deletePilotUseCase; + private final PatchPilotUseCase patchPilotUseCase; @GetMapping(ApiApi.PATH_GET_PILOTS) public ResponseEntity getPilots(@RequestParam Integer page, @RequestParam Integer size, @RequestParam @Nullable String sort) { @@ -28,4 +32,9 @@ public ResponseEntity deletePilotById(@PathVariable UUID pilotId) { deletePilotUseCase.deletePilotById(pilotId); return ResponseEntity.noContent().build(); } + + @PatchMapping(ApiApi.PATH_PATCH_PILOT) + public ResponseEntity patchPilot(@PathVariable UUID pilotId, @RequestBody PilotPatchRequest request) { + return ResponseEntity.ok(patchPilotUseCase.patchPilot(pilotId, request)); + } } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java index 8cbb358..e7a5334 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java @@ -49,6 +49,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers(HttpMethod.POST, "/api/aircraft/**").hasRole("operations_administrator") .requestMatchers(HttpMethod.GET, "/api/aircraft/**").authenticated() .requestMatchers(HttpMethod.DELETE, "/api/pilots/**").hasRole("operations_administrator") + .requestMatchers(HttpMethod.PATCH, "/api/pilots/**").hasRole("operations_administrator") .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) @@ -65,7 +66,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { public CorsConfigurationSource corsConfigurationSource() { var config = new CorsConfiguration(); config.setAllowedOrigins(allowedOrigins); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("Authorization", "Content-Type")); var source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java index d5ae2e8..601e18b 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java @@ -8,7 +8,7 @@ public static AircraftResponse map(Aircraft aircraft) { return new AircraftResponse( aircraft.getId(), aircraft.getRegistrationNumber(), - AircraftTypeInfoMapper.map(aircraft.getAircraftType()), + AircraftTypeInfoMapper.toAircraftTypeInfo(aircraft.getAircraftType()), OperationalBaseInfoMapper.map(aircraft.getOperationalBase()) ); } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java index 05ba98c..4f5eef1 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java @@ -4,11 +4,18 @@ import pl.skyroster.skyroster_backend.generated.model.AircraftTypeInfo; public class AircraftTypeInfoMapper { - public static AircraftTypeInfo map(AircraftType aircraftType) { + public static AircraftTypeInfo toAircraftTypeInfo(AircraftType aircraftType) { return new AircraftTypeInfo( aircraftType.getId(), aircraftType.getIcaoCode(), aircraftType.getName() ); } + public static AircraftType toAircraftTypeInfo(AircraftTypeInfo aircraftType) { + return new AircraftType( + aircraftType.getId(), + aircraftType.getIcaoCode(), + aircraftType.getName() + ); + } } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java index f81572c..98c8816 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java @@ -27,7 +27,7 @@ public static PilotResponse toResponse(Pilot pilot) { pilot.getAircraftTypes() .stream() .sorted(Comparator.comparing(AircraftType::getName)) - .map(AircraftTypeInfoMapper::map) + .map(AircraftTypeInfoMapper::toAircraftTypeInfo) .toList()); } } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java index a26533a..9baaa66 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java @@ -10,4 +10,8 @@ public static PilotQualificationInfo toPilotQualificationInfo(Qualification qual .values(qualification.getValue()) .label(qualification.getLabel()); } + + public static Qualification fromPilotQualificationInfo(PilotQualificationInfo qualification) { + return new Qualification(qualification.getId(), qualification.getValues(), qualification.getLabel()); + } } diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 21c3e56..6f6cbdd 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -344,6 +344,61 @@ paths: '404': description: Pilot not found + patch: + tags: + - Pilots + summary: Partially update a pilot + description: Updates selected pilot fields. Fields omitted from the request body remain unchanged. + operationId: patchPilot + parameters: + - name: pilotId + in: path + required: true + description: Pilot ID + schema: + type: string + format: uuid + example: 0b7a9c1d-86c2-49dc-8ef8-d7f6c7e6e6f1 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PilotPatchRequest' + examples: + updateBasicData: + summary: Update pilot basic data + value: + name: Jan + surname: Kowalski + licence: ATPL-123456 + updateOperationalBase: + summary: Update pilot operational base + value: + operationalBaseId: 2a9dfd20-90fc-4b7e-a996-358d4b5f79c7 + updateQualificationsAndAircraftTypes: + summary: Update qualifications and aircraft types + value: + qualifications: + - PPL + - CPL + - ATPL + aircraftTypes: + - A320 + - B737 + responses: + '200': + description: Pilot updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/PilotResponse' + '400': + description: Invalid request body + '404': + description: Pilot not found + + components: securitySchemes: bearerAuth: @@ -521,6 +576,40 @@ components: items: $ref: '#/components/schemas/AircraftTypeInfo' + PilotPatchRequest: + type: object + description: Request body for partially updating a pilot. Null or omitted fields are not changed. + additionalProperties: false + properties: + name: + type: string + description: Pilot name + example: Jan + surname: + type: string + description: Pilot surname + example: Kowalski + licence: + type: string + description: Pilot licence number + example: ATPL-123456 + operationalBaseIcaoCode: + type: string + description: ICAO operational base code + example: "EPWA" + qualifications: + type: array + description: Pilot qualifications. If provided, replaces the current qualification set. + uniqueItems: true + items: + $ref: '#/components/schemas/PilotQualificationInfo' + aircraftTypes: + type: array + description: Aircraft types assigned to the pilot. If provided, replaces the current aircraft type set. + uniqueItems: true + items: + $ref: '#/components/schemas/AircraftTypeInfo' + PilotQualificationInfo: type: object From 6f9e92175144ff22781b017d9503b8e767fc6034 Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Thu, 14 May 2026 23:16:09 +0200 Subject: [PATCH 4/4] [SKY-20]: Add API POST endpoint for pilots --- .../application/pilot/AddPilotUseCase.java | 24 +++++++ .../PilotAlreadyExistsException.java | 7 ++ .../skyroster_backend/domain/model/Pilot.java | 4 ++ .../domain/port/PilotRepository.java | 1 + .../adapter/in/web/PilotController.java | 8 +++ .../config/GlobalExceptionHandler.java | 12 ++-- .../config/security/SecurityConfig.java | 1 + .../mappers/OperationalBaseInfoMapper.java | 8 +++ .../infrastructure/mappers/PilotMapper.java | 13 ++++ .../src/main/resources/api/openapi.yaml | 72 +++++++++++++++++++ 10 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java new file mode 100644 index 0000000..2234ded --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java @@ -0,0 +1,24 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.exception.PilotAlreadyExistsException; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; +import pl.skyroster.skyroster_backend.generated.model.PilotRequest; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotMapper; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; +@Service +@RequiredArgsConstructor +public class AddPilotUseCase { + private final PilotRepository pilotRepository; + + @Transactional + public PilotResponse addPilot(PilotRequest request) { + if (pilotRepository.existsByLicence(request.getLicence())){ + throw new PilotAlreadyExistsException("Pilot already exists"); + } + var pilot = PilotMapper.toEntity(request); + return PilotMapper.toResponse(pilotRepository.save(pilot)); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java new file mode 100644 index 0000000..1703721 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.exception; + +public class PilotAlreadyExistsException extends RuntimeException { + public PilotAlreadyExistsException(String message) { + super(message); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java index 692eb05..c186fe0 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java @@ -1,7 +1,9 @@ package pl.skyroster.skyroster_backend.domain.model; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import java.util.Set; @@ -10,6 +12,8 @@ @Entity @Getter @Setter +@AllArgsConstructor +@NoArgsConstructor public class Pilot { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java index 62dc727..2213bb6 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java @@ -19,6 +19,7 @@ public interface PilotRepository { Page findAllWithRelations(Pageable pageable); Optional findById(UUID id); boolean existsById(UUID id); + boolean existsByLicence(String licence); void deleteById(UUID id); Pilot save(Pilot pilot); } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java index 3d66bee..9384efd 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java @@ -4,12 +4,14 @@ import org.jspecify.annotations.Nullable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import pl.skyroster.skyroster_backend.application.pilot.AddPilotUseCase; import pl.skyroster.skyroster_backend.application.pilot.DeletePilotUseCase; import pl.skyroster.skyroster_backend.application.pilot.GetPilotUseCase; import pl.skyroster.skyroster_backend.application.pilot.PatchPilotUseCase; import pl.skyroster.skyroster_backend.generated.api.ApiApi; import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; import pl.skyroster.skyroster_backend.generated.model.PilotPatchRequest; +import pl.skyroster.skyroster_backend.generated.model.PilotRequest; import pl.skyroster.skyroster_backend.generated.model.PilotResponse; import java.util.UUID; @@ -21,6 +23,7 @@ public class PilotController { private final GetPilotUseCase getPilotUseCase; private final DeletePilotUseCase deletePilotUseCase; private final PatchPilotUseCase patchPilotUseCase; + private final AddPilotUseCase addPilotUseCase; @GetMapping(ApiApi.PATH_GET_PILOTS) public ResponseEntity getPilots(@RequestParam Integer page, @RequestParam Integer size, @RequestParam @Nullable String sort) { @@ -37,4 +40,9 @@ public ResponseEntity deletePilotById(@PathVariable UUID pilotId) { public ResponseEntity patchPilot(@PathVariable UUID pilotId, @RequestBody PilotPatchRequest request) { return ResponseEntity.ok(patchPilotUseCase.patchPilot(pilotId, request)); } + + @PostMapping(ApiApi.PATH_CREATE_PILOT) + public ResponseEntity addPilot(@RequestBody PilotRequest pilot){ + return ResponseEntity.ok(addPilotUseCase.addPilot(pilot)); + } } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java index 532cf70..8347c88 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java @@ -6,12 +6,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import pl.skyroster.skyroster_backend.domain.exception.AircraftAlreadyExistsException; -import pl.skyroster.skyroster_backend.domain.exception.AircraftHasAssignedFlightsException; -import pl.skyroster.skyroster_backend.domain.exception.AircraftNotFoundException; -import pl.skyroster.skyroster_backend.domain.exception.AircraftTypeNotFoundException; -import pl.skyroster.skyroster_backend.domain.exception.OperationalBaseNotFoundException; -import pl.skyroster.skyroster_backend.domain.exception.PilotNotFoundException; +import pl.skyroster.skyroster_backend.domain.exception.*; import pl.skyroster.skyroster_backend.generated.model.ErrorResponse; import java.time.OffsetDateTime; @@ -56,6 +51,11 @@ public ResponseEntity handlePilotNotFound(PilotNotFoundException return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage(), request); } + @ExceptionHandler(PilotAlreadyExistsException.class) + public ResponseEntity handlePilotAlreadyExists(PilotAlreadyExistsException ex,HttpServletRequest request) { + return buildResponse(HttpStatus.CONFLICT, ex.getMessage(), request); + } + @ExceptionHandler(OperationalBaseNotFoundException.class) public ResponseEntity handleOperationalBaseNotFound(OperationalBaseNotFoundException ex, HttpServletRequest request) { diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java index e7a5334..89a4e0a 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java @@ -50,6 +50,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers(HttpMethod.GET, "/api/aircraft/**").authenticated() .requestMatchers(HttpMethod.DELETE, "/api/pilots/**").hasRole("operations_administrator") .requestMatchers(HttpMethod.PATCH, "/api/pilots/**").hasRole("operations_administrator") + .requestMatchers(HttpMethod.POST, "/api/pilots/**").hasRole("operations_administrator") .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java index 7370ed7..654f49b 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java @@ -11,4 +11,12 @@ public static OperationalBaseInfo map(OperationalBase operationalBase) { operationalBase.getName() ); } + + public static OperationalBase fromInfo(OperationalBaseInfo operationalBase) { + return new OperationalBase( + operationalBase.getId(), + operationalBase.getIcaoCode(), + operationalBase.getName() + ); + } } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java index 98c8816..4b08478 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java @@ -5,8 +5,11 @@ import pl.skyroster.skyroster_backend.domain.model.Pilot; import pl.skyroster.skyroster_backend.domain.model.Qualification; import pl.skyroster.skyroster_backend.generated.model.PilotResponse; +import pl.skyroster.skyroster_backend.generated.model.PilotRequest; import java.util.Comparator; +import java.util.UUID; +import java.util.stream.Collectors; @Component public class PilotMapper { @@ -30,4 +33,14 @@ public static PilotResponse toResponse(Pilot pilot) { .map(AircraftTypeInfoMapper::toAircraftTypeInfo) .toList()); } + public static Pilot toEntity(PilotRequest request){ + return new Pilot( + UUID.randomUUID(), + request.getFirstName(), + request.getLastName(), + request.getLicence(), + OperationalBaseInfoMapper.fromInfo(request.getHomeBase()), + request.getQualifications().stream().map(PilotQualificationMapper::fromPilotQualificationInfo).collect(Collectors.toSet()), + request.getAircraftTypes().stream().map(AircraftTypeInfoMapper::toAircraftTypeInfo).collect(Collectors.toSet())); + } } \ No newline at end of file diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 6f6cbdd..cb1c775 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -204,6 +204,50 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - Pilots + summary: Add a new pilot + description: Creates a new pilot in the system. + operationId: createPilot + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PilotRequest' + responses: + '201': + description: Pilot created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/PilotResponse' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Pilot already exists, for example duplicate license number or email + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/flights: get: operationId: displayFlights @@ -576,6 +620,34 @@ components: items: $ref: '#/components/schemas/AircraftTypeInfo' + PilotRequest: + type: object + required: + - firstName + - lastName + - homeBase + - qualifications + properties: + licence: + type: string + example: PIL-001 + firstName: + type: string + example: Jan + lastName: + type: string + example: Kowalski + homeBase: + $ref: '#/components/schemas/OperationalBaseInfo' + qualifications: + type: array + items: + $ref: '#/components/schemas/PilotQualificationInfo' + aircraftTypes: + type: array + items: + $ref: '#/components/schemas/AircraftTypeInfo' + PilotPatchRequest: type: object description: Request body for partially updating a pilot. Null or omitted fields are not changed.