diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/GetFlightsUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/GetFlightsUseCase.java index 314b41b..9037025 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/GetFlightsUseCase.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/GetFlightsUseCase.java @@ -2,8 +2,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import pl.skyroster.skyroster_backend.domain.model.Flight; import pl.skyroster.skyroster_backend.domain.port.FlightRepository; +import pl.skyroster.skyroster_backend.generated.model.FlightResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.AircraftResponseMapper; +import pl.skyroster.skyroster_backend.infrastructure.mappers.OperationalBaseInfoMapper; import java.util.List; @@ -16,7 +18,19 @@ public GetFlightsUseCase(FlightRepository flightRepository) { } @Transactional(readOnly = true) - public List execute() { - return flightRepository.findAll(); + public List execute() { + return flightRepository.findAll().stream().map(flight -> { + var aircraftResponse = AircraftResponseMapper.map(flight.getAircraft()); + + return new FlightResponse( + flight.getId(), + aircraftResponse, + flight.getFlightStart(), + flight.getFlightEnd(), + OperationalBaseInfoMapper.map(flight.getStartAirport()), + OperationalBaseInfoMapper.map(flight.getEndAirport()), + flight.getDescription() + ); + }).toList(); } } 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/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/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/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/application/rule/GetRulesUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/GetRulesUseCase.java new file mode 100644 index 0000000..2909a23 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/GetRulesUseCase.java @@ -0,0 +1,23 @@ +package pl.skyroster.skyroster_backend.application.rule; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.List; + +@Service +public class GetRulesUseCase { + + private final RuleRepository ruleRepository; + + public GetRulesUseCase(RuleRepository ruleRepository) { + this.ruleRepository = ruleRepository; + } + + @Transactional(readOnly = true) + public List execute() { + return ruleRepository.findAll(); + } +} 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/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/model/Pilot.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java index efe885a..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,11 +1,19 @@ 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; import java.util.UUID; -import jakarta.persistence.*; - @Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor 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..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,10 +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/model/Rule.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java new file mode 100644 index 0000000..36ee6ec --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java @@ -0,0 +1,41 @@ +package pl.skyroster.skyroster_backend.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@Table(name = "rules") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Rule { + + @Id + private UUID id; + + @Column(nullable = false, length = 100) + private String name; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private RuleType type; + + @Column(nullable = false) + private Integer value; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + private RulePeriod period; + + @Column(length = 500) + private String description; +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java new file mode 100644 index 0000000..0ea8f93 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.model; + +public enum RulePeriod { + DAY, + WEEK, + MONTH +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java new file mode 100644 index 0000000..7fa13e3 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.model; + +public enum RuleType { + MAX_WORK_TIME, + MIN_REST_TIME, + MIN_FLIGHT_TIME +} 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 63edc96..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 @@ -1,11 +1,17 @@ package pl.skyroster.skyroster_backend.domain.port; +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 { +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 new file mode 100644 index 0000000..2213bb6 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java @@ -0,0 +1,25 @@ +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; + +import java.util.Optional; +import java.util.UUID; + +public interface PilotRepository { + @EntityGraph(attributePaths = { + "qualifications", + "aircraftTypes", + "operationalBase" + }) + @Query("SELECT p FROM Pilot p") + 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/domain/port/RuleRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java new file mode 100644 index 0000000..b5a45d0 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java @@ -0,0 +1,9 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import pl.skyroster.skyroster_backend.domain.model.Rule; + +import java.util.List; + +public interface RuleRepository { + List findAll(); +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/FlightController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/FlightController.java index 976842f..c653650 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/FlightController.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/FlightController.java @@ -4,7 +4,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import pl.skyroster.skyroster_backend.application.flight.GetFlightsUseCase; -import pl.skyroster.skyroster_backend.domain.model.Flight; +import pl.skyroster.skyroster_backend.generated.model.FlightResponse; import java.util.List; @@ -18,7 +18,7 @@ public FlightController(GetFlightsUseCase getFlightsUseCase) { } @GetMapping - public List getFlights() { + public List getFlights() { return getFlightsUseCase.execute(); } } 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..9384efd --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java @@ -0,0 +1,48 @@ +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.*; +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; + +@RequiredArgsConstructor +@RestController +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) { + 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(); + } + + @PatchMapping(ApiApi.PATH_PATCH_PILOT) + 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/adapter/in/web/RuleController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java new file mode 100644 index 0000000..a4c8217 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java @@ -0,0 +1,27 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import pl.skyroster.skyroster_backend.application.rule.GetRulesUseCase; +import pl.skyroster.skyroster_backend.generated.api.ApiApi; +import pl.skyroster.skyroster_backend.generated.model.RuleResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.RuleResponseMapper; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +public class RuleController { + + private final GetRulesUseCase getRulesUseCase; + + @GetMapping(ApiApi.PATH_GET_RULES) + public ResponseEntity> getRules() { + List response = getRulesUseCase.execute().stream() + .map(RuleResponseMapper::map) + .toList(); + return ResponseEntity.ok(response); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaFlightRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaFlightRepository.java deleted file mode 100644 index 5c496e7..0000000 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaFlightRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; - -import org.springframework.data.jpa.repository.JpaRepository; -import pl.skyroster.skyroster_backend.domain.model.Flight; -import pl.skyroster.skyroster_backend.domain.port.FlightRepository; - -import java.util.UUID; - -public interface JpaFlightRepository extends JpaRepository, FlightRepository { -} 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/adapter/out/persistence/JpaRuleRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java new file mode 100644 index 0000000..23fdc7d --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.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.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.UUID; + +public interface JpaRuleRepository extends JpaRepository, RuleRepository { +} 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..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,11 +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.*; import pl.skyroster.skyroster_backend.generated.model.ErrorResponse; import java.time.OffsetDateTime; @@ -50,6 +46,16 @@ 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(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) { @@ -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..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 @@ -48,6 +48,9 @@ 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(HttpMethod.PATCH, "/api/pilots/**").hasRole("operations_administrator") + .requestMatchers(HttpMethod.POST, "/api/pilots/**").hasRole("operations_administrator") .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) @@ -64,7 +67,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 new file mode 100644 index 0000000..601e18b --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java @@ -0,0 +1,15 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import pl.skyroster.skyroster_backend.domain.model.Aircraft; +import pl.skyroster.skyroster_backend.generated.model.AircraftResponse; + +public class AircraftResponseMapper { + public static AircraftResponse map(Aircraft aircraft) { + return new AircraftResponse( + aircraft.getId(), + aircraft.getRegistrationNumber(), + 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 new file mode 100644 index 0000000..4f5eef1 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java @@ -0,0 +1,21 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import pl.skyroster.skyroster_backend.domain.model.AircraftType; +import pl.skyroster.skyroster_backend.generated.model.AircraftTypeInfo; + +public class AircraftTypeInfoMapper { + 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/OperationalBaseInfoMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java new file mode 100644 index 0000000..654f49b --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java @@ -0,0 +1,22 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import pl.skyroster.skyroster_backend.domain.model.OperationalBase; +import pl.skyroster.skyroster_backend.generated.model.OperationalBaseInfo; + +public class OperationalBaseInfoMapper { + public static OperationalBaseInfo map(OperationalBase operationalBase) { + return new OperationalBaseInfo( + operationalBase.getId(), + operationalBase.getIcaoCode(), + 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 new file mode 100644 index 0000000..4b08478 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java @@ -0,0 +1,46 @@ +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 pl.skyroster.skyroster_backend.generated.model.PilotRequest; + +import java.util.Comparator; +import java.util.UUID; +import java.util.stream.Collectors; + +@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::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/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..9baaa66 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java @@ -0,0 +1,17 @@ +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()); + } + + public static Qualification fromPilotQualificationInfo(PilotQualificationInfo qualification) { + return new Qualification(qualification.getId(), qualification.getValues(), qualification.getLabel()); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java new file mode 100644 index 0000000..6377711 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java @@ -0,0 +1,18 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.generated.model.RulePeriod; +import pl.skyroster.skyroster_backend.generated.model.RuleResponse; +import pl.skyroster.skyroster_backend.generated.model.RuleType; + +public class RuleResponseMapper { + public static RuleResponse map(Rule rule) { + return new RuleResponse( + rule.getId(), + rule.getName(), + RuleType.valueOf(rule.getType().name()), + rule.getValue(), + RulePeriod.valueOf(rule.getPeriod().name()) + ).description(rule.getDescription()); + } +} diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index f394073..474b1c3 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,11 +156,150 @@ 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' + + 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/rules: + get: + operationId: getRules + tags: [ Rules ] + summary: List operational rules + responses: + '200': + description: List of operational rules + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RuleResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/flights: + get: + operationId: displayFlights + tags: [ Flight ] + summary: Display flights timetable + responses: + '200': + description: List of flights + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/FlightResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /api/aircraft/{id}: put: operationId: updateAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Update an aircraft in the fleet parameters: - in: path @@ -214,7 +353,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' delete: operationId: deleteAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Delete an aircraft from the fleet parameters: - in: path @@ -251,6 +390,80 @@ 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 + + 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: @@ -309,7 +522,7 @@ components: description: List of realm roles assigned to the user items: type: string - example: ["operations_administrator"] + example: [ "operations_administrator" ] HealthStatus: type: object @@ -361,6 +574,153 @@ 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' + + 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. + 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 + required: + - id + - aircraftType + - licenseValidUntil + properties: + id: + type: string + format: uuid + values: + type: string + label: + type: string + + + AddAircraftRequest: type: object required: @@ -453,3 +813,75 @@ components: $ref: '#/components/schemas/AircraftTypeInfo' operationalBase: $ref: '#/components/schemas/OperationalBaseInfo' + + RuleType: + type: string + enum: + - MAX_WORK_TIME + - MIN_REST_TIME + - MIN_FLIGHT_TIME + + RulePeriod: + type: string + enum: + - DAY + - WEEK + - MONTH + + RuleResponse: + type: object + required: + - id + - name + - type + - value + - period + properties: + id: + type: string + format: uuid + name: + type: string + type: + $ref: '#/components/schemas/RuleType' + value: + type: integer + minimum: 1 + description: Value in hours + period: + $ref: '#/components/schemas/RulePeriod' + description: + type: string + + FlightResponse: + type: object + required: + - id + - aircraft + - pilot + - flightStart + - flightEnd + - startAirport + - endAirport + - description + properties: + id: + type: string + format: uuid + aircraft: + $ref: '#/components/schemas/AircraftResponse' + flightStart: + type: string + format: date-time + example: "2026-06-20T08:00:00Z" + flightEnd: + type: string + format: date-time + example: "2026-06-20T11:00:00Z" + startAirport: + $ref: '#/components/schemas/OperationalBaseInfo' + endAirport: + $ref: '#/components/schemas/OperationalBaseInfo' + description: + type: string + example: "Lot rejsowy Kraków - Gdańsk" \ No newline at end of file diff --git a/skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql b/skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql new file mode 100644 index 0000000..e49fa0f --- /dev/null +++ b/skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql @@ -0,0 +1,22 @@ +CREATE TABLE rules ( + id UUID PRIMARY KEY, + name VARCHAR(100) NOT NULL, + type VARCHAR(30) NOT NULL, + value INTEGER NOT NULL CHECK (value > 0), + period VARCHAR(10) NOT NULL, + description VARCHAR(500) +); + +INSERT INTO rules (id, name, type, value, period, description) VALUES + ('11111111-0000-0000-0000-000000000001', 'Maksymalny czas pracy miesięczny', + 'MAX_WORK_TIME', 100, 'MONTH', + 'Pilot nie może przekroczyć 100 godzin pracy w miesiącu'), + ('11111111-0000-0000-0000-000000000002', 'Maksymalny czas pracy dzienny', + 'MAX_WORK_TIME', 10, 'DAY', + 'Pilot nie może przekroczyć 10 godzin pracy dziennie'), + ('11111111-0000-0000-0000-000000000003', 'Minimalny odpoczynek między lotami', + 'MIN_REST_TIME', 12, 'DAY', + 'Wymagany minimalny odpoczynek 12 godzin między lotami'), + ('11111111-0000-0000-0000-000000000004', 'Minimalny nalot miesięczny', + 'MIN_FLIGHT_TIME', 10, 'MONTH', + 'Pilot musi wykonać minimum 10 godzin nalotu miesięcznie'); diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java new file mode 100644 index 0000000..2734b6c --- /dev/null +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java @@ -0,0 +1,91 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dasniko.testcontainers.keycloak.KeycloakContainer; +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.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.servlet.MockMvc; +import pl.skyroster.skyroster_backend.TestcontainersConfiguration; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@Import(TestcontainersConfiguration.class) +class RuleIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private KeycloakContainer keycloak; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void getRules_shouldReturn401_whenNoToken() throws Exception { + mockMvc.perform(get("/api/rules")) + .andExpect(status().isUnauthorized()); + } + + @Test + void getRules_shouldReturnSeededRules_whenAuthenticated() throws Exception { + String token = getToken("admin", "test1234"); + + mockMvc.perform(get("/api/rules") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$", hasSize(4))) + .andExpect(jsonPath( + "$[?(@.name == 'Maksymalny czas pracy miesięczny' && @.type == 'MAX_WORK_TIME' && @.value == 100 && @.period == 'MONTH')]") + .exists()) + .andExpect(jsonPath( + "$[?(@.name == 'Maksymalny czas pracy dzienny' && @.type == 'MAX_WORK_TIME' && @.value == 10 && @.period == 'DAY')]") + .exists()) + .andExpect(jsonPath( + "$[?(@.name == 'Minimalny odpoczynek między lotami' && @.type == 'MIN_REST_TIME' && @.value == 12 && @.period == 'DAY')]") + .exists()) + .andExpect(jsonPath( + "$[?(@.name == 'Minimalny nalot miesięczny' && @.type == 'MIN_FLIGHT_TIME' && @.value == 10 && @.period == 'MONTH')]") + .exists()); + } + + private String getToken(String username, String password) throws Exception { + String tokenUrl = keycloak.getAuthServerUrl() + "/realms/skyroster/protocol/openid-connect/token"; + + String reqBody = "grant_type=password" + + "&client_id=skyroster-frontend" + + "&username=" + username + + "&password=" + password; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(tokenUrl)) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(reqBody)) + .build(); + + HttpResponse response = HttpClient.newHttpClient() + .send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Failed to get token: " + response.body()); + } + + JsonNode json = objectMapper.readTree(response.body()); + return json.get("access_token").asText(); + } +}