diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 000000000..ed83212b5
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,29 @@
+name: .NET Tests
+
+on:
+ push:
+ branches: [ "main", "lab_1" ]
+ pull_request:
+ branches: [ "main", "lab_1" ]
+
+jobs:
+ build_and_test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore ./Airline.sln
+
+ - name: Build solution
+ run: dotnet build ./Airline.sln --no-restore
+
+ - name: Run tests
+ run: dotnet test ./Airline.Tests/Airline.Tests.csproj --no-build --verbosity detailed
\ No newline at end of file
diff --git a/Airline.Api.Host/Airline.Api.Host.csproj b/Airline.Api.Host/Airline.Api.Host.csproj
new file mode 100644
index 000000000..7491da9b3
--- /dev/null
+++ b/Airline.Api.Host/Airline.Api.Host.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Airline.Api.Host/Airline.Api.Host.http b/Airline.Api.Host/Airline.Api.Host.http
new file mode 100644
index 000000000..797dadd06
--- /dev/null
+++ b/Airline.Api.Host/Airline.Api.Host.http
@@ -0,0 +1,6 @@
+@Airline.Api.Host_HostAddress = http://localhost:5147
+
+GET {{Airline.Api.Host_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/Airline.Api.Host/Controllers/AnalyticController.cs b/Airline.Api.Host/Controllers/AnalyticController.cs
new file mode 100644
index 000000000..8522c52c6
--- /dev/null
+++ b/Airline.Api.Host/Controllers/AnalyticController.cs
@@ -0,0 +1,172 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Airline.Api.Host.Controllers;
+
+///
+/// Controller for executing analytical queries over airline operational data.
+/// Provides aggregated insights such as top flights, minimal travel time, passenger baggage analysis,
+/// and route-based reporting.
+///
+[ApiController]
+[Route("api/[controller]")]
+public class AnalyticsController(
+ IAnalyticsService analyticsService,
+ ILogger logger
+) : ControllerBase
+{
+ ///
+ /// Retrieves the top N flights by number of passengers.
+ ///
+ /// The number of top flights to return (default: 5).
+ ///
+ /// A list of flight DTOs sorted by passenger count in descending order.
+ ///
+ /// Returns the list of top flights.
+ /// If an unexpected error occurs during processing.
+ [HttpGet("top-flights-by-passenger-count")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetTopFlightsByPassengerCount([FromQuery] int top = 5)
+ {
+ logger.LogInformation("GetTopFlightsByPassengerCount method called with top={Top}", top);
+ try
+ {
+ var flights = await analyticsService.GetTopFlightsByPassengerCountAsync(top);
+ logger.LogInformation("GetTopFlightsByPassengerCount method completed successfully");
+ return Ok(flights);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetTopFlightsByPassengerCount method");
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Retrieves flights with the minimal travel time (duration).
+ ///
+ ///
+ /// A list of flight DTOs with the shortest duration.
+ ///
+ /// Returns the list of flights with minimal travel time.
+ /// If an unexpected error occurs during processing.
+ [HttpGet("flights-with-min-travel-time")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetFlightsWithMinTravelTime()
+ {
+ logger.LogInformation("GetFlightsWithMinTravelTime method called");
+ try
+ {
+ var flights = await analyticsService.GetFlightsWithMinTravelTimeAsync();
+ logger.LogInformation("GetFlightsWithMinTravelTime method completed successfully");
+ return Ok(flights);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetFlightsWithMinTravelTime method");
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Retrieves passengers with zero checked baggage for a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ ///
+ /// A list of passenger DTOs who have no checked baggage on the specified flight.
+ ///
+ /// Returns the list of passengers with zero baggage.
+ /// If the specified flight does not exist.
+ /// If an unexpected error occurs during processing.
+ [HttpGet("passengers-with-zero-baggage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetPassengersWithZeroBaggage([FromQuery] int flightId)
+ {
+ logger.LogInformation("GetPassengersWithZeroBaggage method called with flightId={FlightId}", flightId);
+ try
+ {
+ var passengers = await analyticsService.GetPassengersWithZeroBaggageOnFlightAsync(flightId);
+ logger.LogInformation("GetPassengersWithZeroBaggage method completed successfully for flightId={FlightId}", flightId);
+ return Ok(passengers);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Flight not found for flightId={FlightId}", flightId);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetPassengersWithZeroBaggage method for flightId={FlightId}", flightId);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Retrieves flights of a specific aircraft model within a given date period.
+ ///
+ /// The unique identifier of the aircraft model.
+ /// Start date of the period (inclusive).
+ /// End date of the period (inclusive).
+ ///
+ /// A list of flight DTOs matching the model and date range.
+ ///
+ /// Returns the list of flights matching the criteria.
+ /// If an unexpected error occurs during processing.
+ [HttpGet("flights-by-model-in-period")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetFlightsByModelInPeriod(
+ [FromQuery] int modelId,
+ [FromQuery] DateTime from,
+ [FromQuery] DateTime to)
+ {
+ logger.LogInformation("GetFlightsByModelInPeriod method called with modelId={ModelId}, from={From}, to={To}", modelId, from, to);
+ try
+ {
+ var flights = await analyticsService.GetFlightsByModelInPeriodAsync(modelId, from, to);
+ logger.LogInformation("GetFlightsByModelInPeriod method completed successfully");
+ return Ok(flights);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetFlightsByModelInPeriod method");
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Retrieves flights by route (departure city → arrival city).
+ ///
+ /// The city of departure.
+ /// The city of arrival.
+ ///
+ /// A list of flight DTOs matching the specified route.
+ ///
+ /// Returns the list of flights matching the route.
+ /// If an unexpected error occurs during processing.
+ [HttpGet("flights-by-route")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetFlightsByRoute(
+ [FromQuery] string departure,
+ [FromQuery] string arrival)
+ {
+ logger.LogInformation("GetFlightsByRoute method called with departure={Departure}, arrival={Arrival}", departure, arrival);
+ try
+ {
+ var flights = await analyticsService.GetFlightsByRouteAsync(departure, arrival);
+ logger.LogInformation("GetFlightsByRoute method completed successfully");
+ return Ok(flights);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetFlightsByRoute method");
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Airline.Api.Host/Controllers/BaseCrudController.cs b/Airline.Api.Host/Controllers/BaseCrudController.cs
new file mode 100644
index 000000000..c156df593
--- /dev/null
+++ b/Airline.Api.Host/Controllers/BaseCrudController.cs
@@ -0,0 +1,214 @@
+using Airline.Application.Contracts;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Airline.Api.Host.Controllers;
+
+///
+/// Base controller that provides standardized CRUD (Create, Read, Update, Delete) operations
+/// for entities in the airline management system.
+/// Implements common HTTP methods with unified error handling, logging, and response formatting.
+///
+///
+/// The Data Transfer Object (DTO) type used for read operations.
+/// Represents the shape of data exposed to API clients.
+///
+///
+/// The Data Transfer Object (DTO) type used for create and update operations.
+/// Contains only the fields required for mutation.
+///
+///
+/// The type of the unique identifier for the entity (e.g., ).
+/// Must be a value type ().
+///
+/// The application service that implements business logic.
+/// The logger instance for diagnostics and monitoring.
+[ApiController]
+[Route("api/[controller]")]
+public abstract class CrudControllerBase(
+ IApplicationService service,
+ ILogger> logger
+) : ControllerBase
+ where TDto : class
+ where TCreateUpdateDto : class
+ where TKey : struct
+{
+ ///
+ /// Creates a new entity.
+ ///
+ /// The data transfer object containing creation data.
+ ///
+ /// An representing the result of the operation:
+ ///
+ /// - 201 Created with the created entity if successful.
+ /// - 500 Internal Server Error if an exception occurs.
+ ///
+ ///
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> Create(TCreateUpdateDto dto)
+ {
+ logger.LogInformation("Create method called in {Controller} with data: {@Dto}", GetType().Name, dto);
+ try
+ {
+ var result = await service.CreateAsync(dto);
+ logger.LogInformation("Create method completed successfully in {Controller}", GetType().Name);
+ return CreatedAtAction(nameof(GetById), new { id = GetEntityId(result) }, result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in Create method of controller {Controller}", GetType().Name);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Updates an existing entity by its unique identifier.
+ ///
+ /// The unique identifier of the entity to update.
+ /// The data transfer object containing updated data.
+ ///
+ /// An representing the result of the operation:
+ ///
+ /// - 200 OK with the updated entity if successful.
+ /// - 404 Not Found if the entity does not exist.
+ /// - 500 Internal Server Error if an exception occurs.
+ ///
+ ///
+ [HttpPut("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> Update(TKey id, TCreateUpdateDto dto)
+ {
+ logger.LogInformation("Update method called in {Controller} with id={Id} and data: {@Dto}", GetType().Name, id, dto);
+ try
+ {
+ var result = await service.UpdateAsync(dto, id);
+ logger.LogInformation("Update method completed successfully in {Controller}", GetType().Name);
+ return Ok(result);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Entity with id={Id} not found in {Controller}", id, GetType().Name);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in Update method of controller {Controller}", GetType().Name);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Deletes an entity by its unique identifier.
+ ///
+ /// The unique identifier of the entity to delete.
+ ///
+ /// An representing the result of the operation:
+ ///
+ /// - 204 No Content if the entity was successfully deleted.
+ /// - 404 Not Found if the entity does not exist.
+ /// - 500 Internal Server Error if an exception occurs.
+ ///
+ ///
+ [HttpDelete("{id}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task Delete(TKey id)
+ {
+ logger.LogInformation("Delete method called in {Controller} with id={Id}", GetType().Name, id);
+ try
+ {
+ var success = await service.DeleteAsync(id);
+ if (!success)
+ {
+ logger.LogWarning("Entity with id={Id} not found during deletion in {Controller}", id, GetType().Name);
+ return NotFound();
+ }
+ logger.LogInformation("Delete method completed successfully in {Controller}", GetType().Name);
+ return NoContent();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in Delete method of controller {Controller}", GetType().Name);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Retrieves all entities of the specified type.
+ ///
+ ///
+ /// An representing the result of the operation:
+ ///
+ /// - 200 OK with the list of entities if successful.
+ /// - 500 Internal Server Error if an exception occurs.
+ ///
+ ///
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetAll()
+ {
+ logger.LogInformation("GetAll method called in {Controller}", GetType().Name);
+ try
+ {
+ var result = await service.GetAllAsync();
+ logger.LogInformation("GetAll method completed successfully in {Controller}", GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetAll method of controller {Controller}", GetType().Name);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Retrieves an entity by its unique identifier.
+ ///
+ /// The unique identifier of the entity to retrieve.
+ ///
+ /// An representing the result of the operation:
+ ///
+ /// - 200 OK with the entity if found.
+ /// - 404 Not Found if the entity does not exist.
+ /// - 500 Internal Server Error if an exception occurs.
+ ///
+ ///
+ [HttpGet("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> GetById(TKey id)
+ {
+ logger.LogInformation("GetById method called in {Controller} with id={Id}", GetType().Name, id);
+ try
+ {
+ var result = await service.GetByIdAsync(id);
+ if (result == null)
+ {
+ logger.LogWarning("Entity with id={Id} not found in {Controller}", id, GetType().Name);
+ return NotFound();
+ }
+ logger.LogInformation("GetById method completed successfully in {Controller}", GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetById method of controller {Controller}", GetType().Name);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Extracts the unique identifier from a DTO.
+ /// Must be implemented by derived controllers to support routing.
+ ///
+ /// The DTO from which to extract the identifier.
+ /// The unique identifier of the entity.
+ protected abstract TKey GetEntityId(TDto dto);
+}
\ No newline at end of file
diff --git a/Airline.Api.Host/Controllers/FlightController.cs b/Airline.Api.Host/Controllers/FlightController.cs
new file mode 100644
index 000000000..eeb56a7d9
--- /dev/null
+++ b/Airline.Api.Host/Controllers/FlightController.cs
@@ -0,0 +1,122 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+using Airline.Application.Contracts.PlaneModel;
+using Airline.Application.Contracts.Ticket;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Airline.Api.Host.Controllers;
+
+///
+/// Controller for managing flights and retrieving associated data.
+/// Inherits from
+/// to provide standardized CRUD operations.
+///
+[Route("api/[controller]")]
+public class FlightsController(
+ IFlightService flightService,
+ ILogger logger
+) : CrudControllerBase(flightService, logger)
+{
+ ///
+ protected override int GetEntityId(FlightDto dto) => dto.Id;
+
+ ///
+ /// Retrieves the aircraft model associated with a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// The aircraft model DTO linked to the flight.
+ /// Returns the associated aircraft model.
+ /// If the flight or aircraft model is not found.
+ /// If an unexpected error occurs.
+ [HttpGet("{flightId}/model")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> GetPlaneModel(int flightId)
+ {
+ logger.LogInformation("GetPlaneModel method called with flightId={FlightId}", flightId);
+ try
+ {
+ var model = await flightService.GetPlaneModelAsync(flightId);
+ logger.LogInformation("GetPlaneModel method completed successfully for flightId={FlightId}", flightId);
+ return Ok(model);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Aircraft model not found for flightId={FlightId}", flightId);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetPlaneModel method for flightId={FlightId}", flightId);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Retrieves all tickets associated with a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// A list of ticket DTOs linked to the flight.
+ /// Returns the list of associated tickets.
+ /// If the flight is not found.
+ /// If an unexpected error occurs.
+ [HttpGet("{flightId}/tickets")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetTickets(int flightId)
+ {
+ logger.LogInformation("GetTickets method called with flightId={FlightId}", flightId);
+ try
+ {
+ var tickets = await flightService.GetTicketsAsync(flightId);
+ logger.LogInformation("GetTickets method completed successfully for flightId={FlightId}", flightId);
+ return Ok(tickets);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Tickets not found for flightId={FlightId}", flightId);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetTickets method for flightId={FlightId}", flightId);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Retrieves all passengers associated with a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// A list of passenger DTOs linked to the flight.
+ /// Returns the list of associated passengers.
+ /// If the flight is not found.
+ /// If an unexpected error occurs.
+ [HttpGet("{flightId}/passengers")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetPassengers(int flightId)
+ {
+ logger.LogInformation("GetPassengers method called with flightId={FlightId}", flightId);
+ try
+ {
+ var passengers = await flightService.GetPassengersAsync(flightId);
+ logger.LogInformation("GetPassengers method completed successfully for flightId={FlightId}", flightId);
+ return Ok(passengers);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Passengers not found for flightId={FlightId}", flightId);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetPassengers method for flightId={FlightId}", flightId);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Airline.Api.Host/Controllers/ModelFamilyController.cs b/Airline.Api.Host/Controllers/ModelFamilyController.cs
new file mode 100644
index 000000000..b5fcd8ff5
--- /dev/null
+++ b/Airline.Api.Host/Controllers/ModelFamilyController.cs
@@ -0,0 +1,54 @@
+using Airline.Application.Contracts.ModelFamily;
+using Airline.Application.Contracts.PlaneModel;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Airline.Api.Host.Controllers;
+
+///
+/// Controller for managing aircraft model families and retrieving associated data.
+/// Inherits from
+/// to provide standardized CRUD operations.
+///
+[Route("api/[controller]")]
+public class ModelFamiliesController(
+ IModelFamilyService modelFamilyService,
+ ILogger logger
+) : CrudControllerBase(modelFamilyService, logger)
+{
+ ///
+ protected override int GetEntityId(ModelFamilyDto dto) => dto.Id;
+
+ ///
+ /// Retrieves all aircraft models associated with a specific model family.
+ ///
+ /// The unique identifier of the model family.
+ /// A list of aircraft model DTOs linked to the family.
+ /// Returns the list of associated aircraft models.
+ /// If the model family is not found.
+ /// If an unexpected error occurs.
+ [HttpGet("{familyId}/models")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetPlaneModels(int familyId)
+ {
+ logger.LogInformation("GetPlaneModels method called with familyId={FamilyId}", familyId);
+ try
+ {
+ var models = await modelFamilyService.GetPlaneModelsAsync(familyId);
+ logger.LogInformation("GetPlaneModels method completed successfully for familyId={FamilyId}", familyId);
+ return Ok(models);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Aircraft models not found for familyId={FamilyId}", familyId);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetPlaneModels method for familyId={FamilyId}", familyId);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Airline.Api.Host/Controllers/PassengerController.cs b/Airline.Api.Host/Controllers/PassengerController.cs
new file mode 100644
index 000000000..e6065d7df
--- /dev/null
+++ b/Airline.Api.Host/Controllers/PassengerController.cs
@@ -0,0 +1,88 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+using Airline.Application.Contracts.Ticket;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Airline.Api.Host.Controllers;
+
+///
+/// Controller for managing passengers and retrieving associated data.
+/// Inherits from
+/// to provide standardized CRUD operations.
+///
+[Route("api/[controller]")]
+public class PassengersController(
+ IPassengerService passengerService,
+ ILogger logger
+) : CrudControllerBase(passengerService, logger)
+{
+ ///
+ protected override int GetEntityId(PassengerDto dto) => dto.Id;
+
+ ///
+ /// Retrieves all tickets associated with a specific passenger.
+ ///
+ /// The unique identifier of the passenger.
+ /// A list of ticket DTOs linked to the passenger.
+ /// Returns the list of associated tickets.
+ /// If the passenger is not found.
+ /// If an unexpected error occurs.
+ [HttpGet("{passengerId}/tickets")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetTickets(int passengerId)
+ {
+ logger.LogInformation("GetTickets method called with passengerId={PassengerId}", passengerId);
+ try
+ {
+ var tickets = await passengerService.GetTicketsAsync(passengerId);
+ logger.LogInformation("GetTickets method completed successfully for passengerId={PassengerId}", passengerId);
+ return Ok(tickets);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Tickets not found for passengerId={PassengerId}", passengerId);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetTickets method for passengerId={PassengerId}", passengerId);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Retrieves all flights associated with a specific passenger.
+ ///
+ /// The unique identifier of the passenger.
+ /// A list of flight DTOs linked to the passenger.
+ /// Returns the list of associated flights.
+ /// If the passenger is not found.
+ /// If an unexpected error occurs.
+ [HttpGet("{passengerId}/flights")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetFlights(int passengerId)
+ {
+ logger.LogInformation("GetFlights method called with passengerId={PassengerId}", passengerId);
+ try
+ {
+ var flights = await passengerService.GetFlightsAsync(passengerId);
+ logger.LogInformation("GetFlights method completed successfully for passengerId={PassengerId}", passengerId);
+ return Ok(flights);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Flights not found for passengerId={PassengerId}", passengerId);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetFlights method for passengerId={PassengerId}", passengerId);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Airline.Api.Host/Controllers/PlaneModelController.cs b/Airline.Api.Host/Controllers/PlaneModelController.cs
new file mode 100644
index 000000000..2e4e0d2dc
--- /dev/null
+++ b/Airline.Api.Host/Controllers/PlaneModelController.cs
@@ -0,0 +1,54 @@
+using Airline.Application.Contracts.ModelFamily;
+using Airline.Application.Contracts.PlaneModel;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Airline.Api.Host.Controllers;
+
+///
+/// Controller for managing aircraft models and retrieving associated data.
+/// Inherits from
+/// to provide standardized CRUD operations.
+///
+[Route("api/[controller]")]
+public class PlaneModelsController(
+ IPlaneModelService planeModelService,
+ ILogger logger
+) : CrudControllerBase(planeModelService, logger)
+{
+ ///
+ protected override int GetEntityId(PlaneModelDto dto) => dto.Id;
+
+ ///
+ /// Retrieves the model family associated with a specific aircraft model.
+ ///
+ /// The unique identifier of the aircraft model.
+ /// The model family DTO linked to the aircraft model.
+ /// Returns the associated model family.
+ /// If the aircraft model or model family is not found.
+ /// If an unexpected error occurs.
+ [HttpGet("{modelId}/family")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> GetModelFamily(int modelId)
+ {
+ logger.LogInformation("GetModelFamily method called with modelId={ModelId}", modelId);
+ try
+ {
+ var family = await planeModelService.GetModelFamilyAsync(modelId);
+ logger.LogInformation("GetModelFamily method completed successfully for modelId={ModelId}", modelId);
+ return Ok(family);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Model family not found for modelId={ModelId}", modelId);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetModelFamily method for modelId={ModelId}", modelId);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Airline.Api.Host/Controllers/TicketController.cs b/Airline.Api.Host/Controllers/TicketController.cs
new file mode 100644
index 000000000..2fea0bf3f
--- /dev/null
+++ b/Airline.Api.Host/Controllers/TicketController.cs
@@ -0,0 +1,88 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+using Airline.Application.Contracts.Ticket;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Airline.Api.Host.Controllers;
+
+///
+/// Controller for managing tickets and retrieving associated data.
+/// Inherits from
+/// to provide standardized CRUD operations.
+///
+[Route("api/[controller]")]
+public class TicketsController(
+ ITicketService ticketService,
+ ILogger logger
+) : CrudControllerBase(ticketService, logger)
+{
+ ///
+ protected override int GetEntityId(TicketDto dto) => dto.Id;
+
+ ///
+ /// Retrieves the flight associated with a specific ticket.
+ ///
+ /// The unique identifier of the ticket.
+ /// The flight DTO linked to the ticket.
+ /// Returns the associated flight.
+ /// If the ticket or flight is not found.
+ /// If an unexpected error occurs.
+ [HttpGet("{ticketId}/flight")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> GetFlight(int ticketId)
+ {
+ logger.LogInformation("GetFlight method called with ticketId={TicketId}", ticketId);
+ try
+ {
+ var flight = await ticketService.GetFlightAsync(ticketId);
+ logger.LogInformation("GetFlight method completed successfully for ticketId={TicketId}", ticketId);
+ return Ok(flight);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Flight not found for ticketId={TicketId}", ticketId);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetFlight method for ticketId={TicketId}", ticketId);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Retrieves the passenger associated with a specific ticket.
+ ///
+ /// The unique identifier of the ticket.
+ /// The passenger DTO linked to the ticket.
+ /// Returns the associated passenger.
+ /// If the ticket or passenger is not found.
+ /// If an unexpected error occurs.
+ [HttpGet("{ticketId}/passenger")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> GetPassenger(int ticketId)
+ {
+ logger.LogInformation("GetPassenger method called with ticketId={TicketId}", ticketId);
+ try
+ {
+ var passenger = await ticketService.GetPassengerAsync(ticketId);
+ logger.LogInformation("GetPassenger method completed successfully for ticketId={TicketId}", ticketId);
+ return Ok(passenger);
+ }
+ catch (KeyNotFoundException)
+ {
+ logger.LogWarning("Passenger not found for ticketId={TicketId}", ticketId);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in GetPassenger method for ticketId={TicketId}", ticketId);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs
new file mode 100644
index 000000000..837b54852
--- /dev/null
+++ b/Airline.Api.Host/Program.cs
@@ -0,0 +1,145 @@
+using Airline.Application;
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.ModelFamily;
+using Airline.Application.Contracts.Passenger;
+using Airline.Application.Contracts.PlaneModel;
+using Airline.Application.Contracts.Ticket;
+using Airline.Application.Services;
+using Airline.Domain;
+using Airline.Domain.DataSeed;
+using Airline.Domain.Items;
+using Airline.Infrastructure.EfCore;
+using Airline.Infrastructure.EfCore.Repositories;
+using Airline.Infrastructure.Kafka;
+using Airline.Infrastructure.Kafka.Deserializers;
+using Airline.ServiceDefaults;
+using Microsoft.EntityFrameworkCore;
+using MongoDB.Driver;
+using System.Text.Json.Serialization;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.Services.AddSingleton();
+
+builder.Services.AddAutoMapper(config =>
+{
+ config.AddProfile(new AirlineProfile());
+});
+
+// Repositories
+builder.Services.AddTransient, FlightRepository>();
+builder.Services.AddTransient, PassengerRepository>();
+builder.Services.AddTransient, TicketRepository>();
+builder.Services.AddTransient, PlaneModelRepository>();
+builder.Services.AddTransient, ModelFamilyRepository>();
+
+// Application Services
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+// Controllers
+builder.Services.AddControllers()
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
+ });
+
+// Swagger/OpenAPI
+builder.Services.AddEndpointsApiExplorer();
+
+builder.Services.AddSwaggerGen(c =>
+{
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => a.GetName().Name!.StartsWith("Airline"))
+ .Distinct();
+
+ foreach (var assembly in assemblies)
+ {
+ var xmlFile = $"{assembly.GetName().Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+ if (File.Exists(xmlPath))
+ c.IncludeXmlComments(xmlPath);
+ }
+});
+
+
+// MongoDB
+builder.AddMongoDBClient("airlineClient");
+
+builder.Services.AddDbContext((services, o) =>
+{
+ var client = services.GetRequiredService();
+ o.UseMongoDB(client, "db");
+});
+
+// Kafka Consumer
+builder.Services.AddHostedService();
+builder.AddKafkaConsumer>("airline-kafka",
+ configureBuilder: kafkaBuilder =>
+ {
+ kafkaBuilder.SetKeyDeserializer(new AirlineKeyDeserializer());
+ kafkaBuilder.SetValueDeserializer(new AirlineValueDeserializer());
+ },
+ configureSettings: settings =>
+ {
+ settings.Config.GroupId = "airline-consumer";
+ settings.Config.AutoOffsetReset = Confluent.Kafka.AutoOffsetReset.Earliest;
+ });
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI(c =>
+ {
+ c.SwaggerEndpoint("/swagger/v1/swagger.json", "Airline API v1");
+ });
+}
+
+using (var scope = app.Services.CreateScope())
+{
+ var dbContext = scope.ServiceProvider.GetRequiredService();
+ var seed = scope.ServiceProvider.GetRequiredService();
+
+ var flightsExist = dbContext.Flights.Any();
+ if (!flightsExist)
+ {
+ // ModelFamilies
+ foreach (var family in seed.ModelFamilies)
+ await dbContext.ModelFamilies.AddAsync(family);
+
+ // PlaneModels
+ foreach (var model in seed.PlaneModels)
+ await dbContext.PlaneModels.AddAsync(model);
+
+ // Passengers
+ foreach (var passenger in seed.Passengers)
+ await dbContext.Passengers.AddAsync(passenger);
+
+ // Flights
+ foreach (var flight in seed.Flights)
+ await dbContext.Flights.AddAsync(flight);
+
+ // Tickets
+ foreach (var ticket in seed.Tickets)
+ await dbContext.Tickets.AddAsync(ticket);
+
+ await dbContext.SaveChangesAsync();
+ app.Logger.LogInformation("Database successfully populated with test data.");
+ }
+}
+
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.MapControllers();
+
+app.Run();
\ No newline at end of file
diff --git a/Airline.Api.Host/Properties/launchSettings.json b/Airline.Api.Host/Properties/launchSettings.json
new file mode 100644
index 000000000..43157dc31
--- /dev/null
+++ b/Airline.Api.Host/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:53894",
+ "sslPort": 44359
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5147",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7089;http://localhost:5147",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Airline.Api.Host/appsettings.Development.json b/Airline.Api.Host/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/Airline.Api.Host/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Airline.Api.Host/appsettings.json b/Airline.Api.Host/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/Airline.Api.Host/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/Airline.AppHost/Airline.AppHost.csproj b/Airline.AppHost/Airline.AppHost.csproj
new file mode 100644
index 000000000..40fa4d116
--- /dev/null
+++ b/Airline.AppHost/Airline.AppHost.csproj
@@ -0,0 +1,26 @@
+
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ c5640d6f-84de-4448-bbef-056105f657a0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Airline.AppHost/AppHost.cs b/Airline.AppHost/AppHost.cs
new file mode 100644
index 000000000..88bdd1e8d
--- /dev/null
+++ b/Airline.AppHost/AppHost.cs
@@ -0,0 +1,24 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var db = builder.AddMongoDB("mongo")
+ .AddDatabase("db");
+
+var kafka = builder.AddKafka("airline-kafka", 9092)
+ .WithKafkaUI();
+
+var apiHost = builder.AddProject("airline-api-host")
+ .WithReference(db, "airlineClient")
+ .WithReference(kafka)
+ .WithEnvironment("KAFKA_BOOTSTRAP_SERVERS", kafka.GetEndpoint("tcp"))
+ .WithEnvironment("Kafka:TopicName", "airline-contracts")
+ .WaitFor(db)
+ .WaitFor(kafka);
+
+builder.AddProject("airline-generator-kafka-host")
+ .WithReference(kafka)
+ .WithEnvironment("KAFKA_BOOTSTRAP_SERVERS", kafka.GetEndpoint("tcp"))
+ .WithEnvironment("Kafka:TopicName", "airline-contracts")
+ .WaitFor(kafka)
+ .WaitFor(apiHost);
+
+builder.Build().Run();
\ No newline at end of file
diff --git a/Airline.AppHost/Properties/launchSettings.json b/Airline.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..643d26e64
--- /dev/null
+++ b/Airline.AppHost/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17255;http://localhost:15201",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21277",
+ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23151",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22297"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15201",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19082",
+ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18107",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20078"
+ }
+ }
+ }
+}
diff --git a/Airline.AppHost/appsettings.Development.json b/Airline.AppHost/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/Airline.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Airline.AppHost/appsettings.json b/Airline.AppHost/appsettings.json
new file mode 100644
index 000000000..31c092aa4
--- /dev/null
+++ b/Airline.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/Airline.Application.Contracts/Airline.Application.Contracts.csproj b/Airline.Application.Contracts/Airline.Application.Contracts.csproj
new file mode 100644
index 000000000..90b8a4194
--- /dev/null
+++ b/Airline.Application.Contracts/Airline.Application.Contracts.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
diff --git a/Airline.Application.Contracts/Flight/CreateFlightDto.cs b/Airline.Application.Contracts/Flight/CreateFlightDto.cs
new file mode 100644
index 000000000..8ac342923
--- /dev/null
+++ b/Airline.Application.Contracts/Flight/CreateFlightDto.cs
@@ -0,0 +1,20 @@
+namespace Airline.Application.Contracts.Flight;
+
+///
+/// Data Transfer Object (DTO) for creating a new flight.
+/// Contains scheduling and routing information for an airline flight.
+///
+/// The flight code (e.g., "SU101").
+/// The city of departure.
+/// The city of arrival.
+/// The scheduled departure date and time (local time).
+/// The scheduled arrival date and time (local time).
+/// The unique identifier of the associated aircraft model.
+public record CreateFlightDto(
+ string FlightCode,
+ string DepartureCity,
+ string ArrivalCity,
+ DateTime DepartureDateTime,
+ DateTime ArrivalDateTime,
+ int ModelId
+);
\ No newline at end of file
diff --git a/Airline.Application.Contracts/Flight/FlightDto.cs b/Airline.Application.Contracts/Flight/FlightDto.cs
new file mode 100644
index 000000000..48f03809b
--- /dev/null
+++ b/Airline.Application.Contracts/Flight/FlightDto.cs
@@ -0,0 +1,22 @@
+namespace Airline.Application.Contracts.Flight;
+
+///
+/// Data Transfer Object (DTO) representing a flight in the airline system.
+/// Used for read operations and data exchange with clients.
+///
+/// The unique identifier of the flight.
+/// The flight code (e.g., "SU101").
+/// The city of departure.
+/// The city of arrival.
+/// The scheduled departure date and time (local time).
+/// The scheduled arrival date and time (local time).
+/// The unique identifier of the associated aircraft model.
+public record FlightDto(
+ int Id,
+ string FlightCode,
+ string DepartureCity,
+ string ArrivalCity,
+ DateTime DepartureDateTime,
+ DateTime ArrivalDateTime,
+ int ModelId
+);
\ No newline at end of file
diff --git a/Airline.Application.Contracts/Flight/IFlightService.cs b/Airline.Application.Contracts/Flight/IFlightService.cs
new file mode 100644
index 000000000..e6cbf03e2
--- /dev/null
+++ b/Airline.Application.Contracts/Flight/IFlightService.cs
@@ -0,0 +1,33 @@
+using Airline.Application.Contracts.Passenger;
+using Airline.Application.Contracts.PlaneModel;
+using Airline.Application.Contracts.Ticket;
+
+namespace Airline.Application.Contracts.Flight;
+
+///
+/// Service contract for managing flights in the airline system.
+/// Extends the generic CRUD interface with flight-specific analytical operations.
+///
+public interface IFlightService : IApplicationService
+{
+ ///
+ /// Retrieves the aircraft model associated with a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// The aircraft model DTO linked to the flight.
+ public Task GetPlaneModelAsync(int flightId);
+
+ ///
+ /// Retrieves all tickets associated with a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// A list of ticket DTOs linked to the flight.
+ public Task> GetTicketsAsync(int flightId);
+
+ ///
+ /// Retrieves all passengers associated with a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// A list of passenger DTOs linked to the flight.
+ public Task> GetPassengersAsync(int flightId);
+}
\ No newline at end of file
diff --git a/Airline.Application.Contracts/IAnalyticService.cs b/Airline.Application.Contracts/IAnalyticService.cs
new file mode 100644
index 000000000..a8020624e
--- /dev/null
+++ b/Airline.Application.Contracts/IAnalyticService.cs
@@ -0,0 +1,46 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+
+///
+/// Defines a service contract for analytical and reporting operations over airline data.
+/// Provides aggregated queries for business intelligence, operational insights, and passenger analytics.
+///
+public interface IAnalyticsService
+{
+ ///
+ /// Retrieves the top N flights by number of passengers.
+ ///
+ /// The number of top flights to return (default: 5).
+ /// A list of flight DTOs sorted by passenger count in descending order.
+ public Task> GetTopFlightsByPassengerCountAsync(int top = 5);
+
+ ///
+ /// Retrieves flights with the minimal travel time (duration).
+ ///
+ /// A list of flight DTOs with the shortest duration.
+ public Task> GetFlightsWithMinTravelTimeAsync();
+
+ ///
+ /// Retrieves passengers with zero baggage (no checked luggage) for a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// A list of passenger DTOs who have no baggage on the specified flight.
+ public Task> GetPassengersWithZeroBaggageOnFlightAsync(int flightId);
+
+ ///
+ /// Retrieves flights of a specific aircraft model within a given date period.
+ ///
+ /// The unique identifier of the aircraft model.
+ /// Start date of the period (inclusive).
+ /// End date of the period (inclusive).
+ /// A list of flight DTOs matching the model and date range.
+ public Task> GetFlightsByModelInPeriodAsync(int modelId, DateTime from, DateTime to);
+
+ ///
+ /// Retrieves flights by route (departure city to arrival city).
+ ///
+ /// The city of departure.
+ /// The city of arrival.
+ /// A list of flight DTOs matching the specified route.
+ public Task> GetFlightsByRouteAsync(string departure, string arrival);
+}
\ No newline at end of file
diff --git a/Airline.Application.Contracts/IApplicationService.cs b/Airline.Application.Contracts/IApplicationService.cs
new file mode 100644
index 000000000..ebd069f93
--- /dev/null
+++ b/Airline.Application.Contracts/IApplicationService.cs
@@ -0,0 +1,62 @@
+namespace Airline.Application.Contracts;
+
+///
+/// Defines application service interface that provides
+/// standardized CRUD (Create, Read, Update, Delete) operations for domain entities.
+/// Serves as a contract between the application layer and presentation layer.
+///
+///
+/// The Data Transfer Object (DTO) type used for read operations.
+/// Represents the shape of data exposed to clients.
+///
+///
+/// The Data Transfer Object (DTO) type used for create and update operations.
+/// Contains only the fields required for mutation.
+///
+///
+/// The type of the unique identifier for the entity (e.g., , ).
+/// Must be a value type ().
+///
+public interface IApplicationService
+ where TDto : class
+ where TCreateUpdateDto : class
+ where TKey : struct
+{
+ ///
+ /// Creates a new entity in the system.
+ ///
+ /// The data transfer object containing creation data.
+ /// The created entity as a DTO, typically with an assigned identifier.
+ public Task CreateAsync(TCreateUpdateDto dto);
+
+ ///
+ /// Retrieves an entity by its unique identifier.
+ ///
+ /// The unique identifier of the entity to retrieve.
+ /// The entity as a DTO if found; otherwise, .
+ public Task GetByIdAsync(TKey id);
+
+ ///
+ /// Retrieves all entities of the specified type.
+ ///
+ /// A list of all entities as DTOs.
+ public Task> GetAllAsync();
+
+ ///
+ /// Updates an existing entity with new data.
+ ///
+ /// The data transfer object containing updated data.
+ /// The unique identifier of the entity to update.
+ /// The updated entity as a DTO.
+ public Task UpdateAsync(TCreateUpdateDto dto, TKey id);
+
+ ///
+ /// Deletes an entity by its unique identifier.
+ ///
+ /// The unique identifier of the entity to delete.
+ ///
+ /// if the entity was successfully deleted;
+ /// otherwise, .
+ ///
+ public Task DeleteAsync(TKey id);
+}
\ No newline at end of file
diff --git a/Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs b/Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs
new file mode 100644
index 000000000..c0503e648
--- /dev/null
+++ b/Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs
@@ -0,0 +1,12 @@
+namespace Airline.Application.Contracts.ModelFamily;
+
+///
+/// Data Transfer Object (DTO) for creating a new aircraft model family.
+/// Represents a group of aircraft models with common design features.
+///
+/// The name of the aircraft model family (e.g., "A320 Family").
+/// The name of the manufacturer (e.g., "Airbus", "Boeing").
+public record CreateModelFamilyDto(
+ string NameOfFamily,
+ string ManufacturerName
+);
\ No newline at end of file
diff --git a/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs b/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs
new file mode 100644
index 000000000..d90168ae0
--- /dev/null
+++ b/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs
@@ -0,0 +1,17 @@
+using Airline.Application.Contracts.PlaneModel;
+
+namespace Airline.Application.Contracts.ModelFamily;
+
+///
+/// Service contract for managing aircraft model families in the airline system.
+/// Extends the generic CRUD interface with family-specific analytical operations.
+///
+public interface IModelFamilyService : IApplicationService
+{
+ ///
+ /// Retrieves all aircraft models associated with a specific model family.
+ ///
+ /// The unique identifier of the model family.
+ /// A list of aircraft model DTOs linked to the family.
+ public Task> GetPlaneModelsAsync(int familyId);
+}
\ No newline at end of file
diff --git a/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs b/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs
new file mode 100644
index 000000000..930e74143
--- /dev/null
+++ b/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs
@@ -0,0 +1,14 @@
+namespace Airline.Application.Contracts.ModelFamily;
+
+///
+/// Data Transfer Object (DTO) representing an aircraft model family in the airline system.
+/// Used for read operations and data exchange with clients.
+///
+/// The unique identifier of the model family.
+/// The name of the aircraft model family (e.g., "A320 Family").
+/// The name of the manufacturer (e.g., "Airbus", "Boeing").
+public record ModelFamilyDto(
+ int Id,
+ string NameOfFamily,
+ string ManufacturerName
+);
\ No newline at end of file
diff --git a/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs b/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs
new file mode 100644
index 000000000..5c4f25168
--- /dev/null
+++ b/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs
@@ -0,0 +1,14 @@
+namespace Airline.Application.Contracts.Passenger;
+
+///
+/// Data Transfer Object (DTO) for creating a new passenger.
+/// Contains personal identification and demographic data.
+///
+/// The passport number of the passenger.
+/// The full name of the passenger (must not contain digits).
+/// The date of birth of the passenger.
+public record CreatePassengerDto(
+ string Passport,
+ string PassengerName,
+ DateOnly DateOfBirth
+);
\ No newline at end of file
diff --git a/Airline.Application.Contracts/Passenger/IPassengerService.cs b/Airline.Application.Contracts/Passenger/IPassengerService.cs
new file mode 100644
index 000000000..203cc53d0
--- /dev/null
+++ b/Airline.Application.Contracts/Passenger/IPassengerService.cs
@@ -0,0 +1,25 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Ticket;
+
+namespace Airline.Application.Contracts.Passenger;
+
+///
+/// Service contract for managing passengers in the airline system.
+/// Extends the generic CRUD interface with passenger-specific analytical operations.
+///
+public interface IPassengerService : IApplicationService
+{
+ ///
+ /// Retrieves all tickets associated with a specific passenger.
+ ///
+ /// The unique identifier of the passenger.
+ /// A list of ticket DTOs linked to the passenger.
+ public Task> GetTicketsAsync(int passengerId);
+
+ ///
+ /// Retrieves all flights associated with a specific passenger.
+ ///
+ /// The unique identifier of the passenger.
+ /// A list of flight DTOs linked to the passenger.
+ public Task> GetFlightsAsync(int passengerId);
+}
\ No newline at end of file
diff --git a/Airline.Application.Contracts/Passenger/PassengerDto.cs b/Airline.Application.Contracts/Passenger/PassengerDto.cs
new file mode 100644
index 000000000..abc7a6b23
--- /dev/null
+++ b/Airline.Application.Contracts/Passenger/PassengerDto.cs
@@ -0,0 +1,16 @@
+namespace Airline.Application.Contracts.Passenger;
+
+///
+/// Data Transfer Object (DTO) representing a passenger in the airline system.
+/// Used for read operations and data exchange with clients.
+///
+/// The unique identifier of the passenger.
+/// The passport number of the passenger.
+/// The full name of the passenger.
+/// The date of birth of the passenger.
+public record PassengerDto(
+ int Id,
+ string Passport,
+ string PassengerName,
+ DateOnly DateOfBirth
+);
\ No newline at end of file
diff --git a/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs b/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs
new file mode 100644
index 000000000..cf8f6a06a
--- /dev/null
+++ b/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs
@@ -0,0 +1,18 @@
+namespace Airline.Application.Contracts.PlaneModel;
+
+///
+/// Data Transfer Object (DTO) for creating a new aircraft model.
+/// Contains technical specifications and references to its model family.
+///
+/// The name of the aircraft model (e.g., "A320").
+/// The unique identifier of the associated model family.
+/// The maximum flight range in kilometers (must be non-negative).
+/// The number of passenger seats (must be non-negative).
+/// The cargo capacity in tons (must be non-negative).
+public record CreatePlaneModelDto(
+ string ModelName,
+ int ModelFamilyId,
+ double MaxRange,
+ double PassengerCapacity,
+ double CargoCapacity
+);
\ No newline at end of file
diff --git a/Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs b/Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs
new file mode 100644
index 000000000..3461b208e
--- /dev/null
+++ b/Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs
@@ -0,0 +1,17 @@
+using Airline.Application.Contracts.ModelFamily;
+
+namespace Airline.Application.Contracts.PlaneModel;
+
+///
+/// Service contract for managing aircraft models in the airline system.
+/// Extends the generic CRUD interface with model-specific analytical operations.
+///
+public interface IPlaneModelService : IApplicationService
+{
+ ///
+ /// Retrieves the model family associated with a specific aircraft model.
+ ///
+ /// The unique identifier of the aircraft model.
+ /// The model family DTO linked to the aircraft model.
+ public Task GetModelFamilyAsync(int modelId);
+}
\ No newline at end of file
diff --git a/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs b/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs
new file mode 100644
index 000000000..5590a56d1
--- /dev/null
+++ b/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs
@@ -0,0 +1,20 @@
+namespace Airline.Application.Contracts.PlaneModel;
+
+///
+/// Data Transfer Object (DTO) representing an aircraft model in the airline system.
+/// Used for read operations and data exchange with clients.
+///
+/// The unique identifier of the aircraft model.
+/// The name of the aircraft model (e.g., "A320").
+/// The unique identifier of the associated model family.
+/// The maximum flight range in kilometers.
+/// The number of passenger seats.
+/// The cargo capacity in tons.
+public record PlaneModelDto(
+ int Id,
+ string ModelName,
+ int ModelFamilyId,
+ double MaxRange,
+ double PassengerCapacity,
+ double CargoCapacity
+);
\ No newline at end of file
diff --git a/Airline.Application.Contracts/Ticket/CreateTicketDto.cs b/Airline.Application.Contracts/Ticket/CreateTicketDto.cs
new file mode 100644
index 000000000..11ca5e2be
--- /dev/null
+++ b/Airline.Application.Contracts/Ticket/CreateTicketDto.cs
@@ -0,0 +1,18 @@
+namespace Airline.Application.Contracts.Ticket;
+
+///
+/// Data Transfer Object (DTO) for creating a new ticket.
+/// Contains all required fields for ticket creation in the airline system.
+///
+/// The unique identifier of the associated flight.
+/// The unique identifier of the associated passenger.
+/// The assigned seat number (e.g., "12A").
+/// Indicates whether hand luggage is carried.
+/// The total checked baggage weight in kilograms (null if no baggage).
+public record CreateTicketDto(
+ int FlightId,
+ int PassengerId,
+ string SeatNumber,
+ bool HandLuggage,
+ double? BaggageWeight
+);
\ No newline at end of file
diff --git a/Airline.Application.Contracts/Ticket/ITicketService.cs b/Airline.Application.Contracts/Ticket/ITicketService.cs
new file mode 100644
index 000000000..f57990240
--- /dev/null
+++ b/Airline.Application.Contracts/Ticket/ITicketService.cs
@@ -0,0 +1,25 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+
+namespace Airline.Application.Contracts.Ticket;
+
+///
+/// Service contract for managing tickets in the airline system.
+/// Extends the generic CRUD interface with ticket-specific analytical operations.
+///
+public interface ITicketService : IApplicationService
+{
+ ///
+ /// Retrieves the flight associated with a specific ticket.
+ ///
+ /// The unique identifier of the ticket.
+ /// The flight DTO linked to the ticket.
+ public Task GetFlightAsync(int ticketId);
+
+ ///
+ /// Retrieves the passenger associated with a specific ticket.
+ ///
+ /// The unique identifier of the ticket.
+ /// The passenger DTO linked to the ticket.
+ public Task GetPassengerAsync(int ticketId);
+}
\ No newline at end of file
diff --git a/Airline.Application.Contracts/Ticket/TicketDto.cs b/Airline.Application.Contracts/Ticket/TicketDto.cs
new file mode 100644
index 000000000..d71d3811a
--- /dev/null
+++ b/Airline.Application.Contracts/Ticket/TicketDto.cs
@@ -0,0 +1,20 @@
+namespace Airline.Application.Contracts.Ticket;
+
+///
+/// Data Transfer Object (DTO) representing a ticket in the airline system.
+/// Used for read operations and data exchange with clients.
+///
+/// The unique identifier of the ticket.
+/// The unique identifier of the associated flight.
+/// The unique identifier of the associated passenger.
+/// The assigned seat number (e.g., "12A").
+/// Indicates whether hand luggage is carried.
+/// The total checked baggage weight in kilograms (null if no baggage).
+public record TicketDto(
+ int Id,
+ int FlightId,
+ int PassengerId,
+ string SeatNumber,
+ bool HandLuggage,
+ double? BaggageWeight
+);
\ No newline at end of file
diff --git a/Airline.Application/Airline.Application.csproj b/Airline.Application/Airline.Application.csproj
new file mode 100644
index 000000000..8e1d69d8a
--- /dev/null
+++ b/Airline.Application/Airline.Application.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Airline.Application/AirlineMapperProfile.cs b/Airline.Application/AirlineMapperProfile.cs
new file mode 100644
index 000000000..08ec8b2b1
--- /dev/null
+++ b/Airline.Application/AirlineMapperProfile.cs
@@ -0,0 +1,44 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+using Airline.Application.Contracts.Ticket;
+using Airline.Application.Contracts.ModelFamily;
+using Airline.Application.Contracts.PlaneModel;
+using Airline.Domain.Items;
+using AutoMapper;
+
+namespace Airline.Application;
+
+///
+/// AutoMapper profile that defines mapping configurations between
+/// domain entities and Data Transfer Objects (DTOs).
+/// Ensures consistent and safe conversion between layers of the application.
+///
+public class AirlineProfile : Profile
+{
+ ///
+ /// Initializes a new instance of the class
+ /// and configures all entity-to-DTO and DTO-to-entity mappings.
+ ///
+ public AirlineProfile()
+ {
+ // ModelFamily mappings
+ CreateMap();
+ CreateMap();
+
+ // PlaneModel mappings
+ CreateMap();
+ CreateMap();
+
+ // Flight mappings
+ CreateMap();
+ CreateMap();
+
+ // Passenger mappings
+ CreateMap();
+ CreateMap();
+
+ // Ticket mappings
+ CreateMap();
+ CreateMap();
+ }
+}
\ No newline at end of file
diff --git a/Airline.Application/Services/AnalyticService.cs b/Airline.Application/Services/AnalyticService.cs
new file mode 100644
index 000000000..ad0628637
--- /dev/null
+++ b/Airline.Application/Services/AnalyticService.cs
@@ -0,0 +1,130 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+using Airline.Domain;
+using Airline.Domain.Items;
+using AutoMapper;
+
+namespace Airline.Application.Services;
+
+///
+/// Provides analytical and reporting capabilities over airline data.
+/// Implements aggregated queries for business intelligence and operational insights.
+///
+public class AnalyticsService(
+ IRepository flightRepository,
+ IRepository ticketRepository,
+ IRepository passengerRepository,
+ IMapper mapper
+) : IAnalyticsService
+{
+ ///
+ /// Retrieves the top N flights by number of passengers.
+ ///
+ /// The number of top flights to return (default: 5).
+ /// A list of flight DTOs sorted by passenger count in descending order.
+ public async Task> GetTopFlightsByPassengerCountAsync(int top = 5)
+ {
+ var flights = await flightRepository.GetAllAsync();
+ var tickets = await ticketRepository.GetAllAsync();
+
+ var flightPassengerCounts = tickets
+ .GroupBy(t => t.FlightId)
+ .Select(g => new { FlightId = g.Key, Count = g.Count() })
+ .OrderByDescending(x => x.Count)
+ .Take(top)
+ .Select(x => x.FlightId)
+ .ToHashSet();
+
+ var topFlights = flights
+ .Where(f => flightPassengerCounts.Contains(f.Id))
+ .ToList();
+
+ var flightCountMap = tickets
+ .GroupBy(t => t.FlightId)
+ .ToDictionary(g => g.Key, g => g.Count());
+
+ return topFlights
+ .OrderByDescending(f => flightCountMap.GetValueOrDefault(f.Id, 0))
+ .Select(mapper.Map)
+ .ToList();
+ }
+
+ ///
+ /// Retrieves flights with the minimal travel time.
+ ///
+ /// A list of flight DTOs with the shortest duration.
+ public async Task> GetFlightsWithMinTravelTimeAsync()
+ {
+ var flights = await flightRepository.GetAllAsync();
+ if (!flights.Any()) return [];
+
+ var minTime = flights.Min(f => f.ArrivalDateTime - f.DepartureDateTime);
+ return flights
+ .Where(f => f.ArrivalDateTime - f.DepartureDateTime == minTime)
+ .Select(mapper.Map)
+ .ToList();
+ }
+
+ ///
+ /// Retrieves passengers with zero baggage for a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// A list of passenger DTOs with no baggage, sorted by full name.
+ ///
+ /// Thrown if the specified flight does not exist.
+ ///
+ public async Task> GetPassengersWithZeroBaggageOnFlightAsync(int flightId)
+ {
+ var flight = await flightRepository.GetAsync(flightId);
+ if (flight == null)
+ throw new KeyNotFoundException($"Flight with ID '{flightId}' not found.");
+
+ var tickets = await ticketRepository.GetAllAsync();
+ var passengerIds = tickets
+ .Where(t => t.FlightId == flightId && t.BaggageWeight == null)
+ .Select(t => t.PassengerId)
+ .ToHashSet();
+
+ if (!passengerIds.Any()) return [];
+
+ var passengers = await passengerRepository.GetAllAsync();
+ return passengers
+ .Where(p => passengerIds.Contains(p.Id))
+ .OrderBy(p => p.PassengerName) // сортировка по ФИО
+ .Select(mapper.Map)
+ .ToList();
+ }
+
+ ///
+ /// Retrieves flights of a specific aircraft model within a date period.
+ ///
+ /// The unique identifier of the aircraft model.
+ /// Start date of the period (inclusive).
+ /// End date of the period (inclusive).
+ /// A list of flight DTOs matching the criteria.
+ public async Task> GetFlightsByModelInPeriodAsync(int modelId, DateTime from, DateTime to)
+ {
+ var flights = await flightRepository.GetAllAsync();
+ return flights
+ .Where(f => f.ModelId == modelId &&
+ f.DepartureDateTime >= from &&
+ f.DepartureDateTime <= to)
+ .Select(mapper.Map)
+ .ToList();
+ }
+
+ ///
+ /// Retrieves flights by route (departure city → arrival city).
+ ///
+ /// The city of departure.
+ /// The city of arrival.
+ /// A list of flight DTOs matching the route.
+ public async Task> GetFlightsByRouteAsync(string departure, string arrival)
+ {
+ var flights = await flightRepository.GetAllAsync();
+ return flights
+ .Where(f => f.DepartureCity == departure && f.ArrivalCity == arrival)
+ .Select(mapper.Map)
+ .ToList();
+ }
+}
\ No newline at end of file
diff --git a/Airline.Application/Services/FlightService.cs b/Airline.Application/Services/FlightService.cs
new file mode 100644
index 000000000..ff8b93de3
--- /dev/null
+++ b/Airline.Application/Services/FlightService.cs
@@ -0,0 +1,150 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+using Airline.Application.Contracts.PlaneModel;
+using Airline.Application.Contracts.Ticket;
+using Airline.Domain;
+using Airline.Domain.Items;
+using AutoMapper;
+
+namespace Airline.Application.Services;
+
+///
+/// Provides business logic for managing flights in the airline system.
+/// Handles CRUD operations, validation of aircraft model existence,
+/// and retrieval of associated tickets, passengers, and aircraft models.
+///
+public class FlightService(
+ IRepository flightRepository,
+ IRepository planeModelRepository,
+ IRepository ticketRepository,
+ IRepository passengerRepository,
+ IMapper mapper
+) : IFlightService
+{
+ ///
+ /// Creates a new flight.
+ /// Validates that the associated aircraft model exists.
+ ///
+ /// The flight creation data transfer object.
+ /// The created flight DTO.
+ ///
+ /// Thrown if the specified aircraft model does not exist.
+ ///
+ public async Task CreateAsync(CreateFlightDto dto)
+ {
+ if (await planeModelRepository.GetAsync(dto.ModelId) == null)
+ throw new KeyNotFoundException($"Plane model '{dto.ModelId}' not found.");
+
+ var flight = mapper.Map(dto);
+ var maxId = 0;
+ var last = await flightRepository.GetAllAsync();
+ if (last.Any())
+ {
+ maxId = last.Max(f => f.Id);
+ }
+ flight.Id = maxId + 1;
+ var created = await flightRepository.CreateAsync(flight);
+ return mapper.Map(created);
+ }
+
+ ///
+ /// Retrieves a flight by its unique identifier.
+ ///
+ /// The unique identifier of the flight.
+ /// The flight DTO if found; otherwise, null.
+ public async Task GetByIdAsync(int id)
+ {
+ var entity = await flightRepository.GetAsync(id);
+ return entity == null ? null : mapper.Map(entity);
+ }
+
+ ///
+ /// Retrieves all flights in the system.
+ ///
+ /// A list of all flight DTOs.
+ public async Task> GetAllAsync() =>
+ (await flightRepository.GetAllAsync()).Select(mapper.Map).ToList();
+
+ ///
+ /// Updates an existing flight with new data.
+ ///
+ /// The updated flight data transfer object.
+ /// The unique identifier of the flight to update.
+ /// The updated flight DTO.
+ ///
+ /// Thrown if the flight does not exist.
+ ///
+ public async Task UpdateAsync(CreateFlightDto dto, int id)
+ {
+ var existing = await flightRepository.GetAsync(id)
+ ?? throw new KeyNotFoundException($"Flight '{id}' not found.");
+ mapper.Map(dto, existing);
+ var updated = await flightRepository.UpdateAsync(existing);
+ return mapper.Map(updated);
+ }
+
+ ///
+ /// Deletes a flight by its unique identifier.
+ ///
+ /// The unique identifier of the flight to delete.
+ /// True if the flight was deleted; otherwise, false.
+ public async Task DeleteAsync(int id) => await flightRepository.DeleteAsync(id);
+
+ ///
+ /// Retrieves the aircraft model associated with a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// The aircraft model DTO associated with the flight.
+ ///
+ /// Thrown if the flight does not exist.
+ ///
+ ///
+ /// Thrown if the associated aircraft model is missing (data integrity issue).
+ ///
+ public async Task GetPlaneModelAsync(int flightId)
+ {
+ var flight = await flightRepository.GetAsync(flightId)
+ ?? throw new KeyNotFoundException($"Flight '{flightId}' not found.");
+ var model = await planeModelRepository.GetAsync(flight.ModelId)
+ ?? throw new InvalidOperationException($"Model '{flight.ModelId}' missing.");
+ return mapper.Map(model);
+ }
+
+ ///
+ /// Retrieves all tickets associated with a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// A list of ticket DTOs linked to the flight.
+ ///
+ /// Thrown if the flight does not exist.
+ ///
+ public async Task> GetTicketsAsync(int flightId)
+ {
+ var flight = await flightRepository.GetAsync(flightId) != null;
+ if (!flight)
+ throw new KeyNotFoundException($"Flight with ID '{flightId}' not found.");
+ var all = await ticketRepository.GetAllAsync();
+ return all.Where(t => t.FlightId == flightId)
+ .Select(mapper.Map).ToList();
+ }
+
+ ///
+ /// Retrieves all passengers associated with a specific flight.
+ ///
+ /// The unique identifier of the flight.
+ /// A list of passenger DTOs linked to the flight.
+ ///
+ /// Thrown if the flight does not exist.
+ ///
+ public async Task> GetPassengersAsync(int flightId)
+ {
+ var flight = await flightRepository.GetAsync(flightId) != null;
+ if (!flight)
+ throw new KeyNotFoundException($"Flight with ID '{flightId}' not found.");
+ var ticketDtos = await GetTicketsAsync(flightId);
+ var passengerIds = ticketDtos.Select(t => t.PassengerId).ToHashSet();
+ var allPassengers = await passengerRepository.GetAllAsync();
+ return allPassengers.Where(p => passengerIds.Contains(p.Id))
+ .Select(mapper.Map).ToList();
+ }
+}
\ No newline at end of file
diff --git a/Airline.Application/Services/ModelFamilyService.cs b/Airline.Application/Services/ModelFamilyService.cs
new file mode 100644
index 000000000..4d26179c1
--- /dev/null
+++ b/Airline.Application/Services/ModelFamilyService.cs
@@ -0,0 +1,103 @@
+using Airline.Application.Contracts.ModelFamily;
+using Airline.Application.Contracts.PlaneModel;
+using Airline.Domain;
+using Airline.Domain.Items;
+using AutoMapper;
+
+namespace Airline.Application.Services;
+
+///
+/// Provides business logic for managing aircraft model families in the airline system.
+/// Handles CRUD operations and retrieval of associated aircraft models.
+///
+public class ModelFamilyService(
+ IRepository planeModelRepository,
+ IRepository familyRepository,
+ IMapper mapper
+) : IModelFamilyService
+{
+ ///
+ /// Creates a new aircraft model family.
+ ///
+ /// The model family creation data transfer object.
+ /// The created model family DTO.
+ public async Task CreateAsync(CreateModelFamilyDto dto)
+ {
+ var modelFamily = mapper.Map(dto);
+ var maxId = 0;
+ var last = await familyRepository.GetAllAsync();
+ if (last.Any())
+ {
+ maxId = last.Max(f => f.Id);
+ }
+ modelFamily.Id = maxId + 1;
+ var created = await familyRepository.CreateAsync(modelFamily);
+ return mapper.Map(created);
+ }
+
+ ///
+ /// Retrieves a model family by its unique identifier.
+ ///
+ /// The unique identifier of the model family.
+ /// The model family DTO if found; otherwise, null.
+ public async Task GetByIdAsync(int id)
+ {
+ var entity = await familyRepository.GetAsync(id);
+ return entity == null ? null : mapper.Map(entity);
+ }
+
+ ///
+ /// Retrieves all aircraft model families in the system.
+ ///
+ /// A list of all model family DTOs.
+ public async Task> GetAllAsync() =>
+ (await familyRepository.GetAllAsync()).Select(mapper.Map).ToList();
+
+ ///
+ /// Updates an existing model family with new data.
+ ///
+ /// The updated model family data transfer object.
+ /// The unique identifier of the model family to update.
+ /// The updated model family DTO.
+ ///
+ /// Thrown if the model family does not exist.
+ ///
+ public async Task UpdateAsync(CreateModelFamilyDto dto, int id)
+ {
+ var existing = await familyRepository.GetAsync(id)
+ ?? throw new KeyNotFoundException($"Model family '{id}' not found.");
+ mapper.Map(dto, existing);
+ var updated = await familyRepository.UpdateAsync(existing);
+ return mapper.Map(updated);
+ }
+
+ ///
+ /// Deletes a model family by its unique identifier.
+ ///
+ /// The unique identifier of the model family to delete.
+ /// True if the model family was deleted; otherwise, false.
+ public async Task DeleteAsync(int id) => await familyRepository.DeleteAsync(id);
+
+ ///
+ /// Retrieves all aircraft models associated with a specific model family.
+ ///
+ /// The unique identifier of the model family.
+ /// A list of aircraft model DTOs linked to the family.
+ ///
+ /// Thrown if the model family does not exist.
+ ///
+ public async Task> GetPlaneModelsAsync(int familyId)
+ {
+ var family = await familyRepository.GetAsync(familyId);
+ if (family == null)
+ throw new KeyNotFoundException($"Model family with ID '{familyId}' not found.");
+ var allModels = await planeModelRepository.GetAllAsync();
+
+ var models = allModels
+ .Where(m => m.ModelFamilyId == familyId)
+ .Select(mapper.Map)
+ .ToList();
+
+ return models;
+ }
+}
\ No newline at end of file
diff --git a/Airline.Application/Services/PassengerService.cs b/Airline.Application/Services/PassengerService.cs
new file mode 100644
index 000000000..d11516cd7
--- /dev/null
+++ b/Airline.Application/Services/PassengerService.cs
@@ -0,0 +1,134 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+using Airline.Application.Contracts.Ticket;
+using Airline.Domain;
+using Airline.Domain.Items;
+using AutoMapper;
+using System.Text.RegularExpressions;
+
+namespace Airline.Application.Services;
+
+///
+/// Provides business logic for managing passengers in the airline system.
+/// Handles CRUD operations, validation of personal data (name must not contain digits),
+/// and retrieval of associated tickets and flights.
+///
+public class PassengerService(
+ IRepository passengerRepository,
+ IRepository ticketRepository,
+ IRepository flightRepository,
+ IMapper mapper
+) : IPassengerService
+{
+ ///
+ /// Creates a new passenger.
+ /// Validates that the passenger name is not empty and does not contain digits.
+ ///
+ /// The passenger creation data transfer object.
+ /// The created passenger DTO.
+ ///
+ /// Thrown if the passenger name is empty, whitespace, or contains digits.
+ ///
+ public async Task CreateAsync(CreatePassengerDto dto)
+ {
+ var passenger = mapper.Map(dto);
+ var maxId = 0;
+ var last = await passengerRepository.GetAllAsync();
+ if (last.Any())
+ {
+ maxId = last.Max(p => p.Id);
+ }
+ passenger.Id = maxId + 1;
+ if (string.IsNullOrWhiteSpace(dto.PassengerName) || Regex.IsMatch(dto.PassengerName, @"\d"))
+ throw new ArgumentException("Passenger name must not be empty or contain digits.");
+ var created = await passengerRepository.CreateAsync(passenger);
+ return mapper.Map(created);
+ }
+
+ ///
+ /// Retrieves a passenger by its unique identifier.
+ ///
+ /// The unique identifier of the passenger.
+ /// The passenger DTO if found; otherwise, null.
+ public async Task GetByIdAsync(int id)
+ {
+ var entity = await passengerRepository.GetAsync(id);
+ return entity == null ? null : mapper.Map(entity);
+ }
+
+ ///
+ /// Retrieves all passengers in the system.
+ ///
+ /// A list of all passenger DTOs.
+ public async Task> GetAllAsync() =>
+ (await passengerRepository.GetAllAsync()).Select(mapper.Map).ToList();
+
+ ///
+ /// Updates an existing passenger with new data.
+ /// Validates that the passenger name is not empty and does not contain digits.
+ ///
+ /// The updated passenger data transfer object.
+ /// The unique identifier of the passenger to update.
+ /// The updated passenger DTO.
+ ///
+ /// Thrown if the passenger does not exist.
+ ///
+ ///
+ /// Thrown if the passenger name is empty, whitespace, or contains digits.
+ ///
+ public async Task UpdateAsync(CreatePassengerDto dto, int id)
+ {
+ var existing = await passengerRepository.GetAsync(id)
+ ?? throw new KeyNotFoundException($"Passenger '{id}' not found.");
+ mapper.Map(dto, existing);
+ if (string.IsNullOrWhiteSpace(dto.PassengerName) || Regex.IsMatch(dto.PassengerName, @"\d"))
+ throw new ArgumentException("Passenger name must not be empty or contain digits.");
+ var updated = await passengerRepository.UpdateAsync(existing);
+ return mapper.Map(updated);
+ }
+
+ ///
+ /// Deletes a passenger by its unique identifier.
+ ///
+ /// The unique identifier of the passenger to delete.
+ /// True if the passenger was deleted; otherwise, false.
+ public async Task DeleteAsync(int id) => await passengerRepository.DeleteAsync(id);
+
+ ///
+ /// Retrieves all tickets associated with a specific passenger.
+ ///
+ /// The unique identifier of the passenger.
+ /// A list of ticket DTOs linked to the passenger.
+ ///
+ /// Thrown if the passenger does not exist.
+ ///
+ public async Task> GetTicketsAsync(int passengerId)
+ {
+ var passengerExists = await passengerRepository.GetAsync(passengerId) != null;
+ if (!passengerExists)
+ throw new KeyNotFoundException($"Passenger with ID '{passengerId}' not found.");
+ var all = await ticketRepository.GetAllAsync();
+ return all.Where(t => t.PassengerId == passengerId)
+ .Select(mapper.Map).ToList();
+ }
+
+ ///
+ /// Retrieves all flights associated with a specific passenger.
+ ///
+ /// The unique identifier of the passenger.
+ /// A list of flight DTOs linked to the passenger.
+ ///
+ /// Thrown if the passenger does not exist.
+ ///
+ public async Task> GetFlightsAsync(int passengerId)
+ {
+ var passengerExists = await passengerRepository.GetAsync(passengerId) != null;
+ if (!passengerExists)
+ throw new KeyNotFoundException($"Passenger with ID '{passengerId}' not found.");
+ var ticketDtos = await GetTicketsAsync(passengerId);
+ var flightIds = ticketDtos.Select(t => t.FlightId).ToHashSet();
+ var allFlights = await flightRepository.GetAllAsync();
+ return allFlights.Where(f => flightIds.Contains(f.Id))
+ .Select(mapper.Map).ToList();
+ }
+}
\ No newline at end of file
diff --git a/Airline.Application/Services/PlaneModelService.cs b/Airline.Application/Services/PlaneModelService.cs
new file mode 100644
index 000000000..a4c665c97
--- /dev/null
+++ b/Airline.Application/Services/PlaneModelService.cs
@@ -0,0 +1,119 @@
+using Airline.Application.Contracts.ModelFamily;
+using Airline.Application.Contracts.PlaneModel;
+using Airline.Domain;
+using Airline.Domain.Items;
+using AutoMapper;
+
+namespace Airline.Application.Services;
+
+///
+/// Provides business logic for managing aircraft models in the airline system.
+/// Handles CRUD operations, validation of technical specifications,
+/// and retrieval of associated model families.
+///
+public class PlaneModelService(
+ IRepository planeModelRepository,
+ IRepository familyRepository,
+ IMapper mapper
+) : IPlaneModelService
+{
+ ///
+ /// Creates a new aircraft model.
+ /// Validates that the model family exists and that capacity values are non-negative.
+ ///
+ /// The aircraft model creation data transfer object.
+ /// The created aircraft model DTO.
+ ///
+ /// Thrown if the specified model family does not exist.
+ ///
+ ///
+ /// Thrown if passenger capacity or cargo capacity is negative.
+ ///
+ public async Task CreateAsync(CreatePlaneModelDto dto)
+ {
+ if (await familyRepository.GetAsync(dto.ModelFamilyId) == null)
+ throw new KeyNotFoundException($"Model family '{dto.ModelFamilyId}' not found.");
+
+ var planeModel = mapper.Map(dto);
+ var maxId = 0;
+ var last = await planeModelRepository.GetAllAsync();
+ if (last.Any())
+ {
+ maxId = last.Max(m => m.Id);
+ }
+ planeModel.Id = maxId + 1;
+ if (dto.PassengerCapacity < 0 || dto.CargoCapacity < 0)
+ throw new ArgumentException("Capacity values cannot be negative.");
+ var created = await planeModelRepository.CreateAsync(planeModel);
+ return mapper.Map(created);
+ }
+
+ ///
+ /// Retrieves an aircraft model by its unique identifier.
+ ///
+ /// The unique identifier of the aircraft model.
+ /// The aircraft model DTO if found; otherwise, null.
+ public async Task GetByIdAsync(int id)
+ {
+ var entity = await planeModelRepository.GetAsync(id);
+ return entity == null ? null : mapper.Map(entity);
+ }
+
+ ///
+ /// Retrieves all aircraft models in the system.
+ ///
+ /// A list of all aircraft model DTOs.
+ public async Task> GetAllAsync() =>
+ (await planeModelRepository.GetAllAsync()).Select(mapper.Map).ToList();
+
+ ///
+ /// Updates an existing aircraft model with new data.
+ /// Validates that capacity values are non-negative.
+ ///
+ /// The updated aircraft model data transfer object.
+ /// The unique identifier of the model to update.
+ /// The updated aircraft model DTO.
+ ///
+ /// Thrown if the aircraft model does not exist.
+ ///
+ ///
+ /// Thrown if passenger capacity or cargo capacity is negative.
+ ///
+ public async Task UpdateAsync(CreatePlaneModelDto dto, int id)
+ {
+ var existing = await planeModelRepository.GetAsync(id)
+ ?? throw new KeyNotFoundException($"Plane model '{id}' not found.");
+ mapper.Map(dto, existing);
+ if (dto.PassengerCapacity < 0 || dto.CargoCapacity < 0)
+ throw new ArgumentException("Capacity values cannot be negative.");
+ var updated = await planeModelRepository.UpdateAsync(existing);
+ return mapper.Map(updated);
+ }
+
+ ///
+ /// Deletes an aircraft model by its unique identifier.
+ ///
+ /// The unique identifier of the model to delete.
+ /// True if the model was deleted; otherwise, false.
+ public async Task DeleteAsync(int id) => await planeModelRepository.DeleteAsync(id);
+
+ ///
+ /// Retrieves the model family associated with a specific aircraft model.
+ ///
+ /// The unique identifier of the aircraft model.
+ /// The model family DTO associated with the model.
+ ///
+ /// Thrown if the aircraft model does not exist.
+ ///
+ ///
+ /// Thrown if the associated model family is missing (data integrity issue).
+ ///
+ public async Task GetModelFamilyAsync(int modelId)
+ {
+ var model = await planeModelRepository.GetAsync(modelId)
+ ?? throw new KeyNotFoundException($"Plane model '{modelId}' not found.");
+ var family = await familyRepository.GetAsync(model.ModelFamilyId)
+ ?? throw new InvalidOperationException($"Family '{model.ModelFamilyId}' missing.");
+ return mapper.Map(family);
+ }
+}
\ No newline at end of file
diff --git a/Airline.Application/Services/TicketService.cs b/Airline.Application/Services/TicketService.cs
new file mode 100644
index 000000000..c74909faf
--- /dev/null
+++ b/Airline.Application/Services/TicketService.cs
@@ -0,0 +1,142 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Application.Contracts.Passenger;
+using Airline.Application.Contracts.Ticket;
+using Airline.Domain;
+using Airline.Domain.Items;
+using AutoMapper;
+
+namespace Airline.Application.Services;
+
+///
+/// Provides business logic for managing tickets in the airline system.
+/// Handles CRUD operations, validation, and retrieval of related entities (flight, passenger).
+///
+public class TicketService(
+ IRepository ticketRepository,
+ IRepository flightRepository,
+ IRepository passengerRepository,
+ IMapper mapper
+) : ITicketService
+{
+ ///
+ /// Creates a new ticket for a passenger on a specific flight.
+ /// Validates that the flight and passenger exist and that baggage weight is non-negative.
+ ///
+ /// The ticket creation data transfer object.
+ /// The created ticket DTO.
+ ///
+ /// Thrown if the specified flight or passenger does not exist.
+ ///
+ ///
+ /// Thrown if baggage weight is negative.
+ ///
+ public async Task CreateAsync(CreateTicketDto dto)
+ {
+ if (await flightRepository.GetAsync(dto.FlightId) == null)
+ throw new KeyNotFoundException($"Flight '{dto.FlightId}' not found.");
+ if (await passengerRepository.GetAsync(dto.PassengerId) == null)
+ throw new KeyNotFoundException($"Passenger '{dto.PassengerId}' not found.");
+
+ var ticket = mapper.Map(dto);
+ var maxId = 0;
+ var last = await ticketRepository.GetAllAsync();
+ if (last.Any())
+ {
+ maxId = last.Max(t => t.Id);
+ }
+ ticket.Id = maxId + 1;
+ if (dto.BaggageWeight < 0)
+ throw new ArgumentException("BaggageWeight cannot be negative.");
+ var created = await ticketRepository.CreateAsync(ticket);
+ return mapper.Map(created);
+ }
+
+ ///
+ /// Retrieves a ticket by its unique identifier.
+ ///
+ /// The unique identifier of the ticket.
+ /// The ticket DTO if found; otherwise, null.
+ public async Task GetByIdAsync(int id)
+ {
+ var entity = await ticketRepository.GetAsync(id);
+ return entity == null ? null : mapper.Map(entity);
+ }
+
+ ///
+ /// Retrieves all tickets in the system.
+ ///
+ /// A list of all ticket DTOs.
+ public async Task> GetAllAsync() =>
+ (await ticketRepository.GetAllAsync()).Select(mapper.Map).ToList();
+
+ ///
+ /// Updates an existing ticket with new data.
+ /// Validates that baggage weight is non-negative.
+ ///
+ /// The updated ticket data transfer object.
+ /// The unique identifier of the ticket to update.
+ /// The updated ticket DTO.
+ ///
+ /// Thrown if the ticket does not exist.
+ ///
+ ///
+ /// Thrown if baggage weight is negative.
+ ///
+ public async Task UpdateAsync(CreateTicketDto dto, int id)
+ {
+ var existing = await ticketRepository.GetAsync(id)
+ ?? throw new KeyNotFoundException($"Ticket '{id}' not found.");
+ mapper.Map(dto, existing);
+ if (dto.BaggageWeight < 0)
+ throw new ArgumentException("BaggageWeight cannot be negative.");
+ var updated = await ticketRepository.UpdateAsync(existing);
+ return mapper.Map(updated);
+ }
+
+ ///
+ /// Deletes a ticket by its unique identifier.
+ ///
+ /// The unique identifier of the ticket to delete.
+ /// True if the ticket was deleted; otherwise, false.
+ public async Task DeleteAsync(int id) => await ticketRepository.DeleteAsync(id);
+
+ ///
+ /// Retrieves the flight associated with a specific ticket.
+ ///
+ /// The unique identifier of the ticket.
+ /// The flight DTO associated with the ticket.
+ ///
+ /// Thrown if the ticket does not exist.
+ ///
+ ///
+ /// Thrown if the associated flight is missing (data integrity issue).
+ ///
+ public async Task GetFlightAsync(int ticketId)
+ {
+ var ticket = await ticketRepository.GetAsync(ticketId)
+ ?? throw new KeyNotFoundException($"Ticket '{ticketId}' not found.");
+ var flight = await flightRepository.GetAsync(ticket.FlightId)
+ ?? throw new InvalidOperationException($"Flight '{ticket.FlightId}' missing.");
+ return mapper.Map(flight);
+ }
+
+ ///
+ /// Retrieves the passenger associated with a specific ticket.
+ ///
+ /// The unique identifier of the ticket.
+ /// The passenger DTO associated with the ticket.
+ ///
+ /// Thrown if the ticket does not exist.
+ ///
+ ///
+ /// Thrown if the associated passenger is missing (data integrity issue).
+ ///
+ public async Task GetPassengerAsync(int ticketId)
+ {
+ var ticket = await ticketRepository.GetAsync(ticketId)
+ ?? throw new KeyNotFoundException($"Ticket '{ticketId}' not found.");
+ var passenger = await passengerRepository.GetAsync(ticket.PassengerId)
+ ?? throw new InvalidOperationException($"Passenger '{ticket.PassengerId}' missing.");
+ return mapper.Map(passenger);
+ }
+}
\ No newline at end of file
diff --git a/Airline.Domain/Airline.Domain.csproj b/Airline.Domain/Airline.Domain.csproj
new file mode 100644
index 000000000..fa71b7ae6
--- /dev/null
+++ b/Airline.Domain/Airline.Domain.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/Airline.Domain/DataSeed/DataSeed.cs b/Airline.Domain/DataSeed/DataSeed.cs
new file mode 100644
index 000000000..71ed083ba
--- /dev/null
+++ b/Airline.Domain/DataSeed/DataSeed.cs
@@ -0,0 +1,475 @@
+using Airline.Domain.Items;
+
+namespace Airline.Domain.DataSeed;
+
+///
+/// Seeds test data for the airline system, including model families,
+/// plane models, passengers, flights, and tickets.
+///
+public class DataSeed
+{
+ ///
+ /// The list of model families.
+ ///
+ public List ModelFamilies { get; }
+
+ ///
+ /// The list of plane models.
+ ///
+ public List PlaneModels { get; }
+
+ ///
+ /// The list of passengers.
+ ///
+ public List Passengers { get; }
+
+ ///
+ /// The list of flights.
+ ///
+ public List Flights { get; }
+
+ ///
+ /// The list of tickets.
+ ///
+ public List Tickets { get; }
+
+ ///
+ ///Initializes the DataSeed class and fills it with data.
+ ///
+ public DataSeed()
+ {
+ ModelFamilies = InitModelFamilies();
+ PlaneModels = InitPlaneModels(ModelFamilies);
+ Passengers = InitPassengers();
+ Flights = InitFlights(PlaneModels);
+ Tickets = InitTickets(Flights, Passengers);
+ }
+
+ ///
+ /// Initializes the model families with predefined data.
+ ///
+ private static List InitModelFamilies() =>
+ [
+ new ModelFamily
+ {
+ Id = 1,
+ NameOfFamily = "A320 Family",
+ ManufacturerName = "Airbus"
+ },
+ new ModelFamily
+ {
+ Id = 2,
+ NameOfFamily = "767 Family",
+ ManufacturerName = "Boeing"
+ },
+ new ModelFamily
+ {
+ Id = 3,
+ NameOfFamily = "777 Family",
+ ManufacturerName = "Boeing"
+ },
+ new ModelFamily
+ {
+ Id = 4,
+ NameOfFamily = "787 Dreamliner",
+ ManufacturerName = "Boeing"
+ },
+ new ModelFamily
+ {
+ Id = 5,
+ NameOfFamily = "A330 Family",
+ ManufacturerName = "Airbus"
+ }
+ ];
+
+ ///
+ /// Initializes plane models linked to their families.
+ ///
+ private static List InitPlaneModels(List families) =>
+ [
+ new PlaneModel
+ {
+ Id = 1,
+ ModelName = "A320",
+ PlaneFamily = families[0],
+ MaxRange = 6000,
+ PassengerCapacity = 180,
+ CargoCapacity = 20
+ },
+ new PlaneModel
+ {
+ Id = 2,
+ ModelName = "B767-300",
+ PlaneFamily = families[1],
+ MaxRange = 5500,
+ PassengerCapacity = 189,
+ CargoCapacity = 23
+ },
+ new PlaneModel
+ {
+ Id = 3,
+ ModelName = "B777-300ER",
+ PlaneFamily = families[2],
+ MaxRange = 11000,
+ PassengerCapacity = 370,
+ CargoCapacity = 45
+ },
+ new PlaneModel
+ {
+ Id = 4,
+ ModelName = "B787-9",
+ PlaneFamily = families[3],
+ MaxRange = 12000,
+ PassengerCapacity = 290,
+ CargoCapacity = 40
+ },
+ new PlaneModel
+ {
+ Id = 5,
+ ModelName = "A330-300",
+ PlaneFamily = families[4],
+ MaxRange = 10500,
+ PassengerCapacity = 300,
+ CargoCapacity = 42
+ }
+ ];
+
+ ///
+ /// Initializes a list of passengers.
+ ///
+ private static List InitPassengers() =>
+ [
+ new Passenger
+ {
+ Id = 1,
+ Passport = "477419070",
+ PassengerName = "Ivanov Ivan",
+ DateOfBirth = new(1990, 01, 15)
+ },
+ new Passenger
+ {
+ Id = 2,
+ Passport = "719011722",
+ PassengerName = "Petrov Petr",
+ DateOfBirth = new(1985, 05, 22)
+ },
+ new Passenger
+ {
+ Id = 3,
+ Passport = "269997862",
+ PassengerName = "Alyohin Alexey",
+ DateOfBirth = new(1992, 03, 10)
+ },
+ new Passenger
+ {
+ Id = 4,
+ Passport = "690256588",
+ PassengerName = "Kuzina Anna",
+ DateOfBirth = new(1991, 07, 30)
+ },
+ new Passenger
+ {
+ Id = 5,
+ Passport = "816817823",
+ PassengerName = "Kuzin Dmitry",
+ DateOfBirth = new(1988, 11, 05)
+ },
+ new Passenger
+ {
+ Id = 6,
+ Passport = "303776467",
+ PassengerName = "Nikitich Dobrynya",
+ DateOfBirth = new(1995, 09, 18)
+ },
+ new Passenger
+ {
+ Id = 7,
+ Passport = "510907182",
+ PassengerName = "Popovich Alex",
+ DateOfBirth = new(1993, 04, 12)
+ },
+ new Passenger
+ {
+ Id = 8,
+ Passport = "463835340",
+ PassengerName = "Kolyan",
+ DateOfBirth = new(1987, 08, 25)
+ },
+ new Passenger
+ {
+ Id = 9,
+ Passport = "877654233",
+ PassengerName = "Lebedev Nikolay Ivanovich",
+ DateOfBirth = new(1960, 02, 14)
+ },
+ new Passenger
+ {
+ Id = 10,
+ Passport = "112971133",
+ PassengerName = "Sokolov Tigran",
+ DateOfBirth = new(1994, 12, 03)
+ }
+ ];
+
+ ///
+ /// Initializes flights with plane models and schedules.
+ ///
+ private static List InitFlights(List models) =>
+ [
+
+ new Flight
+ {
+ Id = 1,
+ FlightCode = "SU101",
+ DepartureCity = "Samara",
+ ArrivalCity = "Wonderland",
+ DepartureDateTime = new DateTime(2025, 10, 10, 8, 0, 0),
+ ArrivalDateTime = new DateTime(2025, 10, 10, 15, 0, 0),
+ TravelTime = TimeSpan.FromHours(2),
+ Model = models[0]
+ },
+
+ new Flight
+ {
+ Id = 2,
+ FlightCode = "SU102",
+ DepartureCity = "Moscow",
+ ArrivalCity = "Paris",
+ DepartureDateTime = new(2025, 10, 10, 6, 5, 0),
+ ArrivalDateTime = new(2025, 10, 10, 9, 5, 0),
+ TravelTime = TimeSpan.FromHours(3),
+ Model = models[1]
+ },
+
+ new Flight
+ {
+ Id = 3,
+ FlightCode = "SU103",
+ DepartureCity = "Berlin",
+ ArrivalCity = "Paris",
+ DepartureDateTime = new(2025, 10, 10, 5, 0, 0),
+ ArrivalDateTime = new(2025, 10, 10, 10, 0, 0),
+ TravelTime = TimeSpan.FromHours(5),
+ Model = models[2]
+ },
+
+ new Flight
+ {
+ Id = 4,
+ FlightCode = "SU104",
+ DepartureCity = "Samara",
+ ArrivalCity = "Wonderland",
+ DepartureDateTime = new(2025, 10, 11, 6, 0, 0),
+ ArrivalDateTime = new(2025, 10, 11, 8, 30, 0),
+ TravelTime = TimeSpan.FromHours(2.5),
+ Model = models[3]
+ },
+
+ new Flight
+ {
+ Id = 5,
+ FlightCode = "AZ201",
+ DepartureCity = "Rome",
+ ArrivalCity = "Milan",
+ DepartureDateTime = new(2025, 10, 11, 22, 0, 0),
+ ArrivalDateTime = new(2025, 10, 12, 2, 30, 0),
+ TravelTime = TimeSpan.FromHours(4.5),
+ Model = models[4]
+ },
+
+ new Flight
+ {
+ Id = 6,
+ FlightCode = "SU200",
+ DepartureCity = "Moscow",
+ ArrivalCity = "Tokyo",
+ DepartureDateTime = new(2025, 10, 11, 15, 0, 0),
+ ArrivalDateTime = new(2025, 10, 12, 6, 0, 0),
+ TravelTime = TimeSpan.FromHours(15),
+ Model = models[0]
+ },
+
+ new Flight
+ {
+ Id = 7,
+ FlightCode = "DL100",
+ DepartureCity = "New York",
+ ArrivalCity = "London",
+ DepartureDateTime = new(2025, 10, 12, 7, 20, 0),
+ ArrivalDateTime = new(2025, 10, 13, 13, 20, 0),
+ TravelTime = TimeSpan.FromHours(6),
+ Model = models[1]
+ },
+
+ new Flight
+ {
+ Id = 8,
+ FlightCode = "SU105",
+ DepartureCity = "Paris",
+ ArrivalCity = "Moscow",
+ DepartureDateTime = new(2025, 10, 13, 23, 0, 0),
+ ArrivalDateTime = new(2025, 10, 14, 6, 0, 0),
+ TravelTime = TimeSpan.FromHours(7),
+ Model = models[0]
+ }
+ ];
+
+ ///
+ /// Initializes tickets linking flights to passengers.
+ ///
+ private static List InitTickets(List flights, List passengers) =>
+ [
+
+ new Ticket
+ {
+ Id = 1,
+ Flight = flights[0],
+ Passenger = passengers[0],
+ SeatNumber = "12A",
+ HandLuggage = true,
+ BaggageWeight = 15.6
+ },
+ new Ticket
+ {
+ Id = 2,
+ Flight = flights[0],
+ Passenger = passengers[1],
+ SeatNumber = "12B",
+ HandLuggage = false,
+ BaggageWeight = null
+ },
+ new Ticket
+ {
+ Id = 3,
+ Flight = flights[0],
+ Passenger = passengers[2],
+ SeatNumber = "12C",
+ HandLuggage = true,
+ BaggageWeight = null
+ },
+ new Ticket
+ {
+ Id = 4,
+ Flight = flights[0],
+ Passenger = passengers[3],
+ SeatNumber = "13A",
+ HandLuggage = true,
+ BaggageWeight = 1.2
+ },
+ new Ticket
+ {
+ Id = 5,
+ Flight = flights[0],
+ Passenger = passengers[4],
+ SeatNumber = "13B",
+ HandLuggage = true,
+ BaggageWeight = 10.0
+ },
+
+ new Ticket
+ {
+ Id = 6,
+ Flight = flights[1],
+ Passenger = passengers[5],
+ SeatNumber = "15A",
+ HandLuggage = true,
+ BaggageWeight = 5.2
+ },
+ new Ticket
+ {
+ Id = 7,
+ Flight = flights[1],
+ Passenger = passengers[6],
+ SeatNumber = "15B",
+ HandLuggage = true,
+ BaggageWeight = 18.0
+ },
+ new Ticket
+ {
+ Id = 8,
+ Flight = flights[1],
+ Passenger = passengers[7],
+ SeatNumber = "15C",
+ HandLuggage = false,
+ BaggageWeight = null
+ },
+
+ new Ticket
+ {
+ Id = 9,
+ Flight = flights[2],
+ Passenger = passengers[8],
+ SeatNumber = "20A",
+ HandLuggage = true,
+ BaggageWeight = 3.2
+ },
+ new Ticket
+ {
+ Id = 10,
+ Flight = flights[2],
+ Passenger = passengers[9],
+ SeatNumber = "20B",
+ HandLuggage = true,
+ BaggageWeight = 7.0
+ },
+
+ new Ticket
+ {
+ Id = 11,
+ Flight = flights[3],
+ Passenger = passengers[0],
+ SeatNumber = "10A",
+ HandLuggage = false,
+ BaggageWeight = 4.2
+ },
+
+ new Ticket
+ {
+ Id = 12,
+ Flight = flights[4],
+ Passenger = passengers[1],
+ SeatNumber = "5A",
+ HandLuggage = true,
+ BaggageWeight = 6.0
+ },
+
+ new Ticket
+ {
+ Id = 13,
+ Flight = flights[5],
+ Passenger = passengers[2],
+ SeatNumber = "1A",
+ HandLuggage = true,
+ BaggageWeight = 25.0
+ },
+
+ new Ticket
+ {
+ Id = 14,
+ Flight = flights[6],
+ Passenger = passengers[3],
+ SeatNumber = "8A",
+ HandLuggage = false,
+ BaggageWeight = null
+ },
+
+ new Ticket
+ {
+ Id = 15,
+ Flight = flights[7],
+ Passenger = passengers[4],
+ SeatNumber = "7A",
+ HandLuggage = true,
+ BaggageWeight = 11.6
+ },
+ new Ticket
+ {
+ Id = 16,
+ Flight = flights[7],
+ Passenger = passengers[5],
+ SeatNumber = "7B",
+ HandLuggage = false,
+ BaggageWeight = 0.5
+ }
+ ];
+}
\ No newline at end of file
diff --git a/Airline.Domain/IRepository.cs b/Airline.Domain/IRepository.cs
new file mode 100644
index 000000000..86d0cc24e
--- /dev/null
+++ b/Airline.Domain/IRepository.cs
@@ -0,0 +1,52 @@
+namespace Airline.Domain;
+
+///
+/// Defines a generic repository interface that provides
+/// basic CRUD (Create, Read, Update, Delete) operations.
+///
+/// The type of the entity being managed.
+/// The type of the entity's unique identifier.
+public interface IRepository
+ where TEntity : class
+{
+ ///
+ /// Creates a new entity in the data store.
+ ///
+ /// The entity instance to create.
+ /// The created entity, typically with an assigned identifier.
+ public Task CreateAsync(TEntity entity);
+
+ ///
+ /// Retrieves an entity from the data store by its unique identifier.
+ ///
+ /// The unique identifier of the entity to retrieve.
+ ///
+ /// The entity if found; otherwise, .
+ ///
+ public Task GetAsync(TKey id);
+
+ ///
+ /// Retrieves all entities of the specified type from the data store.
+ ///
+ ///
+ /// A list containing all entities of the specified type.
+ ///
+ public Task> GetAllAsync();
+
+ ///
+ /// Updates an existing entity in the data store.
+ ///
+ /// The entity instance containing updated data.
+ /// The updated entity.
+ public Task UpdateAsync(TEntity entity);
+
+ ///
+ /// Deletes an entity from the data store by its unique identifier.
+ ///
+ /// The unique identifier of the entity to delete.
+ ///
+ /// if the entity was successfully deleted;
+ /// otherwise, .
+ ///
+ public Task DeleteAsync(TKey id);
+}
\ No newline at end of file
diff --git a/Airline.Domain/Items/Flight.cs b/Airline.Domain/Items/Flight.cs
new file mode 100644
index 000000000..fcf3b68e7
--- /dev/null
+++ b/Airline.Domain/Items/Flight.cs
@@ -0,0 +1,52 @@
+namespace Airline.Domain.Items;
+
+///
+/// The class for flight description and information about it.
+///
+public class Flight
+{
+ ///
+ /// Unique flight's Id.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Flight's code.
+ ///
+ public required string FlightCode { get; set; }
+
+ ///
+ /// The place of departure.
+ ///
+ public required string DepartureCity { get; set; }
+
+ ///
+ /// The place of arrival.
+ ///
+ public required string ArrivalCity { get; set; }
+
+ ///
+ /// Date and time of the arrival.
+ ///
+ public DateTime? ArrivalDateTime { get; set; }
+
+ ///
+ /// Flight's departure date and time.
+ ///
+ public DateTime? DepartureDateTime { get; set; }
+
+ ///
+ /// Flight's travel time.
+ ///
+ public TimeSpan? TravelTime { get; set; }
+
+ ///
+ /// The model of plane.
+ ///
+ public required PlaneModel? Model { get; set; }
+
+ ///
+ /// The id of the model.
+ ///
+ public int ModelId { get; set; }
+}
\ No newline at end of file
diff --git a/Airline.Domain/Items/ModelFamily.cs b/Airline.Domain/Items/ModelFamily.cs
new file mode 100644
index 000000000..717d0987e
--- /dev/null
+++ b/Airline.Domain/Items/ModelFamily.cs
@@ -0,0 +1,22 @@
+namespace Airline.Domain.Items;
+
+///
+/// The class for information about a model family.
+///
+public class ModelFamily
+{
+ ///
+ /// Unique model's Id.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The name of model's family.
+ ///
+ public required string NameOfFamily { get; set; }
+
+ ///
+ /// The name of the model manufacturer.
+ ///
+ public required string ManufacturerName { get; set; }
+}
\ No newline at end of file
diff --git a/Airline.Domain/Items/Passengers.cs b/Airline.Domain/Items/Passengers.cs
new file mode 100644
index 000000000..e2a0aaa46
--- /dev/null
+++ b/Airline.Domain/Items/Passengers.cs
@@ -0,0 +1,28 @@
+namespace Airline.Domain.Items;
+
+///
+/// The class for describing a passenger.
+///
+public class Passenger
+{
+ ///
+ /// Unique passenger's Id.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The number of passenger's pasport.
+ ///
+ public required string Passport { get; set; }
+
+ ///
+ /// Passenger's full name.
+ ///
+ public required string PassengerName { get; set; }
+
+ ///
+ /// Passenger's date of birth.
+ ///
+ public DateOnly? DateOfBirth { get; set; }
+
+}
\ No newline at end of file
diff --git a/Airline.Domain/Items/PlaneModel.cs b/Airline.Domain/Items/PlaneModel.cs
new file mode 100644
index 000000000..5c75c43d7
--- /dev/null
+++ b/Airline.Domain/Items/PlaneModel.cs
@@ -0,0 +1,42 @@
+namespace Airline.Domain.Items;
+
+///
+/// The class for information about a plane model.
+///
+public class PlaneModel
+{
+ ///
+ /// Unique plane model's Id.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The name of plane model.
+ ///
+ public required string ModelName { get; set; }
+
+ ///
+ /// The model family of the plane.
+ ///
+ public required ModelFamily? PlaneFamily { get; set; }
+
+ ///
+ /// The max flight range of the plane model.
+ ///
+ public required double MaxRange { get; set; }
+
+ ///
+ /// The passenger capacity of the plane model.
+ ///
+ public required int PassengerCapacity { get; set; }
+
+ ///
+ /// The cargo capacity of the plane model (tons).
+ ///
+ public required double CargoCapacity { get; set; }
+
+ ///
+ /// The id of model family.
+ ///
+ public int ModelFamilyId { get; set; }
+}
\ No newline at end of file
diff --git a/Airline.Domain/Items/Ticket.cs b/Airline.Domain/Items/Ticket.cs
new file mode 100644
index 000000000..1d5bd0ff6
--- /dev/null
+++ b/Airline.Domain/Items/Ticket.cs
@@ -0,0 +1,47 @@
+namespace Airline.Domain.Items;
+
+///
+/// The class for information about ticket.
+///
+public class Ticket
+{
+ ///
+ /// Unique ticket's Id.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The connection between the ticket and the flight.
+ ///
+ public required Flight? Flight { get; set; }
+
+ ///
+ /// The connection between the ticket and the passenger.
+ ///
+ public required Passenger? Passenger { get; set; }
+
+ ///
+ /// The passenger's seat number.
+ ///
+ public required string SeatNumber { get; set; }
+
+ ///
+ /// The flag to indicate if there is a hand luggage.
+ ///
+ public required bool HandLuggage { get; set; }
+
+ ///
+ /// Total baggage weight. (kilograms)
+ ///
+ public double? BaggageWeight { get; set; }
+
+ ///
+ /// The id to connect between the ticket and the flight.
+ ///
+ public int FlightId { get; set; }
+
+ ///
+ /// The id to connect between the ticket and the passenger.
+ ///
+ public int PassengerId { get; set; }
+}
\ No newline at end of file
diff --git a/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj b/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj
new file mode 100644
index 000000000..c5b6e15d5
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs b/Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs
new file mode 100644
index 000000000..cfdd7d8e4
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs
@@ -0,0 +1,46 @@
+using Confluent.Kafka;
+using Airline.Application.Contracts.Flight;
+using Airline.Generator.Kafka.Host.Interfaces;
+
+namespace Airline.Generator.Kafka.Host;
+
+///
+/// Kafka producer service that publishes batches of flight create contracts to a configured topic
+///
+/// Application configuration used to resolve Kafka topic name
+/// Kafka producer used to send messages
+/// Logger instance
+public sealed class AirlineKafkaProducer(
+ IConfiguration configuration,
+ IProducer> producer,
+ ILogger logger) : IProducerService
+{
+ private readonly string _topicName =
+ configuration.GetSection("Kafka")["TopicName"] ?? throw new KeyNotFoundException("TopicName section of Kafka is missing");
+
+ ///
+ /// Sends a batch of flight contracts to Kafka topic using flight code as message key
+ ///
+ /// Batch of flight create contracts
+ public async Task SendAsync(IList batch)
+ {
+ try
+ {
+ logger.LogInformation("Sending a batch of {count} contracts to {topic}", batch.Count, _topicName);
+
+ var key = batch.FirstOrDefault()?.FlightCode ?? Guid.NewGuid().ToString();
+
+ var message = new Message>
+ {
+ Key = key,
+ Value = batch
+ };
+
+ await producer.ProduceAsync(_topicName, message);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Exception occurred during sending a batch of {count} contracts to {topic}", batch.Count, _topicName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs
new file mode 100644
index 000000000..4b4ed28ee
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs
@@ -0,0 +1,67 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Generator.Kafka.Host.Generator;
+using Airline.Generator.Kafka.Host.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Airline.Generator.Kafka.Host.Controllers;
+
+///
+/// Controller used to generate flight contracts and publish them via Kafka message broker
+///
+/// Logger instance
+/// Producer service used to send contracts
+[Route("api/[controller]")]
+[ApiController]
+public sealed class GeneratorController(
+ ILogger logger,
+ IProducerService producerService) : ControllerBase
+{
+ ///
+ /// Generates flight contracts and sends them via Kafka using batches and delay between sends
+ ///
+ /// Batch size
+ /// Total number of contracts to send
+ /// Delay in seconds between batches
+ /// List of generated contracts
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> Get(
+ [FromQuery] int batchSize,
+ [FromQuery] int payloadLimit,
+ [FromQuery] int waitTime)
+ {
+ logger.LogInformation("Generating {limit} contracts via {batchSize} batches and {waitTime}s delay",
+ payloadLimit, batchSize, waitTime);
+
+ try
+ {
+ var results = new List();
+ var counter = 0;
+
+ while (counter < payloadLimit)
+ {
+ var currentBatchSize = Math.Min(batchSize, payloadLimit - counter);
+ var batch = FlightGenerator.GenerateContracts(currentBatchSize);
+
+ await producerService.SendAsync(batch);
+
+ logger.LogInformation("Batch of {batchSize} items has been sent", currentBatchSize);
+
+ results.AddRange(batch);
+ counter += currentBatchSize;
+
+ if (counter < payloadLimit && waitTime > 0)
+ await Task.Delay(waitTime * 1000);
+ }
+
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name);
+ return Ok(results);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs
new file mode 100644
index 000000000..a297db77b
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs
@@ -0,0 +1,58 @@
+using Airline.Application.Contracts.Flight;
+using Bogus;
+
+namespace Airline.Generator.Kafka.Host.Generator;
+
+///
+/// Generates random flight contracts to emulate an external system sending data
+///
+public static class FlightGenerator
+{
+ private static int[]? _modelFamilyIds;
+ private static string[]? _departureCities;
+ private static string[]? _arrivalCities;
+
+ ///
+ /// Initializes the generator with configuration data from the specified configuration source.
+ ///
+ /// Configuration instance containing generator settings.
+ public static void Initialize(IConfiguration configuration)
+ {
+ _modelFamilyIds = configuration.GetSection("FlightGenerator:ModelFamilyId").Get() ?? [];
+ _departureCities = configuration.GetSection("FlightGenerator:DepartureCity").Get() ?? [];
+ _arrivalCities = configuration.GetSection("FlightGenerator:ArrivalCity").Get() ?? [];
+ }
+
+ ///
+ /// Generates a list of flight create contracts
+ ///
+ /// Number of contracts to generate
+ /// Generated list of flight contracts
+ public static List GenerateContracts(int count)
+ {
+ if (_modelFamilyIds?.Length == 0 ||
+ _departureCities?.Length == 0 ||
+ _arrivalCities?.Length == 0)
+ {
+ throw new InvalidOperationException("FlightGenerator configuration is empty");
+ }
+
+ try
+ {
+ return new Faker()
+ .CustomInstantiator(f => new CreateFlightDto(
+ FlightCode: $"{f.Random.Char('A', 'Z')}{f.Random.Char('A', 'Z')}{f.Random.Number(100, 999)}",
+ DepartureCity: f.PickRandom(_departureCities),
+ ArrivalCity: f.PickRandom(_arrivalCities),
+ DepartureDateTime: f.Date.Future(),
+ ArrivalDateTime: f.Date.Future().AddHours(f.Random.Number(1, 10)),
+ ModelId: f.PickRandom(_modelFamilyIds)
+ ))
+ .Generate(count);
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException("Failed to generate contracts", ex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Airline.Generator.Kafka.Host/Interfaces/IProducerService.cs b/Airline.Generator.Kafka.Host/Interfaces/IProducerService.cs
new file mode 100644
index 000000000..37a66623e
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/Interfaces/IProducerService.cs
@@ -0,0 +1,16 @@
+using Airline.Application.Contracts.Flight;
+
+namespace Airline.Generator.Kafka.Host.Interfaces;
+
+///
+/// Abstraction for producing batches of flight create contracts to a Kafka message broker
+///
+public interface IProducerService
+{
+ ///
+ /// Sends a batch of flight create contracts
+ ///
+ /// Batch of flight create contracts to send
+ /// Task representing asynchronous send operation
+ public Task SendAsync(IList batch);
+}
\ No newline at end of file
diff --git a/Airline.Generator.Kafka.Host/Program.cs b/Airline.Generator.Kafka.Host/Program.cs
new file mode 100644
index 000000000..738a0f7f4
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/Program.cs
@@ -0,0 +1,54 @@
+using Airline.Application.Contracts.Flight;
+using Airline.Generator.Kafka.Host;
+using Airline.Generator.Kafka.Host.Generator;
+using Airline.Generator.Kafka.Host.Interfaces;
+using Airline.Generator.Kafka.Host.Serializers;
+using Airline.ServiceDefaults;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+FlightGenerator.Initialize(builder.Configuration);
+
+builder.AddKafkaProducer>("airline-kafka",
+ configureBuilder: kafkaBuilder =>
+ {
+ kafkaBuilder.SetKeySerializer(new AirlineKeySerializer());
+ kafkaBuilder.SetValueSerializer(new AirlineValueSerializer());
+ });
+
+builder.Services.AddScoped();
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(options =>
+{
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => a.GetName().Name!.StartsWith("Airline"))
+ .Distinct();
+
+ foreach (var assembly in assemblies)
+ {
+ var xmlFile = $"{assembly.GetName().Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+ if (File.Exists(xmlPath))
+ options.IncludeXmlComments(xmlPath);
+ }
+});
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.MapControllers();
+
+app.Run();
\ No newline at end of file
diff --git a/Airline.Generator.Kafka.Host/Properties/launchSettings.json b/Airline.Generator.Kafka.Host/Properties/launchSettings.json
new file mode 100644
index 000000000..e99c7bec6
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:60399",
+ "sslPort": 44344
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5206",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7260;http://localhost:5206",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Airline.Generator.Kafka.Host/Serializers/AirlineKeySerializer.cs b/Airline.Generator.Kafka.Host/Serializers/AirlineKeySerializer.cs
new file mode 100644
index 000000000..409614d06
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/Serializers/AirlineKeySerializer.cs
@@ -0,0 +1,19 @@
+using Confluent.Kafka;
+using System.Text.Json;
+
+namespace Airline.Generator.Kafka.Host.Serializers;
+
+///
+/// Kafka serializer that converts a string key into UTF8 JSON bytes
+///
+public sealed class AirlineKeySerializer : ISerializer
+{
+ ///
+ /// Serializes string key into JSON byte array representation
+ ///
+ /// Key value to serialize
+ /// Serialization context provided by Kafka client
+ /// UTF8 JSON byte array
+ public byte[] Serialize(string data, SerializationContext context) =>
+ JsonSerializer.SerializeToUtf8Bytes(data);
+}
\ No newline at end of file
diff --git a/Airline.Generator.Kafka.Host/Serializers/AirlineValueSerializer.cs b/Airline.Generator.Kafka.Host/Serializers/AirlineValueSerializer.cs
new file mode 100644
index 000000000..8b45aa987
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/Serializers/AirlineValueSerializer.cs
@@ -0,0 +1,20 @@
+using Confluent.Kafka;
+using Airline.Application.Contracts.Flight;
+using System.Text.Json;
+
+namespace Airline.Generator.Kafka.Host.Serializers;
+
+///
+/// Kafka serializer that converts a list of flight create contracts into UTF8 JSON bytes
+///
+public sealed class AirlineValueSerializer : ISerializer>
+{
+ ///
+ /// Serializes contract batch into JSON byte array representation
+ ///
+ /// Batch of flight contracts to serialize
+ /// Serialization context provided by Kafka client
+ /// UTF8 JSON byte array
+ public byte[] Serialize(IList data, SerializationContext context) =>
+ JsonSerializer.SerializeToUtf8Bytes(data);
+}
\ No newline at end of file
diff --git a/Airline.Generator.Kafka.Host/appsettings.Development.json b/Airline.Generator.Kafka.Host/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Airline.Generator.Kafka.Host/appsettings.json b/Airline.Generator.Kafka.Host/appsettings.json
new file mode 100644
index 000000000..9cea2c9e0
--- /dev/null
+++ b/Airline.Generator.Kafka.Host/appsettings.json
@@ -0,0 +1,15 @@
+{
+ "FlightGenerator": {
+ "ModelFamilyId": [ 1, 2, 3, 4, 5 ],
+ "DepartureCity": [ "Moscow", "Samara", "Rome", "New York", "Berlin", "Paris" ],
+ "ArrivalCity": [ "Wonderland", "London", "Milan", "Moscow", "Tokyo", "Paris" ]
+ },
+
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj b/Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj
new file mode 100644
index 000000000..2dba4f0a5
--- /dev/null
+++ b/Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Airline.Infrastructure.EfCore/AirlineDbContext.cs b/Airline.Infrastructure.EfCore/AirlineDbContext.cs
new file mode 100644
index 000000000..032c590e6
--- /dev/null
+++ b/Airline.Infrastructure.EfCore/AirlineDbContext.cs
@@ -0,0 +1,108 @@
+using Airline.Domain.Items;
+using Microsoft.EntityFrameworkCore;
+using MongoDB.EntityFrameworkCore.Extensions;
+
+namespace Airline.Infrastructure.EfCore;
+
+///
+/// Represents the database context for the airline management system.
+/// Configures entity-to-collection mappings and property name conventions for MongoDB.
+///
+public class AirlineDbContext(DbContextOptions options) : DbContext(options)
+{
+ ///
+ /// Gets or sets the collection of aircraft model families.
+ /// Mapped to the 'model_families' collection in MongoDB.
+ ///
+ public DbSet ModelFamilies { get; set; }
+
+ ///
+ /// Gets or sets the collection of aircraft models.
+ /// Mapped to the 'plane_models' collection in MongoDB.
+ ///
+ public DbSet PlaneModels { get; set; }
+
+ ///
+ /// Gets or sets the collection of flights.
+ /// Mapped to the 'flights' collection in MongoDB.
+ ///
+ public DbSet Flights { get; set; }
+
+ ///
+ /// Gets or sets the collection of passengers.
+ /// Mapped to the 'passengers' collection in MongoDB.
+ ///
+ public DbSet Passengers { get; set; }
+
+ ///
+ /// Gets or sets the collection of tickets.
+ /// Mapped to the 'tickets' collection in MongoDB.
+ ///
+ public DbSet Tickets { get; set; }
+
+ ///
+ /// Configures the model by mapping entities to MongoDB collections and customizing field names.
+ /// Disables automatic transaction behavior (MongoDB does not support transactions in this context).
+ ///
+ /// The model builder used to configure entity mappings.
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ Database.AutoTransactionBehavior = AutoTransactionBehavior.Never;
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToCollection("model_families");
+ entity.HasKey(f => f.Id);
+ entity.Property(f => f.Id).HasElementName("_id");
+ entity.Property(f => f.NameOfFamily).HasElementName("family_name");
+ entity.Property(f => f.ManufacturerName).HasElementName("manufacturer");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToCollection("plane_models");
+ entity.HasKey(m => m.Id);
+ entity.Property(m => m.Id).HasElementName("_id");
+ entity.Property(m => m.ModelName).HasElementName("model_name");
+ entity.Property(m => m.MaxRange).HasElementName("max_range_km");
+ entity.Property(m => m.PassengerCapacity).HasElementName("passenger_capacity");
+ entity.Property(m => m.CargoCapacity).HasElementName("cargo_capacity_tons");
+ entity.Property(m => m.ModelFamilyId).HasElementName("family_id");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToCollection("passengers");
+ entity.HasKey(p => p.Id);
+ entity.Property(p => p.Id).HasElementName("_id");
+ entity.Property(p => p.Passport).HasElementName("passport_number");
+ entity.Property(p => p.PassengerName).HasElementName("full_name");
+ entity.Property(p => p.DateOfBirth).HasElementName("date_of_birth");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToCollection("flights");
+ entity.HasKey(f => f.Id);
+ entity.Property(f => f.Id).HasElementName("_id");
+ entity.Property(f => f.FlightCode).HasElementName("flight_code");
+ entity.Property(f => f.DepartureCity).HasElementName("departure_city");
+ entity.Property(f => f.ArrivalCity).HasElementName("arrival_city");
+ entity.Property(f => f.DepartureDateTime).HasElementName("departure_datetime");
+ entity.Property(f => f.ArrivalDateTime).HasElementName("arrival_datetime");
+ entity.Property(f => f.ModelId).HasElementName("plane_model_id");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToCollection("tickets");
+ entity.HasKey(t => t.Id);
+ entity.Property(t => t.Id).HasElementName("_id");
+ entity.Property(t => t.SeatNumber).HasElementName("seat_number");
+ entity.Property(t => t.HandLuggage).HasElementName("has_hand_luggage");
+ entity.Property(t => t.BaggageWeight).HasElementName("baggage_weight_kg");
+ entity.Property(t => t.FlightId).HasElementName("flight_id");
+ entity.Property(t => t.PassengerId).HasElementName("passenger_id");
+ });
+ }
+}
\ No newline at end of file
diff --git a/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs b/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs
new file mode 100644
index 000000000..7e490f888
--- /dev/null
+++ b/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs
@@ -0,0 +1,71 @@
+using Airline.Domain.Items;
+using Airline.Domain;
+using Microsoft.EntityFrameworkCore;
+
+namespace Airline.Infrastructure.EfCore.Repositories;
+
+///
+/// Repository implementation for flight entities.
+/// Provides data access operations for the Flight domain model using Entity Framework Core with MongoDB.
+///
+public class FlightRepository(AirlineDbContext context) : IRepository
+{
+ ///
+ /// Creates a new flight in the data store.
+ ///
+ /// The flight entity to create.
+ /// The created flight entity with assigned identifier.
+ public async Task CreateAsync(Flight entity)
+ {
+ var entry = await context.Flights.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entry.Entity;
+ }
+
+ ///
+ /// Deletes a flight from the data store by its unique identifier.
+ ///
+ /// The unique identifier of the flight to delete.
+ ///
+ /// if the flight was successfully deleted;
+ /// otherwise, if the flight was not found.
+ ///
+ public async Task DeleteAsync(int id)
+ {
+ var entity = await context.Flights.FindAsync(id);
+ if (entity == null) return false;
+
+ context.Flights.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+
+ ///
+ /// Retrieves a flight from the data store by its unique identifier.
+ ///
+ /// The unique identifier of the flight.
+ ///
+ /// The flight entity if found; otherwise, .
+ ///
+ public async Task GetAsync(int id) =>
+ await context.Flights.FindAsync(id);
+
+ ///
+ /// Retrieves all flights from the data store.
+ ///
+ /// A list of all flight entities.
+ public async Task> GetAllAsync() =>
+ await context.Flights.ToListAsync();
+
+ ///
+ /// Updates an existing flight in the data store.
+ ///
+ /// The flight entity with updated data.
+ /// The updated flight entity.
+ public async Task UpdateAsync(Flight entity)
+ {
+ context.Flights.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+}
\ No newline at end of file
diff --git a/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs b/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs
new file mode 100644
index 000000000..766c7ea8d
--- /dev/null
+++ b/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs
@@ -0,0 +1,71 @@
+using Airline.Domain.Items;
+using Airline.Domain;
+using Microsoft.EntityFrameworkCore;
+
+namespace Airline.Infrastructure.EfCore.Repositories;
+
+///
+/// Repository implementation for aircraft model family entities.
+/// Provides data access operations for the ModelFamily domain model using Entity Framework Core with MongoDB.
+///
+public class ModelFamilyRepository(AirlineDbContext context) : IRepository
+{
+ ///
+ /// Creates a new aircraft model family in the data store.
+ ///
+ /// The model family entity to create.
+ /// The created model family entity with assigned identifier.
+ public async Task CreateAsync(ModelFamily entity)
+ {
+ var entry = await context.ModelFamilies.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entry.Entity;
+ }
+
+ ///
+ /// Deletes an aircraft model family from the data store by its unique identifier.
+ ///
+ /// The unique identifier of the model family to delete.
+ ///
+ /// if the model family was successfully deleted;
+ /// otherwise, if the family was not found.
+ ///
+ public async Task DeleteAsync(int id)
+ {
+ var entity = await context.ModelFamilies.FindAsync(id);
+ if (entity == null) return false;
+
+ context.ModelFamilies.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+
+ ///
+ /// Retrieves an aircraft model family from the data store by its unique identifier.
+ ///
+ /// The unique identifier of the model family.
+ ///
+ /// The model family entity if found; otherwise, .
+ ///
+ public async Task GetAsync(int id) =>
+ await context.ModelFamilies.FindAsync(id);
+
+ ///
+ /// Retrieves all aircraft model families from the data store.
+ ///
+ /// A list of all model family entities.
+ public async Task> GetAllAsync() =>
+ await context.ModelFamilies.ToListAsync();
+
+ ///
+ /// Updates an existing aircraft model family in the data store.
+ ///
+ /// The model family entity with updated data.
+ /// The updated model family entity.
+ public async Task UpdateAsync(ModelFamily entity)
+ {
+ context.ModelFamilies.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+}
\ No newline at end of file
diff --git a/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs b/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs
new file mode 100644
index 000000000..7383c306d
--- /dev/null
+++ b/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs
@@ -0,0 +1,71 @@
+using Airline.Domain;
+using Airline.Domain.Items;
+using Microsoft.EntityFrameworkCore;
+
+namespace Airline.Infrastructure.EfCore.Repositories;
+
+///
+/// Repository implementation for passenger entities.
+/// Provides data access operations for the Passenger domain model using Entity Framework Core with MongoDB.
+///
+public class PassengerRepository(AirlineDbContext context) : IRepository
+{
+ ///
+ /// Creates a new passenger in the data store.
+ ///
+ /// The passenger entity to create.
+ /// The created passenger entity with assigned identifier.
+ public async Task CreateAsync(Passenger entity)
+ {
+ var entry = await context.Passengers.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entry.Entity;
+ }
+
+ ///
+ /// Deletes a passenger from the data store by its unique identifier.
+ ///
+ /// The unique identifier of the passenger to delete.
+ ///
+ /// if the passenger was successfully deleted;
+ /// otherwise, if the passenger was not found.
+ ///
+ public async Task DeleteAsync(int id)
+ {
+ var entity = await context.Passengers.FindAsync(id);
+ if (entity == null) return false;
+
+ context.Passengers.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+
+ ///
+ /// Retrieves a passenger from the data store by its unique identifier.
+ ///
+ /// The unique identifier of the passenger.
+ ///
+ /// The passenger entity if found; otherwise, .
+ ///
+ public async Task GetAsync(int id) =>
+ await context.Passengers.FindAsync(id);
+
+ ///
+ /// Retrieves all passengers from the data store.
+ ///
+ /// A list of all passenger entities.
+ public async Task> GetAllAsync() =>
+ await context.Passengers.ToListAsync();
+
+ ///
+ /// Updates an existing passenger in the data store.
+ ///
+ /// The passenger entity with updated data.
+ /// The updated passenger entity.
+ public async Task UpdateAsync(Passenger entity)
+ {
+ context.Passengers.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+}
\ No newline at end of file
diff --git a/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs b/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs
new file mode 100644
index 000000000..ed1a8aebe
--- /dev/null
+++ b/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs
@@ -0,0 +1,71 @@
+using Airline.Domain.Items;
+using Airline.Domain;
+using Microsoft.EntityFrameworkCore;
+
+namespace Airline.Infrastructure.EfCore.Repositories;
+
+///
+/// Repository implementation for aircraft model entities.
+/// Provides data access operations for the PlaneModel domain model using Entity Framework Core with MongoDB.
+///
+public class PlaneModelRepository(AirlineDbContext context) : IRepository
+{
+ ///
+ /// Creates a new aircraft model in the data store.
+ ///
+ /// The aircraft model entity to create.
+ /// The created aircraft model entity with assigned identifier.
+ public async Task CreateAsync(PlaneModel entity)
+ {
+ var entry = await context.PlaneModels.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entry.Entity;
+ }
+
+ ///
+ /// Deletes an aircraft model from the data store by its unique identifier.
+ ///
+ /// The unique identifier of the aircraft model to delete.
+ ///
+ /// if the aircraft model was successfully deleted;
+ /// otherwise, if the model was not found.
+ ///
+ public async Task DeleteAsync(int id)
+ {
+ var entity = await context.PlaneModels.FindAsync(id);
+ if (entity == null) return false;
+
+ context.PlaneModels.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+
+ ///
+ /// Retrieves an aircraft model from the data store by its unique identifier.
+ ///
+ /// The unique identifier of the aircraft model.
+ ///
+ /// The aircraft model entity if found; otherwise,