Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package pl.skyroster.skyroster_backend.application.flight;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pl.skyroster.skyroster_backend.domain.exception.AircraftNotFoundException;
import pl.skyroster.skyroster_backend.domain.exception.FlightTimeConflictException;
import pl.skyroster.skyroster_backend.domain.exception.OperationalBaseNotFoundException;
import pl.skyroster.skyroster_backend.domain.exception.PilotNotFoundException;
import pl.skyroster.skyroster_backend.domain.exception.RuleViolationException;
import pl.skyroster.skyroster_backend.domain.model.Aircraft;
import pl.skyroster.skyroster_backend.domain.model.Flight;
import pl.skyroster.skyroster_backend.domain.model.OperationalBase;
import pl.skyroster.skyroster_backend.domain.model.Pilot;
import pl.skyroster.skyroster_backend.domain.model.Rule;
import pl.skyroster.skyroster_backend.domain.model.RuleViolation;
import pl.skyroster.skyroster_backend.domain.port.AircraftRepository;
import pl.skyroster.skyroster_backend.domain.port.FlightRepository;
import pl.skyroster.skyroster_backend.domain.port.OperationalBaseRepository;
import pl.skyroster.skyroster_backend.domain.port.PilotRepository;
import pl.skyroster.skyroster_backend.domain.port.RuleRepository;
import pl.skyroster.skyroster_backend.domain.service.RuleValidator;

import java.time.OffsetDateTime;
import java.time.temporal.TemporalAdjusters;
import java.util.List;
import java.util.UUID;

