From e99d24a230cc1e9a79b69f9671394e2aecfd70f5 Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Sat, 30 May 2026 18:08:43 +0200 Subject: [PATCH 1/3] Rename `RuleResponseMapper` to more generic name --- .../infrastructure/adapter/in/web/RuleController.java | 4 ++-- .../mappers/{RuleResponseMapper.java => RuleMapper.java} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/{RuleResponseMapper.java => RuleMapper.java} (88%) 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 index a4c8217..cecc647 100644 --- 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 @@ -7,7 +7,7 @@ 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 pl.skyroster.skyroster_backend.infrastructure.mappers.RuleMapper; import java.util.List; @@ -20,7 +20,7 @@ public class RuleController { @GetMapping(ApiApi.PATH_GET_RULES) public ResponseEntity> getRules() { List response = getRulesUseCase.execute().stream() - .map(RuleResponseMapper::map) + .map(RuleMapper::toResponse) .toList(); return ResponseEntity.ok(response); } 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/RuleMapper.java similarity index 88% rename from skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java rename to skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleMapper.java index 6377711..ee0c21b 100644 --- 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/RuleMapper.java @@ -5,8 +5,8 @@ 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) { +public class RuleMapper { + public static RuleResponse toResponse(Rule rule) { return new RuleResponse( rule.getId(), rule.getName(), From 63416bc382d40ffadaebbb315538c3560479f930 Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Sat, 30 May 2026 18:11:30 +0200 Subject: [PATCH 2/3] [SKY-50] Add API POST rule endpoint --- .../application/rule/CreateRuleUseCase.java | 21 +++++++ .../domain/port/RuleRepository.java | 1 + .../adapter/in/web/RuleController.java | 15 +++++ .../config/security/SecurityConfig.java | 1 + .../infrastructure/mappers/RuleMapper.java | 23 +++++++ .../src/main/resources/api/openapi.yaml | 62 +++++++++++++++++++ 6 files changed, 123 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/CreateRuleUseCase.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/CreateRuleUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/CreateRuleUseCase.java new file mode 100644 index 0000000..854aad0 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/CreateRuleUseCase.java @@ -0,0 +1,21 @@ +package pl.skyroster.skyroster_backend.application.rule; + +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; +import pl.skyroster.skyroster_backend.generated.model.RuleRequest; +import pl.skyroster.skyroster_backend.infrastructure.mappers.RuleMapper; + +@Service +public class CreateRuleUseCase { + private final RuleRepository ruleRepository; + + public CreateRuleUseCase(RuleRepository ruleRepository) { + this.ruleRepository = ruleRepository; + } + + public Rule execute(RuleRequest ruleRequest) { + Rule rule = RuleMapper.fromRequest(ruleRequest); + return ruleRepository.save(rule); + } +} 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 index b5a45d0..cb4b868 100644 --- 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 @@ -6,4 +6,5 @@ public interface RuleRepository { List findAll(); + Rule save(Rule rule); } 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 index cecc647..e94fd14 100644 --- 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 @@ -1,11 +1,18 @@ package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import pl.skyroster.skyroster_backend.application.rule.CreateRuleUseCase; import pl.skyroster.skyroster_backend.application.rule.GetRulesUseCase; +import pl.skyroster.skyroster_backend.domain.model.Rule; import pl.skyroster.skyroster_backend.generated.api.ApiApi; +import pl.skyroster.skyroster_backend.generated.model.RuleRequest; import pl.skyroster.skyroster_backend.generated.model.RuleResponse; import pl.skyroster.skyroster_backend.infrastructure.mappers.RuleMapper; @@ -16,6 +23,7 @@ public class RuleController { private final GetRulesUseCase getRulesUseCase; + private final CreateRuleUseCase createRuleUseCase; @GetMapping(ApiApi.PATH_GET_RULES) public ResponseEntity> getRules() { @@ -24,4 +32,11 @@ public ResponseEntity> getRules() { .toList(); return ResponseEntity.ok(response); } + + @PostMapping(ApiApi.PATH_CREATE_RULE) + public ResponseEntity createRule(@RequestBody @Valid RuleRequest request) { + Rule rule = createRuleUseCase.execute(request); + return ResponseEntity.status(HttpStatus.CREATED) + .body(RuleMapper.toResponse(rule)); + } } 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 89a4e0a..03426b3 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 @@ -51,6 +51,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .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(HttpMethod.POST,"/api/rule/**").hasRole("compliance_officer") .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleMapper.java index ee0c21b..8ca97b3 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleMapper.java @@ -5,6 +5,8 @@ import pl.skyroster.skyroster_backend.generated.model.RuleResponse; import pl.skyroster.skyroster_backend.generated.model.RuleType; +import java.util.UUID; + public class RuleMapper { public static RuleResponse toResponse(Rule rule) { return new RuleResponse( @@ -15,4 +17,25 @@ public static RuleResponse toResponse(Rule rule) { RulePeriod.valueOf(rule.getPeriod().name()) ).description(rule.getDescription()); } + + public static Rule fromRequest(pl.skyroster.skyroster_backend.generated.model.RuleRequest request) { + pl.skyroster.skyroster_backend.domain.model.RuleType type = switch (request.getType()){ + case MAX_WORK_TIME -> pl.skyroster.skyroster_backend.domain.model.RuleType.MAX_WORK_TIME; + case MIN_REST_TIME -> pl.skyroster.skyroster_backend.domain.model.RuleType.MIN_REST_TIME; + case MIN_FLIGHT_TIME -> pl.skyroster.skyroster_backend.domain.model.RuleType.MIN_FLIGHT_TIME; + }; + pl.skyroster.skyroster_backend.domain.model.RulePeriod period = switch (request.getPeriod()){ + case DAY -> pl.skyroster.skyroster_backend.domain.model.RulePeriod.DAY; + case WEEK -> pl.skyroster.skyroster_backend.domain.model.RulePeriod.WEEK; + case MONTH -> pl.skyroster.skyroster_backend.domain.model.RulePeriod.MONTH; + }; + return new Rule( + UUID.randomUUID(), + request.getName(), + type, + request.getValue(), + period, + request.getDescription() + ); + } } diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 5b21e38..8eaeccf 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -365,6 +365,41 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + post: + summary: Create a new rule + operationId: createRule + tags: [ Rules ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RuleRequest' + example: + type: MAX_WORK_TIME + period: WEEK + value: 40 + name: Weekly work limit + description: Maximum work time per week in hours + responses: + '201': + description: Rule created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/RuleResponse' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Rule with given name already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /api/flights: get: @@ -997,6 +1032,33 @@ components: description: type: string + RuleRequest: + type: object + required: + - type + - period + - value + - name + properties: + type: + $ref: '#/components/schemas/RuleType' + period: + $ref: '#/components/schemas/RulePeriod' + value: + type: integer + description: Numeric value of the rule (e.g. hours, minutes) + example: 40 + name: + type: string + maxLength: 100 + description: Optional human-readable name of the rule + example: Weekly work limit + description: + type: string + maxLength: 500 + description: Optional detailed description of the rule + example: Maximum work time per week in hours + FlightResponse: type: object required: From 8fdae6f10c9c4a9da197f30f67d58c3f2503f17f Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Sat, 30 May 2026 19:32:01 +0200 Subject: [PATCH 3/3] [SKY-56] Add API DELETE flight endpoint --- .../flight/DeleteFlightUseCase.java | 26 ++++++++++++ .../exception/FlightNotFoundException.java | 7 ++++ .../adapter/in/web/FlightController.java | 17 +++++--- .../config/GlobalExceptionHandler.java | 6 +++ .../config/security/SecurityConfig.java | 1 + .../src/main/resources/api/openapi.yaml | 40 +++++++++++++++++++ 6 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/DeleteFlightUseCase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/FlightNotFoundException.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/DeleteFlightUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/DeleteFlightUseCase.java new file mode 100644 index 0000000..5b58e8a --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/DeleteFlightUseCase.java @@ -0,0 +1,26 @@ +package pl.skyroster.skyroster_backend.application.flight; + +import lombok.val; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.exception.FlightNotFoundException; +import pl.skyroster.skyroster_backend.domain.exception.FlightTimeConflictException; +import pl.skyroster.skyroster_backend.domain.port.FlightRepository; + +import java.time.OffsetDateTime; +import java.util.UUID; + +@Service +public class DeleteFlightUseCase { + private final FlightRepository flightRepository; + + public DeleteFlightUseCase(FlightRepository flightRepository) { + this.flightRepository = flightRepository; + } + + public void execute(UUID flightId) { + val flight = flightRepository.findById(flightId).orElseThrow(FlightNotFoundException::new); + val isHappening = flight.getFlightStart().isBefore(OffsetDateTime.now()); + if (isHappening) throw new FlightTimeConflictException("Deleting ongoing flight is not allowed"); + flightRepository.delete(flight); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/FlightNotFoundException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/FlightNotFoundException.java new file mode 100644 index 0000000..d3d57e8 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/FlightNotFoundException.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.exception; + +public class FlightNotFoundException extends RuntimeException { + public FlightNotFoundException() { + super("Flight not found"); + } +} 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 e12a781..da3cdb9 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 @@ -2,11 +2,9 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import pl.skyroster.skyroster_backend.application.flight.CreateFlightUseCase; +import pl.skyroster.skyroster_backend.application.flight.DeleteFlightUseCase; import pl.skyroster.skyroster_backend.application.flight.GetFlightsUseCase; import pl.skyroster.skyroster_backend.domain.model.Flight; import pl.skyroster.skyroster_backend.generated.api.ApiApi; @@ -15,16 +13,19 @@ import pl.skyroster.skyroster_backend.infrastructure.mappers.FlightResponseMapper; import java.util.List; +import java.util.UUID; @RestController public class FlightController { private final GetFlightsUseCase getFlightsUseCase; private final CreateFlightUseCase createFlightUseCase; + private final DeleteFlightUseCase deleteFlightUseCase; - public FlightController(GetFlightsUseCase getFlightsUseCase, CreateFlightUseCase createFlightUseCase) { + public FlightController(GetFlightsUseCase getFlightsUseCase, CreateFlightUseCase createFlightUseCase, DeleteFlightUseCase deleteFlightUseCase) { this.getFlightsUseCase = getFlightsUseCase; this.createFlightUseCase = createFlightUseCase; + this.deleteFlightUseCase = deleteFlightUseCase; } @GetMapping(ApiApi.PATH_DISPLAY_FLIGHTS) @@ -45,4 +46,10 @@ public ResponseEntity createFlight(@RequestBody CreateFlightRequ ); return ResponseEntity.status(HttpStatus.CREATED).body(FlightResponseMapper.map(flight)); } + + @DeleteMapping(ApiApi.PATH_DELETE_FLIGHT) + public ResponseEntity deleteAircraft(@PathVariable("id") UUID id) { + deleteFlightUseCase.execute(id); + return ResponseEntity.noContent().build(); + } } 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 c3ff5a5..fe78514 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 @@ -34,6 +34,12 @@ public ResponseEntity handleAircraftHasAssignedFlights(AircraftHa return buildResponse(HttpStatus.CONFLICT, ex.getMessage(), request); } + @ExceptionHandler(FlightNotFoundException.class) + public ResponseEntity handleFlightNotFound(FlightNotFoundException ex, + HttpServletRequest request) { + return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage(), request); + } + @ExceptionHandler(FlightTimeConflictException.class) public ResponseEntity handleFlightTimeConflict(FlightTimeConflictException 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 03426b3..eedc941 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 @@ -46,6 +46,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() .requestMatchers("/api/compliance/**").hasRole("compliance_officer") .requestMatchers("/api/planning/**").hasRole("schedule_planner") + .requestMatchers(HttpMethod.DELETE,"/api/flights/**").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") diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 8eaeccf..c613fb6 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -475,6 +475,46 @@ paths: schema: $ref: '#/components/schemas/RuleViolationErrorResponse' + /api/flights/{id}: + delete: + operationId: deleteFlight + tags: [ Flights ] + summary: Delete a flight + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Flight deleted + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Flight not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Flight is already happening + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/aircraft/{id}: put: operationId: updateAircraft