From 7e1a2278fd7de03098a94f32f1e82a9d647b5fe1 Mon Sep 17 00:00:00 2001 From: krzysztofgorski Date: Sat, 9 May 2026 17:26:57 +0200 Subject: [PATCH 01/18] feat(SKY-101): add Rules GET API, entity, repository, use-case, controller, tests, and Flyway migration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mvnw | 9 ++ .../application/adapter/RuleMapper.java | 21 +++++ .../application/rules/GetRulesUseCase.java | 22 +++++ .../skyroster_backend/domain/model/Rule.java | 48 +++++++++++ .../domain/port/RuleRepository.java | 11 +++ .../adapter/in/web/RulesController.java | 72 ++++++++++++++++ .../out/persistence/JpaRuleRepository.java | 17 ++++ .../persistence/RuleRepositoryAdapter.java | 33 ++++++++ .../src/main/resources/api/openapi.yaml | 83 ++++++++++++++++++- .../V20260509__create_rules_table.sql | 15 ++++ .../rules/GetRulesUseCaseTest.java | 25 ++++++ .../integration/RulesControllerIT.java | 13 +++ 12 files changed, 368 insertions(+), 1 deletion(-) create mode 100755 mvnw create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/adapter/RuleMapper.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RulesController.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java create mode 100644 skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql create mode 100644 skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCaseTest.java create mode 100644 skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..3545450 --- /dev/null +++ b/mvnw @@ -0,0 +1,9 @@ +#!/bin/sh +# Lightweight mvnw shim: delegate to system mvn if available, otherwise instruct user. +if command -v mvn >/dev/null 2>&1; then + mvn "$@" +else + echo "Maven not found. Please install Maven or add the official Maven Wrapper (mvnw)." + echo "You can also run: curl -o mvnw https://raw.githubusercontent.com/takari/maven-wrapper/master/mvnw && chmod +x mvnw" + exit 1 +fi diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/adapter/RuleMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/adapter/RuleMapper.java new file mode 100644 index 0000000..6a54a68 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/adapter/RuleMapper.java @@ -0,0 +1,21 @@ +package pl.skyroster.skyroster_backend.application.adapter; + +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.generated.model.RuleDto; + +public class RuleMapper { + public static RuleDto toDto(Rule r) { + RuleDto dto = new RuleDto(); + if (r.getId() != null) dto.setId(r.getId()); + dto.setName(r.getName()); + dto.setDescription(r.getDescription()); + dto.setRuleType(r.getRuleType()); + dto.setMaxMonthlyHours(r.getMaxMonthlyHours()); + dto.setMinRestMinutes(r.getMinRestMinutes()); + dto.setMinFlights(r.getMinFlights()); + dto.setActive(r.isActive()); + if (r.getCreatedAt() != null) dto.setCreatedAt(r.getCreatedAt()); + if (r.getUpdatedAt() != null) dto.setUpdatedAt(r.getUpdatedAt()); + return dto; + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCase.java new file mode 100644 index 0000000..370552c --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCase.java @@ -0,0 +1,22 @@ +package pl.skyroster.skyroster_backend.application.rules; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.application.adapter.RuleMapper; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +@Service +public class GetRulesUseCase { + + private final RuleRepository ruleRepository; + + public GetRulesUseCase(RuleRepository ruleRepository) { + this.ruleRepository = ruleRepository; + } + + public Page execute(java.util.Optional type, java.util.Optional active, Pageable pageable) { + Page page = ruleRepository.findAllByTypeAndActive(type, active, pageable); + return page.map(RuleMapper::toDto); + } +} \ No newline at end of file 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..b38d4b3 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java @@ -0,0 +1,48 @@ +package pl.skyroster.skyroster_backend.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "rules") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Rule { + + @Id + private UUID id; + + @Column(nullable = false) + private String name; + + @Column(columnDefinition = "text") + private String description; + + @Column(name = "rule_type") + private String ruleType; + + @Column(name = "max_monthly_hours") + private Integer maxMonthlyHours; + + @Column(name = "min_rest_minutes") + private Integer minRestMinutes; + + @Column(name = "min_flights") + private Integer minFlights; + + @Column(nullable = false) + private boolean active = true; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; +} \ 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..ab3d0d7 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java @@ -0,0 +1,11 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import pl.skyroster.skyroster_backend.domain.model.Rule; + +import java.util.Optional; + +public interface RuleRepository { + Page findAllByTypeAndActive(Optional type, Optional active, Pageable pageable); +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RulesController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RulesController.java new file mode 100644 index 0000000..74464af --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RulesController.java @@ -0,0 +1,72 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import pl.skyroster.skyroster_backend.application.rules.GetRulesUseCase; + +import java.util.ArrayList; +import java.util.List; + +@RestController +public class RulesController { + + private final GetRulesUseCase getRulesUseCase; + + public RulesController(GetRulesUseCase getRulesUseCase) { + this.getRulesUseCase = getRulesUseCase; + } + + @GetMapping("/api/v1/rules") + @PreAuthorize("hasRole('compliance_officer')") + public ResponseEntity getRules( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String sort, + @RequestParam(required = false) String type, + @RequestParam(required = false) Boolean active + ) { + Sort sortObj = Sort.unsorted(); + if (sort != null && !sort.isBlank()) { + // Support multiple sort criteria separated by ';' + // Each criterion can be: "field,asc" or "field:asc" or "-field" + String[] criteria = sort.split(";"); + List orders = new ArrayList<>(); + for (String c : criteria) { + c = c.trim(); + if (c.isEmpty()) continue; + if (c.startsWith("-")) { + orders.add(Sort.Order.desc(c.substring(1))); + } else if (c.contains(",")) { + String[] kv = c.split(",", 2); + String prop = kv[0].trim(); + String dir = kv[1].trim().toLowerCase(); + orders.add("desc".equals(dir) ? Sort.Order.desc(prop) : Sort.Order.asc(prop)); + } else if (c.contains(":")) { + String[] kv = c.split(":", 2); + String prop = kv[0].trim(); + String dir = kv[1].trim().toLowerCase(); + orders.add("desc".equals(dir) ? Sort.Order.desc(prop) : Sort.Order.asc(prop)); + } else { + orders.add(Sort.Order.asc(c)); + } + } + if (!orders.isEmpty()) sortObj = Sort.by(orders); + } + + Pageable pageable = PageRequest.of(page, size, sortObj); + var dtoPage = getRulesUseCase.execute(java.util.Optional.ofNullable(type), java.util.Optional.ofNullable(active), pageable); + var response = new java.util.HashMap(); + response.put("content", dtoPage.getContent()); + response.put("page", dtoPage.getNumber()); + response.put("size", dtoPage.getSize()); + response.put("totalElements", dtoPage.getTotalElements()); + response.put("totalPages", dtoPage.getTotalPages()); + return ResponseEntity.ok(response); + } +} \ No newline at end of file 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..48d63a6 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java @@ -0,0 +1,17 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.UUID; + +@Repository +public interface JpaRuleRepository extends JpaRepository, RuleRepository { + Page findByRuleTypeAndActive(String ruleType, boolean active, Pageable pageable); + Page findByRuleType(String ruleType, Pageable pageable); + Page findByActive(boolean active, Pageable pageable); +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java new file mode 100644 index 0000000..7ecb8a8 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java @@ -0,0 +1,33 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.Optional; + +@Component +public class RuleRepositoryAdapter implements RuleRepository { + + private final JpaRuleRepository jpa; + + public RuleRepositoryAdapter(JpaRuleRepository jpa) { + this.jpa = jpa; + } + + @Override + public Page findAllByTypeAndActive(Optional type, Optional active, Pageable pageable) { + if (type.isPresent() && active.isPresent()) { + return jpa.findByRuleTypeAndActive(type.get(), active.get(), pageable); + } + if (type.isPresent()) { + return jpa.findByRuleType(type.get(), pageable); + } + if (active.isPresent()) { + return jpa.findByActive(active.get(), pageable); + } + return jpa.findAll(pageable); + } +} diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 2e8006f..eb533a4 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -157,6 +157,44 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /api/v1/rules: + get: + summary: Get rules + tags: + - Rules + parameters: + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 20 + - name: sort + in: query + schema: + type: string + - name: type + in: query + schema: + type: string + - name: active + in: query + schema: + type: boolean + responses: + '200': + description: paged list of rules + content: + application/json: + schema: + $ref: '#/components/schemas/PagedRuleDto' + security: + - bearerAuth: [] + components: securitySchemes: bearerAuth: @@ -333,4 +371,47 @@ components: aircraftType: $ref: '#/components/schemas/AircraftTypeInfo' operationalBase: - $ref: '#/components/schemas/OperationalBaseInfo' \ No newline at end of file + $ref: '#/components/schemas/OperationalBaseInfo' + + RuleDto: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + description: + type: string + ruleType: + type: string + maxMonthlyHours: + type: integer + minRestMinutes: + type: integer + minFlights: + type: integer + active: + type: boolean + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + PagedRuleDto: + type: object + properties: + content: + type: array + items: + $ref: '#/components/schemas/RuleDto' + page: + type: integer + size: + type: integer + totalElements: + type: integer + totalPages: + type: integer \ No newline at end of file diff --git a/skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql b/skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql new file mode 100644 index 0000000..1918fe1 --- /dev/null +++ b/skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS rules ( + id UUID PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + rule_type TEXT, + max_monthly_hours INT, + min_rest_minutes INT, + min_flights INT, + active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX IF NOT EXISTS idx_rules_rule_type ON rules(rule_type); +CREATE INDEX IF NOT EXISTS idx_rules_active ON rules(active); diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCaseTest.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCaseTest.java new file mode 100644 index 0000000..5abc65c --- /dev/null +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCaseTest.java @@ -0,0 +1,25 @@ +package pl.skyroster.skyroster_backend.application.rules; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GetRulesUseCaseTest { + @Test + void returnsMappedPage() { + RuleRepository port = Mockito.mock(RuleRepository.class); + var r = new Rule(UUID.randomUUID(), "TestRule", null, null, null, null, null, true, null, null); + Mockito.when(port.findAllByTypeAndActive(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(new PageImpl<>(List.of(r))); + var uc = new GetRulesUseCase(port); + var page = uc.execute(java.util.Optional.empty(), java.util.Optional.empty(), PageRequest.of(0,10)); + assertEquals(1, page.getTotalElements()); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java new file mode 100644 index 0000000..3968c31 --- /dev/null +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java @@ -0,0 +1,13 @@ +package pl.skyroster.skyroster_backend.integration; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +// Integration test requires Testcontainers and Docker. Disabled by default in CI-less local run. +@Disabled("Requires Docker/Testcontainers - enable when environment supports it") +public class RulesControllerIT { + @Test + void placeholder() { + // Implement integration test using Testcontainers, Postgres and Keycloak if needed. + } +} From ad008cedb354b5d33cb0a8812db8ea69c762853e Mon Sep 17 00:00:00 2001 From: krzysztofgorski Date: Sat, 9 May 2026 17:27:41 +0200 Subject: [PATCH 02/18] test(SKY-101): enable RulesController integration test (requires Docker/Testcontainers) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../integration/RulesControllerIT.java | 80 +++++++++++++++++-- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java index 3968c31..4f16aa0 100644 --- a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java @@ -1,13 +1,83 @@ package pl.skyroster.skyroster_backend.integration; -import org.junit.jupiter.api.Disabled; +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 pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence.JpaRuleRepository; +import pl.skyroster.skyroster_backend.domain.model.Rule; -// Integration test requires Testcontainers and Docker. Disabled by default in CI-less local run. -@Disabled("Requires Docker/Testcontainers - enable when environment supports it") +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.OffsetDateTime; +import java.util.UUID; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +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) public class RulesControllerIT { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private KeycloakContainer keycloak; + + @Autowired + private JpaRuleRepository ruleRepository; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + @Test - void placeholder() { - // Implement integration test using Testcontainers, Postgres and Keycloak if needed. + void getRules_returnsSavedRule_forComplianceRole() throws Exception { + // prepare data + Rule r = new Rule(UUID.randomUUID(), "IT-Test-Rule", "desc", "MAX_MONTHLY_HOURS", 160, 720, 5, true, OffsetDateTime.now(), null); + ruleRepository.save(r); + + String token = getToken("compliance", "test1234"); + + mockMvc.perform(get("/api/v1/rules") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content[0].name").value("IT-Test-Rule")); + } + + private String getToken(String username, String password) throws Exception { + String tokenUrl = keycloak.getAuthServerUrl() + "/realms/skyroster/protocol/openid-connect/token"; + + String body = "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(body)) + .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(); } } From a7663a38696443af08885ae0cefc5b4384e15121 Mon Sep 17 00:00:00 2001 From: krzysztofgorski Date: Sat, 9 May 2026 17:30:11 +0200 Subject: [PATCH 03/18] fix(SKY-101): avoid duplicate RuleRepository beans by not extending domain port in JpaRuleRepository Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../adapter/out/persistence/JpaRuleRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 48d63a6..55e4b76 100644 --- 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 @@ -10,7 +10,7 @@ import java.util.UUID; @Repository -public interface JpaRuleRepository extends JpaRepository, RuleRepository { +public interface JpaRuleRepository extends JpaRepository { Page findByRuleTypeAndActive(String ruleType, boolean active, Pageable pageable); Page findByRuleType(String ruleType, Pageable pageable); Page findByActive(boolean active, Pageable pageable); From 2606470dd447477315537e05eeeb0cbe008304ad Mon Sep 17 00:00:00 2001 From: krzysztofgorski Date: Sun, 10 May 2026 12:16:15 +0200 Subject: [PATCH 04/18] endpoint na get --- infrastructure/local/docker-compose.yaml | 31 +++++++- .../local/postgres/init-databases.sh | 0 .../persistence/RuleRepositoryAdapter.java | 76 +++++++++++++++---- .../config/security/SecurityConfig.java | 1 + .../src/main/resources/api/openapi.yaml | 2 +- ...s_table.sql => V3__create_rules_table.sql} | 2 + 6 files changed, 94 insertions(+), 18 deletions(-) mode change 100644 => 100755 infrastructure/local/postgres/init-databases.sh rename skyroster-backend/src/main/resources/db/migration/{V20260509__create_rules_table.sql => V3__create_rules_table.sql} (99%) diff --git a/infrastructure/local/docker-compose.yaml b/infrastructure/local/docker-compose.yaml index ec3481b..024b543 100644 --- a/infrastructure/local/docker-compose.yaml +++ b/infrastructure/local/docker-compose.yaml @@ -24,6 +24,28 @@ services: timeout: 3s retries: 5 + keycloak-db: + image: postgres:17 + container_name: skyroster-keycloak-db + networks: + - skyroster-network + profiles: + - backend + - full + environment: + POSTGRES_DB: ${KEYCLOAK_DB_NAME:-keycloak} + POSTGRES_USER: ${KEYCLOAK_DB_USER:-keycloak} + POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloak} + ports: + - "5433:5432" + volumes: + - keycloak_pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-keycloak}"] + interval: 5s + timeout: 3s + retries: 5 + keycloak: image: quay.io/keycloak/keycloak:26.2 container_name: skyroster-keycloak @@ -35,9 +57,9 @@ services: - full environment: KC_DB: postgres - KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak - KC_DB_USERNAME: ${DB_USERNAME:-skyroster} - KC_DB_PASSWORD: ${DB_PASSWORD:-skyroster} + KC_DB_URL: jdbc:postgresql://keycloak-db:5432/${KEYCLOAK_DB_NAME:-keycloak} + KC_DB_USERNAME: ${KEYCLOAK_DB_USER:-keycloak} + KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloak} KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin} KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} ports: @@ -46,7 +68,7 @@ services: - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json command: start-dev --import-realm depends_on: - postgres: + keycloak-db: condition: service_healthy frontend: @@ -95,6 +117,7 @@ services: volumes: pgdata: + keycloak_pgdata: networks: skyroster-network: diff --git a/infrastructure/local/postgres/init-databases.sh b/infrastructure/local/postgres/init-databases.sh old mode 100644 new mode 100755 diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java index 7ecb8a8..4171297 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java @@ -1,33 +1,83 @@ package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; import pl.skyroster.skyroster_backend.domain.model.Rule; import pl.skyroster.skyroster_backend.domain.port.RuleRepository; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +/** + * Adapter returning a fixed set of predefined rules (in-memory). + * This makes rules "hard-coded" instead of being read from DB. + */ @Component public class RuleRepositoryAdapter implements RuleRepository { - private final JpaRuleRepository jpa; + private static final List PREDEFINED; - public RuleRepositoryAdapter(JpaRuleRepository jpa) { - this.jpa = jpa; + static { + List list = new ArrayList<>(); + + // Example additional fixed rules + list.add(new Rule( + UUID.fromString("11111111-1111-1111-1111-111111111111"), + "Min Rest 12h", + "Minimum rest time between duties", + "MIN_REST_MINUTES", + null, + 720, + null, + true, + OffsetDateTime.parse("2026-05-01T00:00:00Z"), + null + )); + list.add(new Rule( + UUID.fromString("22222222-2222-2222-2222-222222222222"), + "Min Flights", + "Minimum number of flights per month", + "MIN_FLIGHTS", + null, + null, + 3, + true, + OffsetDateTime.parse("2026-05-01T00:00:00Z"), + null + )); + list.add(new Rule( + UUID.fromString("33333333-3333-3333-3333-333333333333"), + "Max Month Hours", + "Maximum flight hours per month", + "MAX_MONTHLY_HOURS", + 160, // przykładowy limit godzin + null, + null, + true, + OffsetDateTime.parse("2026-05-01T00:00:00Z"), + null + )); + + PREDEFINED = List.copyOf(list); } @Override public Page findAllByTypeAndActive(Optional type, Optional active, Pageable pageable) { - if (type.isPresent() && active.isPresent()) { - return jpa.findByRuleTypeAndActive(type.get(), active.get(), pageable); - } - if (type.isPresent()) { - return jpa.findByRuleType(type.get(), pageable); - } - if (active.isPresent()) { - return jpa.findByActive(active.get(), pageable); - } - return jpa.findAll(pageable); + List filtered = PREDEFINED.stream() + .filter(r -> type.map(t -> t.equalsIgnoreCase(r.getRuleType())).orElse(true)) + .filter(r -> active.map(a -> a.equals(r.isActive())).orElse(true)) + .collect(Collectors.toList()); + + int total = filtered.size(); + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), total); + List pageContent = start >= total ? List.of() : filtered.subList(start, end); + return new PageImpl<>(pageContent, pageable, total); } } 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..aba9915 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("/api/v1/rules/**").hasRole("compliance_officer") .requestMatchers(HttpMethod.POST, "/api/aircraft/**").hasRole("operations_administrator") .requestMatchers(HttpMethod.GET, "/api/aircraft/**").authenticated() .requestMatchers("/api/**").authenticated() diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index eb533a4..0ffbd13 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -157,7 +157,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - /api/v1/rules: + /api/rules: get: summary: Get rules tags: diff --git a/skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql b/skyroster-backend/src/main/resources/db/migration/V3__create_rules_table.sql similarity index 99% rename from skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql rename to skyroster-backend/src/main/resources/db/migration/V3__create_rules_table.sql index 1918fe1..8b35690 100644 --- a/skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql +++ b/skyroster-backend/src/main/resources/db/migration/V3__create_rules_table.sql @@ -11,5 +11,7 @@ CREATE TABLE IF NOT EXISTS rules ( updated_at TIMESTAMP WITH TIME ZONE ); + + CREATE INDEX IF NOT EXISTS idx_rules_rule_type ON rules(rule_type); CREATE INDEX IF NOT EXISTS idx_rules_active ON rules(active); From 96d7744a66ce67c116a46377c02bcec04f117dc1 Mon Sep 17 00:00:00 2001 From: kudukm Date: Sun, 10 May 2026 12:11:56 +0200 Subject: [PATCH 05/18] (SKY-41) /api/flights endpoint adapted to OpenAPI --- .../application/flight/GetFlightsUseCase.java | 24 +++++++- .../domain/port/FlightRepository.java | 4 +- .../adapter/in/web/FlightController.java | 3 +- .../out/persistence/JpaFlightRepository.java | 10 ---- .../mappers/AircraftResponseMapper.java | 15 +++++ .../mappers/AircraftTypeInfoMapper.java | 14 +++++ .../mappers/OperationalBaseInfoMapper.java | 14 +++++ .../src/main/resources/api/openapi.yaml | 60 +++++++++++++++++++ 8 files changed, 130 insertions(+), 14 deletions(-) delete mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaFlightRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java 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..8127ea6 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,16 @@ 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.model.Flight; +import pl.skyroster.skyroster_backend.domain.port.AircraftRepository; import pl.skyroster.skyroster_backend.domain.port.FlightRepository; +import pl.skyroster.skyroster_backend.generated.model.AircraftResponse; +import pl.skyroster.skyroster_backend.generated.model.AircraftTypeInfo; +import pl.skyroster.skyroster_backend.generated.model.FlightResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.AircraftResponseMapper; +import pl.skyroster.skyroster_backend.infrastructure.mappers.AircraftTypeInfoMapper; +import pl.skyroster.skyroster_backend.infrastructure.mappers.OperationalBaseInfoMapper; import java.util.List; @@ -16,7 +24,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/domain/port/FlightRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java index 63edc96..4670977 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,13 @@ package pl.skyroster.skyroster_backend.domain.port; +import org.springframework.data.jpa.repository.JpaRepository; import pl.skyroster.skyroster_backend.domain.model.Flight; +import pl.skyroster.skyroster_backend.generated.model.FlightResponse; import java.util.List; import java.util.UUID; -public interface FlightRepository { +public interface FlightRepository extends JpaRepository { List findAll(); boolean existsByAircraftId(UUID aircraftId); } \ No newline at end of file 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..7752aab 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 @@ -5,6 +5,7 @@ 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 +19,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/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/mappers/AircraftResponseMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java new file mode 100644 index 0000000..d5ae2e8 --- /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.map(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..05ba98c --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java @@ -0,0 +1,14 @@ +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 map(AircraftType aircraftType) { + return new AircraftTypeInfo( + 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..7370ed7 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java @@ -0,0 +1,14 @@ +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() + ); + } +} diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index f394073..facf0e1 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -157,6 +157,33 @@ paths: 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 @@ -453,3 +480,36 @@ components: $ref: '#/components/schemas/AircraftTypeInfo' operationalBase: $ref: '#/components/schemas/OperationalBaseInfo' + + 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 From 5762975c0bff5084d66e61cdbbb9cd47fff75e1a Mon Sep 17 00:00:00 2001 From: kudukm Date: Sun, 10 May 2026 12:13:18 +0200 Subject: [PATCH 06/18] (SKY-41) Unused imports deleted --- .../application/flight/GetFlightsUseCase.java | 6 ------ .../skyroster_backend/domain/port/FlightRepository.java | 1 - .../infrastructure/adapter/in/web/FlightController.java | 1 - 3 files changed, 8 deletions(-) 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 8127ea6..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,15 +2,9 @@ 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.model.Flight; -import pl.skyroster.skyroster_backend.domain.port.AircraftRepository; import pl.skyroster.skyroster_backend.domain.port.FlightRepository; -import pl.skyroster.skyroster_backend.generated.model.AircraftResponse; -import pl.skyroster.skyroster_backend.generated.model.AircraftTypeInfo; import pl.skyroster.skyroster_backend.generated.model.FlightResponse; import pl.skyroster.skyroster_backend.infrastructure.mappers.AircraftResponseMapper; -import pl.skyroster.skyroster_backend.infrastructure.mappers.AircraftTypeInfoMapper; import pl.skyroster.skyroster_backend.infrastructure.mappers.OperationalBaseInfoMapper; import java.util.List; 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 4670977..555e749 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 @@ -2,7 +2,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import pl.skyroster.skyroster_backend.domain.model.Flight; -import pl.skyroster.skyroster_backend.generated.model.FlightResponse; import java.util.List; import java.util.UUID; 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 7752aab..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,6 @@ 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; From 28c45fc0d18516f3601fc0516dbd4fc50ef7d252 Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Sat, 9 May 2026 20:16:06 +0200 Subject: [PATCH 07/18] [SKY-26]: Add API GET endpoint for all pilots --- .../application/pilot/GetPilotUseCase.java | 59 +++++++ .../skyroster_backend/domain/model/Pilot.java | 6 +- .../domain/model/Qualification.java | 2 + .../domain/port/PilotRepository.java | 17 ++ .../adapter/in/web/PilotController.java | 23 +++ .../out/persistence/JpaPilotRepository.java | 10 ++ .../infrastructure/mappers/PilotMapper.java | 33 ++++ .../mappers/PilotQualificationMapper.java | 13 ++ .../src/main/resources/api/openapi.yaml | 154 ++++++++++++++++-- 9 files changed, 304 insertions(+), 13 deletions(-) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java new file mode 100644 index 0000000..a42dc85 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java @@ -0,0 +1,59 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; +import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotMapper; + +@Service +@RequiredArgsConstructor +public class GetPilotUseCase { + + private final PilotRepository pilotRepository; + + public PagedPilotResponse getPilots(Integer page, Integer size, String sort) { + Pageable pageable = createPageable(page, size, sort); + + Page pilotsPage = pilotRepository.findAllWithRelations(pageable); + + return new PagedPilotResponse() + .content( + pilotsPage.getContent() + .stream() + .map(PilotMapper::toResponse) + .toList() + ) + .page(pilotsPage.getNumber()) + .size(pilotsPage.getSize()) + .totalElements(pilotsPage.getTotalElements()) + .totalPages(pilotsPage.getTotalPages()) + .first(pilotsPage.isFirst()) + .last(pilotsPage.isLast()); + } + + private Pageable createPageable(Integer page, Integer size, String sort) { + int resolvedPage = page == null ? 0 : page; + int resolvedSize = size == null ? 20 : Math.min(size, 100); + + Sort resolvedSort = Sort.by(Sort.Direction.ASC, "lastName"); + + if (sort != null && !sort.isBlank()) { + String[] parts = sort.split(","); + String property = parts[0]; + + Sort.Direction direction = parts.length > 1 + ? Sort.Direction.fromOptionalString(parts[1]).orElse(Sort.Direction.ASC) + : Sort.Direction.ASC; + + resolvedSort = Sort.by(direction, property); + } + + return PageRequest.of(resolvedPage, resolvedSize, resolvedSort); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java index efe885a..a8f20c6 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java @@ -1,11 +1,13 @@ package pl.skyroster.skyroster_backend.domain.model; +import jakarta.persistence.*; +import lombok.Getter; + import java.util.Set; import java.util.UUID; -import jakarta.persistence.*; - @Entity +@Getter public class Pilot { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java index 750a24a..b43b9a0 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java @@ -3,10 +3,12 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import lombok.Getter; import java.util.UUID; @Entity +@Getter public class Qualification { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java new file mode 100644 index 0000000..f2a8e56 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java @@ -0,0 +1,17 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.Query; +import pl.skyroster.skyroster_backend.domain.model.Pilot; + +public interface PilotRepository { + @EntityGraph(attributePaths = { + "qualifications", + "aircraftTypes", + "operationalBase" + }) + @Query("SELECT p FROM Pilot p") + Page findAllWithRelations(Pageable pageable); +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java new file mode 100644 index 0000000..d71d320 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java @@ -0,0 +1,23 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.Nullable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import pl.skyroster.skyroster_backend.application.pilot.GetPilotUseCase; +import pl.skyroster.skyroster_backend.generated.api.ApiApi; +import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; + +@RequiredArgsConstructor +@RestController +public class PilotController { + + private final GetPilotUseCase getPilotUseCase; + + @GetMapping(ApiApi.PATH_GET_PILOTS) + public ResponseEntity getPilots(@RequestParam Integer page, @RequestParam Integer size, @RequestParam @Nullable String sort) { + return ResponseEntity.ok(getPilotUseCase.getPilots(page, size, sort)); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java new file mode 100644 index 0000000..e659210 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java @@ -0,0 +1,10 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; + +import java.util.UUID; + +public interface JpaPilotRepository extends JpaRepository, PilotRepository { +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java new file mode 100644 index 0000000..f81572c --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java @@ -0,0 +1,33 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import org.springframework.stereotype.Component; +import pl.skyroster.skyroster_backend.domain.model.AircraftType; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.model.Qualification; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; + +import java.util.Comparator; + +@Component +public class PilotMapper { + public static PilotResponse toResponse(Pilot pilot) { + return new PilotResponse() + .id(pilot.getId()) + .firstName(pilot.getName()) + .surname(pilot.getSurname()) + .licence(pilot.getLicence()) + .homeBase(OperationalBaseInfoMapper.map(pilot.getOperationalBase())) + .qualifications( + pilot.getQualifications() + .stream() + .sorted(Comparator.comparing(Qualification::getLabel)) + .map(PilotQualificationMapper::toPilotQualificationInfo) + .toList()) + .aircraftTypes( + pilot.getAircraftTypes() + .stream() + .sorted(Comparator.comparing(AircraftType::getName)) + .map(AircraftTypeInfoMapper::map) + .toList()); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java new file mode 100644 index 0000000..a26533a --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java @@ -0,0 +1,13 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import pl.skyroster.skyroster_backend.domain.model.Qualification; +import pl.skyroster.skyroster_backend.generated.model.PilotQualificationInfo; + +public class PilotQualificationMapper { + public static PilotQualificationInfo toPilotQualificationInfo(Qualification qualification) { + return new PilotQualificationInfo() + .id(qualification.getId()) + .values(qualification.getValue()) + .label(qualification.getLabel()); + } +} diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index facf0e1..64e077f 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -5,15 +5,15 @@ info: version: 0.1.0 security: - - bearerAuth: [] + - bearerAuth: [ ] paths: /api/health: get: operationId: getHealth - tags: [Health] + tags: [ Health ] summary: Health check - security: [] + security: [ ] responses: '200': description: Service is healthy @@ -25,7 +25,7 @@ paths: /api/me: get: operationId: getCurrentUser - tags: [User] + tags: [ User ] summary: Get current authenticated user info responses: '200': @@ -44,7 +44,7 @@ paths: /api/compliance/reports: get: operationId: getComplianceReports - tags: [Compliance] + tags: [ Compliance ] summary: List compliance reports responses: '200': @@ -71,7 +71,7 @@ paths: /api/planning/schedules: get: operationId: getPlanningSchedules - tags: [Planning] + tags: [ Planning ] summary: List schedules responses: '200': @@ -98,7 +98,7 @@ paths: /api/aircraft: post: operationId: addAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Add a new aircraft to the fleet requestBody: required: true @@ -139,7 +139,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' get: operationId: getAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: List all aircraft in the fleet responses: '200': @@ -156,6 +156,53 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + /api/pilots: + get: + tags: + - Pilots + operationId: getPilots + summary: Get paginated list of pilots + parameters: + - name: page + in: query + required: false + schema: + type: integer + minimum: 0 + default: 0 + - name: size + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: sort + in: query + required: false + schema: + type: string + example: surname,asc + responses: + '200': + description: Paginated list of pilots + content: + application/json: + schema: + $ref: '#/components/schemas/PagedPilotResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /api/flights: get: @@ -187,7 +234,7 @@ paths: /api/aircraft/{id}: put: operationId: updateAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Update an aircraft in the fleet parameters: - in: path @@ -241,7 +288,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' delete: operationId: deleteAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Delete an aircraft from the fleet parameters: - in: path @@ -336,7 +383,7 @@ components: description: List of realm roles assigned to the user items: type: string - example: ["operations_administrator"] + example: [ "operations_administrator" ] HealthStatus: type: object @@ -388,6 +435,91 @@ components: format: date-time example: "2026-03-20T11:00:00Z" + PagedPilotResponse: + type: object + required: + - content + - page + - size + - totalElements + - totalPages + - first + - last + properties: + content: + type: array + items: + $ref: '#/components/schemas/PilotResponse' + page: + type: integer + example: 0 + size: + type: integer + example: 20 + totalElements: + type: integer + format: int64 + example: 42 + totalPages: + type: integer + example: 3 + first: + type: boolean + example: true + last: + type: boolean + example: false + + PilotResponse: + type: object + required: + - id + - firstName + - surname + - homeBase + - qualifications + properties: + id: + type: string + format: uuid + licence: + type: string + example: PIL-001 + firstName: + type: string + example: Jan + surname: + type: string + example: Kowalski + homeBase: + $ref: '#/components/schemas/OperationalBaseInfo' + qualifications: + type: array + items: + $ref: '#/components/schemas/PilotQualificationInfo' + aircraftTypes: + type: array + items: + $ref: '#/components/schemas/AircraftTypeInfo' + + + PilotQualificationInfo: + type: object + required: + - id + - aircraftType + - licenseValidUntil + properties: + id: + type: string + format: uuid + values: + type: string + label: + type: string + + + AddAircraftRequest: type: object required: From 1eee3c638bce3b0d877660d0a5a6a55f052a4da7 Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Sun, 10 May 2026 21:28:34 +0200 Subject: [PATCH 08/18] [SKY-28]: Add API DELETE endpoint for pilot --- .../application/pilot/DeletePilotUseCase.java | 35 +++++++++++++++++++ .../exception/PilotNotFoundException.java | 9 +++++ .../domain/port/FlightRepository.java | 5 +++ .../domain/port/PilotRepository.java | 4 +++ .../adapter/in/web/PilotController.java | 14 ++++++-- .../config/GlobalExceptionHandler.java | 11 ++++++ .../config/security/SecurityConfig.java | 1 + .../src/main/resources/api/openapi.yaml | 19 ++++++++++ 8 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java new file mode 100644 index 0000000..6acb599 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java @@ -0,0 +1,35 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.exception.PilotNotFoundException; +import pl.skyroster.skyroster_backend.domain.port.FlightRepository; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; + +import java.time.OffsetDateTime; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class DeletePilotUseCase { + + private final PilotRepository pilotRepository; + private final FlightRepository flightRepository; + + @Transactional + public void deletePilotById(UUID pilotId) { + if (!pilotRepository.existsById(pilotId)) { + throw new PilotNotFoundException(pilotId); + } + + if(pilotHasScheduledFlights(pilotId)) + throw new IllegalStateException("Pilot has ongoing or incoming flights"); + + pilotRepository.deleteById(pilotId); + } + + private boolean pilotHasScheduledFlights(UUID pilotId){ + return flightRepository.existsByPilotIdAndFlightEndAfter(pilotId, OffsetDateTime.now()); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java new file mode 100644 index 0000000..fa5720e --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java @@ -0,0 +1,9 @@ +package pl.skyroster.skyroster_backend.domain.exception; + +import java.util.UUID; + +public class PilotNotFoundException extends RuntimeException { + public PilotNotFoundException(UUID pilotId) { + super("Pilot with id " + pilotId + " not found"); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java index 555e749..e335ab3 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java @@ -3,10 +3,15 @@ import org.springframework.data.jpa.repository.JpaRepository; import pl.skyroster.skyroster_backend.domain.model.Flight; +import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; public interface FlightRepository extends JpaRepository { List findAll(); boolean existsByAircraftId(UUID aircraftId); + boolean existsByPilotIdAndFlightEndAfter( + UUID pilotId, + OffsetDateTime now + ); } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java index f2a8e56..84f5900 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java @@ -6,6 +6,8 @@ import org.springframework.data.jpa.repository.Query; import pl.skyroster.skyroster_backend.domain.model.Pilot; +import java.util.UUID; + public interface PilotRepository { @EntityGraph(attributePaths = { "qualifications", @@ -14,4 +16,6 @@ public interface PilotRepository { }) @Query("SELECT p FROM Pilot p") Page findAllWithRelations(Pageable pageable); + boolean existsById(UUID id); + void deleteById(UUID id); } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java index d71d320..b2aa8d5 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java @@ -3,21 +3,29 @@ import lombok.RequiredArgsConstructor; import org.jspecify.annotations.Nullable; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import pl.skyroster.skyroster_backend.application.pilot.DeletePilotUseCase; import pl.skyroster.skyroster_backend.application.pilot.GetPilotUseCase; import pl.skyroster.skyroster_backend.generated.api.ApiApi; import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; +import java.util.UUID; + @RequiredArgsConstructor @RestController public class PilotController { private final GetPilotUseCase getPilotUseCase; + private final DeletePilotUseCase deletePilotUseCase; @GetMapping(ApiApi.PATH_GET_PILOTS) public ResponseEntity getPilots(@RequestParam Integer page, @RequestParam Integer size, @RequestParam @Nullable String sort) { return ResponseEntity.ok(getPilotUseCase.getPilots(page, size, sort)); } + + @DeleteMapping(ApiApi.PATH_DELETE_PILOT_BY_ID) + public ResponseEntity deletePilotById(@PathVariable UUID pilotId) { + deletePilotUseCase.deletePilotById(pilotId); + return ResponseEntity.noContent().build(); + } } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java index 6123af3..532cf70 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java @@ -11,6 +11,7 @@ import pl.skyroster.skyroster_backend.domain.exception.AircraftNotFoundException; import pl.skyroster.skyroster_backend.domain.exception.AircraftTypeNotFoundException; import pl.skyroster.skyroster_backend.domain.exception.OperationalBaseNotFoundException; +import pl.skyroster.skyroster_backend.domain.exception.PilotNotFoundException; import pl.skyroster.skyroster_backend.generated.model.ErrorResponse; import java.time.OffsetDateTime; @@ -50,6 +51,11 @@ public ResponseEntity handleAircraftTypeNotFound(AircraftTypeNotF return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); } + @ExceptionHandler(PilotNotFoundException.class) + public ResponseEntity handlePilotNotFound(PilotNotFoundException ex,HttpServletRequest request) { + return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage(), request); + } + @ExceptionHandler(OperationalBaseNotFoundException.class) public ResponseEntity handleOperationalBaseNotFound(OperationalBaseNotFoundException ex, HttpServletRequest request) { @@ -62,6 +68,11 @@ public ResponseEntity handleIllegalArgument(IllegalArgumentExcept return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); } + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleIllegalState(Exception ex, HttpServletRequest request) { + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); + } + private ResponseEntity buildResponse(HttpStatus status, String message, HttpServletRequest request) { ErrorResponse error = new ErrorResponse() diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java index 2df005b..8cbb358 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java @@ -48,6 +48,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers("/api/planning/**").hasRole("schedule_planner") .requestMatchers(HttpMethod.POST, "/api/aircraft/**").hasRole("operations_administrator") .requestMatchers(HttpMethod.GET, "/api/aircraft/**").authenticated() + .requestMatchers(HttpMethod.DELETE, "/api/pilots/**").hasRole("operations_administrator") .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 64e077f..21c3e56 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -325,6 +325,25 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /api/pilots/{pilotId}: + delete: + tags: + - AdminPilots + operationId: deletePilotById + summary: Delete pilot by id + parameters: + - name: pilotId + in: path + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Pilot deleted successfully + '404': + description: Pilot not found + components: securitySchemes: bearerAuth: From 87a45c65f57425b00274898cbc724704b4bb36b5 Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Thu, 14 May 2026 22:41:59 +0200 Subject: [PATCH 09/18] [SKY-29]: Add API PATCH endpoint for pilots --- .../application/pilot/PatchPilotUseCase.java | 55 ++++++++++++ .../skyroster_backend/domain/model/Pilot.java | 2 + .../domain/model/Qualification.java | 4 + .../domain/port/PilotRepository.java | 3 + .../adapter/in/web/PilotController.java | 9 ++ .../config/security/SecurityConfig.java | 3 +- .../mappers/AircraftResponseMapper.java | 2 +- .../mappers/AircraftTypeInfoMapper.java | 9 +- .../infrastructure/mappers/PilotMapper.java | 2 +- .../mappers/PilotQualificationMapper.java | 4 + .../src/main/resources/api/openapi.yaml | 89 +++++++++++++++++++ 11 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java new file mode 100644 index 0000000..5e8b88c --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java @@ -0,0 +1,55 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.model.AircraftType; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.model.Qualification; +import pl.skyroster.skyroster_backend.domain.port.OperationalBaseRepository; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; +import pl.skyroster.skyroster_backend.generated.model.PilotPatchRequest; +import pl.skyroster.skyroster_backend.infrastructure.mappers.AircraftTypeInfoMapper; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotMapper; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotQualificationMapper; + +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PatchPilotUseCase { + private final PilotRepository pilotRepository; + private final OperationalBaseRepository operationalBaseRepository; + + @Transactional + public PilotResponse patchPilot(UUID id, PilotPatchRequest request) { + Pilot pilot = pilotRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Pilot not found: " + id)); + + String name = request.getName(); + if (name != null) { + pilot.setName(name); + } + String surname = request.getSurname(); + if (surname != null) { + pilot.setSurname(surname); + } + String icaoCode = request.getOperationalBaseIcaoCode(); + if (icaoCode != null) { + pilot.setOperationalBase( + operationalBaseRepository.findByIcaoCode(icaoCode) + .orElseThrow(() -> new IllegalArgumentException("Operational base not found: " + icaoCode)) + ); + } + Set qualifications = request.getQualifications().stream().map(PilotQualificationMapper::fromPilotQualificationInfo).collect(Collectors.toSet()); + pilot.setQualifications(qualifications); + + Set aircraftTypes = request.getAircraftTypes().stream().map(AircraftTypeInfoMapper::toAircraftTypeInfo).collect(Collectors.toSet()); + pilot.setAircraftTypes(aircraftTypes); + + return PilotMapper.toResponse(pilotRepository.save(pilot)); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java index a8f20c6..692eb05 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java @@ -2,12 +2,14 @@ import jakarta.persistence.*; import lombok.Getter; +import lombok.Setter; import java.util.Set; import java.util.UUID; @Entity @Getter +@Setter public class Pilot { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java index b43b9a0..7cdb3d9 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java @@ -3,12 +3,16 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.UUID; @Entity @Getter +@AllArgsConstructor +@NoArgsConstructor public class Qualification { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java index 84f5900..62dc727 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import pl.skyroster.skyroster_backend.domain.model.Pilot; +import java.util.Optional; import java.util.UUID; public interface PilotRepository { @@ -16,6 +17,8 @@ public interface PilotRepository { }) @Query("SELECT p FROM Pilot p") Page findAllWithRelations(Pageable pageable); + Optional findById(UUID id); boolean existsById(UUID id); void deleteById(UUID id); + Pilot save(Pilot pilot); } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java index b2aa8d5..3d66bee 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java @@ -6,8 +6,11 @@ import org.springframework.web.bind.annotation.*; import pl.skyroster.skyroster_backend.application.pilot.DeletePilotUseCase; import pl.skyroster.skyroster_backend.application.pilot.GetPilotUseCase; +import pl.skyroster.skyroster_backend.application.pilot.PatchPilotUseCase; import pl.skyroster.skyroster_backend.generated.api.ApiApi; import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; +import pl.skyroster.skyroster_backend.generated.model.PilotPatchRequest; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; import java.util.UUID; @@ -17,6 +20,7 @@ public class PilotController { private final GetPilotUseCase getPilotUseCase; private final DeletePilotUseCase deletePilotUseCase; + private final PatchPilotUseCase patchPilotUseCase; @GetMapping(ApiApi.PATH_GET_PILOTS) public ResponseEntity getPilots(@RequestParam Integer page, @RequestParam Integer size, @RequestParam @Nullable String sort) { @@ -28,4 +32,9 @@ public ResponseEntity deletePilotById(@PathVariable UUID pilotId) { deletePilotUseCase.deletePilotById(pilotId); return ResponseEntity.noContent().build(); } + + @PatchMapping(ApiApi.PATH_PATCH_PILOT) + public ResponseEntity patchPilot(@PathVariable UUID pilotId, @RequestBody PilotPatchRequest request) { + return ResponseEntity.ok(patchPilotUseCase.patchPilot(pilotId, request)); + } } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java index 8cbb358..e7a5334 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java @@ -49,6 +49,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers(HttpMethod.POST, "/api/aircraft/**").hasRole("operations_administrator") .requestMatchers(HttpMethod.GET, "/api/aircraft/**").authenticated() .requestMatchers(HttpMethod.DELETE, "/api/pilots/**").hasRole("operations_administrator") + .requestMatchers(HttpMethod.PATCH, "/api/pilots/**").hasRole("operations_administrator") .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) @@ -65,7 +66,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { public CorsConfigurationSource corsConfigurationSource() { var config = new CorsConfiguration(); config.setAllowedOrigins(allowedOrigins); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("Authorization", "Content-Type")); var source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java index d5ae2e8..601e18b 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java @@ -8,7 +8,7 @@ public static AircraftResponse map(Aircraft aircraft) { return new AircraftResponse( aircraft.getId(), aircraft.getRegistrationNumber(), - AircraftTypeInfoMapper.map(aircraft.getAircraftType()), + AircraftTypeInfoMapper.toAircraftTypeInfo(aircraft.getAircraftType()), OperationalBaseInfoMapper.map(aircraft.getOperationalBase()) ); } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java index 05ba98c..4f5eef1 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java @@ -4,11 +4,18 @@ import pl.skyroster.skyroster_backend.generated.model.AircraftTypeInfo; public class AircraftTypeInfoMapper { - public static AircraftTypeInfo map(AircraftType aircraftType) { + public static AircraftTypeInfo toAircraftTypeInfo(AircraftType aircraftType) { return new AircraftTypeInfo( aircraftType.getId(), aircraftType.getIcaoCode(), aircraftType.getName() ); } + public static AircraftType toAircraftTypeInfo(AircraftTypeInfo aircraftType) { + return new AircraftType( + aircraftType.getId(), + aircraftType.getIcaoCode(), + aircraftType.getName() + ); + } } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java index f81572c..98c8816 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java @@ -27,7 +27,7 @@ public static PilotResponse toResponse(Pilot pilot) { pilot.getAircraftTypes() .stream() .sorted(Comparator.comparing(AircraftType::getName)) - .map(AircraftTypeInfoMapper::map) + .map(AircraftTypeInfoMapper::toAircraftTypeInfo) .toList()); } } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java index a26533a..9baaa66 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java @@ -10,4 +10,8 @@ public static PilotQualificationInfo toPilotQualificationInfo(Qualification qual .values(qualification.getValue()) .label(qualification.getLabel()); } + + public static Qualification fromPilotQualificationInfo(PilotQualificationInfo qualification) { + return new Qualification(qualification.getId(), qualification.getValues(), qualification.getLabel()); + } } diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 21c3e56..6f6cbdd 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -344,6 +344,61 @@ paths: '404': description: Pilot not found + patch: + tags: + - Pilots + summary: Partially update a pilot + description: Updates selected pilot fields. Fields omitted from the request body remain unchanged. + operationId: patchPilot + parameters: + - name: pilotId + in: path + required: true + description: Pilot ID + schema: + type: string + format: uuid + example: 0b7a9c1d-86c2-49dc-8ef8-d7f6c7e6e6f1 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PilotPatchRequest' + examples: + updateBasicData: + summary: Update pilot basic data + value: + name: Jan + surname: Kowalski + licence: ATPL-123456 + updateOperationalBase: + summary: Update pilot operational base + value: + operationalBaseId: 2a9dfd20-90fc-4b7e-a996-358d4b5f79c7 + updateQualificationsAndAircraftTypes: + summary: Update qualifications and aircraft types + value: + qualifications: + - PPL + - CPL + - ATPL + aircraftTypes: + - A320 + - B737 + responses: + '200': + description: Pilot updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/PilotResponse' + '400': + description: Invalid request body + '404': + description: Pilot not found + + components: securitySchemes: bearerAuth: @@ -521,6 +576,40 @@ components: items: $ref: '#/components/schemas/AircraftTypeInfo' + PilotPatchRequest: + type: object + description: Request body for partially updating a pilot. Null or omitted fields are not changed. + additionalProperties: false + properties: + name: + type: string + description: Pilot name + example: Jan + surname: + type: string + description: Pilot surname + example: Kowalski + licence: + type: string + description: Pilot licence number + example: ATPL-123456 + operationalBaseIcaoCode: + type: string + description: ICAO operational base code + example: "EPWA" + qualifications: + type: array + description: Pilot qualifications. If provided, replaces the current qualification set. + uniqueItems: true + items: + $ref: '#/components/schemas/PilotQualificationInfo' + aircraftTypes: + type: array + description: Aircraft types assigned to the pilot. If provided, replaces the current aircraft type set. + uniqueItems: true + items: + $ref: '#/components/schemas/AircraftTypeInfo' + PilotQualificationInfo: type: object From 6f9e92175144ff22781b017d9503b8e767fc6034 Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Thu, 14 May 2026 23:16:09 +0200 Subject: [PATCH 10/18] [SKY-20]: Add API POST endpoint for pilots --- .../application/pilot/AddPilotUseCase.java | 24 +++++++ .../PilotAlreadyExistsException.java | 7 ++ .../skyroster_backend/domain/model/Pilot.java | 4 ++ .../domain/port/PilotRepository.java | 1 + .../adapter/in/web/PilotController.java | 8 +++ .../config/GlobalExceptionHandler.java | 12 ++-- .../config/security/SecurityConfig.java | 1 + .../mappers/OperationalBaseInfoMapper.java | 8 +++ .../infrastructure/mappers/PilotMapper.java | 13 ++++ .../src/main/resources/api/openapi.yaml | 72 +++++++++++++++++++ 10 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java new file mode 100644 index 0000000..2234ded --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java @@ -0,0 +1,24 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.exception.PilotAlreadyExistsException; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; +import pl.skyroster.skyroster_backend.generated.model.PilotRequest; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotMapper; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; +@Service +@RequiredArgsConstructor +public class AddPilotUseCase { + private final PilotRepository pilotRepository; + + @Transactional + public PilotResponse addPilot(PilotRequest request) { + if (pilotRepository.existsByLicence(request.getLicence())){ + throw new PilotAlreadyExistsException("Pilot already exists"); + } + var pilot = PilotMapper.toEntity(request); + return PilotMapper.toResponse(pilotRepository.save(pilot)); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java new file mode 100644 index 0000000..1703721 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.exception; + +public class PilotAlreadyExistsException extends RuntimeException { + public PilotAlreadyExistsException(String message) { + super(message); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java index 692eb05..c186fe0 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java @@ -1,7 +1,9 @@ package pl.skyroster.skyroster_backend.domain.model; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import java.util.Set; @@ -10,6 +12,8 @@ @Entity @Getter @Setter +@AllArgsConstructor +@NoArgsConstructor public class Pilot { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java index 62dc727..2213bb6 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java @@ -19,6 +19,7 @@ public interface PilotRepository { Page findAllWithRelations(Pageable pageable); Optional findById(UUID id); boolean existsById(UUID id); + boolean existsByLicence(String licence); void deleteById(UUID id); Pilot save(Pilot pilot); } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java index 3d66bee..9384efd 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java @@ -4,12 +4,14 @@ import org.jspecify.annotations.Nullable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import pl.skyroster.skyroster_backend.application.pilot.AddPilotUseCase; import pl.skyroster.skyroster_backend.application.pilot.DeletePilotUseCase; import pl.skyroster.skyroster_backend.application.pilot.GetPilotUseCase; import pl.skyroster.skyroster_backend.application.pilot.PatchPilotUseCase; import pl.skyroster.skyroster_backend.generated.api.ApiApi; import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; import pl.skyroster.skyroster_backend.generated.model.PilotPatchRequest; +import pl.skyroster.skyroster_backend.generated.model.PilotRequest; import pl.skyroster.skyroster_backend.generated.model.PilotResponse; import java.util.UUID; @@ -21,6 +23,7 @@ public class PilotController { private final GetPilotUseCase getPilotUseCase; private final DeletePilotUseCase deletePilotUseCase; private final PatchPilotUseCase patchPilotUseCase; + private final AddPilotUseCase addPilotUseCase; @GetMapping(ApiApi.PATH_GET_PILOTS) public ResponseEntity getPilots(@RequestParam Integer page, @RequestParam Integer size, @RequestParam @Nullable String sort) { @@ -37,4 +40,9 @@ public ResponseEntity deletePilotById(@PathVariable UUID pilotId) { public ResponseEntity patchPilot(@PathVariable UUID pilotId, @RequestBody PilotPatchRequest request) { return ResponseEntity.ok(patchPilotUseCase.patchPilot(pilotId, request)); } + + @PostMapping(ApiApi.PATH_CREATE_PILOT) + public ResponseEntity addPilot(@RequestBody PilotRequest pilot){ + return ResponseEntity.ok(addPilotUseCase.addPilot(pilot)); + } } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java index 532cf70..8347c88 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java @@ -6,12 +6,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import pl.skyroster.skyroster_backend.domain.exception.AircraftAlreadyExistsException; -import pl.skyroster.skyroster_backend.domain.exception.AircraftHasAssignedFlightsException; -import pl.skyroster.skyroster_backend.domain.exception.AircraftNotFoundException; -import pl.skyroster.skyroster_backend.domain.exception.AircraftTypeNotFoundException; -import pl.skyroster.skyroster_backend.domain.exception.OperationalBaseNotFoundException; -import pl.skyroster.skyroster_backend.domain.exception.PilotNotFoundException; +import pl.skyroster.skyroster_backend.domain.exception.*; import pl.skyroster.skyroster_backend.generated.model.ErrorResponse; import java.time.OffsetDateTime; @@ -56,6 +51,11 @@ public ResponseEntity handlePilotNotFound(PilotNotFoundException return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage(), request); } + @ExceptionHandler(PilotAlreadyExistsException.class) + public ResponseEntity handlePilotAlreadyExists(PilotAlreadyExistsException ex,HttpServletRequest request) { + return buildResponse(HttpStatus.CONFLICT, ex.getMessage(), request); + } + @ExceptionHandler(OperationalBaseNotFoundException.class) public ResponseEntity handleOperationalBaseNotFound(OperationalBaseNotFoundException ex, HttpServletRequest request) { diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java index e7a5334..89a4e0a 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java @@ -50,6 +50,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers(HttpMethod.GET, "/api/aircraft/**").authenticated() .requestMatchers(HttpMethod.DELETE, "/api/pilots/**").hasRole("operations_administrator") .requestMatchers(HttpMethod.PATCH, "/api/pilots/**").hasRole("operations_administrator") + .requestMatchers(HttpMethod.POST, "/api/pilots/**").hasRole("operations_administrator") .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java index 7370ed7..654f49b 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java @@ -11,4 +11,12 @@ public static OperationalBaseInfo map(OperationalBase operationalBase) { operationalBase.getName() ); } + + public static OperationalBase fromInfo(OperationalBaseInfo operationalBase) { + return new OperationalBase( + operationalBase.getId(), + operationalBase.getIcaoCode(), + operationalBase.getName() + ); + } } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java index 98c8816..4b08478 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java @@ -5,8 +5,11 @@ import pl.skyroster.skyroster_backend.domain.model.Pilot; import pl.skyroster.skyroster_backend.domain.model.Qualification; import pl.skyroster.skyroster_backend.generated.model.PilotResponse; +import pl.skyroster.skyroster_backend.generated.model.PilotRequest; import java.util.Comparator; +import java.util.UUID; +import java.util.stream.Collectors; @Component public class PilotMapper { @@ -30,4 +33,14 @@ public static PilotResponse toResponse(Pilot pilot) { .map(AircraftTypeInfoMapper::toAircraftTypeInfo) .toList()); } + public static Pilot toEntity(PilotRequest request){ + return new Pilot( + UUID.randomUUID(), + request.getFirstName(), + request.getLastName(), + request.getLicence(), + OperationalBaseInfoMapper.fromInfo(request.getHomeBase()), + request.getQualifications().stream().map(PilotQualificationMapper::fromPilotQualificationInfo).collect(Collectors.toSet()), + request.getAircraftTypes().stream().map(AircraftTypeInfoMapper::toAircraftTypeInfo).collect(Collectors.toSet())); + } } \ No newline at end of file diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 6f6cbdd..cb1c775 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -204,6 +204,50 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - Pilots + summary: Add a new pilot + description: Creates a new pilot in the system. + operationId: createPilot + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PilotRequest' + responses: + '201': + description: Pilot created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/PilotResponse' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Pilot already exists, for example duplicate license number or email + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/flights: get: operationId: displayFlights @@ -576,6 +620,34 @@ components: items: $ref: '#/components/schemas/AircraftTypeInfo' + PilotRequest: + type: object + required: + - firstName + - lastName + - homeBase + - qualifications + properties: + licence: + type: string + example: PIL-001 + firstName: + type: string + example: Jan + lastName: + type: string + example: Kowalski + homeBase: + $ref: '#/components/schemas/OperationalBaseInfo' + qualifications: + type: array + items: + $ref: '#/components/schemas/PilotQualificationInfo' + aircraftTypes: + type: array + items: + $ref: '#/components/schemas/AircraftTypeInfo' + PilotPatchRequest: type: object description: Request body for partially updating a pilot. Null or omitted fields are not changed. From 864c874028c10b611e0e0d1ed5a8ba872c7289ce Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:17:34 +0200 Subject: [PATCH 11/18] feat(SKY-25): add OpenAPI spec for GET /api/rules --- .../src/main/resources/api/openapi.yaml | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index cb1c775..474b1c3 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -248,6 +248,27 @@ paths: 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 @@ -793,6 +814,45 @@ components: 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: From fb5f98e4b2abf8e29625f07432bc1823a963c073 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:18:05 +0200 Subject: [PATCH 12/18] feat(SKY-25): add Rule domain model and repository port --- .../skyroster_backend/domain/model/Rule.java | 41 +++++++++++++++++++ .../domain/model/RulePeriod.java | 7 ++++ .../domain/model/RuleType.java | 7 ++++ .../domain/port/RuleRepository.java | 9 ++++ 4 files changed, 64 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java 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/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(); +} From b41bf4112a5ef7a549de2fed2ed18342621c60a9 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:18:18 +0200 Subject: [PATCH 13/18] feat(SKY-25): add rules table migration with seed data --- .../migration/V3__rules_schema_and_seed.sql | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql 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'); From 310ec1eb76bc4621f1bd2e1c334fbb50da6147a4 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:18:34 +0200 Subject: [PATCH 14/18] feat(SKY-25): add JPA adapter for RuleRepository --- .../adapter/out/persistence/JpaRuleRepository.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java 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 { +} From 72194a115e5e5c5ed7781179acc1377945b2e6f2 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:18:47 +0200 Subject: [PATCH 15/18] feat(SKY-25): add GetRulesUseCase --- .../application/rule/GetRulesUseCase.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/GetRulesUseCase.java 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(); + } +} From 69eb3243f0738186d57f831a55456b24a00311da Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:19:33 +0200 Subject: [PATCH 16/18] feat(SKY-25): add RuleController with GET /api/rules --- .../adapter/in/web/RuleController.java | 27 +++++++++++++++++++ .../mappers/RuleResponseMapper.java | 18 +++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java 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/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()); + } +} From 822e64f561ce878709a7ce8ad0b4ed7ee68c0746 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:24:28 +0200 Subject: [PATCH 17/18] test(SKY-25): add integration tests for GET /api/rules --- .../adapter/in/web/RuleIntegrationTest.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java 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(); + } +} From b8f7334c20b7c73b9224311d31e573d843e6eb14 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:41:31 +0200 Subject: [PATCH 18/18] revert(SKY-25): drop docker-compose Keycloak DB split from SKY-101 merge Not needed for this task; reverts infrastructure/local/docker-compose.yaml and init-databases.sh mode change to match main. --- infrastructure/local/docker-compose.yaml | 31 +++---------------- .../local/postgres/init-databases.sh | 0 2 files changed, 4 insertions(+), 27 deletions(-) mode change 100755 => 100644 infrastructure/local/postgres/init-databases.sh diff --git a/infrastructure/local/docker-compose.yaml b/infrastructure/local/docker-compose.yaml index 024b543..ec3481b 100644 --- a/infrastructure/local/docker-compose.yaml +++ b/infrastructure/local/docker-compose.yaml @@ -24,28 +24,6 @@ services: timeout: 3s retries: 5 - keycloak-db: - image: postgres:17 - container_name: skyroster-keycloak-db - networks: - - skyroster-network - profiles: - - backend - - full - environment: - POSTGRES_DB: ${KEYCLOAK_DB_NAME:-keycloak} - POSTGRES_USER: ${KEYCLOAK_DB_USER:-keycloak} - POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloak} - ports: - - "5433:5432" - volumes: - - keycloak_pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-keycloak}"] - interval: 5s - timeout: 3s - retries: 5 - keycloak: image: quay.io/keycloak/keycloak:26.2 container_name: skyroster-keycloak @@ -57,9 +35,9 @@ services: - full environment: KC_DB: postgres - KC_DB_URL: jdbc:postgresql://keycloak-db:5432/${KEYCLOAK_DB_NAME:-keycloak} - KC_DB_USERNAME: ${KEYCLOAK_DB_USER:-keycloak} - KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloak} + KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak + KC_DB_USERNAME: ${DB_USERNAME:-skyroster} + KC_DB_PASSWORD: ${DB_PASSWORD:-skyroster} KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin} KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} ports: @@ -68,7 +46,7 @@ services: - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json command: start-dev --import-realm depends_on: - keycloak-db: + postgres: condition: service_healthy frontend: @@ -117,7 +95,6 @@ services: volumes: pgdata: - keycloak_pgdata: networks: skyroster-network: diff --git a/infrastructure/local/postgres/init-databases.sh b/infrastructure/local/postgres/init-databases.sh old mode 100755 new mode 100644