From 3ebd0c3a2c50c3a354427e331c2ed0e0be115c36 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Sat, 3 May 2025 10:09:52 +0300 Subject: [PATCH] add vehicles hierarchy management --- src/Application/Aircrafts/AircraftDto.cs | 23 + .../AddAircraft/AddAircraftCommand.cs | 12 + .../AddAircraftCommandAuthorizer.cs | 31 ++ .../AddAircraft/AddAircraftCommandHandler.cs | 51 ++ .../AddAircraftCommandValidator.cs | 37 ++ .../DeleteAircraft/DeleteAircraftCommand.cs | 8 + .../DeleteAircraftCommandAuthorizer.cs | 31 ++ .../DeleteAircraftCommandHandler.cs | 34 ++ .../DeleteAircraftCommandValidator.cs | 14 + .../UpdateAircraft/UpdateAircraftCommand.cs | 14 + .../UpdateAircraftCommandAuthorizer.cs | 31 ++ .../UpdateAircraftCommandHandler.cs | 56 +++ .../UpdateAircraftCommandValidator.cs | 41 ++ .../Queries/GetAircraft/GetAircraftQuery.cs | 8 + .../GetAircraft/GetAircraftQueryAuthorizer.cs | 31 ++ .../GetAircraft/GetAircraftQueryHandler.cs | 38 ++ .../GetAircraft/GetAircraftQueryValidator.cs | 14 + .../GetAircraftsPage/GetAircraftsPageQuery.cs | 23 + .../GetAircraftsPageQueryAuthorizer.cs | 31 ++ .../GetAircraftsPageQueryHandler.cs | 53 ++ .../GetAircraftsPageQueryValidator.cs | 43 ++ .../ViewModels/AddAircraftViewModel.cs | 10 + .../GetAircraftsPageFilterViewModel.cs | 15 + .../ViewModels/UpdateAircraftViewModel.cs | 10 + src/Application/Buses/BusDto.cs | 23 + .../Buses/Commands/AddBus/AddBusCommand.cs | 12 + .../AddBus/AddBusCommandAuthorizer.cs | 31 ++ .../Commands/AddBus/AddBusCommandHandler.cs | 51 ++ .../Commands/AddBus/AddBusCommandValidator.cs | 37 ++ .../Commands/DeleteBus/DeleteBusCommand.cs | 8 + .../DeleteBus/DeleteBusCommandAuthorizer.cs | 31 ++ .../DeleteBus/DeleteBusCommandHandler.cs | 34 ++ .../DeleteBus/DeleteBusCommandValidator.cs | 14 + .../Commands/UpdateBus/UpdateBusCommand.cs | 14 + .../UpdateBus/UpdateBusCommandAuthorizer.cs | 31 ++ .../UpdateBus/UpdateBusCommandHandler.cs | 56 +++ .../UpdateBus/UpdateBusCommandValidator.cs | 41 ++ .../Buses/Queries/GetBus/GetBusQuery.cs | 8 + .../Queries/GetBus/GetBusQueryAuthorizer.cs | 31 ++ .../Queries/GetBus/GetBusQueryHandler.cs | 38 ++ .../Queries/GetBus/GetBusQueryValidator.cs | 14 + .../Queries/GetBusesPage/GetBusesPageQuery.cs | 23 + .../GetBusesPageQueryAuthorizer.cs | 31 ++ .../GetBusesPage/GetBusesPageQueryHandler.cs | 53 ++ .../GetBusesPageQueryValidator.cs | 43 ++ .../Buses/ViewModels/AddBusViewModel.cs | 10 + .../ViewModels/GetBusesPageFilterViewModel.cs | 15 + .../Buses/ViewModels/UpdateBusViewModel.cs | 10 + .../Repositories/AircraftRepository.cs | 6 + .../Persistence/Repositories/BusRepository.cs | 6 + .../Repositories/TrainRepository.cs | 6 + .../Repositories/VehicleRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 8 + .../Commands/AddTrain/AddTrainCommand.cs | 12 + .../AddTrain/AddTrainCommandAuthorizer.cs | 31 ++ .../AddTrain/AddTrainCommandHandler.cs | 51 ++ .../AddTrain/AddTrainCommandValidator.cs | 37 ++ .../DeleteTrain/DeleteTrainCommand.cs | 8 + .../DeleteTrainCommandAuthorizer.cs | 31 ++ .../DeleteTrain/DeleteTrainCommandHandler.cs | 34 ++ .../DeleteTrainCommandValidator.cs | 14 + .../UpdateTrain/UpdateTrainCommand.cs | 14 + .../UpdateTrainCommandAuthorizer.cs | 31 ++ .../UpdateTrain/UpdateTrainCommandHandler.cs | 56 +++ .../UpdateTrainCommandValidator.cs | 41 ++ .../Trains/Queries/GetTrain/GetTrainQuery.cs | 8 + .../GetTrain/GetTrainQueryAuthorizer.cs | 31 ++ .../Queries/GetTrain/GetTrainQueryHandler.cs | 38 ++ .../GetTrain/GetTrainQueryValidator.cs | 14 + .../GetTrainsPage/GetTrainsPageQuery.cs | 23 + .../GetTrainsPageQueryAuthorizer.cs | 31 ++ .../GetTrainsPageQueryHandler.cs | 53 ++ .../GetTrainsPageQueryValidator.cs | 43 ++ src/Application/Trains/TrainDto.cs | 23 + .../Trains/ViewModels/AddTrainViewModel.cs | 10 + .../GetTrainsPageFilterViewModel.cs | 15 + .../Trains/ViewModels/UpdateTrainViewModel.cs | 10 + .../Persistence/Configuration.cs | 1 + src/Domain/Entities/Aircraft.cs | 12 + src/Domain/Entities/Bus.cs | 12 + src/Domain/Entities/Train.cs | 12 + src/Domain/Entities/Vehicle.cs | 8 + .../Controllers/AircraftsController.cs | 193 +++++++ src/HttpApi/Controllers/BusesController.cs | 193 +++++++ src/HttpApi/Controllers/TestsController.cs | 27 +- src/HttpApi/Controllers/TrainsController.cs | 193 +++++++ src/HttpApi/appsettings.json | 31 ++ .../InMemory/InMemoryUnitOfWork.cs | 12 + .../InMemoryAircraftRepository.cs | 11 + .../Repositories/InMemoryBusRepository.cs | 11 + .../Repositories/InMemoryTrainRepository.cs | 11 + .../Repositories/InMemoryVehicleRepository.cs | 11 + .../Configurations/AircraftConfiguration.cs | 33 ++ .../Configurations/BaseConfiguration.cs | 21 +- .../Configurations/BusConfiguration.cs | 33 ++ .../Configurations/TrainConfiguration.cs | 33 ++ .../Configurations/VehicleConfiguration.cs | 41 ++ ...ft_Train_with_basic_properties.Designer.cs | 476 ++++++++++++++++++ ...us_Aircraft_Train_with_basic_properties.cs | 50 ++ .../PostgreSqlDbContextModelSnapshot.cs | 124 +++++ .../PostgreSql/PostgreSqlDbContext.cs | 1 - .../PostgreSql/PostgreSqlUnitOfWork.cs | 12 + .../PostgreSqlAircraftRepository.cs | 11 + .../Repositories/PostgreSqlBusRepository.cs | 11 + .../Repositories/PostgreSqlTrainRepository.cs | 11 + .../PostgreSqlVehicleRepository.cs | 11 + 106 files changed, 3599 insertions(+), 16 deletions(-) create mode 100644 src/Application/Aircrafts/AircraftDto.cs create mode 100644 src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs create mode 100644 src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs create mode 100644 src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs create mode 100644 src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs create mode 100644 src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs create mode 100644 src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs create mode 100644 src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs create mode 100644 src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs create mode 100644 src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs create mode 100644 src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs create mode 100644 src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs create mode 100644 src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs create mode 100644 src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs create mode 100644 src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs create mode 100644 src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs create mode 100644 src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs create mode 100644 src/Application/Buses/BusDto.cs create mode 100644 src/Application/Buses/Commands/AddBus/AddBusCommand.cs create mode 100644 src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs create mode 100644 src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs create mode 100644 src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs create mode 100644 src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs create mode 100644 src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs create mode 100644 src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs create mode 100644 src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs create mode 100644 src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs create mode 100644 src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs create mode 100644 src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs create mode 100644 src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs create mode 100644 src/Application/Buses/Queries/GetBus/GetBusQuery.cs create mode 100644 src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs create mode 100644 src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs create mode 100644 src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs create mode 100644 src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs create mode 100644 src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs create mode 100644 src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs create mode 100644 src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs create mode 100644 src/Application/Buses/ViewModels/AddBusViewModel.cs create mode 100644 src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs create mode 100644 src/Application/Buses/ViewModels/UpdateBusViewModel.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs create mode 100644 src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs create mode 100644 src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs create mode 100644 src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs create mode 100644 src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs create mode 100644 src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs create mode 100644 src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs create mode 100644 src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs create mode 100644 src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs create mode 100644 src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs create mode 100644 src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs create mode 100644 src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs create mode 100644 src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs create mode 100644 src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs create mode 100644 src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs create mode 100644 src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs create mode 100644 src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs create mode 100644 src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs create mode 100644 src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs create mode 100644 src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs create mode 100644 src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs create mode 100644 src/Application/Trains/TrainDto.cs create mode 100644 src/Application/Trains/ViewModels/AddTrainViewModel.cs create mode 100644 src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs create mode 100644 src/Application/Trains/ViewModels/UpdateTrainViewModel.cs create mode 100644 src/Domain/Entities/Aircraft.cs create mode 100644 src/Domain/Entities/Bus.cs create mode 100644 src/Domain/Entities/Train.cs create mode 100644 src/Domain/Entities/Vehicle.cs create mode 100644 src/HttpApi/Controllers/AircraftsController.cs create mode 100644 src/HttpApi/Controllers/BusesController.cs create mode 100644 src/HttpApi/Controllers/TrainsController.cs create mode 100644 src/HttpApi/appsettings.json create mode 100644 src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/BusConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs diff --git a/src/Application/Aircrafts/AircraftDto.cs b/src/Application/Aircrafts/AircraftDto.cs new file mode 100644 index 0000000..8549160 --- /dev/null +++ b/src/Application/Aircrafts/AircraftDto.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Aircrafts; + +public sealed class AircraftDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs new file mode 100644 index 0000000..f938a36 --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public record AddAircraftCommand : IRequest +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs new file mode 100644 index 0000000..8c98851 --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public class AddAircraftCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddAircraftCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddAircraftCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs new file mode 100644 index 0000000..8ffe4e7 --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public class AddAircraftCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddAircraftCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddAircraftCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Number == request.Number, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Aircraft with given number already exists."); + } + + entity = new Aircraft() + { + Number = request.Number, + Model = request.Model, + Capacity = request.Capacity + }; + + entity = await _unitOfWork.AircraftRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs new file mode 100644 index 0000000..76f5fe1 --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs @@ -0,0 +1,37 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public class AddAircraftCommandValidator : AbstractValidator +{ + public AddAircraftCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs new file mode 100644 index 0000000..88bd0c7 --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public record DeleteAircraftCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs new file mode 100644 index 0000000..97f1463 --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public class DeleteAircraftCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteAircraftCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteAircraftCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs new file mode 100644 index 0000000..2188c03 --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public class DeleteAircraftCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteAircraftCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteAircraftCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.AircraftRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs new file mode 100644 index 0000000..8d43bfc --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public class DeleteAircraftCommandValidator : AbstractValidator +{ + public DeleteAircraftCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs new file mode 100644 index 0000000..77a174e --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public record UpdateAircraftCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs new file mode 100644 index 0000000..c7036f5 --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public class UpdateAircraftCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateAircraftCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateAircraftCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs new file mode 100644 index 0000000..952af00 --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs @@ -0,0 +1,56 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public class UpdateAircraftCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateAircraftCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateAircraftCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var duplicateEntity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Number == request.Number, + cancellationToken); + + if (duplicateEntity != null) + { + throw new DuplicateEntityException( + "Aircraft with given number already exists."); + } + + entity.Number = request.Number; + entity.Model = request.Model; + entity.Capacity = request.Capacity; + + entity = await _unitOfWork.AircraftRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs new file mode 100644 index 0000000..70fb15a --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public class UpdateAircraftCommandValidator : AbstractValidator +{ + public UpdateAircraftCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs new file mode 100644 index 0000000..60fb5f1 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public record GetAircraftQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs new file mode 100644 index 0000000..121cf58 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public class GetAircraftQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAircraftQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAircraftQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs new file mode 100644 index 0000000..a92c658 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public class GetAircraftQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAircraftQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetAircraftQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs new file mode 100644 index 0000000..c5a5e09 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public class GetAircraftQueryValidator : AbstractValidator +{ + public GetAircraftQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs new file mode 100644 index 0000000..df99a61 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public record GetAircraftsPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public string? Number { get; set; } + + public string? Model { get; set; } + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs new file mode 100644 index 0000000..79a0546 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public class GetAircraftsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAircraftsPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAircraftsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs new file mode 100644 index 0000000..e75b5f0 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs @@ -0,0 +1,53 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public class GetAircraftsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAircraftsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetAircraftsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.AircraftRepository.GetPageAsync( + e => + (e.Number.ToLower().Contains(request.Search.ToLower()) || + e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.CapacityGreaterOrEqualThan != null + ? e.Capacity >= request.CapacityGreaterOrEqualThan + : true) && + (request.CapacityLessOrEqualThan != null + ? e.Capacity <= request.CapacityLessOrEqualThan + : true), + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs new file mode 100644 index 0000000..bed93b0 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public class GetAircraftsPageQueryValidator : AbstractValidator +{ + public GetAircraftsPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs new file mode 100644 index 0000000..3a6e5bd --- /dev/null +++ b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +public sealed class AddAircraftViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs new file mode 100644 index 0000000..c956d2b --- /dev/null +++ b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs @@ -0,0 +1,15 @@ +namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +public sealed class GetAircraftsPageFilterViewModel +{ + public string? Number { get; set; } + + public string? Model { get; set; } + + // TODO: Consider adding strict equals rule although it is not + // necessarily needed to filter with exact capacity + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs new file mode 100644 index 0000000..1c4e71e --- /dev/null +++ b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +public sealed class UpdateAircraftViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Buses/BusDto.cs b/src/Application/Buses/BusDto.cs new file mode 100644 index 0000000..3ba7ae5 --- /dev/null +++ b/src/Application/Buses/BusDto.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Buses; + +public sealed class BusDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommand.cs b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs new file mode 100644 index 0000000..786a773 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public record AddBusCommand : IRequest +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs new file mode 100644 index 0000000..60a0581 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public class AddBusCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddBusCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddBusCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs new file mode 100644 index 0000000..c5a8488 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public class AddBusCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddBusCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddBusCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Number == request.Number, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Bus with given number already exists."); + } + + entity = new Bus() + { + Number = request.Number, + Model = request.Model, + Capacity = request.Capacity + }; + + entity = await _unitOfWork.BusRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs new file mode 100644 index 0000000..46bf658 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs @@ -0,0 +1,37 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public class AddBusCommandValidator : AbstractValidator +{ + public AddBusCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs new file mode 100644 index 0000000..32ea1d6 --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public record DeleteBusCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs new file mode 100644 index 0000000..2f61edc --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public class DeleteBusCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteBusCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteBusCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs new file mode 100644 index 0000000..f226338 --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public class DeleteBusCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteBusCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteBusCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.BusRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs new file mode 100644 index 0000000..c4e71a5 --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public class DeleteBusCommandValidator : AbstractValidator +{ + public DeleteBusCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs new file mode 100644 index 0000000..9754c7a --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public record UpdateBusCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs new file mode 100644 index 0000000..17201fa --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public class UpdateBusCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateBusCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateBusCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs new file mode 100644 index 0000000..152e220 --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs @@ -0,0 +1,56 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public class UpdateBusCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateBusCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateBusCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var duplicateEntity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Number == request.Number, + cancellationToken); + + if (duplicateEntity != null) + { + throw new DuplicateEntityException( + "Bus with given number already exists."); + } + + entity.Number = request.Number; + entity.Model = request.Model; + entity.Capacity = request.Capacity; + + entity = await _unitOfWork.BusRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs new file mode 100644 index 0000000..7097ab2 --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public class UpdateBusCommandValidator : AbstractValidator +{ + public UpdateBusCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQuery.cs b/src/Application/Buses/Queries/GetBus/GetBusQuery.cs new file mode 100644 index 0000000..8419f16 --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public record GetBusQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs new file mode 100644 index 0000000..372b46e --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public class GetBusQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetBusQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetBusQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs new file mode 100644 index 0000000..26ba345 --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public class GetBusQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetBusQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetBusQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs new file mode 100644 index 0000000..b97a62f --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public class GetBusQueryValidator : AbstractValidator +{ + public GetBusQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs new file mode 100644 index 0000000..cf97c50 --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public record GetBusesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public string? Number { get; set; } + + public string? Model { get; set; } + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs new file mode 100644 index 0000000..f51a43c --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public class GetBusesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetBusesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetBusesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs new file mode 100644 index 0000000..920ea9e --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs @@ -0,0 +1,53 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public class GetBusesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetBusesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetBusesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.BusRepository.GetPageAsync( + e => + (e.Number.ToLower().Contains(request.Search.ToLower()) || + e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.CapacityGreaterOrEqualThan != null + ? e.Capacity >= request.CapacityGreaterOrEqualThan + : true) && + (request.CapacityLessOrEqualThan != null + ? e.Capacity <= request.CapacityLessOrEqualThan + : true), + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs new file mode 100644 index 0000000..f72c75e --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public class GetBusesPageQueryValidator : AbstractValidator +{ + public GetBusesPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Buses/ViewModels/AddBusViewModel.cs b/src/Application/Buses/ViewModels/AddBusViewModel.cs new file mode 100644 index 0000000..4be8485 --- /dev/null +++ b/src/Application/Buses/ViewModels/AddBusViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; + +public sealed class AddBusViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs new file mode 100644 index 0000000..a05dd27 --- /dev/null +++ b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs @@ -0,0 +1,15 @@ +namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; + +public sealed class GetBusesPageFilterViewModel +{ + public string? Number { get; set; } + + public string? Model { get; set; } + + // TODO: Consider adding strict equals rule although it is not + // necessarily needed to filter with exact capacity + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Buses/ViewModels/UpdateBusViewModel.cs b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs new file mode 100644 index 0000000..6ee2c90 --- /dev/null +++ b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; + +public sealed class UpdateBusViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs new file mode 100644 index 0000000..8ccd4ca --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/AircraftRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface AircraftRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs new file mode 100644 index 0000000..18c76dc --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/BusRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface BusRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs new file mode 100644 index 0000000..640a507 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/TrainRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface TrainRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs new file mode 100644 index 0000000..db9fde8 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/VehicleRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface VehicleRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index fde3ef8..e4cad5f 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -14,6 +14,14 @@ public interface UnitOfWork : IDisposable RouteRepository RouteRepository { get; } + VehicleRepository VehicleRepository { get; } + + BusRepository BusRepository { get; } + + AircraftRepository AircraftRepository { get; } + + TrainRepository TrainRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs new file mode 100644 index 0000000..a5e81d1 --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public record AddTrainCommand : IRequest +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs new file mode 100644 index 0000000..36fdbf4 --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public class AddTrainCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddTrainCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddTrainCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs new file mode 100644 index 0000000..8edd60c --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public class AddTrainCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddTrainCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddTrainCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Number == request.Number, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Train with given number already exists."); + } + + entity = new Train() + { + Number = request.Number, + Model = request.Model, + Capacity = request.Capacity + }; + + entity = await _unitOfWork.TrainRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs new file mode 100644 index 0000000..f8797c5 --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs @@ -0,0 +1,37 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public class AddTrainCommandValidator : AbstractValidator +{ + public AddTrainCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs new file mode 100644 index 0000000..6d714c7 --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public record DeleteTrainCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs new file mode 100644 index 0000000..df31b90 --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public class DeleteTrainCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteTrainCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteTrainCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs new file mode 100644 index 0000000..94d3ab0 --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public class DeleteTrainCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteTrainCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteTrainCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.TrainRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs new file mode 100644 index 0000000..f1008cd --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public class DeleteTrainCommandValidator : AbstractValidator +{ + public DeleteTrainCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs new file mode 100644 index 0000000..4aa128f --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public record UpdateTrainCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs new file mode 100644 index 0000000..c4dd607 --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public class UpdateTrainCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateTrainCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateTrainCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs new file mode 100644 index 0000000..70ea0ce --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs @@ -0,0 +1,56 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public class UpdateTrainCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateTrainCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateTrainCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var duplicateEntity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Number == request.Number, + cancellationToken); + + if (duplicateEntity != null) + { + throw new DuplicateEntityException( + "Train with given number already exists."); + } + + entity.Number = request.Number; + entity.Model = request.Model; + entity.Capacity = request.Capacity; + + entity = await _unitOfWork.TrainRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs new file mode 100644 index 0000000..689d2e8 --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public class UpdateTrainCommandValidator : AbstractValidator +{ + public UpdateTrainCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs new file mode 100644 index 0000000..9285936 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public record GetTrainQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs new file mode 100644 index 0000000..7416f04 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public class GetTrainQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetTrainQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetTrainQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs new file mode 100644 index 0000000..b0c89fd --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public class GetTrainQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetTrainQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetTrainQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs new file mode 100644 index 0000000..f3bcd7e --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public class GetTrainQueryValidator : AbstractValidator +{ + public GetTrainQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs new file mode 100644 index 0000000..ba41889 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public record GetTrainsPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public string? Number { get; set; } + + public string? Model { get; set; } + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs new file mode 100644 index 0000000..2a68aaa --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public class GetTrainsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetTrainsPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetTrainsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs new file mode 100644 index 0000000..2690cfa --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs @@ -0,0 +1,53 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public class GetTrainsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetTrainsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetTrainsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.TrainRepository.GetPageAsync( + e => + (e.Number.ToLower().Contains(request.Search.ToLower()) || + e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.CapacityGreaterOrEqualThan != null + ? e.Capacity >= request.CapacityGreaterOrEqualThan + : true) && + (request.CapacityLessOrEqualThan != null + ? e.Capacity <= request.CapacityLessOrEqualThan + : true), + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs new file mode 100644 index 0000000..1b56968 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public class GetTrainsPageQueryValidator : AbstractValidator +{ + public GetTrainsPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Trains/TrainDto.cs b/src/Application/Trains/TrainDto.cs new file mode 100644 index 0000000..2e02c73 --- /dev/null +++ b/src/Application/Trains/TrainDto.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Trains; + +public sealed class TrainDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Trains/ViewModels/AddTrainViewModel.cs b/src/Application/Trains/ViewModels/AddTrainViewModel.cs new file mode 100644 index 0000000..998097e --- /dev/null +++ b/src/Application/Trains/ViewModels/AddTrainViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Trains.ViewModels; + +public sealed class AddTrainViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs b/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs new file mode 100644 index 0000000..ef0026b --- /dev/null +++ b/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs @@ -0,0 +1,15 @@ +namespace cuqmbr.TravelGuide.Application.Trains.ViewModels; + +public sealed class GetTrainsPageFilterViewModel +{ + public string? Number { get; set; } + + public string? Model { get; set; } + + // TODO: Consider adding strict equals rule although it is not + // necessarily needed to filter with exact capacity + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs b/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs new file mode 100644 index 0000000..3b7682e --- /dev/null +++ b/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Trains.ViewModels; + +public sealed class UpdateTrainViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } +} diff --git a/src/Configuration/Persistence/Configuration.cs b/src/Configuration/Persistence/Configuration.cs index 1eb83be..396aeba 100644 --- a/src/Configuration/Persistence/Configuration.cs +++ b/src/Configuration/Persistence/Configuration.cs @@ -29,6 +29,7 @@ public static class Configuration configuration.ConnectionString, options => { + // TODO: Move to persistence project options.MigrationsHistoryTable( "ef_migrations_history", configuration.PartitionName); diff --git a/src/Domain/Entities/Aircraft.cs b/src/Domain/Entities/Aircraft.cs new file mode 100644 index 0000000..4401999 --- /dev/null +++ b/src/Domain/Entities/Aircraft.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class Aircraft : Vehicle +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + // TODO: Add more properties to describe aircraft's capabilities +} diff --git a/src/Domain/Entities/Bus.cs b/src/Domain/Entities/Bus.cs new file mode 100644 index 0000000..eed23c9 --- /dev/null +++ b/src/Domain/Entities/Bus.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class Bus : Vehicle +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + // TODO: Add more properties to describe bus' capabilities +} diff --git a/src/Domain/Entities/Train.cs b/src/Domain/Entities/Train.cs new file mode 100644 index 0000000..215c21b --- /dev/null +++ b/src/Domain/Entities/Train.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class Train : Vehicle +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + // TODO: Add more properties to describe train's capabilities +} diff --git a/src/Domain/Entities/Vehicle.cs b/src/Domain/Entities/Vehicle.cs new file mode 100644 index 0000000..b0882df --- /dev/null +++ b/src/Domain/Entities/Vehicle.cs @@ -0,0 +1,8 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public abstract class Vehicle : EntityBase +{ + public VehicleType VehicleType { get; set; } +} diff --git a/src/HttpApi/Controllers/AircraftsController.cs b/src/HttpApi/Controllers/AircraftsController.cs new file mode 100644 index 0000000..1aaa592 --- /dev/null +++ b/src/HttpApi/Controllers/AircraftsController.cs @@ -0,0 +1,193 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Aircrafts; +using cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; +using cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("aircrafts")] +public class AircraftsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a aircraft")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddAircraftViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddAircraftCommand() + { + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all aircrafts")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetAircraftsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetAircraftsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CapacityGreaterOrEqualThan = + filterQuery.CapacityGreaterOrEqualThan, + CapacityLessOrEqualThan = + filterQuery.CapacityLessOrEqualThan + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a aircraft by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetAircraftQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a aircraft")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateAircraftViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateAircraftCommand() + { + Guid = uuid, + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a aircraft")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteAircraftCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/BusesController.cs b/src/HttpApi/Controllers/BusesController.cs new file mode 100644 index 0000000..48eb35d --- /dev/null +++ b/src/HttpApi/Controllers/BusesController.cs @@ -0,0 +1,193 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Buses; +using cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; +using cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; +using cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; +using cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; +using cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; +using cuqmbr.TravelGuide.Application.Buses.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("buses")] +public class BusesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a bus")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddBusViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddBusCommand() + { + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all buses")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetBusesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetBusesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CapacityGreaterOrEqualThan = + filterQuery.CapacityGreaterOrEqualThan, + CapacityLessOrEqualThan = + filterQuery.CapacityLessOrEqualThan + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a bus by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetBusQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a bus")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateBusViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateBusCommand() + { + Guid = uuid, + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a bus")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteBusCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/TestsController.cs b/src/HttpApi/Controllers/TestsController.cs index 4533d4e..b75627a 100644 --- a/src/HttpApi/Controllers/TestsController.cs +++ b/src/HttpApi/Controllers/TestsController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -8,19 +9,41 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class TestsController : ControllerBase { private readonly IStringLocalizer _localizer; + private readonly UnitOfWork _unitOfWork; public TestsController( CultureService cultureService, - IStringLocalizer localizer) + IStringLocalizer localizer, + UnitOfWork unitOfWork) { _localizer = localizer; + _unitOfWork = unitOfWork; } [HttpGet("getLocalizedString/{inputString}")] - public Task getLocalizedString( + public Task GetLocalizedString( [FromRoute] string inputString, CancellationToken cancellationToken) { return Task.FromResult(_localizer[inputString]); } + + [HttpGet("trigger")] + public async Task Trigger(CancellationToken cancellationToken) + { + // await _unitOfWork.BusRepository.AddOneAsync( + // new Domain.Entities.Bus() + // { + // Number = "AB1234MK", + // Model = "This is a fancy bus model", + // Capacity = 40 + // }, + // cancellationToken); + // + // await _unitOfWork.SaveAsync(cancellationToken); + // _unitOfWork.Dispose(); + + var vehicles = await _unitOfWork.VehicleRepository + .GetPageAsync(1, 10, cancellationToken); + } } diff --git a/src/HttpApi/Controllers/TrainsController.cs b/src/HttpApi/Controllers/TrainsController.cs new file mode 100644 index 0000000..4d9baef --- /dev/null +++ b/src/HttpApi/Controllers/TrainsController.cs @@ -0,0 +1,193 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Trains; +using cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; +using cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; +using cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; +using cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; +using cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; +using cuqmbr.TravelGuide.Application.Trains.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("trains")] +public class TrainsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a train")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddTrainViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddTrainCommand() + { + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all trains")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetTrainsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetTrainsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CapacityGreaterOrEqualThan = + filterQuery.CapacityGreaterOrEqualThan, + CapacityLessOrEqualThan = + filterQuery.CapacityLessOrEqualThan + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a train by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetTrainQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a train")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateTrainViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateTrainCommand() + { + Guid = uuid, + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a train")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteTrainCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/appsettings.json b/src/HttpApi/appsettings.json new file mode 100644 index 0000000..2bdff62 --- /dev/null +++ b/src/HttpApi/appsettings.json @@ -0,0 +1,31 @@ +{ + "Application": { + "Logging": { + "Type": "SimpleConsole", + "LogLevel": "Information", + "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK", + "UseUtcTimestamp": true + }, + "Datastore": { + "Type": "postgresql", + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true" + }, + "Localization": { + "DefaultCultureName": "en-US", + "CacheDuration": "00:30:00" + } + }, + "Identity": { + "Datastore": { + "Type": "postgresql", + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000" + }, + "JsonWebToken": { + "Issuer": "https://api.travel-guide.cuqmbr.xyz", + "Audience": "https://travel-guide.cuqmbr.xyz", + "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", + "AccessTokenValidity": "24:00:00", + "RefreshTokenValidity": "72:00:00" + } + } +} diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index b719aad..e7faa4b 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -18,6 +18,10 @@ public sealed class InMemoryUnitOfWork : UnitOfWork CityRepository = new InMemoryCityRepository(_dbContext); AddressRepository = new InMemoryAddressRepository(_dbContext); RouteRepository = new InMemoryRouteRepository(_dbContext); + VehicleRepository = new InMemoryVehicleRepository(_dbContext); + BusRepository = new InMemoryBusRepository(_dbContext); + AircraftRepository = new InMemoryAircraftRepository(_dbContext); + TrainRepository = new InMemoryTrainRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -30,6 +34,14 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public RouteRepository RouteRepository { get; init; } + public VehicleRepository VehicleRepository { get; init; } + + public BusRepository BusRepository { get; init; } + + public AircraftRepository AircraftRepository { get; init; } + + public TrainRepository TrainRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs new file mode 100644 index 0000000..86373d5 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryAircraftRepository : + InMemoryBaseRepository, AircraftRepository +{ + public InMemoryAircraftRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs new file mode 100644 index 0000000..2180258 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryBusRepository : + InMemoryBaseRepository, BusRepository +{ + public InMemoryBusRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs new file mode 100644 index 0000000..603b223 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryTrainRepository : + InMemoryBaseRepository, TrainRepository +{ + public InMemoryTrainRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs new file mode 100644 index 0000000..a0bd558 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryVehicleRepository : + InMemoryBaseRepository, VehicleRepository +{ + public InMemoryVehicleRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs new file mode 100644 index 0000000..186e510 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class AircraftConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasBaseType(); + + + builder + .Property(b => b.Number) + .HasColumnName("number") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(b => b.Model) + .HasColumnName("model") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(b => b.Capacity) + .HasColumnName("capacity") + .HasColumnType("smallint") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs index 2407bbf..c7a8bac 100644 --- a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs @@ -9,23 +9,21 @@ public class BaseConfiguration : IEntityTypeConfiguration { public virtual void Configure(EntityTypeBuilder builder) { + // Set table name for inherited types using type name + // instead of mapped table name + var tableName = builder.Metadata.GetTableName(); + builder .HasKey(b => b.Id) - .HasName($"pk_{builder.Metadata.GetTableName() ?? - // Set primary key for inherited types using type name - // instead of mapped table name - builder.Metadata.ShortName().ToLower()}"); + .HasName($"pk_{tableName}"); builder .Property(b => b.Id) .HasColumnName("id") .HasColumnType("bigint") .UseSequence( - $"{builder.Metadata.GetTableName() ?? - // Set sequence for inherited types using type name - // instead of mapped table name - builder.Metadata.ShortName().ToLower()}_" + - $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + + $"{tableName}_" + + $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + "sequence"); @@ -39,10 +37,7 @@ public class BaseConfiguration : IEntityTypeConfiguration .HasAlternateKey(b => b.Guid) .HasName( "altk_" + - $"{builder.Metadata.GetTableName() ?? - // Set alternate key for inherited types using type name - // instead of mapped table name - builder.Metadata.ShortName().ToLower()}_" + + $"{tableName}_" + $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Configurations/BusConfiguration.cs b/src/Persistence/PostgreSql/Configurations/BusConfiguration.cs new file mode 100644 index 0000000..593f72f --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/BusConfiguration.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class BusConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasBaseType(); + + + builder + .Property(b => b.Number) + .HasColumnName("number") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(b => b.Model) + .HasColumnName("model") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(b => b.Capacity) + .HasColumnName("capacity") + .HasColumnType("smallint") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs new file mode 100644 index 0000000..fccefeb --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class TrainConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasBaseType(); + + + builder + .Property(b => b.Number) + .HasColumnName("number") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(b => b.Model) + .HasColumnName("model") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(b => b.Capacity) + .HasColumnName("capacity") + .HasColumnType("smallint") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs b/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs new file mode 100644 index 0000000..e95accf --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class VehicleConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(a => a.VehicleType) + .HasColumnName("vehicle_type") + .HasColumnType("varchar(16)") + .IsRequired(true); + + builder + .ToTable( + "vehicles", + v => v.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(v => v.VehicleType) + .Metadata.GetColumnName()}", + $"{builder.Property(v => v.VehicleType) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", VehicleType.Enumerations + .Values.Select(v => v.Name))}')")); + + builder + .ToTable("vehicles") + .UseTphMappingStrategy() + .HasDiscriminator(v => v.VehicleType) + .HasValue(VehicleType.Bus) + .HasValue(VehicleType.Aircraft) + .HasValue(VehicleType.Train); + + base.Configure(builder); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs new file mode 100644 index 0000000..d32eb83 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs @@ -0,0 +1,476 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250503053607_Add_Bus_Aircraft_Train_with_basic_properties")] + partial class Add_Bus_Aircraft_Train_with_basic_properties + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs new file mode 100644 index 0000000..b071d55 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Bus_Aircraft_Train_with_basic_properties : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "vehicles_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "vehicles", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.vehicles_id_sequence')"), + vehicle_type = table.Column(type: "varchar(16)", nullable: false), + number = table.Column(type: "varchar(32)", nullable: true), + model = table.Column(type: "varchar(64)", nullable: true), + capacity = table.Column(type: "smallint", nullable: true), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_vehicles", x => x.id); + table.UniqueConstraint("altk_vehicles_uuid", x => x.uuid); + table.CheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "vehicles", + schema: "application"); + + migrationBuilder.DropSequence( + name: "vehicles_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 7b41978..8140f00 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -35,6 +35,8 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("routes_id_sequence"); + modelBuilder.HasSequence("vehicles_id_sequence"); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Property("Id") @@ -262,6 +264,128 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("route_addresses", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index 814849e..afede19 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -29,7 +29,6 @@ public class PostgreSqlDbContext : DbContext Assembly.GetExecutingAssembly(), t => t.Namespace == "cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations"); - } protected override void ConfigureConventions( diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index a2d696f..0642a3e 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -18,6 +18,10 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork CityRepository = new PostgreSqlCityRepository(_dbContext); AddressRepository = new PostgreSqlAddressRepository(_dbContext); RouteRepository = new PostgreSqlRouteRepository(_dbContext); + VehicleRepository = new PostgreSqlVehicleRepository(_dbContext); + BusRepository = new PostgreSqlBusRepository(_dbContext); + AircraftRepository = new PostgreSqlAircraftRepository(_dbContext); + TrainRepository = new PostgreSqlTrainRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -30,6 +34,14 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public RouteRepository RouteRepository { get; init; } + public VehicleRepository VehicleRepository { get; init; } + + public BusRepository BusRepository { get; init; } + + public AircraftRepository AircraftRepository { get; init; } + + public TrainRepository TrainRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs new file mode 100644 index 0000000..292fcea --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlAircraftRepository : + PostgreSqlBaseRepository, AircraftRepository +{ + public PostgreSqlAircraftRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs new file mode 100644 index 0000000..da4c844 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlBusRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlBusRepository : + PostgreSqlBaseRepository, BusRepository +{ + public PostgreSqlBusRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs new file mode 100644 index 0000000..2dffccc --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlTrainRepository : + PostgreSqlBaseRepository, TrainRepository +{ + public PostgreSqlTrainRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs new file mode 100644 index 0000000..da7cf91 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlVehicleRepository : + PostgreSqlBaseRepository, VehicleRepository +{ + public PostgreSqlVehicleRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +}