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/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/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/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/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/adapter/in/web/RuleController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java index a4c8217..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,13 +1,20 @@ 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.RuleResponseMapper; +import pl.skyroster.skyroster_backend.infrastructure.mappers.RuleMapper; import java.util.List; @@ -16,12 +23,20 @@ public class RuleController { private final GetRulesUseCase getRulesUseCase; + private final CreateRuleUseCase createRuleUseCase; @GetMapping(ApiApi.PATH_GET_RULES) public ResponseEntity> getRules() { List response = getRulesUseCase.execute().stream() - .map(RuleResponseMapper::map) + .map(RuleMapper::toResponse) .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/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 89a4e0a..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,11 +46,13 @@ 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") .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 new file mode 100644 index 0000000..8ca97b3 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleMapper.java @@ -0,0 +1,41 @@ +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; + +import java.util.UUID; + +public class RuleMapper { + public static RuleResponse toResponse(Rule rule) { + return new RuleResponse( + rule.getId(), + rule.getName(), + RuleType.valueOf(rule.getType().name()), + rule.getValue(), + 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/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java deleted file mode 100644 index 6377711..0000000 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java +++ /dev/null @@ -1,18 +0,0 @@ -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 5b21e38..c613fb6 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: @@ -440,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 @@ -997,6 +1072,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: