diff --git a/README.md b/README.md index 2c2f16d4..8e5108cf 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ polling `GET /api/v1/flight/:id/events`. | `flight.subscribe.error` | `{ flightId, message }` | Emitted when `subscribe` cannot be fulfilled (e.g. the flight ID does not exist). | Any event emitted by the domain that `EventsRepository` persists (boarding started/finished, off-block, takeoff, -arrival, on-block, offboarding, close, gate/runway/timesheet/loadsheet changes, emergencies, rotation links, track +arrival, on-block, offboarding, close, gate/runway/timesheet/loadsheet changes, emergencies, track saves, live positions) is forwarded as a `flight.event` to subscribers of the matching flight. **Browser example** diff --git a/features/flight/actions/flight.check-in-pilot.feature b/features/flight/actions/flight.check-in-pilot.feature index d00cbbf3..5f79a365 100644 --- a/features/flight/actions/flight.check-in-pilot.feature +++ b/features/flight/actions/flight.check-in-pilot.feature @@ -202,7 +202,6 @@ Feature: Check in pilot for flight "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", @@ -275,7 +274,6 @@ Feature: Check in pilot for flight "role": "CabinCrew", "pilotLicenseId": "UK-31270", "currentFlightId": "23952e79-6b38-49ed-a1db-bd4d9b3cedab", - "currentRotationId": null, "homeAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportId": "c03a79fb-c5ae-46c3-95fe-f3b5dc7b85f3", "lastAirportUpdatedAt": "@date('within 1 minute from now')" @@ -609,329 +607,6 @@ Feature: Check in pilot for flight """ And I set database to initial state - Scenario: As a cabin crew I can check in pilot for flight that starts rotation - Given I open a WebSocket connection as "cabin crew" - When I subscribe to flight events for "006f0754-1ed7-4ae1-9f91-fae2d446a6e7" - Then I should receive flight event history within 2000ms - Given I am signed in as "Alan Doe" - When I send a "POST" request to "/api/v1/flight/006f0754-1ed7-4ae1-9f91-fae2d446a6e7/check-in" with body: - """json - { - "arrivalTime": "2025-01-02T15:50:00.000Z", - "onBlockTime": "2025-01-02T16:08:00.000Z", - "takeoffTime": "2025-01-02T13:15:00.000Z", - "offBlockTime": "2025-01-02T13:00:00.000Z" - } - """ - Then the response status should be 204 - When I send a "GET" request to "/api/v1/flight/006f0754-1ed7-4ae1-9f91-fae2d446a6e7" - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "006f0754-1ed7-4ae1-9f91-fae2d446a6e7", - "flightNumber": "LH42", - "callsign": "DLH42", - "atcCallsign": null, - "isEtops": true, - "status": "checked_in", - "timesheet": { - "estimated": { - "arrivalTime": "2025-01-02T15:50:00.000Z", - "onBlockTime": "2025-01-02T16:08:00.000Z", - "takeoffTime": "2025-01-02T13:15:00.000Z", - "offBlockTime": "2025-01-02T13:00:00.000Z" - }, - "scheduled": { - "arrivalTime": "2025-01-03T02:00:00.000Z", - "onBlockTime": "2025-01-03T02:15:00.000Z", - "takeoffTime": "2025-01-02T18:00:00.000Z", - "offBlockTime": "2025-01-02T17:40:00.000Z" - } - }, - "loadsheets": { - "preliminary": { - "cargo": 7.3, - "payload": 30.6, - "blockFuel": 53, - "fuel": { - "block": 53, - "taxi": 0.8, - "trip": 44.1, - "alternate": 3.2, - "reserve": 2.7, - "contingencyType": "5%", - "contingencyAmount": 2.2, - "mel": 0, - "atc": 0, - "wxx": 0, - "extra": 0, - "tankering": 0 - }, - "flightCrew": { - "pilots": 2, - "cabinCrew": 12, - "reliefPilots": 1 - }, - "passengers": 293, - "zeroFuelWeight": 157.9 - } - }, - "aircraft": { - "id": "9f5da1a4-f09e-4961-8299-82d688337d1f", - "airframe": { - "type": "A339", - "name": "Airbus A330-900", - "cruiseSpeed": { "value": 0.8, "unit": "mach" }, - "serviceCeiling": 41400, - "performanceCode": "D", - "weightCategory": "heavy" - }, - "registration": "D-AIMC", - "selcal": "LR-CK", - "livery": "Fanhansa (2024)", - "operator": { - "id": "40b1b34e-aea1-4cec-acbe-f2bf97c06d7d", - "icaoCode": "DLH", - "iataCode": "LH", - "shortName": "Lufthansa", - "fullName": "Deutsche Lufthansa AG", - "callsign": "LUFTHANSA" - } - }, - "operator": { - "id": "40b1b34e-aea1-4cec-acbe-f2bf97c06d7d", - "icaoCode": "DLH", - "iataCode": "LH", - "shortName": "Lufthansa", - "fullName": "Deutsche Lufthansa AG", - "callsign": "LUFTHANSA" - }, - "airports": [ - { - "id": "3c721cc6-c653-4fad-be43-dc9d6a149383", - "icaoCode": "KJFK", - "iataCode": "JFK", - "city": "New York", - "name": "New York JFK", - "country": "United States of America", - "timezone": "America/New_York", - "continent": "north_america", - "location": { - "latitude": 40.6413, - "longitude": -73.7781 - }, - "type": "departure", - "shape": "@coordinates" - }, - { - "id": "f35c094a-bec5-4803-be32-bd80a14b441a", - "icaoCode": "EDDF", - "iataCode": "FRA", - "city": "Frankfurt", - "name": "Frankfurt Rhein/Main", - "country": "Germany", - "timezone": "Europe/Berlin", - "continent": "europe", - "location": { - "latitude": 50.04693, - "longitude": 8.57397 - }, - "type": "destination", - "shape": "@coordinates" - }, - { - "id": "5c88ea21-f482-47ff-8b1f-3d0c9bbd6caf", - "icaoCode": "EDDW", - "iataCode": "BRE", - "city": "Bremen", - "name": "Bremen", - "country": "Germany", - "timezone": "Europe/Berlin", - "continent": "europe", - "location": { - "latitude": 53.0475, - "longitude": 8.786667 - }, - "type": "destination_alternate", - "shape": "@coordinates" - }, - { - "id": "523b2d2f-9b60-405a-bd5a-90eed1b58e9a", - "icaoCode": "BIKF", - "iataCode": "KEF", - "city": "Reykjavik", - "name": "Reykjavik Keflavik", - "country": "Iceland", - "timezone": "Atlantic/Reykjavik", - "continent": "europe", - "location": { - "latitude": 63.985, - "longitude": -22.6056 - }, - "type": "etops_entry", - "shape": "@coordinates" - }, - { - "id": "6cf1fcd8-d072-46b5-8132-bd885b43dd97", - "icaoCode": "CYYT", - "iataCode": "YYT", - "city": "St. Johns", - "name": "St. Johns Intl", - "country": "Canada", - "timezone": "America/St_Johns", - "continent": "north_america", - "location": { - "latitude": 47.61861, - "longitude": -52.751945 - }, - "type": "etops_exit", - "shape": "@coordinates" - } - ], - "departureParkingPositionId": null, - "departureRunwayId": "6bbf43a4-9242-4f04-b195-6a7bcd1f14c4", - "arrivalParkingPositionId": null, - "arrivalRunwayId": null, - "isFlightDiverted": false, - "isEmergencyDeclared": false, - "hasFlightPath": false, - "isOffBlockDelayed": false, - "actualFuelBurned": null, - "source": "manual", - "tracking": "private", - "rotationId": "4cb9b5a8-7cac-4526-a0f7-f158fd14e9d1", - "createdAt": "2025-01-01T00:00:00.000Z", - "pilot": { - "id": "725f5df2-0c78-4fe8-89a2-52566c89cf7f", - "name": "Alan Doe", - "pilotLicenseId": "UK-34560", - "totalFlightTime": 0 - } - } - """ - When I send a "GET" request to "/api/v1/flight/006f0754-1ed7-4ae1-9f91-fae2d446a6e7/events" - Then the response status should be 200 - And the response body should contain: - """json - [ - { - "id": "9e61ccc9-d6be-4f42-a38f-947cbfe9dcf9", - "scope": "operations", - "type": "flight.created", - "payload": {}, - "actor": { - "id": "721ab705-8608-4386-86b4-2f391a3655a7", - "name": "Alice Doe" - }, - "createdAt": "2025-01-02T11:00:00.000Z" - }, - { - "id": "9eb7eae9-af3c-4eac-bdff-e82e9b852cfe", - "scope": "operations", - "type": "flight.preliminary-loadsheet-updated", - "payload": {}, - "actor": { - "id": "721ab705-8608-4386-86b4-2f391a3655a7", - "name": "Alice Doe" - }, - "createdAt": "2025-01-02T11:05:00.000Z" - }, - { - "id": "1fce1306-6bfc-45a2-8c38-b61a61aa760a", - "scope": "operations", - "type": "flight.released", - "payload": {}, - "actor": { - "id": "721ab705-8608-4386-86b4-2f391a3655a7", - "name": "Alice Doe" - }, - "createdAt": "2025-01-02T11:10:00.000Z" - }, - { - "id": "96d9b78b-fe2c-4ce5-98f2-807ccaf74b85", - "scope": "user", - "type": "flight.pilot-checked-in", - "payload": {}, - "actor": { - "id": "725f5df2-0c78-4fe8-89a2-52566c89cf7f", - "name": "Alan Doe" - }, - "createdAt": "2025-01-02T12:00:00.000Z" - }, - { - "id": "@uuid", - "scope": "user", - "type": "flight.pilot-checked-in", - "payload": {}, - "actor": { - "id": "725f5df2-0c78-4fe8-89a2-52566c89cf7f", - "name": "Alan Doe" - }, - "createdAt": "@date('within 1 minute from now')" - } - ] - """ - Given I am signed in as "admin" - When I send a "GET" request to "/api/v1/user/725f5df2-0c78-4fe8-89a2-52566c89cf7f" - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "725f5df2-0c78-4fe8-89a2-52566c89cf7f", - "name": "Alan Doe", - "email": "alan.doe@example.com", - "role": "CabinCrew", - "pilotLicenseId": "UK-34560", - "currentFlightId": "006f0754-1ed7-4ae1-9f91-fae2d446a6e7", - "currentRotationId": "4cb9b5a8-7cac-4526-a0f7-f158fd14e9d1", - "homeAirportId": "f35c094a-bec5-4803-be32-bd80a14b441a", - "lastAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", - "lastAirportUpdatedAt": "@date('within 1 minute from now')" - } - """ - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/aircraft/9f5da1a4-f09e-4961-8299-82d688337d1f" - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "9f5da1a4-f09e-4961-8299-82d688337d1f", - "airframe": { - "type": "A339", - "name": "Airbus A330-900", - "cruiseSpeed": { "value": 0.8, "unit": "mach" }, - "serviceCeiling": 41400, - "performanceCode": "D", - "weightCategory": "heavy" - }, - "livery": "Fanhansa (2024)", - "registration": "D-AIMC", - "selcal": "LR-CK", - "currentState": "checked_in", - "etopsThresholdMinutes": 180, - "baseAirport": { - "id": "f35c094a-bec5-4803-be32-bd80a14b441a", - "iataCode": "FRA", - "name": "Frankfurt Rhein/Main", - "city": "Frankfurt", - "country": "Germany", - "location": "@coordinates" - }, - "lastAirport": { - "id": "3c721cc6-c653-4fad-be43-dc9d6a149383", - "iataCode": "JFK", - "name": "New York JFK", - "city": "New York", - "country": "United States of America", - "location": "@coordinates" - }, - "lastAirportUpdatedAt": "@date('within 1 minute from now')", - "lastParkingPosition": null - } - """ - And I should receive a live flight event of type "flight.pilot-checked-in" within 2000ms - And I set database to initial state - Scenario: As a cabin crew I cannot check in pilot for flight twice Given I am signed in as "cabin crew" When I send a "POST" request to "/api/v1/flight/23952e79-6b38-49ed-a1db-bd4d9b3cedab/check-in" with body: diff --git a/features/flight/actions/flight.close.feature b/features/flight/actions/flight.close.feature index a2df7847..431e2730 100644 --- a/features/flight/actions/flight.close.feature +++ b/features/flight/actions/flight.close.feature @@ -214,7 +214,6 @@ Feature: Close flight "actualFuelBurned": 10.2, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", @@ -394,7 +393,6 @@ Feature: Close flight "role": "CabinCrew", "pilotLicenseId": "UK-31270", "currentFlightId": null, - "currentRotationId": null, "homeAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportUpdatedAt": null @@ -442,377 +440,6 @@ Feature: Close flight And I should receive a live flight event of type "flight.closed" within 2000ms And I set database to initial state - Scenario: As a cabin crew I close flight for flight that finished offboarding and is last in rotation - Given I open a WebSocket connection as "cabin crew" - When I subscribe to flight events for "d4a25ef2-39cf-484c-af00-a548999e8699" - Then I should receive flight event history within 2000ms - Given I am signed in as "Michael Doe" - When I send a "POST" request to "/api/v1/flight/d4a25ef2-39cf-484c-af00-a548999e8699/close" - Then the response status should be 204 - When I send a "GET" request to "/api/v1/flight/d4a25ef2-39cf-484c-af00-a548999e8699" - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "d4a25ef2-39cf-484c-af00-a548999e8699", - "flightNumber": "LH43", - "callsign": "DLH43", - "atcCallsign": null, - "isEtops": true, - "status": "closed", - "timesheet": { - "actual": { - "arrivalTime": "2025-01-03T11:30:00.000Z", - "onBlockTime": "2025-01-03T11:45:00.000Z", - "takeoffTime": "2025-01-03T04:20:00.000Z", - "offBlockTime": "2025-01-03T04:00:00.000Z" - }, - "estimated": { - "arrivalTime": "2025-01-03T11:30:00.000Z", - "onBlockTime": "2025-01-03T11:45:00.000Z", - "takeoffTime": "2025-01-03T04:20:00.000Z", - "offBlockTime": "2025-01-03T04:00:00.000Z" - }, - "scheduled": { - "arrivalTime": "2025-01-03T11:30:00.000Z", - "onBlockTime": "2025-01-03T11:45:00.000Z", - "takeoffTime": "2025-01-03T04:20:00.000Z", - "offBlockTime": "2025-01-03T04:00:00.000Z" - } - }, - "loadsheets": { - "final": null, - "preliminary": { - "cargo": 8.4, - "payload": 34.9, - "blockFuel": 47.9, - "fuel": { - "block": 47.9, - "taxi": 0.8, - "trip": 39.8, - "alternate": 2.9, - "reserve": 2.4, - "contingencyType": "5%", - "contingencyAmount": 2, - "mel": 0, - "atc": 0, - "wxx": 0, - "extra": 0, - "tankering": 0 - }, - "flightCrew": { - "pilots": 2, - "cabinCrew": 12, - "reliefPilots": 1 - }, - "passengers": 335, - "zeroFuelWeight": 162.3 - } - }, - "aircraft": { - "id": "a9b9205d-53b1-4eec-bb24-548a12159997", - "airframe": { - "type": "A339", - "name": "Airbus A330-900", - "cruiseSpeed": { "value": 0.8, "unit": "mach" }, - "serviceCeiling": 41400, - "performanceCode": "D", - "weightCategory": "heavy" - }, - "registration": "D-AIMF", - "selcal": "BD-FG", - "livery": "New Livery (2018)", - "operator": { - "id": "40b1b34e-aea1-4cec-acbe-f2bf97c06d7d", - "icaoCode": "DLH", - "iataCode": "LH", - "shortName": "Lufthansa", - "fullName": "Deutsche Lufthansa AG", - "callsign": "LUFTHANSA" - } - }, - "operator": { - "id": "40b1b34e-aea1-4cec-acbe-f2bf97c06d7d", - "icaoCode": "DLH", - "iataCode": "LH", - "shortName": "Lufthansa", - "fullName": "Deutsche Lufthansa AG", - "callsign": "LUFTHANSA" - }, - "airports": [ - { - "id": "3c721cc6-c653-4fad-be43-dc9d6a149383", - "icaoCode": "KJFK", - "iataCode": "JFK", - "city": "New York", - "name": "New York JFK", - "country": "United States of America", - "timezone": "America/New_York", - "continent": "north_america", - "location": { - "latitude": 40.6413, - "longitude": -73.7781 - }, - "type": "departure", - "shape": "@coordinates" - }, - { - "id": "f35c094a-bec5-4803-be32-bd80a14b441a", - "icaoCode": "EDDF", - "iataCode": "FRA", - "city": "Frankfurt", - "name": "Frankfurt Rhein/Main", - "country": "Germany", - "timezone": "Europe/Berlin", - "continent": "europe", - "location": { - "latitude": 50.04693, - "longitude": 8.57397 - }, - "type": "destination", - "shape": "@coordinates" - }, - { - "id": "5c88ea21-f482-47ff-8b1f-3d0c9bbd6caf", - "icaoCode": "EDDW", - "iataCode": "BRE", - "city": "Bremen", - "name": "Bremen", - "country": "Germany", - "timezone": "Europe/Berlin", - "continent": "europe", - "location": { - "latitude": 53.0475, - "longitude": 8.786667 - }, - "type": "destination_alternate", - "shape": "@coordinates" - }, - { - "id": "523b2d2f-9b60-405a-bd5a-90eed1b58e9a", - "icaoCode": "BIKF", - "iataCode": "KEF", - "city": "Reykjavik", - "name": "Reykjavik Keflavik", - "country": "Iceland", - "timezone": "Atlantic/Reykjavik", - "continent": "europe", - "location": { - "latitude": 63.985, - "longitude": -22.6056 - }, - "type": "etops_entry", - "shape": "@coordinates" - }, - { - "id": "6cf1fcd8-d072-46b5-8132-bd885b43dd97", - "icaoCode": "CYYT", - "iataCode": "YYT", - "city": "St. Johns", - "name": "St. Johns Intl", - "country": "Canada", - "timezone": "America/St_Johns", - "continent": "north_america", - "location": { - "latitude": 47.61861, - "longitude": -52.751945 - }, - "type": "etops_exit", - "shape": "@coordinates" - } - ], - "departureParkingPositionId": null, - "departureRunwayId": "6bbf43a4-9242-4f04-b195-6a7bcd1f14c4", - "arrivalParkingPositionId": null, - "arrivalRunwayId": null, - "isFlightDiverted": false, - "isEmergencyDeclared": false, - "source": "manual", - "tracking": "private", - "rotationId": "c2e12afb-a712-45aa-9ba5-fec71868e59a", - "hasFlightPath": false, - "isOffBlockDelayed": false, - "actualFuelBurned": null, - "createdAt": "2025-01-01T00:00:00.000Z", - "pilot": { - "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", - "name": "Rick Doe", - "pilotLicenseId": "UK-31270", - "totalFlightTime": 1797 - } - } - """ - When I send a "GET" request to "/api/v1/flight/d4a25ef2-39cf-484c-af00-a548999e8699/events" - Then the response status should be 200 - And the response body should contain: - """json - [ - { - "id": "865a28a4-5154-4e35-a6d4-e198a1ceaa31", - "scope": "operations", - "type": "flight.created", - "payload": {}, - "actor": { - "id": "721ab705-8608-4386-86b4-2f391a3655a7", - "name": "Alice Doe" - }, - "createdAt": "2025-01-01T11:00:00.000Z" - }, - { - "id": "2b2cecc3-6af9-4335-9029-873c6da142c5", - "scope": "operations", - "type": "flight.preliminary-loadsheet-updated", - "payload": {}, - "actor": { - "id": "721ab705-8608-4386-86b4-2f391a3655a7", - "name": "Alice Doe" - }, - "createdAt": "2025-01-01T11:05:00.000Z" - }, - { - "id": "8505951b-9fdd-4262-8460-248039f8e7cd", - "scope": "operations", - "type": "flight.released", - "payload": {}, - "actor": { - "id": "721ab705-8608-4386-86b4-2f391a3655a7", - "name": "Alice Doe" - }, - "createdAt": "2025-01-01T11:10:00.000Z" - }, - { - "id": "c8f5dd49-03f7-4656-8976-8907175c0017", - "scope": "user", - "type": "flight.pilot-checked-in", - "payload": {}, - "actor": { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe" - }, - "createdAt": "2025-01-01T12:00:00.000Z" - }, - { - "id": "eaf13533-5aef-4451-aeea-82152deda67d", - "scope": "user", - "type": "flight.boarding-started", - "payload": {}, - "actor": { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe" - }, - "createdAt": "2025-01-01T12:40:00.000Z" - }, - { - "id": "7d337c5c-6a9b-4d6c-bd87-319706ea55d4", - "scope": "user", - "type": "flight.boarding-finished", - "payload": {}, - "actor": { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe" - }, - "createdAt": "2025-01-01T13:05:00.000Z" - }, - { - "id": "30e6a5bb-8945-44ca-b6dc-537155c24287", - "scope": "user", - "type": "flight.off-block-reported", - "payload": {}, - "actor": { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe" - }, - "createdAt": "2025-01-01T13:10:00.000Z" - }, - { - "id": "0c1703d9-af9b-49f7-9a7a-26ff6cd53e36", - "scope": "user", - "type": "flight.takeoff-reported", - "payload": {}, - "actor": { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe" - }, - "createdAt": "2025-01-01T13:25:00.000Z" - }, - { - "id": "8fa22b88-71db-40c9-90ed-0525d35c4af2", - "scope": "user", - "type": "flight.arrival-reported", - "payload": {}, - "actor": { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe" - }, - "createdAt": "2025-01-01T16:10:00.000Z" - }, - { - "id": "9fb04dc3-062b-4f68-a2f3-a35a5e76e272", - "scope": "user", - "type": "flight.on-block-reported", - "payload": {}, - "actor": { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe" - }, - "createdAt": "2025-01-01T16:28:00.000Z" - }, - { - "id": "bd5ae37a-2efe-4ca9-83c9-6ed6e9218fad", - "scope": "user", - "type": "flight.offboarding-started", - "payload": {}, - "actor": { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe" - }, - "createdAt": "2025-01-01T16:30:00.000Z" - }, - { - "id": "6391d61d-988a-43c2-abd0-49a9f6aa25a5", - "scope": "user", - "type": "flight.offboarding-finished", - "payload": {}, - "actor": { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe" - }, - "createdAt": "2025-01-01T16:50:00.000Z" - }, - { - "id": "@uuid", - "scope": "user", - "type": "flight.closed", - "payload": {}, - "actor": { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe" - }, - "createdAt": "@date('within 1 minute from now')" - } - ] - """ - Given I am signed in as "admin" - When I send a "GET" request to "/api/v1/user/629be07f-5e65-429a-9d69-d34b99185f50" - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "629be07f-5e65-429a-9d69-d34b99185f50", - "name": "Michael Doe", - "email": "michael.doe@example.com", - "role": "CabinCrew", - "pilotLicenseId": "UK-98540", - "currentFlightId": null, - "currentRotationId": null, - "homeAirportId": "616cbdd7-ccfc-4687-8cf6-1e7236435046", - "lastAirportId": "616cbdd7-ccfc-4687-8cf6-1e7236435046", - "lastAirportUpdatedAt": null - } - """ - And I should receive a live flight event of type "flight.closed" within 2000ms - And I set database to initial state - Scenario: As a cabin crew I cannot close flight plan for flight twice Given I am signed in as "cabin crew" When I send a "POST" request to "/api/v1/flight/38644393-deee-434d-bfd1-7242abdbc4e1/close" @@ -1062,7 +689,6 @@ Feature: Close flight "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", @@ -1265,7 +891,6 @@ Feature: Close flight "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", diff --git a/features/flight/actions/flight.finish-boarding.feature b/features/flight/actions/flight.finish-boarding.feature index 1a0c6d45..8e6dd506 100644 --- a/features/flight/actions/flight.finish-boarding.feature +++ b/features/flight/actions/flight.finish-boarding.feature @@ -231,7 +231,6 @@ Feature: Finish flight boarding "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", diff --git a/features/flight/actions/flight.finish-offboarding.feature b/features/flight/actions/flight.finish-offboarding.feature index 7be24575..1fa9ed62 100644 --- a/features/flight/actions/flight.finish-offboarding.feature +++ b/features/flight/actions/flight.finish-offboarding.feature @@ -209,7 +209,6 @@ Feature: Finish offboarding for flight that started onboarding "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", diff --git a/features/flight/actions/flight.mark-as-ready.feature b/features/flight/actions/flight.mark-as-ready.feature index f528e7c7..af9ee511 100644 --- a/features/flight/actions/flight.mark-as-ready.feature +++ b/features/flight/actions/flight.mark-as-ready.feature @@ -170,7 +170,6 @@ Feature: Mark flight as ready "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } diff --git a/features/flight/actions/flight.report-arrival.feature b/features/flight/actions/flight.report-arrival.feature index eb0715ee..ea775e26 100644 --- a/features/flight/actions/flight.report-arrival.feature +++ b/features/flight/actions/flight.report-arrival.feature @@ -215,7 +215,6 @@ Feature: Report arrival "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", diff --git a/features/flight/actions/flight.report-off-block.feature b/features/flight/actions/flight.report-off-block.feature index 09b4ee36..4537188e 100644 --- a/features/flight/actions/flight.report-off-block.feature +++ b/features/flight/actions/flight.report-off-block.feature @@ -210,7 +210,6 @@ Feature: Report off-block "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", diff --git a/features/flight/actions/flight.report-on-block.feature b/features/flight/actions/flight.report-on-block.feature index 1b4b7628..875e008d 100644 --- a/features/flight/actions/flight.report-on-block.feature +++ b/features/flight/actions/flight.report-on-block.feature @@ -380,7 +380,6 @@ Feature: Report on-block "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", @@ -598,7 +597,6 @@ Feature: Report on-block "role": "CabinCrew", "pilotLicenseId": "UK-31270", "currentFlightId": "b3899775-278e-4496-add1-21385a13d93e", - "currentRotationId": null, "homeAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportId": "e764251b-bb25-4e8b-8cc7-11b0397b4554", "lastAirportUpdatedAt": "@date('within 1 minute from now')" diff --git a/features/flight/actions/flight.report-takeoff.feature b/features/flight/actions/flight.report-takeoff.feature index 75ba1d85..f45a53da 100644 --- a/features/flight/actions/flight.report-takeoff.feature +++ b/features/flight/actions/flight.report-takeoff.feature @@ -209,7 +209,6 @@ Feature: Report takeoff "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", diff --git a/features/flight/actions/flight.start-boarding.feature b/features/flight/actions/flight.start-boarding.feature index ba03756a..37f29dda 100644 --- a/features/flight/actions/flight.start-boarding.feature +++ b/features/flight/actions/flight.start-boarding.feature @@ -178,7 +178,6 @@ Feature: Start boarding "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", diff --git a/features/flight/actions/flight.start-offboarding.feature b/features/flight/actions/flight.start-offboarding.feature index 4650395d..84360574 100644 --- a/features/flight/actions/flight.start-offboarding.feature +++ b/features/flight/actions/flight.start-offboarding.feature @@ -209,7 +209,6 @@ Feature: Start offboarding "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", diff --git a/features/flight/diversion/flight.report-diversion.feature b/features/flight/diversion/flight.report-diversion.feature index 465f2362..c0751b51 100644 --- a/features/flight/diversion/flight.report-diversion.feature +++ b/features/flight/diversion/flight.report-diversion.feature @@ -234,7 +234,6 @@ Feature: Report a flight diversion "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", @@ -573,7 +572,6 @@ Feature: Report a flight diversion "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", diff --git a/features/flight/emergency/flight.resolve-emergency.feature b/features/flight/emergency/flight.resolve-emergency.feature index cde7f071..3870a7e7 100644 --- a/features/flight/emergency/flight.resolve-emergency.feature +++ b/features/flight/emergency/flight.resolve-emergency.feature @@ -150,7 +150,6 @@ Feature: Resolve a flight emergency "shape": "@coordinates" } ], - "rotationId": null, "source": "manual", "tracking": "public", "departureParkingPositionId": null, @@ -332,7 +331,6 @@ Feature: Resolve a flight emergency "shape": "@coordinates" } ], - "rotationId": null, "source": "manual", "tracking": "public", "departureParkingPositionId": null, diff --git a/features/flight/management/flight.change-visibility.feature b/features/flight/management/flight.change-visibility.feature index 30b06d6a..0098d4aa 100644 --- a/features/flight/management/flight.change-visibility.feature +++ b/features/flight/management/flight.change-visibility.feature @@ -177,7 +177,6 @@ Feature: Change flight visibility "actualFuelBurned": null, "source": "manual", "tracking": "disabled", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } diff --git a/features/flight/management/flight.create-with-simbrief.feature b/features/flight/management/flight.create-with-simbrief.feature index 5d9118e0..2834201a 100644 --- a/features/flight/management/flight.create-with-simbrief.feature +++ b/features/flight/management/flight.create-with-simbrief.feature @@ -222,7 +222,6 @@ Feature: Create a flight with Simbrief "hasFlightPath": false, "isOffBlockDelayed": false, "actualFuelBurned": null, - "rotationId": null, "source": "simbrief", "tracking": "private", "createdAt": "@date('within 1 minute from now')", @@ -463,7 +462,6 @@ Feature: Create a flight with Simbrief "hasFlightPath": false, "isOffBlockDelayed": false, "actualFuelBurned": null, - "rotationId": null, "source": "simbrief", "tracking": "private", "createdAt": "@date('within 1 minute from now')", @@ -659,7 +657,6 @@ Feature: Create a flight with Simbrief "hasFlightPath": false, "isOffBlockDelayed": false, "actualFuelBurned": null, - "rotationId": null, "source": "simbrief", "tracking": "private", "createdAt": "@date('within 1 minute from now')", diff --git a/features/flight/management/flight.create.feature b/features/flight/management/flight.create.feature index e1d3120e..3bbae8c3 100644 --- a/features/flight/management/flight.create.feature +++ b/features/flight/management/flight.create.feature @@ -156,7 +156,6 @@ Feature: Create a flight "actualFuelBurned": null, "source": "manual", "tracking": "private", - "rotationId": null, "createdAt": "@date('within 1 minute from now')", "pilot": null } @@ -382,7 +381,6 @@ Feature: Create a flight "actualFuelBurned": null, "source": "manual", "tracking": "private", - "rotationId": null, "createdAt": "@date('within 1 minute from now')", "pilot": null } @@ -553,7 +551,6 @@ Feature: Create a flight "actualFuelBurned": null, "source": "manual", "tracking": "private", - "rotationId": null, "createdAt": "@date('within 1 minute from now')", "pilot": null } diff --git a/features/flight/management/flight.get.feature b/features/flight/management/flight.get.feature index def42389..48aa7c73 100644 --- a/features/flight/management/flight.get.feature +++ b/features/flight/management/flight.get.feature @@ -148,7 +148,6 @@ Feature: Get flight "actualFuelBurned": null, "source": "manual", "tracking": "private", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } @@ -351,7 +350,6 @@ Feature: Get flight "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": { "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", diff --git a/features/flight/management/flight.update-arrival-parking-position.feature b/features/flight/management/flight.update-arrival-parking-position.feature index 589917da..e900bf1c 100644 --- a/features/flight/management/flight.update-arrival-parking-position.feature +++ b/features/flight/management/flight.update-arrival-parking-position.feature @@ -177,7 +177,6 @@ Feature: Update flight arrival parking position "actualFuelBurned": null, "source": "manual", "tracking": "private", - "rotationId": "bd8f2d64-a647-42da-be63-c6589915e6c9", "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } diff --git a/features/flight/management/flight.update-arrival-runway.feature b/features/flight/management/flight.update-arrival-runway.feature index 6d88b6cc..55fe8eed 100644 --- a/features/flight/management/flight.update-arrival-runway.feature +++ b/features/flight/management/flight.update-arrival-runway.feature @@ -176,7 +176,6 @@ Feature: Update flight arrival runway "actualFuelBurned": null, "source": "manual", "tracking": "private", - "rotationId": "bd8f2d64-a647-42da-be63-c6589915e6c9", "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } diff --git a/features/flight/management/flight.update-departure-parking-position.feature b/features/flight/management/flight.update-departure-parking-position.feature index 32bc05f7..bec34691 100644 --- a/features/flight/management/flight.update-departure-parking-position.feature +++ b/features/flight/management/flight.update-departure-parking-position.feature @@ -153,7 +153,6 @@ Feature: Update flight departure parking position "actualFuelBurned": null, "source": "manual", "tracking": "private", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } diff --git a/features/flight/management/flight.update-departure-runway.feature b/features/flight/management/flight.update-departure-runway.feature index e230b21f..3540a337 100644 --- a/features/flight/management/flight.update-departure-runway.feature +++ b/features/flight/management/flight.update-departure-runway.feature @@ -152,7 +152,6 @@ Feature: Update flight departure runway "actualFuelBurned": null, "source": "manual", "tracking": "private", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } @@ -335,7 +334,6 @@ Feature: Update flight departure runway "actualFuelBurned": null, "source": "manual", "tracking": "private", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } diff --git a/features/flight/management/flight.update-predicted-timesheet.feature b/features/flight/management/flight.update-predicted-timesheet.feature index 844baf7e..9d367015 100644 --- a/features/flight/management/flight.update-predicted-timesheet.feature +++ b/features/flight/management/flight.update-predicted-timesheet.feature @@ -198,7 +198,6 @@ Feature: Update flight predicted timesheet "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } @@ -400,7 +399,6 @@ Feature: Update flight predicted timesheet "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } @@ -613,7 +611,6 @@ Feature: Update flight predicted timesheet "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } diff --git a/features/flight/management/flight.update-preliminary-loadsheet.feature b/features/flight/management/flight.update-preliminary-loadsheet.feature index 2e8f6909..d75100a2 100644 --- a/features/flight/management/flight.update-preliminary-loadsheet.feature +++ b/features/flight/management/flight.update-preliminary-loadsheet.feature @@ -184,7 +184,6 @@ Feature: Update flight preliminary loadsheet "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } @@ -415,7 +414,6 @@ Feature: Update flight preliminary loadsheet "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } diff --git a/features/flight/management/flight.update-scheduled-timesheet.feature b/features/flight/management/flight.update-scheduled-timesheet.feature index 2031db7e..2c6a40d9 100644 --- a/features/flight/management/flight.update-scheduled-timesheet.feature +++ b/features/flight/management/flight.update-scheduled-timesheet.feature @@ -186,7 +186,6 @@ Feature: Update flight scheduled timesheet "actualFuelBurned": null, "source": "manual", "tracking": "public", - "rotationId": null, "createdAt": "2025-01-01T00:00:00.000Z", "pilot": null } diff --git a/features/flight/rotation/rotation.add-flight.feature b/features/flight/rotation/rotation.add-flight.feature deleted file mode 100644 index e236ca80..00000000 --- a/features/flight/rotation/rotation.add-flight.feature +++ /dev/null @@ -1,148 +0,0 @@ -Feature: Add flight to rotation - - Scenario: As admin I cannot add a flight to a rotation - Given I am signed in as "admin" - When I send a "POST" request to "/api/v1/flight/e91e13a9-09d8-48bf-8453-283cef467b88/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 403 - And the response body should contain: - """json - { - "message": "Forbidden resource", - "error": "Forbidden", - "statusCode": 403 - } - """ - - Scenario: As operations I can add a valid flight to a rotation - Given I open a WebSocket connection as "cabin crew" - When I subscribe to flight events for "e91e13a9-09d8-48bf-8453-283cef467b88" - Then I should receive flight event history within 2000ms - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/flight/e91e13a9-09d8-48bf-8453-283cef467b88/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 204 - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "bd8f2d64-a647-42da-be63-c6589915e6c9", - "name": "2025-01", - "pilot": { - "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", - "name": "Rick Doe", - "pilotLicenseId": "UK-31270" - }, - "flights": [ - { - "id": "e91e13a9-09d8-48bf-8453-283cef467b88", - "flightNumber": "AA4907" - }, - { - "id": "48760636-9520-4863-b32f-f3618556feb7", - "flightNumber": "LH40" - }, - { - "id": "e8e17e59-67d7-4a6c-a0bd-425ffa6bed66", - "flightNumber": "LH41" - } - ], - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": "@date('within 1 minute from now')" - } - """ - And I should receive a live flight event of type "flight.added-to-rotation" within 2000ms - And I set database to initial state - - Scenario: As cabin crew I cannot add a flight to a rotation - Given I am signed in as "cabin crew" - When I send a "POST" request to "/api/v1/flight/e91e13a9-09d8-48bf-8453-283cef467b88/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 403 - - Scenario: As operations I cannot add a flight that is already assigned to a rotation - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/flight/e8e17e59-67d7-4a6c-a0bd-425ffa6bed66/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 409 - And the response body should contain: - """json - { - "message": "Flight is already assigned to rotation.", - "error": "Conflict", - "statusCode": 409 - } - """ - - Scenario: As operations I cannot add a flight in non-created status to a rotation - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/flight/05986dd3-ff01-4112-ad35-ecd85db05c77/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Flight is in incorrect state to modify its rotation.", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As operations I cannot add a non-existent flight to a rotation - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/flight/963ab4bd-1d3b-44e2-9c87-628a9c7197b8/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Flight with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: As operations I cannot add a flight with incorrect ID to a rotation - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/flight/incorrect-uuid/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As operations I cannot add a flight to a non-existent rotation - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/flight/e91e13a9-09d8-48bf-8453-283cef467b88/rotation/963ab4bd-1d3b-44e2-9c87-628a9c7197b8" - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Rotation with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: As operations I cannot add a flight to a rotation with incorrect rotation ID - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/flight/e91e13a9-09d8-48bf-8453-283cef467b88/rotation/incorrect-uuid" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As an unauthorized user I cannot add a flight to a rotation - When I send a "POST" request to "/api/v1/flight/e91e13a9-09d8-48bf-8453-283cef467b88/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 401 - And the response body should contain: - """json - { - "message": "Unauthorized", - "statusCode": 401 - } - """ diff --git a/features/flight/rotation/rotation.remove-flight.feature b/features/flight/rotation/rotation.remove-flight.feature deleted file mode 100644 index 9ca10097..00000000 --- a/features/flight/rotation/rotation.remove-flight.feature +++ /dev/null @@ -1,135 +0,0 @@ -Feature: Remove flights from rotation - - Scenario: As admin I cannot remove a flight from a rotation - Given I am signed in as "admin" - When I send a "DELETE" request to "/api/v1/flight/e8e17e59-67d7-4a6c-a0bd-425ffa6bed66/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 403 - And the response body should contain: - """json - { - "message": "Forbidden resource", - "error": "Forbidden", - "statusCode": 403 - } - """ - - Scenario: As operations I can remove a flight from a rotation - Given I open a WebSocket connection as "cabin crew" - When I subscribe to flight events for "e8e17e59-67d7-4a6c-a0bd-425ffa6bed66" - Then I should receive flight event history within 2000ms - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/flight/e8e17e59-67d7-4a6c-a0bd-425ffa6bed66/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 204 - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "bd8f2d64-a647-42da-be63-c6589915e6c9", - "name": "2025-01", - "pilot": { - "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", - "name": "Rick Doe", - "pilotLicenseId": "UK-31270" - }, - "flights": [ - { - "id": "48760636-9520-4863-b32f-f3618556feb7", - "flightNumber": "LH40" - } - ], - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": "@date('within 1 minute from now')" - } - """ - And I should receive a live flight event of type "flight.removed-from-rotation" within 2000ms - And I set database to initial state - - Scenario: As cabin crew I cannot remove a flight from a rotation - Given I am signed in as "cabin crew" - When I send a "DELETE" request to "/api/v1/flight/e8e17e59-67d7-4a6c-a0bd-425ffa6bed66/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 403 - And the response body should contain: - """json - { - "message": "Forbidden resource", - "error": "Forbidden", - "statusCode": 403 - } - """ - - Scenario: As operations I cannot remove a flight in non-created status from a rotation - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/flight/48760636-9520-4863-b32f-f3618556feb7/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Flight is in incorrect state to modify its rotation.", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As operations I cannot remove a non-existent flight from a rotation - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/flight/9303a459-72f0-453a-ac3e-54e6eff5a29a/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Flight with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: As operations I cannot remove a flight with incorrect ID from a rotation - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/flight/incorrect-uuid/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As operations I cannot remove a flight from a non-existent rotation - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/flight/e91e13a9-09d8-48bf-8453-283cef467b88/rotation/49c8e76d-eccc-44b0-accb-4f0822c4c311" - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Rotation with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: As operations I cannot remove a flight from a rotation with incorrect ID - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/flight/e91e13a9-09d8-48bf-8453-283cef467b88/rotation/incorrect-uuid" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As an unauthorized user I cannot remove a flight from a rotation - When I send a "DELETE" request to "/api/v1/flight/e8e17e59-67d7-4a6c-a0bd-425ffa6bed66/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 401 - And the response body should contain: - """json - { - "message": "Unauthorized", - "statusCode": 401 - } - """ diff --git a/features/operator/rotation/rotation.create.feature b/features/operator/rotation/rotation.create.feature deleted file mode 100644 index 300b0d2b..00000000 --- a/features/operator/rotation/rotation.create.feature +++ /dev/null @@ -1,160 +0,0 @@ -Feature: Create rotation for operator - - Scenario: As admin I cannot create a rotation - Given I am signed in as "admin" - When I send a "POST" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation" with body: - """json - { - "name": "LH-2025-01", - "pilotId": "fcf6f4bc-290d-43a9-843c-409cd47e143d" - } - """ - Then the response status should be 403 - And the response body should contain: - """json - { - "message": "Forbidden resource", - "error": "Forbidden", - "statusCode": 403 - } - """ - - Scenario: As operations I can create a rotation - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation" with body: - """json - { - "name": "LH-2025-01", - "pilotId": "fcf6f4bc-290d-43a9-843c-409cd47e143d" - } - """ - Then the response status should be 201 - And the response body should contain: - """json - { - "id": "@uuid", - "name": "LH-2025-01", - "pilot": { - "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", - "name": "Rick Doe", - "pilotLicenseId": "UK-31270" - }, - "flights": [], - "createdAt": "@date('within 1 minute from now')", - "updatedAt": null - } - """ - And I set database to initial state - - Scenario: As cabin crew I cannot create a rotation - Given I am signed in as "cabin crew" - When I send a "POST" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation" with body: - """json - { - "name": "LH-2025-01", - "pilotId": "fcf6f4bc-290d-43a9-843c-409cd47e143d" - } - """ - Then the response status should be 403 - And the response body should contain: - """json - { - "message": "Forbidden resource", - "error": "Forbidden", - "statusCode": 403 - } - """ - - Scenario: As an operations I cannot create a rotation without required fields - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation" with body: - """json - {} - """ - Then the response status should be 400 - And the response body should contain: - """json - { - "error": "Bad Request", - "message": "Request validation failed.", - "statusCode": 400, - "violations": { - "name": ["name should not be empty", "name must be a string"], - "pilotId": ["pilotId should not be empty", "pilotId must be a UUID"] - } - } - """ - - Scenario: As operations I cannot create a rotation when pilot is not found - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation" with body: - """json - { - "name": "2025-01", - "pilotId": "cb715c36-ecfb-4dfc-8700-3c262b52abac" - } - """ - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "User with given id does not exist.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: As operations I cannot create rotation with non-existing operator - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/operator/16b531c3-d817-4326-841c-2a4c243f9c1f/rotation" with body: - """json - { - "name": "LH-2025-01", - "pilotId": "fcf6f4bc-290d-43a9-843c-409cd47e143d" - } - """ - Then the response status should be 404 - And the response body should contain: - """json - { - "statusCode": 404, - "error": "Not Found", - "message": "Operator with given ID not found." - } - """ - - Scenario: As operations I cannot create rotation with incorrect uuid - Given I am signed in as "operations" - When I send a "POST" request to "/api/v1/operator/incorrect-uuid/rotation" with body: - """json - { - "name": "LH-2025-01", - "pilotId": "fcf6f4bc-290d-43a9-843c-409cd47e143d" - } - """ - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As an unauthorized user I cannot create a rotation - When I send a "POST" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation" with body: - """json - { - "name": "2025-01", - "pilotId": "fcf6f4bc-290d-43a9-843c-409cd47e143d" - } - """ - Then the response status should be 401 - And the response body should contain: - """json - { - "message": "Unauthorized", - "statusCode": 401 - } - """ diff --git a/features/operator/rotation/rotation.delete.feature b/features/operator/rotation/rotation.delete.feature deleted file mode 100644 index 88f7dc00..00000000 --- a/features/operator/rotation/rotation.delete.feature +++ /dev/null @@ -1,106 +0,0 @@ -Feature: Delete rotation - - Scenario: As admin I cannot delete a rotation - Given I am signed in as "admin" - When I send a "DELETE" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 403 - And the response body should contain: - """json - { - "message": "Forbidden resource", - "error": "Forbidden", - "statusCode": 403 - } - """ - - Scenario: As operations I can delete a rotation - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 204 - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Rotation with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - And I set database to initial state - - Scenario: As cabin crew I cannot delete a rotation - Given I am signed in as "cabin crew" - When I send a "DELETE" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 403 - And the response body should contain: - """json - { - "message": "Forbidden resource", - "error": "Forbidden", - "statusCode": 403 - } - """ - - Scenario: As operations I cannot delete non-existing rotation - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/4dbf997d-8d51-421e-b7df-fc69dd31f7c6" - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Rotation with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: As operations I cannot delete rotation that operator does not exist - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/operator/0e37fd75-141d-4f01-b040-bcde2f7be839/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Operator with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: As operations I cannot delete rotation with incorrect ID - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/incorrect-uuid" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As operations I cannot delete rotation with incorrect operator ID - Given I am signed in as "operations" - When I send a "DELETE" request to "/api/v1/operator/incorrect-uuid/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As unauthorized user I cannot remove rotation - When I send a "DELETE" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 401 - And the response body should contain: - """json - { - "message": "Unauthorized", - "statusCode": 401 - } - """ diff --git a/features/operator/rotation/rotation.get.feature b/features/operator/rotation/rotation.get.feature deleted file mode 100644 index 5a757c1d..00000000 --- a/features/operator/rotation/rotation.get.feature +++ /dev/null @@ -1,151 +0,0 @@ -Feature: Get rotation for operator - - Scenario: As admin I can get one rotation - Given I am signed in as "admin" - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "bd8f2d64-a647-42da-be63-c6589915e6c9", - "name": "2025-01", - "pilot": { - "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", - "name": "Rick Doe", - "pilotLicenseId": "UK-31270" - }, - "flights": [ - { - "id": "48760636-9520-4863-b32f-f3618556feb7", - "flightNumber": "LH40" - }, - { - "id": "e8e17e59-67d7-4a6c-a0bd-425ffa6bed66", - "flightNumber": "LH41" - } - ], - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": null - } - """ - - Scenario: As operations I can get one rotation - Given I am signed in as "operations" - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "bd8f2d64-a647-42da-be63-c6589915e6c9", - "name": "2025-01", - "pilot": { - "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", - "name": "Rick Doe", - "pilotLicenseId": "UK-31270" - }, - "flights": [ - { - "id": "48760636-9520-4863-b32f-f3618556feb7", - "flightNumber": "LH40" - }, - { - "id": "e8e17e59-67d7-4a6c-a0bd-425ffa6bed66", - "flightNumber": "LH41" - } - ], - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": null - } - """ - - Scenario: As cabin crew I can get one rotation - Given I am signed in as "cabin crew" - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "bd8f2d64-a647-42da-be63-c6589915e6c9", - "name": "2025-01", - "pilot": { - "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", - "name": "Rick Doe", - "pilotLicenseId": "UK-31270" - }, - "flights": [ - { - "id": "48760636-9520-4863-b32f-f3618556feb7", - "flightNumber": "LH40" - }, - { - "id": "e8e17e59-67d7-4a6c-a0bd-425ffa6bed66", - "flightNumber": "LH41" - } - ], - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": null - } - """ - - Scenario: Get rotation that does not exist - Given I am signed in as "cabin crew" - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/892299aa-debe-4f2f-87d3-4cdd513da26c" - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Rotation with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: Get rotation from operator that does not exist - Given I am signed in as "cabin crew" - When I send a "GET" request to "/api/v1/operator/0e37fd75-141d-4f01-b040-bcde2f7be839/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Operator with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: Get rotation with incorrect rotation ID - Given I am signed in as "cabin crew" - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/incorrect-uuid" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As a cabin crew I cannot get rotation with incorrect operator ID - Given I am signed in as "cabin crew" - When I send a "GET" request to "/api/v1/operator/incorrect-uuid/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As an unauthorized user I cannot get one rotation - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" - Then the response status should be 401 - And the response body should contain: - """json - { - "message": "Unauthorized", - "statusCode": 401 - } - """ diff --git a/features/operator/rotation/rotation.list.feature b/features/operator/rotation/rotation.list.feature deleted file mode 100644 index d2117041..00000000 --- a/features/operator/rotation/rotation.list.feature +++ /dev/null @@ -1,53 +0,0 @@ -Feature: List rotations for operator - - Scenario: As admin I can list rotations - Given I am signed in as "admin" - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation" - Then the response status should be 200 - - Scenario: As operations I can list rotations - Given I am signed in as "operations" - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation" - Then the response status should be 200 - - Scenario: As cabin crew I can list rotations - Given I am signed in as "cabin crew" - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation" - Then the response status should be 200 - - Scenario: As operations I cannot get rotation list for non-existing operator - Given I am signed in as "operations" - When I send a "GET" request to "/api/v1/operator/16b531c3-d817-4326-841c-2a4c243f9c1f/rotation" - Then the response status should be 404 - And the response body should contain: - """json - { - "statusCode": 404, - "error": "Not Found", - "message": "Operator with given ID not found." - } - """ - - Scenario: As operations I cannot create rotation with incorrect operator ID - Given I am signed in as "operations" - When I send a "GET" request to "/api/v1/operator/incorrect-uuid/rotation" - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As an unauthorized user I cannot list rotations - When I send a "GET" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation" - Then the response status should be 401 - And the response body should contain: - """json - { - "message": "Unauthorized", - "statusCode": 401 - } - """ diff --git a/features/operator/rotation/rotation.update.feature b/features/operator/rotation/rotation.update.feature deleted file mode 100644 index 637caf89..00000000 --- a/features/operator/rotation/rotation.update.feature +++ /dev/null @@ -1,178 +0,0 @@ -Feature: Update rotation - - Scenario: As an admin I cannot update rotation - Given I am signed in as "admin" - When I send a "PATCH" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" with body: - """json - { - "name": "New 2025-01" - } - """ - Then the response status should be 403 - And the response body should contain: - """json - { - "message": "Forbidden resource", - "error": "Forbidden", - "statusCode": 403 - } - """ - - Scenario: As operations I can update rotation - Given I am signed in as "operations" - When I send a "PATCH" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" with body: - """json - { - "name": "New 2025-01" - } - """ - Then the response status should be 200 - And the response body should contain: - """json - { - "id": "bd8f2d64-a647-42da-be63-c6589915e6c9", - "name": "New 2025-01", - "pilot": { - "id": "fcf6f4bc-290d-43a9-843c-409cd47e143d", - "name": "Rick Doe", - "pilotLicenseId": "UK-31270" - }, - "flights": [ - { - "id": "48760636-9520-4863-b32f-f3618556feb7", - "flightNumber": "LH40" - }, - { - "id": "e8e17e59-67d7-4a6c-a0bd-425ffa6bed66", - "flightNumber": "LH41" - } - ], - "createdAt": "2025-01-01T00:00:00.000Z", - "updatedAt": "@date('within 1 minute from now')" - } - """ - And I set database to initial state - - Scenario: As cabin crew I cannot update rotation - Given I am signed in as "cabin crew" - When I send a "PATCH" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" with body: - """json - { - "name": "New 2025-01" - } - """ - Then the response status should be 403 - And the response body should contain: - """json - { - "message": "Forbidden resource", - "error": "Forbidden", - "statusCode": 403 - } - """ - - Scenario: As operations I cannot update a rotation with pilot that does not exist - Given I am signed in as "operations" - When I send a "PATCH" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" with body: - """json - { - "pilotId": "c4087663-f2c2-4acf-aa86-73417b15d54a" - } - """ - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "User with given id does not exist.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: As operations I cannot update non-existing rotation - Given I am signed in as "operations" - When I send a "PATCH" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/00fb8d16-9f33-4c60-9c86-9722390b16a1" with body: - """json - { - "name": "New 2025-01" - } - """ - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Rotation with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: As operations I cannot update rotation with incorrect id - Given I am signed in as "operations" - When I send a "PATCH" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/incorrect-id" with body: - """json - { - "name": "New 2025-01" - } - """ - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As operations I cannot update rotation for non-existing operator - Given I am signed in as "operations" - When I send a "PATCH" request to "/api/v1/operator/2c219a12-26bf-4e11-8bbd-ed75b34881b0/rotation/00fb8d16-9f33-4c60-9c86-9722390b16a1" with body: - """json - { - "name": "New 2025-01" - } - """ - Then the response status should be 404 - And the response body should contain: - """json - { - "message": "Operator with given ID not found.", - "error": "Not Found", - "statusCode": 404 - } - """ - - Scenario: As operations I cannot update rotation for operator with incorrect id - Given I am signed in as "operations" - When I send a "PATCH" request to "/api/v1/operator/incorrect-id/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" with body: - """json - { - "name": "New 2025-01" - } - """ - Then the response status should be 400 - And the response body should contain: - """json - { - "message": "Validation failed (uuid v 4 is expected)", - "error": "Bad Request", - "statusCode": 400 - } - """ - - Scenario: As an unauthorized user I cannot remove a rotation - When I send a "PATCH" request to "/api/v1/operator/40b1b34e-aea1-4cec-acbe-f2bf97c06d7d/rotation/bd8f2d64-a647-42da-be63-c6589915e6c9" with body: - """json - { - "name": "New 2025-01" - } - """ - Then the response status should be 401 - And the response body should contain: - """json - { - "message": "Unauthorized", - "statusCode": 401 - } - """ diff --git a/features/user/user.create.feature b/features/user/user.create.feature index 7aec8da1..634d9303 100644 --- a/features/user/user.create.feature +++ b/features/user/user.create.feature @@ -21,7 +21,6 @@ Feature: Create user "email": "anna.doe@example.com", "role": "Admin", "currentFlightId": null, - "currentRotationId": null, "pilotLicenseId": null, "homeAirportId": null, "lastAirportId": null, @@ -53,7 +52,6 @@ Feature: Create user "role": "CabinCrew", "pilotLicenseId": "UK-12345", "currentFlightId": null, - "currentRotationId": null, "homeAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportUpdatedAt": null diff --git a/features/user/user.get.feature b/features/user/user.get.feature index 6ed54af1..a7301cdb 100644 --- a/features/user/user.get.feature +++ b/features/user/user.get.feature @@ -13,7 +13,6 @@ Feature: Get user "role": "Admin", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -33,7 +32,6 @@ Feature: Get user "role": "Admin", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null diff --git a/features/user/user.list.feature b/features/user/user.list.feature index 1bab48f9..5491440c 100644 --- a/features/user/user.list.feature +++ b/features/user/user.list.feature @@ -14,7 +14,6 @@ Feature: List users "role": "Admin", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -26,7 +25,6 @@ Feature: List users "role": "Operations", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -38,7 +36,6 @@ Feature: List users "role": "Operations", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -50,7 +47,6 @@ Feature: List users "role": "Operations", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -62,7 +58,6 @@ Feature: List users "role": "Operations", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -74,7 +69,6 @@ Feature: List users "role": "Operations", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -86,7 +80,6 @@ Feature: List users "role": "CabinCrew", "pilotLicenseId": "UK-31270", "currentFlightId": "b3899775-278e-4496-add1-21385a13d93e", - "currentRotationId": null, "homeAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportUpdatedAt": null @@ -98,7 +91,6 @@ Feature: List users "role": "CabinCrew", "pilotLicenseId": "UK-34560", "currentFlightId": "006f0754-1ed7-4ae1-9f91-fae2d446a6e7", - "currentRotationId": null, "homeAirportId": "f35c094a-bec5-4803-be32-bd80a14b441a", "lastAirportId": "f35c094a-bec5-4803-be32-bd80a14b441a", "lastAirportUpdatedAt": null @@ -110,7 +102,6 @@ Feature: List users "role": "CabinCrew", "pilotLicenseId": "UK-98540", "currentFlightId": "d4a25ef2-39cf-484c-af00-a548999e8699", - "currentRotationId": "c2e12afb-a712-45aa-9ba5-fec71868e59a", "homeAirportId": "616cbdd7-ccfc-4687-8cf6-1e7236435046", "lastAirportId": "616cbdd7-ccfc-4687-8cf6-1e7236435046", "lastAirportUpdatedAt": null @@ -170,7 +161,6 @@ Feature: List users "role": "CabinCrew", "pilotLicenseId": "UK-31270", "currentFlightId": "b3899775-278e-4496-add1-21385a13d93e", - "currentRotationId": null, "homeAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportUpdatedAt": null diff --git a/features/user/user.me.feature b/features/user/user.me.feature index b3a59205..a3685d86 100644 --- a/features/user/user.me.feature +++ b/features/user/user.me.feature @@ -13,7 +13,6 @@ Feature: Get current user "role": "Admin", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -33,7 +32,6 @@ Feature: Get current user "role": "Operations", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -53,7 +51,6 @@ Feature: Get current user "role": "CabinCrew", "pilotLicenseId": "UK-31270", "currentFlightId": "b3899775-278e-4496-add1-21385a13d93e", - "currentRotationId": null, "homeAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportUpdatedAt": null diff --git a/features/user/user.travel.create.feature b/features/user/user.travel.create.feature index 636c357c..93bf8876 100644 --- a/features/user/user.travel.create.feature +++ b/features/user/user.travel.create.feature @@ -65,7 +65,6 @@ Feature: Request a manual dead-head travel "role": "CabinCrew", "pilotLicenseId": "UK-31270", "currentFlightId": "b3899775-278e-4496-add1-21385a13d93e", - "currentRotationId": null, "homeAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportId": "f35c094a-bec5-4803-be32-bd80a14b441a", "lastAirportUpdatedAt": "@date('within 1 minute from now')" diff --git a/features/user/user.update.feature b/features/user/user.update.feature index d99fabfa..517a9915 100644 --- a/features/user/user.update.feature +++ b/features/user/user.update.feature @@ -20,7 +20,6 @@ Feature: Update user "role": "Admin", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -46,7 +45,6 @@ Feature: Update user "role": "Admin", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -72,7 +70,6 @@ Feature: Update user "role": "CabinCrew", "pilotLicenseId": null, "currentFlightId": null, - "currentRotationId": null, "homeAirportId": null, "lastAirportId": null, "lastAirportUpdatedAt": null @@ -208,7 +205,6 @@ Feature: Update user "role": "CabinCrew", "pilotLicenseId": "UK-12345", "currentFlightId": "b3899775-278e-4496-add1-21385a13d93e", - "currentRotationId": null, "homeAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportId": "3c721cc6-c653-4fad-be43-dc9d6a149383", "lastAirportUpdatedAt": null diff --git a/openspec/changes/archive/2026-07-22-remove-flight-rotation/.openspec.yaml b/openspec/changes/archive/2026-07-22-remove-flight-rotation/.openspec.yaml new file mode 100644 index 00000000..7250f8fb --- /dev/null +++ b/openspec/changes/archive/2026-07-22-remove-flight-rotation/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-22 diff --git a/openspec/changes/archive/2026-07-22-remove-flight-rotation/design.md b/openspec/changes/archive/2026-07-22-remove-flight-rotation/design.md new file mode 100644 index 00000000..03da7dd5 --- /dev/null +++ b/openspec/changes/archive/2026-07-22-remove-flight-rotation/design.md @@ -0,0 +1,66 @@ +## Context + +Flight rotation is implemented across three modules rather than a dedicated one: + +- **`operators`** owns the `Rotation` entity CRUD (5 actions, repository, 5 command/query handlers, an `AssertRotationExistsQuery` cross-module contract, model, request DTOs, error). +- **`flights`** owns flight↔rotation assignment (`AddFlightToRotationCommand`, `RemoveFlightFromRotationCommand`, `GetLastFlightInRotationQuery`, 2 actions, repository methods) and threads `rotationId` through every flight lifecycle event payload. +- **`users`** consumes rotation via `flight-lifecycle.listener.ts`, maintaining `User.currentRotation` on pilot check-in and flight close through `GetLastFlightInRotationQuery`. + +The Prisma schema has a `Rotation` model with relations from `Flight` (`rotationId`), `User` (`currentRotationId` + unique index, plus `rotations` back-relation) and `Operator` (`rotations`). Rotation is not captured in any active `openspec/specs/` capability, so its removal changes no documented requirement — the delta spec records the retired behavior only. + +The whole feature is being reimplemented differently; the user has confirmed all existing rotation code can be removed now. + +## Goals / Non-Goals + +**Goals:** + +- Remove every rotation touchpoint: source files, DI registrations, Prisma model + relations, event contract, seed data, tests, and docs. +- Leave the codebase compiling, linting, and passing the functional + unit suites with no dangling references. +- Drop the database objects cleanly via a new migration so a fresh `prisma migrate deploy` and `db push` both succeed. + +**Non-Goals:** + +- Designing or stubbing the new rotation feature — that is a separate future change. +- Preserving or migrating existing rotation data (it is discarded). +- Refactoring unrelated flight lifecycle behavior beyond dropping the `rotationId` field from event payloads. + +## Decisions + +**Decision: Delete rotation files outright rather than deprecate.** +The feature is being fully retired, so soft-deprecation adds no value. All files listed in the removal inventory are deleted, and their entries removed from `operators.module.ts` and `flights.module.ts` `controllers`/`providers` arrays (handlers and actions are not auto-discovered, so both the import and the array entry must go). + +**Decision: Drop `rotationId` from the shared flight event payload, not just null it.** +`FlightEventPayload.rotationId` is threaded through ~23 lifecycle commands plus `off-block-delay.listener.ts` and `position.service.ts`. Rather than leave a permanently-null field, remove the field from the payload type and delete every `rotationId: ...` assignment. This keeps the event contract honest and avoids a vestigial field the new feature would have to reason about. Alternative considered: keep the field nullable and stop populating it — rejected because it leaves dead surface area in a cross-module contract. + +**Decision: Remove the two rotation domain events and their subscribers.** +`FlightWasAddedToRotation` / `FlightWasRemovedFromRotation` enum members and event classes are deleted, along with the three `@OnEvent` subscriptions in the flights module (`events.repository`, `broadcast-flight-event.listener`, `flight-cache.listener`). The `users` lifecycle listener loses its rotation branches (check-in set, close clear) and its `GetLastFlightInRotationQuery` import. + +**Decision: One new Prisma migration drops all rotation DB objects.** +Add a migration that drops FKs (`flight_rotationId_fkey`, `rotation_pilotId_fkey`, `rotation_operatorId_fkey`, `user_currentRotationId_fkey`), the unique index `user_currentRotationId_key`, the columns `flight.rotationId` and `user.currentRotationId`, and the `rotation` table. Remove the `Rotation` model and all relation fields from `schema.prisma` so the generated client (`prisma/client/`) no longer emits `Rotation` types. Per project practice, apply to the dev DB with `prisma db push` (migrate deploy P3005-fails on the existing DB); the migration file exists for fresh deploys. Alternative considered: leave columns in place — rejected because dangling FK columns and a `Rotation` model with no code is exactly the clutter this change removes. + +**Decision: Delete dedicated rotation features; edit shared feature/unit fixtures.** +The 7 rotation-only Cucumber features are deleted. The ~25 remaining flight/user feature files and 6 unit-test spec fixtures that assert `rotationId`/`rotation`/`currentRotationId` are edited to drop those keys (full-body assertions must match exact keys, per the deep-compare contract). Scenarios that exist only to exercise the rotation flow (e.g. "check-in that starts rotation", "close last flight in rotation") are removed rather than retained without their assertions. + +## Risks / Trade-offs + +- **Full-body assertion mismatches** → The Cucumber deep-compare matches exact key sets, so any missed `rotationId`/`rotation`/`currentRotationId` key breaks a scenario. Mitigation: statically reconcile every edited feature against the new payloads before running the suite (align tests and seed first), then run the functional suite. +- **Generated Prisma client drift** → `prisma/client/` files still reference `Rotation` until regenerated. Mitigation: run `prisma generate` after editing the schema; do not hand-edit generated files. +- **Missed DI entry leaves a broken module** → Removing an import but not its `providers`/`controllers` array entry (or vice versa) fails to compile. Mitigation: build inside the container after edits; treat a clean `npm run build` as the gate. +- **Dead repository method** → `flights.repository.ts:getRotationIdByFlightId()` has no callers; delete it with the rest to avoid leaving orphaned rotation code. +- **Breaking API change** → Consumers of the removed endpoints/fields break. Accepted: the feature is being retired deliberately and the replacement is out of scope here. + +## Migration Plan + +1. Edit `schema.prisma` (drop model + relations), add the drop migration, run `prisma generate` + `prisma db push` in the container. +2. Delete rotation source files and remove their DI registrations. +3. Strip `rotationId` from the event payload type and all producers; remove the two events and their subscribers; rewrite the users lifecycle listener. +4. Remove rotation seed data and update user/flight seed fixtures. +5. Delete rotation features; edit remaining feature files and unit fixtures. +6. Update `CLAUDE.md`, `README.md`, Swagger description. +7. Build, lint, run unit + functional suites. + +Rollback: revert the branch; no data migration to reverse (rotation data is discarded on the forward migration only). + +## Open Questions + +None — the user has confirmed all rotation code can be removed now, and the replacement design is out of scope for this change. diff --git a/openspec/changes/archive/2026-07-22-remove-flight-rotation/proposal.md b/openspec/changes/archive/2026-07-22-remove-flight-rotation/proposal.md new file mode 100644 index 00000000..2232a0f0 --- /dev/null +++ b/openspec/changes/archive/2026-07-22-remove-flight-rotation/proposal.md @@ -0,0 +1,32 @@ +## Why + +Flight rotation (grouping flights under a named rotation, tracking a pilot's "current rotation") is going to be reimplemented from scratch in a fundamentally different way. The existing implementation is spread across the `operators`, `flights`, and `users` modules and threads a `rotationId` through every flight lifecycle event, adding weight to code we no longer want to keep. Removing it now clears the ground for the new design and stops us maintaining a feature that is about to be discarded. + +## What Changes + +- **BREAKING** Remove the operator rotation CRUD endpoints: `POST/GET/PATCH/DELETE /api/v1/operator/:operatorId/rotation[/:rotationId]`. +- **BREAKING** Remove the flight↔rotation assignment endpoints: `POST/DELETE /api/v1/flight/:flightId/rotation/:rotationId`. +- **BREAKING** Remove `rotation` and `rotationId` from the flight API payloads, `currentRotationId` from the user payload, and `rotationId` from flight event payloads. +- **BREAKING** Drop the domain events `flight.added-to-rotation` and `flight.removed-from-rotation`. +- Stop maintaining `User.currentRotation` on pilot check-in / flight close (the users module lifecycle listener no longer reacts to rotation). +- Remove the `Rotation` Prisma model and every relation to it (`Flight.rotationId`, `User.currentRotationId` + its unique index, `User.rotations`, `Operator.rotations`), via a new migration that drops the table, columns, and constraints. +- Delete all rotation source files (models, errors, requests, repository methods, commands, queries, actions, assert handler) and their DI registrations in `operators.module.ts` and `flights.module.ts`. +- Remove rotation seed data and update user/flight seed fixtures to drop the dropped fields. +- Delete the dedicated rotation Cucumber features and strip rotation assertions from the remaining flight/user feature files and unit test fixtures. +- Remove stale rotation mentions from `CLAUDE.md`, `README.md`, and the Swagger API description. + +## Capabilities + +### New Capabilities + + +### Modified Capabilities + + +## Impact + +- **API (breaking):** removes the operator-rotation and flight-rotation endpoints; removes `rotation`/`rotationId`/`currentRotationId` fields from flight, user, and flight-event responses. +- **Database (breaking):** drops the `rotation` table plus the `flight.rotationId` and `user.currentRotationId` columns and their foreign keys / unique index. Existing rotation data is discarded (acceptable — the feature is being retired). +- **Domain events:** removes two `FlightEvent` enum members and their event classes; strips `rotationId` from the shared flight event payload consumed across the `flights` and `users` modules. +- **Modules touched:** `operators`, `flights`, `users`, plus `core/domain/events` and the Swagger config. +- **Tests:** deletes 7 rotation feature files and edits ~25 remaining feature files and 6 unit-test spec fixtures that assert rotation fields. diff --git a/openspec/changes/archive/2026-07-22-remove-flight-rotation/specs/flight-rotation/spec.md b/openspec/changes/archive/2026-07-22-remove-flight-rotation/specs/flight-rotation/spec.md new file mode 100644 index 00000000..9ecb1637 --- /dev/null +++ b/openspec/changes/archive/2026-07-22-remove-flight-rotation/specs/flight-rotation/spec.md @@ -0,0 +1,40 @@ +## REMOVED Requirements + +### Requirement: Operator rotation management + +**Reason**: The rotation feature is being retired and reimplemented from scratch in a fundamentally different way. Its current shape (operator-owned named rotations threaded through flight and user state) does not fit the new design, so all existing behavior is removed rather than migrated. + +**Migration**: None. Existing rotation records are discarded when the `rotation` table is dropped. Any client relying on `/api/v1/operator/:operatorId/rotation[/:rotationId]` must stop calling these endpoints; the replacement will be documented when the new rotation feature ships. + +The system SHALL allow operators to create, read, update, list, and delete named rotations that reference a pilot. + +#### Scenario: Operator rotation CRUD + +- **WHEN** a client calls any of `POST/GET/PATCH/DELETE /api/v1/operator/:operatorId/rotation[/:rotationId]` +- **THEN** the endpoint no longer exists and the behavior is removed entirely + +### Requirement: Flight rotation assignment + +**Reason**: Assigning a flight to a rotation depended on the operator rotation entity being removed above, and the assignment model changes in the new design. + +**Migration**: None. The `flight.rotationId` column is dropped and `rotation`/`rotationId` no longer appear in flight payloads. Clients must stop calling `/api/v1/flight/:flightId/rotation/:rotationId`. + +The system SHALL allow a flight to be assigned to and removed from a rotation, exposing the linked rotation on the flight resource and emitting `flight.added-to-rotation` / `flight.removed-from-rotation` domain events. + +#### Scenario: Flight rotation assignment + +- **WHEN** a client calls `POST/DELETE /api/v1/flight/:flightId/rotation/:rotationId` +- **THEN** the endpoints no longer exist, `rotation`/`rotationId` are absent from flight and flight-event payloads, and the two rotation domain events are no longer emitted + +### Requirement: Pilot current rotation tracking + +**Reason**: Automatically tracking a pilot's current rotation across check-in and flight close is superseded by the new design. + +**Migration**: None. The `user.currentRotationId` column and unique index are dropped and `currentRotationId` no longer appears in the user payload. + +The system SHALL set a pilot's current rotation when they check in to a flight belonging to a rotation, and clear it when the last flight in that rotation is closed. + +#### Scenario: Pilot current rotation tracking + +- **WHEN** a pilot checks in to a flight or the last flight in a rotation is closed +- **THEN** no current-rotation state is maintained and `currentRotationId` is absent from the user payload diff --git a/openspec/changes/archive/2026-07-22-remove-flight-rotation/tasks.md b/openspec/changes/archive/2026-07-22-remove-flight-rotation/tasks.md new file mode 100644 index 00000000..3a08924f --- /dev/null +++ b/openspec/changes/archive/2026-07-22-remove-flight-rotation/tasks.md @@ -0,0 +1,56 @@ +## 1. Database & Prisma schema + +- [x] 1.1 Remove the `Rotation` model from `prisma/schema.prisma`. +- [x] 1.2 Remove `Flight.rotationId` field and the `rotation` relation from the `Flight` model. +- [x] 1.3 Remove `User.currentRotationId` (+ `@unique`), the `currentRotation` relation, and the `rotations` back-relation from the `User` model. +- [x] 1.4 Remove the `rotations` back-relation from the `Operator` model. +- [x] 1.5 Add a new Prisma migration that drops FKs (`flight_rotationId_fkey`, `rotation_pilotId_fkey`, `rotation_operatorId_fkey`, `user_currentRotationId_fkey`), the unique index `user_currentRotationId_key`, the columns `flight.rotationId` and `user.currentRotationId`, and the `rotation` table. +- [x] 1.6 Run `docker compose exec app npx prisma generate` and `docker compose exec app npx prisma db push` so the dev DB and generated client (`prisma/client/`) drop all `Rotation` types. + +## 2. Delete operators-module rotation code + +- [x] 2.1 Delete `src/modules/operators/model/rotation.model.ts` and `src/modules/operators/model/error/rotation.error.ts`. +- [x] 2.2 Delete `src/modules/operators/infra/http/request/rotation.request.ts`. +- [x] 2.3 Delete `src/modules/operators/infra/database/repository/rotations.repository.ts`. +- [x] 2.4 Delete the 5 command/query handlers under `application/command/rotation/` and `application/query/rotation/`, plus `application/assert/assert-rotation-exists.query.ts`. +- [x] 2.5 Delete the 5 actions under `infra/http/action/rotation/`. +- [x] 2.6 Remove all rotation imports, `controllers` entries, and `providers` entries from `src/modules/operators/operators.module.ts`. + +## 3. Delete flights-module rotation code + +- [x] 3.1 Delete `application/command/rotation/add-flight-to-rotation.command.ts` and `remove-flight-from-rotation.command.ts`. +- [x] 3.2 Delete `application/query/rotation/get-last-flight-in-rotation.query.ts`. +- [x] 3.3 Delete the 2 actions under `infra/http/action/rotation/`. +- [x] 3.4 Remove all rotation imports, `controllers` entries, and `providers` entries from `src/modules/flights/flights.module.ts`. +- [x] 3.5 Remove the flight-rotation error classes (`FlightAlreadyAssignedToRotationError`, `FlightRotationNotMatchingError`, `FlightIncorrectStateToChangeRotationError`) from `src/modules/flights/model/error/flight.error.ts`. +- [x] 3.6 Remove rotation repository methods from `flights.repository.ts`: `getRotationIdByFlightId` (dead code), `addRotationForFlight`, `removeRotationForFlight`, `getLastFlightIdInRotation`, and the `rotationId: true` select entry. + +## 4. Flight & user models / DTOs + +- [x] 4.1 Remove the `Rotation` import and the `rotation` / `rotationId` properties from `src/modules/flights/model/flight.model.ts`. +- [x] 4.2 Remove `rotationId` from `src/modules/flights/model/event.model.ts` and the `OmitType` lists in `flight.dto.ts` (`CreateFlightRequest`, `GetFlightResponse`) and `event.dto.ts` (`FlightEventResponse`). +- [x] 4.3 Remove `currentRotationId` from `src/modules/users/model/user.model.ts`, and remove `setCurrentRotation()` + the `currentRotationId` mapping from `users.repository.ts`. + +## 5. Domain events & listeners + +- [x] 5.1 In `src/core/domain/events/dto/flight.events.ts`: remove the `FlightWasAddedToRotation` / `FlightWasRemovedFromRotation` enum members, the two event classes, and the `rotationId` field on `FlightEventPayload`. +- [x] 5.2 Remove `rotationId: ...` from every flight lifecycle command payload (~23 commands under `application/command/`, incl. `diversion/` and `emergency/`), `off-block-delay.listener.ts`, and `position.service.ts`. +- [x] 5.3 Remove the rotation `@OnEvent` subscriptions from `events.repository.ts`, `broadcast-flight-event.listener.ts`, and `flight-cache.listener.ts`. +- [x] 5.4 In `src/modules/users/application/event/external/flight-lifecycle.listener.ts`: remove the `GetLastFlightInRotationQuery` import and the rotation branches in `onPilotCheckedIn` and `onFlightWasClosed`. + +## 6. Seed data + +- [x] 6.1 Delete `prisma/seed/resource/rotations.seed.ts` and remove the `loadRotations` import + call from `prisma/seed/load-resources.ts`. +- [x] 6.2 Remove `rotationId` / `currentRotationId` fields (and their comment banners) from `prisma/seed/resource/flights.seed.ts` and `prisma/seed/resource/users.seed.ts`. + +## 7. Tests + +- [x] 7.1 Delete the dedicated rotation feature dirs: `features/flight/rotation/` and `features/operator/rotation/`. +- [x] 7.2 Remove `rotationId` / `rotation` / `currentRotationId` assertions from the remaining flight/user feature files, and delete rotation-only scenarios (e.g. "check-in that starts rotation", "close last flight in rotation"). +- [x] 7.3 Remove `rotationId: null` from the unit-test fixtures in `report-arrival/off-block/takeoff.command.spec.ts` and `detect-arrival/off-block/takeoff.listener.spec.ts`. + +## 8. Docs & final verification + +- [x] 8.1 Remove rotation mentions from `CLAUDE.md` (functional-test domains), `README.md`, and the Swagger API description in `swagger.config.ts`. +- [x] 8.2 Run `docker compose exec app npm run build` and `npm run lint` — both clean, no dangling rotation references. +- [x] 8.3 Run `docker compose exec app npm test` (unit) and `npm run test:functional` (Cucumber) — all green. diff --git a/package-lock.json b/package-lock.json index f1c854db..74c1e17d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "flight-tracker-api", - "version": "2.43.0", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "flight-tracker-api", - "version": "2.43.0", + "version": "3.0.0", "license": "Unlicense", "dependencies": { "@nestjs/cache-manager": "^3.1.2", @@ -987,33 +987,33 @@ "license": "MIT" }, "node_modules/@electric-sql/pglite": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", - "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz", + "integrity": "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", - "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.3.tgz", + "integrity": "sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==", "devOptional": true, "license": "Apache-2.0", "bin": { "pglite-server": "dist/scripts/server.js" }, "peerDependencies": { - "@electric-sql/pglite": "0.4.1" + "@electric-sql/pglite": "0.4.3" } }, "node_modules/@electric-sql/pglite-tools": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", - "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.3.tgz", + "integrity": "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==", "devOptional": true, "license": "Apache-2.0", "peerDependencies": { - "@electric-sql/pglite": "0.4.1" + "@electric-sql/pglite": "0.4.3" } }, "node_modules/@emnapi/core": { @@ -1178,19 +1178,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -2257,13 +2244,6 @@ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", "license": "MIT" }, - "node_modules/@kurkle/color": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", - "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", - "devOptional": true, - "license": "MIT" - }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -2894,9 +2874,9 @@ "license": "Apache-2.0" }, "node_modules/@prisma/config": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", - "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.9.0.tgz", + "integrity": "sha512-CsoK2mhl0u+N4/8V+XroQMOUNIic4isqD+E2HBG8l1yGEKo62CFDu3FHo0FdwItjl6XkW+omA1STSzeN1DAXlg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -2913,23 +2893,21 @@ "license": "Apache-2.0" }, "node_modules/@prisma/dev": { - "version": "0.24.3", - "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", - "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "version": "0.24.14", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.14.tgz", + "integrity": "sha512-NhFO49O2JPTdzYiLHvceQn/HiwmcKF/iGV39ko3CpYsoGqS3rz3ko6gzuxFSIeHNwNJeuNcDexyyGeTO3DW80A==", "devOptional": true, "license": "ISC", "dependencies": { - "@electric-sql/pglite": "0.4.1", - "@electric-sql/pglite-socket": "0.1.1", - "@electric-sql/pglite-tools": "0.3.1", - "@hono/node-server": "1.19.11", + "@electric-sql/pglite": "0.4.3", + "@electric-sql/pglite-socket": "0.1.3", + "@electric-sql/pglite-tools": "0.3.3", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", - "@prisma/streams-local": "0.1.2", + "@prisma/streams-local": "0.1.11", + "find-my-way": "9.6.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", - "hono": "^4.12.8", - "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", @@ -2948,56 +2926,70 @@ } }, "node_modules/@prisma/engines": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", - "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.9.0.tgz", + "integrity": "sha512-lDWJp/pgSWCLfYsupmmNo96jfsbQnH1yjia8XVM2Kh8nRZhD0bQU2jCHuy3ZTPMLR3apRD3k145ybENalAYjYw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.8.0", - "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", - "@prisma/fetch-engine": "7.8.0", - "@prisma/get-platform": "7.8.0" + "@prisma/debug": "7.9.0", + "@prisma/engines-version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", + "@prisma/fetch-engine": "7.9.0", + "@prisma/get-platform": "7.9.0" } }, "node_modules/@prisma/engines-version": { - "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", - "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", + "version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad.tgz", + "integrity": "sha512-2BsPPFksz3CQUXG6af3rVCtJKg6+JJGJTtfgu2fU8DdXhOfkBjulCq8mwybCd6ge0/jhZq2kOtLAbmUDMyI1nA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/debug": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.0.tgz", + "integrity": "sha512-i0KdVQuKUE6N9NloHs+sUNAk2c9svR3myBndQbA3BoeoArsSpwtNgTdHZL+wBtCLCcdS2OOC/PKhgTe36jkF5A==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", - "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.0.tgz", + "integrity": "sha512-4awv6ATdgrHdLms0XKikCyfArn8BrUHZfqg0mtCKrI4+WJe24nmpsdwsypM9ozd03wa846AngY+zSbnngkMrXQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.8.0" + "@prisma/debug": "7.9.0" } }, "node_modules/@prisma/fetch-engine": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", - "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.9.0.tgz", + "integrity": "sha512-F0XlIgjbE3EywRVR/HpCerNI/dxo40vK66tHcWpsWYwH/Jk9+FsICEzATeMsZ7bdnpZz93hkD4sAb5rKLsCCpA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.8.0", - "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", - "@prisma/get-platform": "7.8.0" + "@prisma/debug": "7.9.0", + "@prisma/engines-version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", + "@prisma/get-platform": "7.9.0" } }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/debug": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.0.tgz", + "integrity": "sha512-i0KdVQuKUE6N9NloHs+sUNAk2c9svR3myBndQbA3BoeoArsSpwtNgTdHZL+wBtCLCcdS2OOC/PKhgTe36jkF5A==", + "devOptional": true, + "license": "Apache-2.0" + }, "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", - "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.0.tgz", + "integrity": "sha512-4awv6ATdgrHdLms0XKikCyfArn8BrUHZfqg0mtCKrI4+WJe24nmpsdwsypM9ozd03wa846AngY+zSbnngkMrXQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.8.0" + "@prisma/debug": "7.9.0" } }, "node_modules/@prisma/get-platform": { @@ -3032,9 +3024,9 @@ "license": "Apache-2.0" }, "node_modules/@prisma/streams-local": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", - "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.11.tgz", + "integrity": "sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -3044,19 +3036,28 @@ "proper-lockfile": "^4.1.2" }, "engines": { - "bun": ">=1.3.6", + "bun": ">=1.2.0", "node": ">=22.0.0" } }, "node_modules/@prisma/studio-core": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", - "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.33.0.tgz", + "integrity": "sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { "@radix-ui/react-toggle": "1.1.10", - "chart.js": "4.5.1" + "@visx/curve": "4.0.1-alpha.0", + "@visx/event": "4.0.1-alpha.0", + "@visx/grid": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/responsive": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "d3-array": "3.2.4", + "d3-shape": "3.2.0", + "elkjs": "0.11.1" }, "engines": { "node": "^20.19 || ^22.12 || >=24.0", @@ -3419,6 +3420,95 @@ "@types/node": "*" } }, + "node_modules/@types/d3-array": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", + "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", + "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -3480,6 +3570,13 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -3542,6 +3639,13 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/luxon": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", @@ -3596,12 +3700,11 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4179,6 +4282,157 @@ "win32" ] }, + "node_modules/@visx/curve": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz", + "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/event": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz", + "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/point": "4.0.1-alpha.0" + } + }, + "node_modules/@visx/grid": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz", + "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/point": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/group": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz", + "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/point": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz", + "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@visx/responsive": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz", + "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/scale": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz", + "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/shape": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz", + "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/vendor": "4.0.0-alpha.0", + "classnames": "^2.3.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/vendor": { + "version": "4.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz", + "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==", + "devOptional": true, + "license": "MIT and ISC", + "dependencies": { + "@types/d3-array": "3.0.3", + "@types/d3-color": "3.1.0", + "@types/d3-delaunay": "6.0.1", + "@types/d3-format": "3.0.1", + "@types/d3-geo": "3.1.0", + "@types/d3-interpolate": "3.0.1", + "@types/d3-path": "3.1.1", + "@types/d3-scale": "4.0.2", + "@types/d3-shape": "3.1.7", + "@types/d3-time": "3.0.0", + "@types/d3-time-format": "2.1.0", + "d3-array": "3.2.1", + "d3-color": "3.1.0", + "d3-delaunay": "6.0.2", + "d3-format": "3.1.0", + "d3-geo": "3.1.0", + "d3-interpolate": "3.0.1", + "d3-path": "3.1.0", + "d3-scale": "4.0.2", + "d3-shape": "3.2.0", + "d3-time": "3.1.0", + "d3-time-format": "4.1.0", + "internmap": "2.0.3" + } + }, + "node_modules/@visx/vendor/node_modules/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -4818,9 +5072,9 @@ } }, "node_modules/better-result": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz", - "integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz", + "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==", "devOptional": true, "license": "MIT" }, @@ -5227,19 +5481,6 @@ "dev": true, "license": "MIT" }, - "node_modules/chart.js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", - "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=8" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -5306,6 +5547,13 @@ "validator": "^13.15.22" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "devOptional": true, + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5658,8 +5906,145 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } }, "node_modules/debug": { "version": "4.4.3", @@ -5740,6 +6125,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5889,6 +6284,13 @@ "dev": true, "license": "ISC" }, + "node_modules/elkjs": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz", + "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==", + "devOptional": true, + "license": "EPL-2.0" + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -6509,9 +6911,9 @@ } }, "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", "devOptional": true, "license": "MIT" }, @@ -6555,6 +6957,13 @@ ], "license": "MIT" }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6583,6 +6992,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -6590,9 +7009,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "devOptional": true, "funding": [ { @@ -6725,6 +7144,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7061,9 +7495,9 @@ } }, "node_modules/giget": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", - "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.0.tgz", + "integrity": "sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==", "devOptional": true, "license": "MIT", "bin": { @@ -7157,9 +7591,9 @@ "license": "ISC" }, "node_modules/grammex": { - "version": "3.1.12", - "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", - "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz", + "integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==", "devOptional": true, "license": "MIT" }, @@ -7286,16 +7720,6 @@ "node": ">=18.0.0" } }, - "node_modules/hono": { - "version": "4.12.26", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", - "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, "node_modules/hookified": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", @@ -7352,13 +7776,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-status-codes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", - "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", - "devOptional": true, - "license": "MIT" - }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -7527,6 +7944,16 @@ "node": ">=10" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -10044,17 +10471,17 @@ } }, "node_modules/prisma": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", - "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.9.0.tgz", + "integrity": "sha512-isQTJEK4pyOlAVzm6kBUDjzgdsgs0A/snpB38ycTHeOHW34qfepP+ClQltgDXqjZBnXALhEtE4duh9L3tN5fHw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "7.8.0", - "@prisma/dev": "0.24.3", - "@prisma/engines": "7.8.0", - "@prisma/studio-core": "0.27.3", + "@prisma/config": "7.9.0", + "@prisma/dev": "0.24.14", + "@prisma/engines": "7.9.0", + "@prisma/studio-core": "0.33.0", "mysql2": "3.15.3", "postgres": "3.4.7" }, @@ -10214,9 +10641,9 @@ } }, "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "devOptional": true, "license": "MIT", "peer": true, @@ -10225,9 +10652,9 @@ } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "devOptional": true, "license": "MIT", "peer": true, @@ -10235,7 +10662,7 @@ "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.8" } }, "node_modules/react-is-18": { @@ -10503,6 +10930,16 @@ "dev": true, "license": "ISC" }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -10513,6 +10950,13 @@ "node": ">= 4" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "devOptional": true, + "license": "Unlicense" + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -10558,6 +11002,29 @@ ], "license": "MIT" }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", diff --git a/package.json b/package.json index cc15f7db..3874f28a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flight-tracker-api", - "version": "2.43.0", + "version": "3.0.0", "private": true, "homepage": "https://flights.barcz.me", "author": "oskarbarcz", diff --git a/prisma/migrations/20260722120000_remove_flight_rotation/migration.sql b/prisma/migrations/20260722120000_remove_flight_rotation/migration.sql new file mode 100644 index 00000000..c9ac9c8f --- /dev/null +++ b/prisma/migrations/20260722120000_remove_flight_rotation/migration.sql @@ -0,0 +1,23 @@ +-- DropForeignKey +ALTER TABLE "flight" DROP CONSTRAINT "flight_rotationId_fkey"; + +-- DropForeignKey +ALTER TABLE "rotation" DROP CONSTRAINT "rotation_operatorId_fkey"; + +-- DropForeignKey +ALTER TABLE "rotation" DROP CONSTRAINT "rotation_pilotId_fkey"; + +-- DropForeignKey +ALTER TABLE "user" DROP CONSTRAINT "user_currentRotationId_fkey"; + +-- DropIndex +DROP INDEX "user_currentRotationId_key"; + +-- AlterTable +ALTER TABLE "flight" DROP COLUMN "rotationId"; + +-- AlterTable +ALTER TABLE "user" DROP COLUMN "currentRotationId"; + +-- DropTable +DROP TABLE "rotation"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dd6c2cb1..d9129c9a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -178,7 +178,6 @@ model Operator { aircrafts Aircraft[] flights Flight[] - rotations Rotation[] crew Crew[] @@map("operator") @@ -194,7 +193,6 @@ model Flight { timesheet Json loadsheets Json @default("{\"final\": null, \"preliminary\": null}") operatorId String @db.Uuid - rotationId String? @db.Uuid captainId String? @db.Uuid source FlightSource @default(manual) tracking FlightTracking @default(private) @@ -219,7 +217,6 @@ model Flight { isPathAvailable Boolean @default(false) operator Operator @relation(fields: [operatorId], references: [id]) - rotation Rotation? @relation(fields: [rotationId], references: [id]) captain User? @relation("FlightCaptain", fields: [captainId], references: [id]) aircraft Aircraft @relation(fields: [aircraftId], references: [id]) airports AirportsOnFlights[] @@ -343,7 +340,6 @@ model User { role UserRole pilotLicenseId String? @db.VarChar(8) currentFlightId String? @unique @db.Uuid - currentRotationId String? @unique @db.Uuid simbriefUserId String? totalFlightTime Int @default(0) totalGreatCircleDistance Int @default(0) @@ -354,10 +350,8 @@ model User { homeAirport Airport? @relation("UserHomeAirport", fields: [homeAirportId], references: [id], onDelete: SetNull) lastAirport Airport? @relation("UserLastAirport", fields: [lastAirportId], references: [id], onDelete: SetNull) - rotations Rotation[] jwtRefreshToken JwtRefreshToken[] currentFlight Flight? @relation("UserCurrentFlight", fields: [currentFlightId], references: [id]) - currentRotation Rotation? @relation(fields: [currentRotationId], references: [id], name: "CurrentRotation") flightEvent FlightEvent[] diversion Diversion[] emergenciesReported EmergencyDeclaration[] @relation("EmergencyReporter") @@ -458,21 +452,6 @@ model AircraftReposition { @@map("aircraft_reposition") } -model Rotation { - id String @id @default(uuid()) @db.Uuid - name String - pilotId String @db.Uuid - createdAt DateTime @default(now()) - updatedAt DateTime? - operatorId String? @db.Uuid - - flights Flight[] - pilot User @relation(fields: [pilotId], references: [id]) - currentForUser User? @relation("CurrentRotation") - operator Operator? @relation(fields: [operatorId], references: [id]) - - @@map("rotation") -} model Crew { id String @id @default(uuid()) @db.Uuid diff --git a/prisma/seed/load-resources.ts b/prisma/seed/load-resources.ts index 1d607978..2859a6f8 100644 --- a/prisma/seed/load-resources.ts +++ b/prisma/seed/load-resources.ts @@ -4,7 +4,6 @@ import { loadOperators } from './resource/operators.seed'; import { loadAirports } from './resource/airports.seed'; import { loadFlights } from './resource/flights.seed'; import { loadUsers } from './resource/users.seed'; -import { loadRotations } from './resource/rotations.seed'; import { loadTerminals } from './resource/terminals.seed'; import { loadParkingPositions } from './resource/parking-positions.seed'; import { loadGates } from './resource/gates.seed'; @@ -32,7 +31,6 @@ export async function loadResources() { await loadCrew(tx); await loadAircraft(tx); await loadUsers(tx); - await loadRotations(tx); await loadFlights(tx); await loadFlightCrew(tx); await loadDelay(tx); diff --git a/prisma/seed/resource/aircrafts.seed.ts b/prisma/seed/resource/aircrafts.seed.ts index 7ed9abfb..667a11ec 100644 --- a/prisma/seed/resource/aircrafts.seed.ts +++ b/prisma/seed/resource/aircrafts.seed.ts @@ -84,7 +84,7 @@ export async function loadAircraft( id: string, type: string, registration: string, - selcal: string, + selcal: string | null, livery: string, operatorId: string, baseAirportId: string, diff --git a/prisma/seed/resource/flights.seed.ts b/prisma/seed/resource/flights.seed.ts index 3048f904..ac89605c 100644 --- a/prisma/seed/resource/flights.seed.ts +++ b/prisma/seed/resource/flights.seed.ts @@ -3982,7 +3982,6 @@ async function loadAAL4919(tx: Prisma.TransactionClient): Promise { /** * DLH40 | 48760636-9520-4863-b32f-f3618556feb7 - * Rotation 2025-01 * Boston Frankfurt (EDDF) -> New York JFK (KJFK) * status: Closed */ @@ -4001,7 +4000,6 @@ async function loadDLH40(tx: Prisma.TransactionClient): Promise { status: FlightStatus.Closed, aircraftId: 'b84e4c67-7565-4846-84c4-ab8215308fbd', // D-AIMD A339 operatorId: '40b1b34e-aea1-4cec-acbe-f2bf97c06d7d', // Lufthansa, - rotationId: 'bd8f2d64-a647-42da-be63-c6589915e6c9', // 2025-01 timesheet: { scheduled: { offBlockTime: new Date('2025-01-01 17:40'), @@ -4223,7 +4221,6 @@ async function loadDLH40(tx: Prisma.TransactionClient): Promise { /** * DLH41 | e8e17e59-67d7-4a6c-a0bd-425ffa6bed66 - * Rotation 2025-01 * New York JFK (KJFK) -> Frankfurt (EDDF) * status: Created */ @@ -4241,7 +4238,6 @@ async function loadDLH41(tx: Prisma.TransactionClient): Promise { status: FlightStatus.Created, aircraftId: 'becc1596-dfa0-452b-81ec-3f1f2fa0dce2', // D-AIME A339 operatorId: '40b1b34e-aea1-4cec-acbe-f2bf97c06d7d', // Lufthansa, - rotationId: 'bd8f2d64-a647-42da-be63-c6589915e6c9', // 2025-01 timesheet: { scheduled: { offBlockTime: new Date('2025-01-02 04:00'), @@ -4350,7 +4346,6 @@ async function loadDLH41(tx: Prisma.TransactionClient): Promise { /** * DLH42 | 006f0754-1ed7-4ae1-9f91-fae2d446a6e7 - * Rotation 2025-02 * Frankfurt (EDDF) -> New York JFK (KJFK) * status: Ready */ @@ -4368,7 +4363,6 @@ async function loadDLH42(tx: Prisma.TransactionClient): Promise { status: FlightStatus.Ready, aircraftId: '9f5da1a4-f09e-4961-8299-82d688337d1f', // A330 operatorId: '40b1b34e-aea1-4cec-acbe-f2bf97c06d7d', // Lufthansa, - rotationId: '4cb9b5a8-7cac-4526-a0f7-f158fd14e9d1', // 2025-02 timesheet: { scheduled: { offBlockTime: new Date('2025-01-02 17:40'), @@ -4522,7 +4516,6 @@ async function loadDLH42(tx: Prisma.TransactionClient): Promise { /** * DLH43 | d4a25ef2-39cf-484c-af00-a548999e8699 - * Rotation 2025-03 * New York JFK (KJFK) -> Frankfurt (EDDF) * status: Offboarding finished */ @@ -4539,7 +4532,6 @@ async function loadDLH43(tx: Prisma.TransactionClient): Promise { status: FlightStatus.OffboardingFinished, aircraftId: 'a9b9205d-53b1-4eec-bb24-548a12159997', // D-AIMF A339 operatorId: '40b1b34e-aea1-4cec-acbe-f2bf97c06d7d', // Lufthansa, - rotationId: 'c2e12afb-a712-45aa-9ba5-fec71868e59a', // 2025-03 timesheet: { scheduled: { offBlockTime: new Date('2025-01-03 04:00'), @@ -4662,7 +4654,6 @@ async function loadDLH43(tx: Prisma.TransactionClient): Promise { where: { id: '629be07f-5e65-429a-9d69-d34b99185f50' }, // Michael Doe user data: { currentFlightId: flight.id, - currentRotationId: 'c2e12afb-a712-45aa-9ba5-fec71868e59a', }, }); diff --git a/prisma/seed/resource/rotations.seed.ts b/prisma/seed/resource/rotations.seed.ts deleted file mode 100644 index e00ac0d4..00000000 --- a/prisma/seed/resource/rotations.seed.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Prisma } from '../../client/client'; - -async function load012025(tx: Prisma.TransactionClient): Promise { - await tx.rotation.create({ - data: { - id: 'bd8f2d64-a647-42da-be63-c6589915e6c9', - name: '2025-01', - // Rick Doe user - pilotId: 'fcf6f4bc-290d-43a9-843c-409cd47e143d', - operatorId: '40b1b34e-aea1-4cec-acbe-f2bf97c06d7d', - createdAt: new Date('2025-01-01T00:00:00.000Z'), - }, - }); -} - -async function load022025(tx: Prisma.TransactionClient): Promise { - await tx.rotation.create({ - data: { - id: '4cb9b5a8-7cac-4526-a0f7-f158fd14e9d1', - name: '2025-02', - // Alan Doe user - pilotId: '725f5df2-0c78-4fe8-89a2-52566c89cf7f', - operatorId: '40b1b34e-aea1-4cec-acbe-f2bf97c06d7d', - createdAt: new Date('2025-01-02T00:00:00.000Z'), - }, - }); -} - -async function load032025(tx: Prisma.TransactionClient): Promise { - await tx.rotation.create({ - data: { - id: 'c2e12afb-a712-45aa-9ba5-fec71868e59a', - name: '2025-03', - // Michael Doe user - pilotId: '629be07f-5e65-429a-9d69-d34b99185f50', - operatorId: '40b1b34e-aea1-4cec-acbe-f2bf97c06d7d', - createdAt: new Date('2025-01-03T00:00:00.000Z'), - }, - }); - - await tx.user.update({ - where: { id: '629be07f-5e65-429a-9d69-d34b99185f50' }, // Michael Doe user - data: { - currentRotationId: 'c2e12afb-a712-45aa-9ba5-fec71868e59a', - }, - }); -} - -export async function loadRotations( - tx: Prisma.TransactionClient, -): Promise { - await load012025(tx); - await load022025(tx); - await load032025(tx); -} diff --git a/prisma/seed/resource/users.seed.ts b/prisma/seed/resource/users.seed.ts index a8508c2b..f2355330 100644 --- a/prisma/seed/resource/users.seed.ts +++ b/prisma/seed/resource/users.seed.ts @@ -10,7 +10,6 @@ export async function loadUsers(tx: Prisma.TransactionClient): Promise { password: '$2a$12$9MvL6NtPLtmU3GSfANn5IuRd64UJNTxWv3ZQE6Cs/AJQFW6zw3S/2', pilotLicenseId: null, currentFlightId: null, - currentRotationId: null, simbriefUserId: null, totalFlightTime: 0, totalGreatCircleDistance: 0, @@ -29,7 +28,6 @@ export async function loadUsers(tx: Prisma.TransactionClient): Promise { password: '$2a$12$9MvL6NtPLtmU3GSfANn5IuRd64UJNTxWv3ZQE6Cs/AJQFW6zw3S/2', pilotLicenseId: null, currentFlightId: null, - currentRotationId: null, simbriefUserId: null, totalFlightTime: 0, totalGreatCircleDistance: 0, @@ -48,7 +46,6 @@ export async function loadUsers(tx: Prisma.TransactionClient): Promise { password: '$2a$12$9MvL6NtPLtmU3GSfANn5IuRd64UJNTxWv3ZQE6Cs/AJQFW6zw3S/2', pilotLicenseId: null, currentFlightId: null, - currentRotationId: null, simbriefUserId: '123456', totalFlightTime: 0, totalGreatCircleDistance: 0, @@ -67,7 +64,6 @@ export async function loadUsers(tx: Prisma.TransactionClient): Promise { password: '$2a$12$9MvL6NtPLtmU3GSfANn5IuRd64UJNTxWv3ZQE6Cs/AJQFW6zw3S/2', pilotLicenseId: null, currentFlightId: null, - currentRotationId: null, simbriefUserId: '654321', totalFlightTime: 0, totalGreatCircleDistance: 0, @@ -87,7 +83,6 @@ export async function loadUsers(tx: Prisma.TransactionClient): Promise { // null because seed flights loaded later than seed users // AAL4908 attached in flights.seed.ts currentFlightId: null, - currentRotationId: null, pilotLicenseId: 'UK-31270', simbriefUserId: null, totalFlightTime: 1797, @@ -109,8 +104,6 @@ export async function loadUsers(tx: Prisma.TransactionClient): Promise { // DLH42 attached in flights.seed.ts currentFlightId: null, // null because seed flights loaded later than seed users - // 02-2025 attached in rotations.seed.ts - currentRotationId: null, pilotLicenseId: 'UK-34560', simbriefUserId: null, totalFlightTime: 0, @@ -132,8 +125,6 @@ export async function loadUsers(tx: Prisma.TransactionClient): Promise { // DLH43 attached in flights.seed.ts currentFlightId: null, // null because seed flights loaded later than seed users - // 03-2025 attached in rotations.seed.ts - currentRotationId: null, pilotLicenseId: 'UK-98540', simbriefUserId: null, totalFlightTime: 0, @@ -153,7 +144,6 @@ export async function loadUsers(tx: Prisma.TransactionClient): Promise { password: '$2a$12$9MvL6NtPLtmU3GSfANn5IuRd64UJNTxWv3ZQE6Cs/AJQFW6zw3S/2', pilotLicenseId: null, currentFlightId: null, - currentRotationId: null, simbriefUserId: '111222', totalFlightTime: 0, totalGreatCircleDistance: 0, @@ -172,7 +162,6 @@ export async function loadUsers(tx: Prisma.TransactionClient): Promise { password: '$2a$12$9MvL6NtPLtmU3GSfANn5IuRd64UJNTxWv3ZQE6Cs/AJQFW6zw3S/2', pilotLicenseId: null, currentFlightId: null, - currentRotationId: null, simbriefUserId: '333444', totalFlightTime: 0, totalGreatCircleDistance: 0, diff --git a/src/core/domain/events/dto/flight.events.ts b/src/core/domain/events/dto/flight.events.ts index 44587af6..0d4cc66c 100644 --- a/src/core/domain/events/dto/flight.events.ts +++ b/src/core/domain/events/dto/flight.events.ts @@ -16,8 +16,6 @@ export enum FlightEventType { DepartureRunwayWasChanged = 'flight.departure-runway-changed', ArrivalParkingPositionWasChanged = 'flight.arrival-parking-position-changed', ArrivalRunwayWasChanged = 'flight.arrival-runway-changed', - FlightWasAddedToRotation = 'flight.added-to-rotation', - FlightWasRemovedFromRotation = 'flight.removed-from-rotation', FlightWasReleased = 'flight.released', PilotCheckedIn = 'flight.pilot-checked-in', BoardingWasStarted = 'flight.boarding-started', @@ -47,7 +45,6 @@ export type FlightEventPayload = { flightId: string; scope: FlightEventScope; actorId: string | null; - rotationId?: string | null; payload?: InputJsonValue; }; @@ -121,14 +118,6 @@ export class ArrivalRunwayWasChangedEvent extends FlightLifecycleEvent { static readonly name = FlightEventType.ArrivalRunwayWasChanged; } -export class FlightWasAddedToRotationEvent extends FlightLifecycleEvent { - static readonly name = FlightEventType.FlightWasAddedToRotation; -} - -export class FlightWasRemovedFromRotationEvent extends FlightLifecycleEvent { - static readonly name = FlightEventType.FlightWasRemovedFromRotation; -} - export class FlightWasReleasedEvent extends FlightLifecycleEvent { static readonly name = FlightEventType.FlightWasReleased; } diff --git a/src/core/http/swagger/swagger.config.ts b/src/core/http/swagger/swagger.config.ts index 43fbb24e..9f9f2bd6 100644 --- a/src/core/http/swagger/swagger.config.ts +++ b/src/core/http/swagger/swagger.config.ts @@ -9,7 +9,7 @@ function createSwaggerConfig() { [ 'REST API for managing virtual flight operations, fleet monitoring, and logbook tracking.', '', - 'Includes aircraft management, route planning, operator workflows, and rotation logs.', + 'Includes aircraft management, route planning, and operator workflows.', ].join('\n'), ) .setVersion(pack.version) diff --git a/src/modules/flights/application/command/check-in-pilot.command.ts b/src/modules/flights/application/command/check-in-pilot.command.ts index 51228db4..e554f7c6 100644 --- a/src/modules/flights/application/command/check-in-pilot.command.ts +++ b/src/modules/flights/application/command/check-in-pilot.command.ts @@ -55,7 +55,6 @@ export class CheckInPilotForFlightHandler implements ICommandHandler { this.domainEvents.emit( new FlightWasClosedEvent({ flightId, - rotationId: flight.rotationId, scope: FlightEventScope.User, actorId: initiatorId, aircraftId: flight.aircraft.id, diff --git a/src/modules/flights/application/command/diversion/report-flight-diversion.command.ts b/src/modules/flights/application/command/diversion/report-flight-diversion.command.ts index 6589b85a..a85c3378 100644 --- a/src/modules/flights/application/command/diversion/report-flight-diversion.command.ts +++ b/src/modules/flights/application/command/diversion/report-flight-diversion.command.ts @@ -1,10 +1,9 @@ -import { CommandHandler, ICommandHandler, QueryBus } from '@nestjs/cqrs'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { DomainEventEmitter } from '../../../../../core/domain/events/domain-event-emitter'; import { DiversionRepository } from '../../../infra/database/repository/diversion.repository'; import { ReportDiversionRequest } from '../../../infra/http/request/diversion.dto'; import { JwtUser } from '../../../../auth/infra/http/request/jwt-user.dto'; -import { GetFlightQuery } from '../../query/get-flight.query'; import { DiversionWasReportedEvent } from '../../../../../core/domain/events/dto/flight.events'; import { scopeForActor } from '../../../model/event.model'; @@ -19,7 +18,6 @@ export class ReportFlightDiversionCommand { @CommandHandler(ReportFlightDiversionCommand) export class ReportFlightDiversionHandler implements ICommandHandler { constructor( - private readonly queryBus: QueryBus, private readonly diversionRepository: DiversionRepository, private readonly domainEvents: DomainEventEmitter, ) {} @@ -29,13 +27,9 @@ export class ReportFlightDiversionHandler implements ICommandHandler { - constructor( - private readonly repository: FlightsRepository, - private readonly queryBus: QueryBus, - private readonly domainEvents: DomainEventEmitter, - ) {} - - async execute(command: AddFlightToRotationCommand): Promise { - const { flightId, rotationId, initiatorId } = command; - - const flight = await this.repository.findOneBy({ id: flightId }); - - if (!flight) throw new FlightNotFoundError(); - if (flight.rotationId) throw new FlightAlreadyAssignedToRotationError(); - if (flight.status !== FlightStatus.Created) { - throw new FlightIncorrectStateToChangeRotationError(); - } - - await this.queryBus.execute(new AssertRotationExistsQuery(rotationId)); - await this.repository.addRotationForFlight(flightId, rotationId); - - this.domainEvents.emit( - new FlightWasAddedToRotationEvent({ - flightId, - rotationId, - scope: FlightEventScope.Operations, - actorId: initiatorId, - }), - ); - } -} diff --git a/src/modules/flights/application/command/rotation/remove-flight-from-rotation.command.ts b/src/modules/flights/application/command/rotation/remove-flight-from-rotation.command.ts deleted file mode 100644 index edb9e598..00000000 --- a/src/modules/flights/application/command/rotation/remove-flight-from-rotation.command.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { CommandHandler, ICommandHandler, QueryBus } from '@nestjs/cqrs'; -import { DomainEventEmitter } from '../../../../../core/domain/events/domain-event-emitter'; -import { FlightsRepository } from '../../../infra/database/repository/flights.repository'; -import { AssertRotationExistsQuery } from '../../../../operators/application/assert/assert-rotation-exists.query'; -import { - FlightIncorrectStateToChangeRotationError, - FlightNotFoundError, - FlightRotationNotMatchingError, -} from '../../../model/error/flight.error'; -import { FlightStatus } from '../../../model/flight.model'; -import { FlightWasRemovedFromRotationEvent } from '../../../../../core/domain/events/dto/flight.events'; -import { FlightEventScope } from '../../../model/event.model'; - -export class RemoveFlightFromRotationCommand { - constructor( - public readonly flightId: string, - public readonly rotationId: string, - public readonly initiatorId: string, - ) {} -} - -@CommandHandler(RemoveFlightFromRotationCommand) -export class RemoveFlightFromRotationHandler implements ICommandHandler { - constructor( - private readonly repository: FlightsRepository, - private readonly queryBus: QueryBus, - private readonly domainEvents: DomainEventEmitter, - ) {} - - async execute(command: RemoveFlightFromRotationCommand): Promise { - const { flightId, rotationId, initiatorId } = command; - - const flight = await this.repository.findOneBy({ id: flightId }); - if (!flight) throw new FlightNotFoundError(); - - await this.queryBus.execute(new AssertRotationExistsQuery(rotationId)); - - if (flight.rotationId !== rotationId) { - throw new FlightRotationNotMatchingError(); - } - - if (flight.status !== FlightStatus.Created) { - throw new FlightIncorrectStateToChangeRotationError(); - } - - await this.repository.removeRotationForFlight( - command.flightId, - command.rotationId, - ); - - this.domainEvents.emit( - new FlightWasRemovedFromRotationEvent({ - flightId, - rotationId, - scope: FlightEventScope.Operations, - actorId: initiatorId, - }), - ); - } -} diff --git a/src/modules/flights/application/command/start-boarding.command.ts b/src/modules/flights/application/command/start-boarding.command.ts index 5079ac59..f379d3ee 100644 --- a/src/modules/flights/application/command/start-boarding.command.ts +++ b/src/modules/flights/application/command/start-boarding.command.ts @@ -45,7 +45,6 @@ export class StartBoardingHandler implements ICommandHandler = {}) { return { id: FLIGHT_ID, status: FlightStatus.InCruise, - rotationId: null, airports: [ { type: AirportType.Departure, shape: PERIMETER }, { type: AirportType.Destination, shape: PERIMETER }, @@ -67,7 +66,6 @@ describe('DetectArrivalListener', () => { const event = new FlightPathWasUpdatedEvent({ flightId: FLIGHT_ID, - rotationId: null, scope: FlightEventScope.System, actorId: null, }); diff --git a/src/modules/flights/application/event/internal/detect-off-block.listener.spec.ts b/src/modules/flights/application/event/internal/detect-off-block.listener.spec.ts index f206f40c..68012def 100644 --- a/src/modules/flights/application/event/internal/detect-off-block.listener.spec.ts +++ b/src/modules/flights/application/event/internal/detect-off-block.listener.spec.ts @@ -29,7 +29,6 @@ function flight(overrides: Record = {}) { return { id: FLIGHT_ID, status: FlightStatus.BoardingFinished, - rotationId: null, ...overrides, }; } @@ -53,7 +52,6 @@ describe('DetectOffBlockListener', () => { const event = new FlightPathWasUpdatedEvent({ flightId: FLIGHT_ID, - rotationId: null, scope: FlightEventScope.System, actorId: null, }); diff --git a/src/modules/flights/application/event/internal/detect-takeoff.listener.spec.ts b/src/modules/flights/application/event/internal/detect-takeoff.listener.spec.ts index b6b4eaa9..baf26333 100644 --- a/src/modules/flights/application/event/internal/detect-takeoff.listener.spec.ts +++ b/src/modules/flights/application/event/internal/detect-takeoff.listener.spec.ts @@ -41,7 +41,6 @@ function flight(overrides: Record = {}) { return { id: FLIGHT_ID, status: FlightStatus.TaxiingOut, - rotationId: null, airports: [ { type: AirportType.Departure, shape: PERIMETER }, { type: AirportType.Destination, shape: PERIMETER }, @@ -69,7 +68,6 @@ describe('DetectTakeoffListener', () => { const event = new FlightPathWasUpdatedEvent({ flightId: FLIGHT_ID, - rotationId: null, scope: FlightEventScope.System, actorId: null, }); diff --git a/src/modules/flights/application/event/internal/flight-cache.listener.ts b/src/modules/flights/application/event/internal/flight-cache.listener.ts index 850c9be1..552886af 100644 --- a/src/modules/flights/application/event/internal/flight-cache.listener.ts +++ b/src/modules/flights/application/event/internal/flight-cache.listener.ts @@ -23,8 +23,6 @@ export class FlightCacheListener { @OnEvent(FlightEventType.DepartureRunwayWasChanged) @OnEvent(FlightEventType.ArrivalParkingPositionWasChanged) @OnEvent(FlightEventType.ArrivalRunwayWasChanged) - @OnEvent(FlightEventType.FlightWasAddedToRotation) - @OnEvent(FlightEventType.FlightWasRemovedFromRotation) @OnEvent(FlightEventType.FlightWasReleased) @OnEvent(FlightEventType.PilotCheckedIn) @OnEvent(FlightEventType.BoardingWasStarted) diff --git a/src/modules/flights/application/event/internal/off-block-delay.listener.ts b/src/modules/flights/application/event/internal/off-block-delay.listener.ts index 5dedac97..d643d30b 100644 --- a/src/modules/flights/application/event/internal/off-block-delay.listener.ts +++ b/src/modules/flights/application/event/internal/off-block-delay.listener.ts @@ -45,7 +45,6 @@ export class OffBlockDelayListener { this.domainEvents.emit( new DelayRequestWasCreatedEvent({ flightId, - rotationId: flight.rotationId, scope: FlightEventScope.Operations, actorId: null, }), diff --git a/src/modules/flights/application/query/rotation/get-last-flight-in-rotation.query.ts b/src/modules/flights/application/query/rotation/get-last-flight-in-rotation.query.ts deleted file mode 100644 index 47aaec76..00000000 --- a/src/modules/flights/application/query/rotation/get-last-flight-in-rotation.query.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Query, QueryHandler, IQueryHandler } from '@nestjs/cqrs'; -import { FlightsRepository } from '../../../infra/database/repository/flights.repository'; - -export class GetLastFlightInRotationQuery extends Query { - constructor(public readonly rotationId: string) { - super(); - } -} - -@QueryHandler(GetLastFlightInRotationQuery) -export class GetLastFlightInRotationHandler implements IQueryHandler { - constructor(private readonly repository: FlightsRepository) {} - - async execute(query: GetLastFlightInRotationQuery): Promise { - return this.repository.getLastFlightIdInRotation(query.rotationId); - } -} diff --git a/src/modules/flights/flights.module.ts b/src/modules/flights/flights.module.ts index d43bbf21..2abf0568 100644 --- a/src/modules/flights/flights.module.ts +++ b/src/modules/flights/flights.module.ts @@ -46,8 +46,6 @@ import { AssignFlightCrewAction } from './infra/http/action/crew/assign-flight-c import { UnassignFlightCrewAction } from './infra/http/action/crew/unassign-flight-crew.action'; import { GetOfpAction } from './infra/http/action/ofp/get-ofp.action'; import { GetPathAction } from './infra/http/action/path/get-path.action'; -import { AddFlightToRotationAction } from './infra/http/action/rotation/add-flight-to-rotation.action'; -import { RemoveFlightFromRotationAction } from './infra/http/action/rotation/remove-flight-from-rotation.action'; import { DeclareEmergencyHandler } from './application/command/emergency/declare-emergency.command'; import { UpdateEmergencyHandler } from './application/command/emergency/update-emergency.command'; import { ResolveEmergencyHandler } from './application/command/emergency/resolve-emergency.command'; @@ -80,8 +78,6 @@ import { UpdateDepartureParkingPositionHandler } from './application/command/upd import { UpdateDepartureRunwayHandler } from './application/command/update-departure-runway.command'; import { UpdateArrivalParkingPositionHandler } from './application/command/update-arrival-parking-position.command'; import { UpdateArrivalRunwayHandler } from './application/command/update-arrival-runway.command'; -import { AddFlightToRotationHandler } from './application/command/rotation/add-flight-to-rotation.command'; -import { RemoveFlightFromRotationHandler } from './application/command/rotation/remove-flight-from-rotation.command'; import { ListEventsHandler } from './application/query/events/list-events.query'; import { GetDiversionHandler } from './application/query/diversion/get-diversion.query'; import { ReportFlightDiversionHandler } from './application/command/diversion/report-flight-diversion.command'; @@ -104,7 +100,6 @@ import { RejectDelayReportHandler } from './application/command/delay/reject-del import { GetArrivalParkingPositionHandler } from './application/query/get-arrival-parking-position.query'; import { GetRepositionDataHandler } from './application/query/reposition/get-reposition-data.query'; import { GetFlightCompletionStatsHandler } from './application/query/get-flight-completion-stats.query'; -import { GetLastFlightInRotationHandler } from './application/query/rotation/get-last-flight-in-rotation.query'; import { GetDelayRequestAction } from './infra/http/action/delay/get-delay-request.action'; import { ListDelayReportsAction } from './infra/http/action/delay/list-delay-reports.action'; import { ReportDelayAction } from './infra/http/action/delay/report-delay.action'; @@ -153,8 +148,6 @@ import { RejectDelayReportAction } from './infra/http/action/delay/reject-delay- UnassignFlightCrewAction, GetOfpAction, GetPathAction, - AddFlightToRotationAction, - RemoveFlightFromRotationAction, GetDelayRequestAction, ReportDelayAction, RemoveDelayReportAction, @@ -200,8 +193,6 @@ import { RejectDelayReportAction } from './infra/http/action/delay/reject-delay- UpdateDepartureRunwayHandler, UpdateArrivalParkingPositionHandler, UpdateArrivalRunwayHandler, - AddFlightToRotationHandler, - RemoveFlightFromRotationHandler, ListEventsHandler, GetDiversionHandler, ReportFlightDiversionHandler, @@ -224,7 +215,6 @@ import { RejectDelayReportAction } from './infra/http/action/delay/reject-delay- GetArrivalParkingPositionHandler, GetRepositionDataHandler, GetFlightCompletionStatsHandler, - GetLastFlightInRotationHandler, ], }) export class FlightsModule {} diff --git a/src/modules/flights/infra/database/repository/events.repository.ts b/src/modules/flights/infra/database/repository/events.repository.ts index 8357e815..7db702a2 100644 --- a/src/modules/flights/infra/database/repository/events.repository.ts +++ b/src/modules/flights/infra/database/repository/events.repository.ts @@ -61,8 +61,6 @@ export class EventsRepository { @OnEvent(FlightEventType.PreliminaryLoadsheetWasUpdated) @OnEvent(FlightEventType.ScheduledTimesheetWasUpdated) @OnEvent(FlightEventType.PredictedTimesheetWasUpdated) - @OnEvent(FlightEventType.FlightWasAddedToRotation) - @OnEvent(FlightEventType.FlightWasRemovedFromRotation) @OnEvent(FlightEventType.DepartureParkingPositionWasChanged) @OnEvent(FlightEventType.DepartureRunwayWasChanged) @OnEvent(FlightEventType.ArrivalParkingPositionWasChanged) diff --git a/src/modules/flights/infra/database/repository/flights.repository.ts b/src/modules/flights/infra/database/repository/flights.repository.ts index 54dd8b8e..f520bc51 100644 --- a/src/modules/flights/infra/database/repository/flights.repository.ts +++ b/src/modules/flights/infra/database/repository/flights.repository.ts @@ -44,7 +44,6 @@ export const flightWithAircraftAndAirportsFields = { status: true, timesheet: true, loadsheets: true, - rotationId: true, captainId: true, source: true, tracking: true, @@ -231,15 +230,6 @@ type FlightsWithCount = { export class FlightsRepository { constructor(private readonly prisma: PrismaService) {} - async getRotationIdByFlightId(flightId: string): Promise { - const flight = await this.prisma.flight.findUnique({ - where: { id: flightId }, - select: { rotationId: true }, - }); - - return flight?.rotationId ?? null; - } - async create( flightId: string, flightData: CreateFlightRequest, @@ -663,38 +653,6 @@ export class FlightsRepository { }); } - async addRotationForFlight( - flightId: string, - rotationId: string, - ): Promise { - await Promise.all([ - this.prisma.flight.update({ - where: { id: flightId }, - data: { rotationId }, - }), - this.prisma.rotation.update({ - where: { id: rotationId }, - data: { updatedAt: new Date() }, - }), - ]); - } - - async removeRotationForFlight( - flightId: string, - rotationId: string, - ): Promise { - await Promise.all([ - this.prisma.flight.update({ - where: { id: flightId }, - data: { rotationId: null }, - }), - this.prisma.rotation.update({ - where: { id: rotationId }, - data: { updatedAt: new Date() }, - }), - ]); - } - async getArrivalParkingPositionId(flightId: string): Promise { const flight = await this.prisma.flight.findUnique({ where: { id: flightId }, @@ -760,14 +718,4 @@ export class FlightsRepository { timesheet: flight.timesheet as FilledTimesheet, }; } - - async getLastFlightIdInRotation(rotationId: string): Promise { - const flight = await this.prisma.flight.findFirst({ - select: { id: true }, - where: { rotationId }, - orderBy: { createdAt: 'desc' }, - }); - - return flight?.id ?? null; - } } diff --git a/src/modules/flights/infra/http/action/rotation/add-flight-to-rotation.action.ts b/src/modules/flights/infra/http/action/rotation/add-flight-to-rotation.action.ts deleted file mode 100644 index 03478e0c..00000000 --- a/src/modules/flights/infra/http/action/rotation/add-flight-to-rotation.action.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Controller, HttpCode, HttpStatus, Post, Req } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiConflictResponse, - ApiForbiddenResponse, - ApiNoContentResponse, - ApiNotFoundResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { CommandBus } from '@nestjs/cqrs'; -import { UserRole } from '../../../../../users/model/user-role'; -import { GenericBadRequestResponse } from '../../../../../../core/http/response/bad-request.response'; -import { GenericNotFoundResponse } from '../../../../../../core/http/response/not-found.response'; -import { GenericConflictResponse } from '../../../../../../core/http/response/conflict.response'; -import { UnauthorizedResponse } from '../../../../../../core/http/response/unauthorized.response'; -import { ForbiddenResponse } from '../../../../../../core/http/response/forbidden.response'; -import { Role } from '../../../../../../core/http/auth/decorator/role.decorator'; -import { UuidParam } from '../../../../../../core/validation/uuid.param'; -import { AddFlightToRotationCommand } from '../../../../application/command/rotation/add-flight-to-rotation.command'; -import { AuthorizedRequest } from '../../../../../../core/http/request/authorized.request'; - -@ApiTags('flight rotations') -@Controller('/api/v1/flight/:flightId/rotation') -export class AddFlightToRotationAction { - constructor(private readonly commandBus: CommandBus) {} - - @ApiOperation({ - summary: 'Assign a flight to a rotation', - description: - '**NOTE:** This endpoint is only available for users with `operations` role.', - }) - @ApiBearerAuth('jwt') - @ApiParam({ name: 'flightId', description: 'Flight unique identifier' }) - @ApiParam({ name: 'rotationId', description: 'Rotation unique identifier' }) - @ApiNoContentResponse() - @ApiBadRequestResponse({ type: GenericBadRequestResponse }) - @ApiUnauthorizedResponse({ type: UnauthorizedResponse }) - @ApiForbiddenResponse({ type: ForbiddenResponse }) - @ApiNotFoundResponse({ type: GenericNotFoundResponse }) - @ApiConflictResponse({ type: GenericConflictResponse }) - @Post(':rotationId') - @Role(UserRole.Operations) - @HttpCode(HttpStatus.NO_CONTENT) - async run( - @UuidParam('flightId') flightId: string, - @UuidParam('rotationId') rotationId: string, - @Req() request: AuthorizedRequest, - ): Promise { - const command = new AddFlightToRotationCommand( - flightId, - rotationId, - request.user.sub, - ); - await this.commandBus.execute(command); - } -} diff --git a/src/modules/flights/infra/http/action/rotation/remove-flight-from-rotation.action.ts b/src/modules/flights/infra/http/action/rotation/remove-flight-from-rotation.action.ts deleted file mode 100644 index 06171c18..00000000 --- a/src/modules/flights/infra/http/action/rotation/remove-flight-from-rotation.action.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Controller, Delete, HttpCode, HttpStatus, Req } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiConflictResponse, - ApiForbiddenResponse, - ApiNoContentResponse, - ApiNotFoundResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { CommandBus } from '@nestjs/cqrs'; -import { UserRole } from '../../../../../users/model/user-role'; -import { GenericBadRequestResponse } from '../../../../../../core/http/response/bad-request.response'; -import { GenericNotFoundResponse } from '../../../../../../core/http/response/not-found.response'; -import { GenericConflictResponse } from '../../../../../../core/http/response/conflict.response'; -import { UnauthorizedResponse } from '../../../../../../core/http/response/unauthorized.response'; -import { ForbiddenResponse } from '../../../../../../core/http/response/forbidden.response'; -import { Role } from '../../../../../../core/http/auth/decorator/role.decorator'; -import { UuidParam } from '../../../../../../core/validation/uuid.param'; -import { RemoveFlightFromRotationCommand } from '../../../../application/command/rotation/remove-flight-from-rotation.command'; -import { AuthorizedRequest } from '../../../../../../core/http/request/authorized.request'; - -@ApiTags('flight rotations') -@Controller('/api/v1/flight/:flightId/rotation') -export class RemoveFlightFromRotationAction { - constructor(private readonly commandBus: CommandBus) {} - - @ApiOperation({ - summary: 'Remove a flight from a rotation', - description: - '**NOTE:** This endpoint is only available for users with `operations` role.', - }) - @ApiBearerAuth('jwt') - @ApiParam({ name: 'flightId', description: 'Flight unique identifier' }) - @ApiParam({ name: 'rotationId', description: 'Rotation unique identifier' }) - @ApiNoContentResponse() - @ApiBadRequestResponse({ type: GenericBadRequestResponse }) - @ApiUnauthorizedResponse({ type: UnauthorizedResponse }) - @ApiForbiddenResponse({ type: ForbiddenResponse }) - @ApiNotFoundResponse({ type: GenericNotFoundResponse }) - @ApiConflictResponse({ type: GenericConflictResponse }) - @Delete(':rotationId') - @Role(UserRole.Operations) - @HttpCode(HttpStatus.NO_CONTENT) - async run( - @UuidParam('flightId') flightId: string, - @UuidParam('rotationId') rotationId: string, - @Req() request: AuthorizedRequest, - ): Promise { - const command = new RemoveFlightFromRotationCommand( - flightId, - rotationId, - request.user.sub, - ); - await this.commandBus.execute(command); - } -} diff --git a/src/modules/flights/infra/http/request/event.dto.ts b/src/modules/flights/infra/http/request/event.dto.ts index 2868eafa..42585cd3 100644 --- a/src/modules/flights/infra/http/request/event.dto.ts +++ b/src/modules/flights/infra/http/request/event.dto.ts @@ -8,7 +8,6 @@ import { export class FlightEventResponse extends OmitType(FlightEvent, [ 'actorId', 'flightId', - 'rotationId', ]) {} export type FlightBroadcastEvent = FlightEventPayload & { diff --git a/src/modules/flights/infra/http/request/flight.dto.ts b/src/modules/flights/infra/http/request/flight.dto.ts index b44216cd..e1fb4298 100644 --- a/src/modules/flights/infra/http/request/flight.dto.ts +++ b/src/modules/flights/infra/http/request/flight.dto.ts @@ -47,8 +47,6 @@ export class CreateFlightRequest extends OmitType(Flight, [ 'timesheet', 'loadsheets', 'operator', - 'rotationId', - 'rotation', 'source', 'isFlightDiverted', 'isEmergencyDeclared', @@ -107,7 +105,6 @@ export class CreateFlightRequest extends OmitType(Flight, [ } export class GetFlightResponse extends OmitType(Flight, [ - 'rotation', 'aircraftId', 'operatorId', ]) { diff --git a/src/modules/flights/infra/service/position.service.ts b/src/modules/flights/infra/service/position.service.ts index 9e8d9014..b1922ae5 100644 --- a/src/modules/flights/infra/service/position.service.ts +++ b/src/modules/flights/infra/service/position.service.ts @@ -98,7 +98,6 @@ export class PositionService { this.domainEvents.emit( new LivePositionReceivedEvent({ flightId, - rotationId: null, scope: FlightEventScope.User, actorId: null, }), @@ -109,7 +108,6 @@ export class PositionService { this.domainEvents.emit( new FlightPathWasUpdatedEvent({ flightId, - rotationId: null, scope: FlightEventScope.System, actorId: null, }), diff --git a/src/modules/flights/model/error/flight.error.ts b/src/modules/flights/model/error/flight.error.ts index b375bc77..bd6da1bf 100644 --- a/src/modules/flights/model/error/flight.error.ts +++ b/src/modules/flights/model/error/flight.error.ts @@ -1,6 +1,5 @@ import { BadRequestError, - ConflictError, NotFoundError, UnprocessableError, } from '../../../../core/errors/domain-error'; @@ -29,24 +28,6 @@ export class FlightOfpNotFoundError extends NotFoundError { } } -export class FlightAlreadyAssignedToRotationError extends ConflictError { - constructor() { - super('Flight is already assigned to rotation.'); - } -} - -export class FlightRotationNotMatchingError extends ConflictError { - constructor() { - super('Flight is already assigned to a different rotation.'); - } -} - -export class FlightIncorrectStateToChangeRotationError extends BadRequestError { - constructor() { - super('Flight is in incorrect state to modify its rotation.'); - } -} - export class InvalidStatusToUpdateDepartureParkingPositionError extends UnprocessableError { constructor() { super( diff --git a/src/modules/flights/model/event.model.ts b/src/modules/flights/model/event.model.ts index b32c0bd6..1ef961ec 100644 --- a/src/modules/flights/model/event.model.ts +++ b/src/modules/flights/model/event.model.ts @@ -80,6 +80,4 @@ export class FlightEvent { example: '2025-01-01T00:00:00Z', }) createdAt!: Date; - - rotationId?: string | null = null; } diff --git a/src/modules/flights/model/flight.model.ts b/src/modules/flights/model/flight.model.ts index eadd2b1c..ab9e7858 100644 --- a/src/modules/flights/model/flight.model.ts +++ b/src/modules/flights/model/flight.model.ts @@ -3,7 +3,6 @@ import { AirportWithType } from '../../airports/model/airport.model'; import { FullTimesheet } from './timesheet.model'; import { Operator } from '../../operators/model/operator.model'; import { Loadsheets } from './loadsheet.model'; -import { Rotation } from '../../operators/model/rotation.model'; import { IsBoolean, IsEnum, @@ -144,21 +143,6 @@ export class Flight { }) operator!: ShortOperatorResponse; - @ApiProperty({ - description: 'Rotation this flight belongs to', - type: Rotation, - required: false, - nullable: true, - }) - rotation?: Rotation; - - @ApiProperty({ - description: 'Unique identifier of rotation this flight belongs to', - type: 'string', - nullable: true, - }) - rotationId!: string | null; - @ApiProperty({ description: 'Source how flight was created', type: 'string', diff --git a/src/modules/operators/application/assert/assert-rotation-exists.query.ts b/src/modules/operators/application/assert/assert-rotation-exists.query.ts deleted file mode 100644 index a52954f8..00000000 --- a/src/modules/operators/application/assert/assert-rotation-exists.query.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { QueryHandler, IQueryHandler } from '@nestjs/cqrs'; -import { RotationsRepository } from '../../infra/database/repository/rotations.repository'; -import { RotationNotFoundError } from '../../model/error/rotation.error'; - -export class AssertRotationExistsQuery { - constructor(public readonly id: string) {} -} - -@QueryHandler(AssertRotationExistsQuery) -export class AssertRotationExistsHandler implements IQueryHandler { - constructor(private readonly repository: RotationsRepository) {} - - async execute(query: AssertRotationExistsQuery): Promise { - const exists = await this.repository.exists(query.id); - - if (exists) return; - - throw new RotationNotFoundError(); - } -} diff --git a/src/modules/operators/application/command/rotation/create-rotation.command.ts b/src/modules/operators/application/command/rotation/create-rotation.command.ts deleted file mode 100644 index 05957f48..00000000 --- a/src/modules/operators/application/command/rotation/create-rotation.command.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { CommandHandler, ICommandHandler, QueryBus } from '@nestjs/cqrs'; -import { OperatorNotFoundError } from '../../../model/error/operator.error'; -import { OperatorsRepository } from '../../../infra/database/repository/operators.repository'; -import { CreateRotationRequest } from '../../../infra/http/request/rotation.request'; -import { RotationsRepository } from '../../../infra/database/repository/rotations.repository'; -import { AssertUserExistsQuery } from '../../../../users/application/assert/assert-user-exists.query'; - -export class CreateRotationCommand { - constructor( - public readonly operatorId: string, - public readonly rotationId: string, - public readonly data: CreateRotationRequest, - ) {} -} - -@CommandHandler(CreateRotationCommand) -export class CreateRotationHandler implements ICommandHandler { - constructor( - private readonly rotationsRepository: RotationsRepository, - private readonly operatorsRepository: OperatorsRepository, - private readonly queryBus: QueryBus, - ) {} - - async execute(command: CreateRotationCommand): Promise { - const { operatorId, rotationId, data } = command; - - const operatorExists = await this.operatorsRepository.exists( - command.operatorId, - ); - - if (!operatorExists) { - throw new OperatorNotFoundError(); - } - - await this.queryBus.execute( - new AssertUserExistsQuery(command.data.pilotId), - ); - - await this.rotationsRepository.create(rotationId, operatorId, data); - } -} diff --git a/src/modules/operators/application/command/rotation/remove-rotation.command.ts b/src/modules/operators/application/command/rotation/remove-rotation.command.ts deleted file mode 100644 index 02c1293b..00000000 --- a/src/modules/operators/application/command/rotation/remove-rotation.command.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { OperatorNotFoundError } from '../../../model/error/operator.error'; -import { OperatorsRepository } from '../../../infra/database/repository/operators.repository'; -import { RotationsRepository } from '../../../infra/database/repository/rotations.repository'; -import { RotationNotFoundError } from '../../../model/error/rotation.error'; - -export class RemoveRotationCommand { - constructor( - public readonly operatorId: string, - public readonly rotationId: string, - ) {} -} - -@CommandHandler(RemoveRotationCommand) -export class RemoveRotationHandler implements ICommandHandler { - constructor( - private readonly rotationsRepository: RotationsRepository, - private readonly operatorsRepository: OperatorsRepository, - ) {} - - async execute(command: RemoveRotationCommand): Promise { - const { operatorId, rotationId } = command; - - const operatorExists = await this.operatorsRepository.exists(operatorId); - - if (!operatorExists) { - throw new OperatorNotFoundError(); - } - - const rotationExists = await this.rotationsRepository.exists(rotationId); - - if (!rotationExists) { - throw new RotationNotFoundError(); - } - - await this.rotationsRepository.remove(rotationId); - } -} diff --git a/src/modules/operators/application/command/rotation/update-rotation.command.ts b/src/modules/operators/application/command/rotation/update-rotation.command.ts deleted file mode 100644 index 02f93463..00000000 --- a/src/modules/operators/application/command/rotation/update-rotation.command.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { CommandHandler, ICommandHandler, QueryBus } from '@nestjs/cqrs'; -import { OperatorNotFoundError } from '../../../model/error/operator.error'; -import { OperatorsRepository } from '../../../infra/database/repository/operators.repository'; -import { RotationsRepository } from '../../../infra/database/repository/rotations.repository'; -import { RotationNotFoundError } from '../../../model/error/rotation.error'; -import { UpdateRotationRequest } from '../../../infra/http/request/rotation.request'; -import { AssertUserExistsQuery } from '../../../../users/application/assert/assert-user-exists.query'; - -export class UpdateRotationCommand { - constructor( - public readonly operatorId: string, - public readonly rotationId: string, - public readonly data: UpdateRotationRequest, - ) {} -} - -@CommandHandler(UpdateRotationCommand) -export class UpdateRotationHandler implements ICommandHandler { - constructor( - private readonly rotationsRepository: RotationsRepository, - private readonly operatorsRepository: OperatorsRepository, - private readonly queryBus: QueryBus, - ) {} - - async execute(command: UpdateRotationCommand): Promise { - const { operatorId, rotationId, data } = command; - - const operatorExists = await this.operatorsRepository.exists(operatorId); - - if (!operatorExists) { - throw new OperatorNotFoundError(); - } - - const rotationExists = await this.rotationsRepository.exists(rotationId); - - if (!rotationExists) { - throw new RotationNotFoundError(); - } - - if (data.pilotId) { - await this.queryBus.execute(new AssertUserExistsQuery(data.pilotId)); - } - - await this.rotationsRepository.update(rotationId, data); - } -} diff --git a/src/modules/operators/application/query/rotation/get-rotation-by-id.query.ts b/src/modules/operators/application/query/rotation/get-rotation-by-id.query.ts deleted file mode 100644 index 1db613bd..00000000 --- a/src/modules/operators/application/query/rotation/get-rotation-by-id.query.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Query, QueryHandler, IQueryHandler } from '@nestjs/cqrs'; -import { OperatorsRepository } from '../../../infra/database/repository/operators.repository'; -import { OperatorNotFoundError } from '../../../model/error/operator.error'; -import { RotationsRepository } from '../../../infra/database/repository/rotations.repository'; -import { GetRotationResponse } from '../../../infra/http/request/rotation.request'; -import { RotationNotFoundError } from '../../../model/error/rotation.error'; - -export class GetRotationByIdQuery extends Query { - constructor( - public readonly operatorId: string, - public readonly rotationId: string, - ) { - super(); - } -} - -@QueryHandler(GetRotationByIdQuery) -export class GetRotationByIdHandler implements IQueryHandler { - constructor( - private readonly rotationsRepository: RotationsRepository, - private readonly operatorsRepository: OperatorsRepository, - ) {} - - async execute(query: GetRotationByIdQuery): Promise { - const operatorExists = await this.operatorsRepository.exists( - query.operatorId, - ); - - if (!operatorExists) { - throw new OperatorNotFoundError(); - } - - const rotation = await this.rotationsRepository.findOneBy({ - id: query.rotationId, - operatorId: query.operatorId, - }); - - if (!rotation) { - throw new RotationNotFoundError(); - } - - return rotation; - } -} diff --git a/src/modules/operators/application/query/rotation/list-all-rotations.query.ts b/src/modules/operators/application/query/rotation/list-all-rotations.query.ts deleted file mode 100644 index 5eb14eed..00000000 --- a/src/modules/operators/application/query/rotation/list-all-rotations.query.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Query, QueryHandler, IQueryHandler } from '@nestjs/cqrs'; -import { OperatorsRepository } from '../../../infra/database/repository/operators.repository'; -import { OperatorNotFoundError } from '../../../model/error/operator.error'; -import { RotationsRepository } from '../../../infra/database/repository/rotations.repository'; -import { GetRotationResponse } from '../../../infra/http/request/rotation.request'; - -export class ListAllRotationsQuery extends Query { - constructor(public readonly operatorId: string) { - super(); - } -} - -@QueryHandler(ListAllRotationsQuery) -export class ListAllRotationsQueryHandler implements IQueryHandler { - constructor( - private readonly rotationsRepository: RotationsRepository, - private readonly operatorsRepository: OperatorsRepository, - ) {} - - async execute(query: ListAllRotationsQuery): Promise { - const operatorExists = await this.operatorsRepository.exists( - query.operatorId, - ); - - if (!operatorExists) { - throw new OperatorNotFoundError(); - } - - return this.rotationsRepository.findAllForOperator(query.operatorId); - } -} diff --git a/src/modules/operators/infra/database/repository/rotations.repository.ts b/src/modules/operators/infra/database/repository/rotations.repository.ts deleted file mode 100644 index 53533ad0..00000000 --- a/src/modules/operators/infra/database/repository/rotations.repository.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Prisma } from 'prisma/client/client'; -import { RotationWhereInput } from 'prisma/client/models/Rotation'; -import { PrismaService } from '../../../../../core/provider/prisma/prisma.service'; -import { - CreateRotationRequest, - UpdateRotationRequest, -} from '../../http/request/rotation.request'; - -const rotationSchema = { - id: true, - name: true, - pilot: { - select: { - id: true, - name: true, - pilotLicenseId: true, - }, - }, - flights: { - select: { - id: true, - flightNumber: true, - }, - orderBy: { flightNumber: 'asc' }, - }, - createdAt: true, - updatedAt: true, -} as const satisfies Prisma.RotationSelect; - -type RotationEntity = Prisma.RotationGetPayload<{ - select: typeof rotationSchema; -}>; - -@Injectable() -export class RotationsRepository { - constructor(private readonly prisma: PrismaService) {} - - async create( - rotationId: string, - operatorId: string, - data: CreateRotationRequest, - ): Promise { - return this.prisma.rotation.create({ - data: { - id: rotationId, - name: data.name, - pilotId: data.pilotId, - operatorId: operatorId, - }, - select: rotationSchema, - }); - } - - async findOneBy( - criteria: RotationWhereInput, - ): Promise { - return this.prisma.rotation.findFirst({ - where: criteria, - select: rotationSchema, - }); - } - - async findAllForOperator(operatorId: string): Promise { - return this.prisma.rotation.findMany({ - where: { operatorId }, - select: rotationSchema, - }); - } - - async update(id: string, data: UpdateRotationRequest): Promise { - await this.prisma.rotation.update({ - where: { id }, - data: { ...data, updatedAt: new Date() }, - }); - } - - async remove(id: string): Promise { - await this.prisma.flight.updateMany({ - where: { rotationId: id }, - data: { rotationId: null }, - }); - - await this.prisma.rotation.delete({ where: { id } }); - } - - async exists(id: string): Promise { - const count = await this.prisma.rotation.count({ - where: { id }, - }); - - return count > 0; - } -} diff --git a/src/modules/operators/infra/http/action/rotation/create-rotation.action.ts b/src/modules/operators/infra/http/action/rotation/create-rotation.action.ts deleted file mode 100644 index c2e60579..00000000 --- a/src/modules/operators/infra/http/action/rotation/create-rotation.action.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiBody, - ApiCreatedResponse, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { Body, Controller, Post } from '@nestjs/common'; -import { UserRole } from '../../../../../users/model/user-role'; -import { - CreateRotationRequest, - GetRotationResponse, -} from '../../request/rotation.request'; -import { GenericBadRequestResponse } from '../../../../../../core/http/response/bad-request.response'; -import { UnauthorizedResponse } from '../../../../../../core/http/response/unauthorized.response'; -import { ForbiddenResponse } from '../../../../../../core/http/response/forbidden.response'; -import { GenericNotFoundResponse } from '../../../../../../core/http/response/not-found.response'; -import { Role } from '../../../../../../core/http/auth/decorator/role.decorator'; -import { CommandBus, QueryBus } from '@nestjs/cqrs'; -import { v4 } from 'uuid'; -import { CreateRotationCommand } from '../../../../application/command/rotation/create-rotation.command'; -import { UuidParam } from '../../../../../../core/validation/uuid.param'; -import { GetRotationByIdQuery } from '../../../../application/query/rotation/get-rotation-by-id.query'; - -@ApiTags('operator rotations') -@Controller('/api/v1/operator/:operatorId/rotation') -export class CreateRotationAction { - constructor( - private readonly commandBus: CommandBus, - private readonly queryBus: QueryBus, - ) {} - - @ApiOperation({ - summary: 'Create rotation for operator', - description: - '**NOTE:** This endpoint is only available for users with `operations` role.', - }) - @ApiBearerAuth('jwt') - @ApiParam({ name: 'operatorId', description: 'Operator unique identifier' }) - @ApiBody({ type: CreateRotationRequest }) - @ApiCreatedResponse({ type: GetRotationResponse }) - @ApiBadRequestResponse({ type: GenericBadRequestResponse }) - @ApiUnauthorizedResponse({ type: UnauthorizedResponse }) - @ApiForbiddenResponse({ type: ForbiddenResponse }) - @ApiNotFoundResponse({ type: GenericNotFoundResponse }) - @Post() - @Role(UserRole.Operations) - async create( - @UuidParam('operatorId') operatorId: string, - @Body() request: CreateRotationRequest, - ): Promise { - const rotationId = v4(); - - const command = new CreateRotationCommand(operatorId, rotationId, request); - await this.commandBus.execute(command); - - const query = new GetRotationByIdQuery(operatorId, rotationId); - return this.queryBus.execute(query); - } -} diff --git a/src/modules/operators/infra/http/action/rotation/delete-rotation.action.ts b/src/modules/operators/infra/http/action/rotation/delete-rotation.action.ts deleted file mode 100644 index faa31a01..00000000 --- a/src/modules/operators/infra/http/action/rotation/delete-rotation.action.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiForbiddenResponse, - ApiNoContentResponse, - ApiNotFoundResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { Controller, Delete, HttpCode, HttpStatus } from '@nestjs/common'; -import { UserRole } from '../../../../../users/model/user-role'; -import { GenericBadRequestResponse } from '../../../../../../core/http/response/bad-request.response'; -import { UnauthorizedResponse } from '../../../../../../core/http/response/unauthorized.response'; -import { ForbiddenResponse } from '../../../../../../core/http/response/forbidden.response'; -import { GenericNotFoundResponse } from '../../../../../../core/http/response/not-found.response'; -import { Role } from '../../../../../../core/http/auth/decorator/role.decorator'; -import { CommandBus, QueryBus } from '@nestjs/cqrs'; -import { UuidParam } from '../../../../../../core/validation/uuid.param'; -import { RemoveRotationCommand } from '../../../../application/command/rotation/remove-rotation.command'; - -@ApiTags('operator rotations') -@Controller('/api/v1/operator/:operatorId/rotation') -export class DeleteRotationAction { - constructor( - private readonly commandBus: CommandBus, - private readonly queryBus: QueryBus, - ) {} - - @ApiOperation({ - summary: 'Remove a rotation', - description: - '**NOTE:** This endpoint is only available for users with `operations` role.', - }) - @ApiBearerAuth('jwt') - @ApiParam({ name: 'operatorId', description: 'Operator unique identifier' }) - @ApiParam({ name: 'rotationId', description: 'Rotation unique identifier' }) - @ApiNoContentResponse({ - description: 'Rotation was removed successfully', - }) - @ApiBadRequestResponse({ type: GenericBadRequestResponse }) - @ApiUnauthorizedResponse({ type: UnauthorizedResponse }) - @ApiForbiddenResponse({ type: ForbiddenResponse }) - @ApiNotFoundResponse({ type: GenericNotFoundResponse }) - @Delete(':rotationId') - @Role(UserRole.Operations) - @HttpCode(HttpStatus.NO_CONTENT) - async remove( - @UuidParam('rotationId') rotationId: string, - @UuidParam('operatorId') operatorId: string, - ): Promise { - const command = new RemoveRotationCommand(operatorId, rotationId); - await this.commandBus.execute(command); - } -} diff --git a/src/modules/operators/infra/http/action/rotation/get-rotation.action.ts b/src/modules/operators/infra/http/action/rotation/get-rotation.action.ts deleted file mode 100644 index 6f576294..00000000 --- a/src/modules/operators/infra/http/action/rotation/get-rotation.action.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { Controller, Get } from '@nestjs/common'; -import { GetRotationResponse } from '../../request/rotation.request'; -import { GenericBadRequestResponse } from '../../../../../../core/http/response/bad-request.response'; -import { UnauthorizedResponse } from '../../../../../../core/http/response/unauthorized.response'; -import { ForbiddenResponse } from '../../../../../../core/http/response/forbidden.response'; -import { GenericNotFoundResponse } from '../../../../../../core/http/response/not-found.response'; -import { CommandBus, QueryBus } from '@nestjs/cqrs'; -import { UuidParam } from '../../../../../../core/validation/uuid.param'; -import { GetRotationByIdQuery } from '../../../../application/query/rotation/get-rotation-by-id.query'; - -@ApiTags('operator rotations') -@Controller('/api/v1/operator/:operatorId/rotation') -export class GetRotationAction { - constructor( - private readonly commandBus: CommandBus, - private readonly queryBus: QueryBus, - ) {} - - @ApiOperation({ summary: 'Retrieve one rotation' }) - @ApiBearerAuth('jwt') - @ApiParam({ name: 'operatorId', description: 'Operator unique identifier' }) - @ApiParam({ name: 'rotationId', description: 'Rotation unique identifier' }) - @ApiOkResponse({ type: GetRotationResponse }) - @ApiBadRequestResponse({ type: GenericBadRequestResponse }) - @ApiUnauthorizedResponse({ type: UnauthorizedResponse }) - @ApiForbiddenResponse({ type: ForbiddenResponse }) - @ApiNotFoundResponse({ type: GenericNotFoundResponse }) - @Get(':rotationId') - async findById( - @UuidParam('rotationId') rotationId: string, - @UuidParam('operatorId') operatorId: string, - ): Promise { - const query = new GetRotationByIdQuery(operatorId, rotationId); - return this.queryBus.execute(query); - } -} diff --git a/src/modules/operators/infra/http/action/rotation/list-rotations.action.ts b/src/modules/operators/infra/http/action/rotation/list-rotations.action.ts deleted file mode 100644 index 66140857..00000000 --- a/src/modules/operators/infra/http/action/rotation/list-rotations.action.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - ApiBearerAuth, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { Controller, Get } from '@nestjs/common'; -import { GetRotationResponse } from '../../request/rotation.request'; -import { UnauthorizedResponse } from '../../../../../../core/http/response/unauthorized.response'; -import { ForbiddenResponse } from '../../../../../../core/http/response/forbidden.response'; -import { GenericNotFoundResponse } from '../../../../../../core/http/response/not-found.response'; -import { CommandBus, QueryBus } from '@nestjs/cqrs'; -import { UuidParam } from '../../../../../../core/validation/uuid.param'; -import { ListAllRotationsQuery } from '../../../../application/query/rotation/list-all-rotations.query'; - -@ApiTags('operator rotations') -@Controller('/api/v1/operator/:operatorId/rotation') -export class ListRotationsAction { - constructor( - private readonly commandBus: CommandBus, - private readonly queryBus: QueryBus, - ) {} - - @ApiOperation({ summary: 'Retrieve all rotations' }) - @ApiBearerAuth('jwt') - @ApiParam({ name: 'operatorId', description: 'Operator unique identifier' }) - @ApiOkResponse({ type: GetRotationResponse, isArray: true }) - @ApiUnauthorizedResponse({ type: UnauthorizedResponse }) - @ApiForbiddenResponse({ type: ForbiddenResponse }) - @ApiNotFoundResponse({ type: GenericNotFoundResponse }) - @Get() - findAllForOperator( - @UuidParam('operatorId') operatorId: string, - ): Promise { - const query = new ListAllRotationsQuery(operatorId); - return this.queryBus.execute(query); - } -} diff --git a/src/modules/operators/infra/http/action/rotation/update-rotation.action.ts b/src/modules/operators/infra/http/action/rotation/update-rotation.action.ts deleted file mode 100644 index 6e23824d..00000000 --- a/src/modules/operators/infra/http/action/rotation/update-rotation.action.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiBody, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { Body, Controller, Patch } from '@nestjs/common'; -import { UserRole } from '../../../../../users/model/user-role'; -import { - GetRotationResponse, - UpdateRotationRequest, -} from '../../request/rotation.request'; -import { GenericBadRequestResponse } from '../../../../../../core/http/response/bad-request.response'; -import { UnauthorizedResponse } from '../../../../../../core/http/response/unauthorized.response'; -import { ForbiddenResponse } from '../../../../../../core/http/response/forbidden.response'; -import { GenericNotFoundResponse } from '../../../../../../core/http/response/not-found.response'; -import { Role } from '../../../../../../core/http/auth/decorator/role.decorator'; -import { CommandBus, QueryBus } from '@nestjs/cqrs'; -import { UuidParam } from '../../../../../../core/validation/uuid.param'; -import { GetRotationByIdQuery } from '../../../../application/query/rotation/get-rotation-by-id.query'; -import { UpdateRotationCommand } from '../../../../application/command/rotation/update-rotation.command'; - -@ApiTags('operator rotations') -@Controller('/api/v1/operator/:operatorId/rotation') -export class UpdateRotationAction { - constructor( - private readonly commandBus: CommandBus, - private readonly queryBus: QueryBus, - ) {} - - @ApiOperation({ - summary: 'Update a rotation', - description: - '**NOTE:** This endpoint is only available for users with `operations` role.', - }) - @ApiBearerAuth('jwt') - @ApiParam({ name: 'operatorId', description: 'Operator unique identifier' }) - @ApiParam({ name: 'rotationId', description: 'Rotation unique identifier' }) - @ApiBody({ type: UpdateRotationRequest }) - @ApiOkResponse({ type: GetRotationResponse }) - @ApiBadRequestResponse({ type: GenericBadRequestResponse }) - @ApiUnauthorizedResponse({ type: UnauthorizedResponse }) - @ApiForbiddenResponse({ type: ForbiddenResponse }) - @ApiNotFoundResponse({ type: GenericNotFoundResponse }) - @Patch(':rotationId') - @Role(UserRole.Operations) - async update( - @UuidParam('rotationId') rotationId: string, - @UuidParam('operatorId') operatorId: string, - @Body() request: UpdateRotationRequest, - ): Promise { - const command = new UpdateRotationCommand(operatorId, rotationId, request); - await this.commandBus.execute(command); - - const query = new GetRotationByIdQuery(operatorId, rotationId); - return this.queryBus.execute(query); - } -} diff --git a/src/modules/operators/infra/http/request/rotation.request.ts b/src/modules/operators/infra/http/request/rotation.request.ts deleted file mode 100644 index e3111523..00000000 --- a/src/modules/operators/infra/http/request/rotation.request.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { OmitType, PartialType, PickType } from '@nestjs/swagger'; -import { Rotation } from '../../../model/rotation.model'; - -export class CreateRotationRequest extends PickType(Rotation, [ - 'name', - 'pilotId', -]) {} - -export class UpdateRotationRequest extends PartialType(CreateRotationRequest) {} - -export class GetRotationResponse extends OmitType(Rotation, ['pilotId']) {} diff --git a/src/modules/operators/model/error/rotation.error.ts b/src/modules/operators/model/error/rotation.error.ts deleted file mode 100644 index 48b3e45a..00000000 --- a/src/modules/operators/model/error/rotation.error.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NotFoundError } from '../../../../core/errors/domain-error'; - -export class RotationNotFoundError extends NotFoundError { - constructor() { - super('Rotation with given ID not found.'); - } -} diff --git a/src/modules/operators/model/rotation.model.ts b/src/modules/operators/model/rotation.model.ts deleted file mode 100644 index 29fb3dd6..00000000 --- a/src/modules/operators/model/rotation.model.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Uuid } from '../../../core/types/id'; -import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; -import { PilotDto } from '../../users/infra/http/request/get-user.dto'; - -export type RotationId = Uuid & { _entity: '__rotation' }; - -export class FlightPreview { - @ApiProperty({ - description: 'Flight unique system identifier', - example: 'bd8f2d64-a647-42da-be63-c6589915e6c9', - }) - id!: string; - - @ApiProperty({ - description: 'Flight number', - example: 'DAL 1234', - }) - flightNumber!: string; -} - -export class Rotation { - @ApiProperty({ - description: 'Rotation unique system identifier', - example: 'bd8f2d64-a647-42da-be63-c6589915e6c9', - }) - id!: string; - - @ApiProperty({ - description: 'Rotation name', - example: '2025-01', - }) - @IsString() - @IsNotEmpty() - name!: string; - - @ApiProperty({ - description: 'Pilot unique system identifier', - example: 'bd8f2d64-a647-42da-be63-c6589915e6c9', - }) - @IsUUID(4) - @IsNotEmpty() - pilotId!: string; - - @ApiProperty({ - description: 'Pilot', - type: PilotDto, - }) - pilot!: PilotDto; - - @ApiProperty({ - description: 'Flights in this rotation', - type: FlightPreview, - isArray: true, - }) - flights!: FlightPreview[]; - - @ApiProperty({ - description: 'Creation timestamp', - example: '2025-01-01T00:00:00.000Z', - }) - createdAt!: Date; - - @ApiProperty({ - description: 'Last update timestamp', - example: '2025-01-01T00:00:00.000Z', - nullable: true, - type: Date, - }) - updatedAt!: Date | null; -} diff --git a/src/modules/operators/operators.module.ts b/src/modules/operators/operators.module.ts index 39cb61ae..c2382ed5 100644 --- a/src/modules/operators/operators.module.ts +++ b/src/modules/operators/operators.module.ts @@ -28,18 +28,6 @@ import { GetAircraftByRegistrationHandler } from './application/query/aircraft/g import { CheckAircraftExistsHandler } from './application/query/aircraft/check-aircraft-exists.query'; import { RemoveAircraftHandler } from './application/command/aircraft/remove-aircraft.command'; import { UpdateAircraftHandler } from './application/command/aircraft/update-aircraft.command'; -import { CreateRotationAction } from './infra/http/action/rotation/create-rotation.action'; -import { ListRotationsAction } from './infra/http/action/rotation/list-rotations.action'; -import { GetRotationAction } from './infra/http/action/rotation/get-rotation.action'; -import { UpdateRotationAction } from './infra/http/action/rotation/update-rotation.action'; -import { DeleteRotationAction } from './infra/http/action/rotation/delete-rotation.action'; -import { RotationsRepository } from './infra/database/repository/rotations.repository'; -import { CreateRotationHandler } from './application/command/rotation/create-rotation.command'; -import { GetRotationByIdHandler } from './application/query/rotation/get-rotation-by-id.query'; -import { ListAllRotationsQueryHandler } from './application/query/rotation/list-all-rotations.query'; -import { RemoveRotationHandler } from './application/command/rotation/remove-rotation.command'; -import { UpdateRotationHandler } from './application/command/rotation/update-rotation.command'; -import { AssertRotationExistsHandler } from './application/assert/assert-rotation-exists.query'; import { AircraftLifecycleListener } from './application/event/internal/aircraft-lifecycle.listener'; import { OperatorCacheListener } from './application/event/internal/operator-cache.listener'; import { FlightLifecycleListener } from './application/event/external/flight-lifecycle.listener'; @@ -71,11 +59,6 @@ import { ListFlightCrewQueryHandler } from './application/query/crew/list-flight UpdateAircraftAction, DeleteAircraftAction, GetAircraftFlightHistoryAction, - CreateRotationAction, - ListRotationsAction, - GetRotationAction, - UpdateRotationAction, - DeleteRotationAction, CreateAircraftRepositionAction, ListAircraftRepositionAction, ListOperatorCrewAction, @@ -83,7 +66,6 @@ import { ListFlightCrewQueryHandler } from './application/query/crew/list-flight providers: [ OperatorsRepository, AircraftRepository, - RotationsRepository, CreateOperatorHandler, UpdateOperatorHandler, RemoveOperatorHandler, @@ -98,12 +80,6 @@ import { ListFlightCrewQueryHandler } from './application/query/crew/list-flight UpdateAircraftHandler, GetAircraftByRegistrationHandler, CheckAircraftExistsHandler, - CreateRotationHandler, - UpdateRotationHandler, - RemoveRotationHandler, - GetRotationByIdHandler, - ListAllRotationsQueryHandler, - AssertRotationExistsHandler, AircraftLifecycleListener, OperatorCacheListener, FlightLifecycleListener, diff --git a/src/modules/users/application/event/external/flight-lifecycle.listener.ts b/src/modules/users/application/event/external/flight-lifecycle.listener.ts index f829193a..5c922f55 100644 --- a/src/modules/users/application/event/external/flight-lifecycle.listener.ts +++ b/src/modules/users/application/event/external/flight-lifecycle.listener.ts @@ -15,7 +15,6 @@ import { haversineDistanceNm } from '../../../../../core/utils/distance'; import { FlightSource, UserTravelType } from 'prisma/client/enums'; import { UserRole } from '../../../model/user-role'; import { GetFlightCompletionStatsQuery } from '../../../../flights/application/query/get-flight-completion-stats.query'; -import { GetLastFlightInRotationQuery } from '../../../../flights/application/query/rotation/get-last-flight-in-rotation.query'; import { GetRepositionDataQuery } from '../../../../flights/application/query/reposition/get-reposition-data.query'; type Coordinates = { latitude: number; longitude: number }; @@ -35,15 +34,6 @@ export class FlightLifecycleListener { await this.usersRepository.setCurrentFlight(userId, event.payload.flightId); await this.recordCheckInTravel(userId, event.payload.flightId); - - if (!event.payload.rotationId) { - return; - } - - await this.usersRepository.setCurrentRotation( - userId, - event.payload.rotationId, - ); } @OnEvent(FlightEventType.OnBlockWasReported) @@ -79,21 +69,6 @@ export class FlightLifecycleListener { const userId = event.payload.actorId as string; await this.usersRepository.setCurrentFlight(userId, null); - - if (!event.payload.rotationId) { - return; - } - - const lastFlightQuery = new GetLastFlightInRotationQuery( - event.payload.rotationId, - ); - const lastFlightInRotationId = await this.queryBus.execute(lastFlightQuery); - - if (lastFlightInRotationId !== event.payload.flightId) { - return; - } - - await this.usersRepository.setCurrentRotation(userId, null); } private async recordCheckInTravel( diff --git a/src/modules/users/infra/database/repository/users.repository.ts b/src/modules/users/infra/database/repository/users.repository.ts index 8bc6284b..33e419c9 100644 --- a/src/modules/users/infra/database/repository/users.repository.ts +++ b/src/modules/users/infra/database/repository/users.repository.ts @@ -221,18 +221,6 @@ export class UsersRepository { await this.cacheManager.del(cacheByUser(CACHE_KEYS.USER_ME, userId)); } - async setCurrentRotation( - userId: string, - rotationId: string | null, - ): Promise { - await this.prisma.user.update({ - where: { id: userId }, - data: { currentRotationId: rotationId }, - }); - - await this.cacheManager.del(cacheByUser(CACHE_KEYS.USER_ME, userId)); - } - async addCompletedFlightStats( userId: string, stats: { @@ -277,7 +265,6 @@ export class UsersRepository { role: user.role, pilotLicenseId: user.pilotLicenseId, currentFlightId: user.currentFlightId, - currentRotationId: user.currentRotationId, homeAirportId: user.homeAirportId, lastAirportId: user.lastAirportId, lastAirportUpdatedAt: user.lastAirportUpdatedAt, diff --git a/src/modules/users/model/user.model.ts b/src/modules/users/model/user.model.ts index f0747e2d..d3f5f4b5 100644 --- a/src/modules/users/model/user.model.ts +++ b/src/modules/users/model/user.model.ts @@ -83,14 +83,6 @@ export class User { }) currentFlightId!: string | null; - @ApiProperty({ - description: 'Current rotation user is performing', - example: '8f525809-9b37-4c2c-80c8-72be06023fd4', - type: 'string', - nullable: true, - }) - currentRotationId!: string | null; - @ApiProperty({ description: 'Home base airport of the pilot (required for CabinCrew)', example: 'ba9ac708-0cef-4d92-a824-4e95f60bd752',