@Service
public class CreateFlightUseCase {

private final AircraftRepository aircraftRepository;
private final PilotRepository pilotRepository;
private final OperationalBaseRepository operationalBaseRepository;
private final FlightRepository flightRepository;
private final RuleRepository ruleRepository;
private final RuleValidator ruleValidator = new RuleValidator();

public CreateFlightUseCase(AircraftRepository aircraftRepository,
PilotRepository pilotRepository,
OperationalBaseRepository operationalBaseRepository,
FlightRepository flightRepository,
RuleRepository ruleRepository) {
this.aircraftRepository = aircraftRepository;
this.pilotRepository = pilotRepository;
this.operationalBaseRepository = operationalBaseRepository;
this.flightRepository = flightRepository;
this.ruleRepository = ruleRepository;
}

@Transactional
public Flight execute(UUID aircraftId, UUID pilotId,
OffsetDateTime startDateTime, OffsetDateTime endDateTime,
UUID startAirportId, UUID endAirportIdOrNull,
String descriptionOrNull) {
if (startDateTime == null || endDateTime == null || !startDateTime.isBefore(endDateTime)) {
throw new IllegalArgumentException("startDateTime must be before endDateTime");
}

Aircraft aircraft = aircraftRepository.findById(aircraftId)
.orElseThrow(AircraftNotFoundException::new);
Pilot pilot = pilotRepository.findById(pilotId)
.orElseThrow(() -> new PilotNotFoundException(pilotId));
OperationalBase startAirport = operationalBaseRepository.findById(startAirportId)
.orElseThrow(() -> new OperationalBaseNotFoundException(startAirportId.toString()));
OperationalBase endAirport = null;
if (endAirportIdOrNull != null) {
endAirport = operationalBaseRepository.findById(endAirportIdOrNull)
.orElseThrow(() -> new OperationalBaseNotFoundException(endAirportIdOrNull.toString()));
}

if (!flightRepository.findOverlappingByAircraft(aircraftId, startDateTime, endDateTime).isEmpty()) {
throw new FlightTimeConflictException("Aircraft already has a flight in this time window");
}
if (!flightRepository.findOverlappingByPilot(pilotId, startDateTime, endDateTime).isEmpty()) {
throw new FlightTimeConflictException("Pilot already has a flight in this time window");
}

OffsetDateTime monthStart = startDateTime
.with(TemporalAdjusters.firstDayOfMonth())
.toLocalDate()
.atStartOfDay()
.atOffset(startDateTime.getOffset());
OffsetDateTime monthEnd = monthStart.plusMonths(1);
List<Flight> contextFlights = flightRepository.findByPilotInPeriod(pilotId, monthStart, monthEnd);
List<RuleValidator.TimeRange> ranges = contextFlights.stream()
.map(f -> new RuleValidator.TimeRange(f.getFlightStart(), f.getFlightEnd()))
.toList();
List<Rule> rules = ruleRepository.findAll();
List<RuleViolation> violations = ruleValidator.validate(pilotId, startDateTime, endDateTime, rules, ranges);
if (!violations.isEmpty()) {
throw new RuleViolationException(violations);
}

Flight flight = new Flight(
UUID.randomUUID(),
aircraft,
pilot,
startDateTime,
endDateTime,
startAirport,
endAirport,
descriptionOrNull
);
return flightRepository.save(flight);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package pl.skyroster.skyroster_backend.application.flight;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pl.skyroster.skyroster_backend.domain.model.Aircraft;
import pl.skyroster.skyroster_backend.domain.port.AircraftRepository;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;

@Service
public class GetAvailableAircraftUseCase {

private final AircraftRepository aircraftRepository;

public GetAvailableAircraftUseCase(AircraftRepository aircraftRepository) {
this.aircraftRepository = aircraftRepository;
}

@Transactional(readOnly = true)
public List<Aircraft> execute(OffsetDateTime from, OffsetDateTime to, UUID baseIdOrNull) {
if (from == null || to == null || !from.isBefore(to)) {
throw new IllegalArgumentException("from must be before to and both required");
}
return aircraftRepository.findAvailable(from, to, baseIdOrNull);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package pl.skyroster.skyroster_backend.application.flight;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pl.skyroster.skyroster_backend.domain.model.Pilot;
import pl.skyroster.skyroster_backend.domain.port.PilotRepository;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;

@Service
public class GetAvailablePilotsUseCase {

private final PilotRepository pilotRepository;

public GetAvailablePilotsUseCase(PilotRepository pilotRepository) {
this.pilotRepository = pilotRepository;
}

@Transactional(readOnly = true)
public List<Pilot> execute(OffsetDateTime from, OffsetDateTime to, UUID aircraftTypeIdOrNull) {
if (from == null || to == null || !from.isBefore(to)) {
throw new IllegalArgumentException("from must be before to and both required");
}
return pilotRepository.findAvailable(from, to, aircraftTypeIdOrNull);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import org.springframework.transaction.annotation.Transactional;
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 pl.skyroster.skyroster_backend.infrastructure.mappers.FlightResponseMapper;

import java.util.List;

Expand All @@ -19,18 +18,8 @@ public GetFlightsUseCase(FlightRepository flightRepository) {

@Transactional(readOnly = true)
public List<FlightResponse> 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();
return flightRepository.findAll().stream()
.map(FlightResponseMapper::map)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package pl.skyroster.skyroster_backend.domain.exception;

public class FlightTimeConflictException extends RuntimeException {
public FlightTimeConflictException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package pl.skyroster.skyroster_backend.domain.exception;

import pl.skyroster.skyroster_backend.domain.model.RuleViolation;

import java.util.List;

public class RuleViolationException extends RuntimeException {
private final List<RuleViolation> violations;

public RuleViolationException(List<RuleViolation> violations) {
super("Rule violations detected: " + violations.size());
this.violations = List.copyOf(violations);
}

public List<RuleViolation> getViolations() {
return violations;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package pl.skyroster.skyroster_backend.domain.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.OffsetDateTime;
import java.util.UUID;

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Setter
public class Flight {

@Id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package pl.skyroster.skyroster_backend.domain.model;

import java.util.UUID;

public record RuleViolation(UUID ruleId, String ruleName, RuleType ruleType, String message) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pl.skyroster.skyroster_backend.domain.model.Aircraft;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
Expand All @@ -13,4 +14,5 @@ public interface AircraftRepository {
boolean existsByRegistrationNumber(String registrationNumber);
boolean existsByRegistrationNumberAndIdNot(String registrationNumber, UUID id);
void deleteById(UUID id);
List<Aircraft> findAvailable(OffsetDateTime from, OffsetDateTime to, UUID baseIdOrNull);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,71 @@
package pl.skyroster.skyroster_backend.domain.port;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import pl.skyroster.skyroster_backend.domain.model.Flight;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

public interface FlightRepository extends JpaRepository<Flight, UUID> {

List<Flight> findAll();

boolean existsByAircraftId(UUID aircraftId);
boolean existsByPilotIdAndFlightEndAfter(
UUID pilotId,
OffsetDateTime now
);
}

boolean existsByPilotIdAndFlightEndAfter(UUID pilotId, OffsetDateTime now);

@Query("""
SELECT f FROM Flight f
WHERE f.aircraft.id = :aircraftId
AND f.flightStart < :to
AND f.flightEnd > :from
""")
List<Flight> findOverlappingByAircraft(@Param("aircraftId") UUID aircraftId,
@Param("from") OffsetDateTime from,
@Param("to") OffsetDateTime to);

@Query("""
SELECT f FROM Flight f
WHERE f.pilot.id = :pilotId
AND f.flightStart < :to
AND f.flightEnd > :from
""")
List<Flight> findOverlappingByPilot(@Param("pilotId") UUID pilotId,
@Param("from") OffsetDateTime from,
@Param("to") OffsetDateTime to);

@Query("""
SELECT f FROM Flight f
WHERE f.pilot.id = :pilotId
AND f.flightStart < :to
AND f.flightEnd > :from
ORDER BY f.flightStart
""")
List<Flight> findByPilotInPeriod(@Param("pilotId") UUID pilotId,
@Param("from") OffsetDateTime from,
@Param("to") OffsetDateTime to);

@Query("""
SELECT f FROM Flight f
WHERE f.pilot.id = :pilotId
AND f.flightEnd <= :before
ORDER BY f.flightEnd DESC
LIMIT 1
""")
Optional<Flight> findLastByPilotBefore(@Param("pilotId") UUID pilotId,
@Param("before") OffsetDateTime before);

@Query("""
SELECT f FROM Flight f
WHERE f.pilot.id = :pilotId
AND f.flightStart >= :after
ORDER BY f.flightStart
LIMIT 1
""")
Optional<Flight> findFirstByPilotAfter(@Param("pilotId") UUID pilotId,
@Param("after") OffsetDateTime after);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import pl.skyroster.skyroster_backend.domain.model.OperationalBase;

import java.util.Optional;
import java.util.UUID;

public interface OperationalBaseRepository {
Optional<OperationalBase> findByIcaoCode(String icaoCode);
Optional<OperationalBase> findById(UUID id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import pl.skyroster.skyroster_backend.domain.model.Pilot;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

Expand All @@ -17,9 +20,28 @@ public interface PilotRepository {
})
@Query("SELECT p FROM Pilot p")
Page<Pilot> findAllWithRelations(Pageable pageable);

Optional<Pilot> findById(UUID id);

boolean existsById(UUID id);

boolean existsByLicence(String licence);

void deleteById(UUID id);

Pilot save(Pilot pilot);
}

@EntityGraph(attributePaths = {"qualifications", "aircraftTypes", "operationalBase"})
@Query("""
SELECT DISTINCT p FROM Pilot p
LEFT JOIN p.aircraftTypes at
WHERE (:aircraftTypeId IS NULL OR at.id = :aircraftTypeId)
AND p.id NOT IN (
SELECT f.pilot.id FROM Flight f
WHERE f.flightStart < :to AND f.flightEnd > :from
)
""")
List<Pilot> findAvailable(@Param("from") OffsetDateTime from,
@Param("to") OffsetDateTime to,
@Param("aircraftTypeId") UUID aircraftTypeIdOrNull);
}
Loading
Loading