diff --git a/src/Application/Aircrafts/AircraftDto.cs b/src/Application/Aircrafts/AircraftDto.cs index 8549160..58c8866 100644 --- a/src/Application/Aircrafts/AircraftDto.cs +++ b/src/Application/Aircrafts/AircraftDto.cs @@ -13,11 +13,16 @@ public sealed class AircraftDto : IMapFrom public short Capacity { get; set; } + public Guid CompanyUuid { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() .ForMember( d => d.Uuid, - opt => opt.MapFrom(s => s.Guid)); + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.CompanyUuid, + opt => opt.MapFrom(s => s.Company.Guid)); } } diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs index f938a36..3e3a9ff 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs @@ -9,4 +9,6 @@ public record AddAircraftCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs index 8ffe4e7..a210946 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs @@ -33,11 +33,23 @@ public class AddAircraftCommandHandler : "Aircraft with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + + entity = new Aircraft() { Number = request.Number, Model = request.Model, - Capacity = request.Capacity + Capacity = request.Capacity, + CompanyId = parentEntity.Id, + Company = parentEntity }; entity = await _unitOfWork.AircraftRepository.AddOneAsync( diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs index 617c394..b37dbac 100644 --- a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs @@ -33,5 +33,9 @@ public class AddAircraftCommandValidator : AbstractValidator RuleFor(v => v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs index 77a174e..0de5717 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs @@ -11,4 +11,6 @@ public record UpdateAircraftCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs index 952af00..d046aa9 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs @@ -32,7 +32,7 @@ public class UpdateAircraftCommandHandler : } var duplicateEntity = await _unitOfWork.AircraftRepository.GetOneAsync( - e => e.Number == request.Number, + e => e.Number == request.Number && e.Guid != request.Guid, cancellationToken); if (duplicateEntity != null) @@ -41,9 +41,20 @@ public class UpdateAircraftCommandHandler : "Aircraft with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + entity.Number = request.Number; entity.Model = request.Model; entity.Capacity = request.Capacity; + entity.CompanyId = parentEntity.Id; + entity.Company = parentEntity; entity = await _unitOfWork.AircraftRepository.UpdateOneAsync( entity, cancellationToken); diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs index c33cecc..fb9dbc8 100644 --- a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs @@ -37,5 +37,9 @@ public class UpdateAircraftCommandValidator : AbstractValidator v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs index a92c658..01e7454 100644 --- a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs @@ -24,7 +24,8 @@ public class GetAircraftQueryHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.AircraftRepository.GetOneAsync( - e => e.Guid == request.Guid, cancellationToken); + e => e.Guid == request.Guid, e => e.Company, + cancellationToken); _unitOfWork.Dispose(); diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs index df99a61..655a576 100644 --- a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs @@ -13,11 +13,9 @@ public record GetAircraftsPageQuery : IRequest> public string Sort { get; set; } = String.Empty; - public string? Number { get; set; } + public Guid? CompanyGuid { get; set; } - public string? Model { get; set; } + public short? CapacityGreaterThanOrEqualTo { get; set; } - public short? CapacityGreaterOrEqualThan { get; set; } - - public short? CapacityLessOrEqualThan { get; set; } + public short? CapacityLessThanOrEqualTo { get; set; } } diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs index e75b5f0..e4a59c3 100644 --- a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs @@ -28,12 +28,16 @@ public class GetAircraftsPageQueryHandler : e => (e.Number.ToLower().Contains(request.Search.ToLower()) || e.Model.ToLower().Contains(request.Search.ToLower())) && - (request.CapacityGreaterOrEqualThan != null - ? e.Capacity >= request.CapacityGreaterOrEqualThan + (request.CompanyGuid != null + ? e.Company.Guid == request.CompanyGuid : true) && - (request.CapacityLessOrEqualThan != null - ? e.Capacity <= request.CapacityLessOrEqualThan + (request.CapacityGreaterThanOrEqualTo != null + ? e.Capacity >= request.CapacityGreaterThanOrEqualTo + : true) && + (request.CapacityLessThanOrEqualTo != null + ? e.Capacity <= request.CapacityLessThanOrEqualTo : true), + e => e.Company, request.PageNumber, request.PageSize, cancellationToken); diff --git a/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs index 3a6e5bd..57b64e0 100644 --- a/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs +++ b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs @@ -7,4 +7,6 @@ public sealed class AddAircraftViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs index c956d2b..51994b5 100644 --- a/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs +++ b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs @@ -2,14 +2,12 @@ namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; public sealed class GetAircraftsPageFilterViewModel { - public string? Number { get; set; } - - public string? Model { get; set; } + public Guid? CompanyUuid { 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? CapacityGreaterThanOrEqualTo { get; set; } - public short? CapacityLessOrEqualThan { get; set; } + public short? CapacityLessThanOrEqualTo { get; set; } } diff --git a/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs index 1c4e71e..52e9374 100644 --- a/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs +++ b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs @@ -7,4 +7,6 @@ public sealed class UpdateAircraftViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Application/Buses/BusDto.cs b/src/Application/Buses/BusDto.cs index 3ba7ae5..1be10a6 100644 --- a/src/Application/Buses/BusDto.cs +++ b/src/Application/Buses/BusDto.cs @@ -13,11 +13,16 @@ public sealed class BusDto : IMapFrom public short Capacity { get; set; } + public Guid CompanyUuid { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() .ForMember( d => d.Uuid, - opt => opt.MapFrom(s => s.Guid)); + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.CompanyUuid, + opt => opt.MapFrom(s => s.Company.Guid)); } } diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommand.cs b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs index 786a773..00f0405 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommand.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs @@ -9,4 +9,6 @@ public record AddBusCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs index c5a8488..346bd4b 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs @@ -33,11 +33,22 @@ public class AddBusCommandHandler : "Bus with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + entity = new Bus() { Number = request.Number, Model = request.Model, - Capacity = request.Capacity + Capacity = request.Capacity, + CompanyId = parentEntity.Id, + Company = parentEntity }; entity = await _unitOfWork.BusRepository.AddOneAsync( diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs index 1ce4caa..084cfcc 100644 --- a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs @@ -33,5 +33,9 @@ public class AddBusCommandValidator : AbstractValidator RuleFor(v => v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs index 9754c7a..4a5f18a 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs @@ -11,4 +11,6 @@ public record UpdateBusCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs index 152e220..2360617 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs @@ -32,7 +32,7 @@ public class UpdateBusCommandHandler : } var duplicateEntity = await _unitOfWork.BusRepository.GetOneAsync( - e => e.Number == request.Number, + e => e.Number == request.Number && e.Guid != request.Guid, cancellationToken); if (duplicateEntity != null) @@ -41,9 +41,20 @@ public class UpdateBusCommandHandler : "Bus with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + entity.Number = request.Number; entity.Model = request.Model; entity.Capacity = request.Capacity; + entity.CompanyId = parentEntity.Id; + entity.Company = parentEntity; entity = await _unitOfWork.BusRepository.UpdateOneAsync( entity, cancellationToken); diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs index 4224d6f..74c6b26 100644 --- a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs @@ -37,5 +37,9 @@ public class UpdateBusCommandValidator : AbstractValidator RuleFor(v => v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs index 26ba345..b1fc747 100644 --- a/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs @@ -24,7 +24,8 @@ public class GetBusQueryHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.BusRepository.GetOneAsync( - e => e.Guid == request.Guid, cancellationToken); + e => e.Guid == request.Guid, e => e.Company, + cancellationToken); _unitOfWork.Dispose(); diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs index cf97c50..343e576 100644 --- a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs @@ -13,11 +13,9 @@ public record GetBusesPageQuery : IRequest> public string Sort { get; set; } = String.Empty; - public string? Number { get; set; } + public Guid? CompanyGuid { get; set; } - public string? Model { get; set; } + public short? CapacityGreaterThanOrEqualTo { get; set; } - public short? CapacityGreaterOrEqualThan { get; set; } - - public short? CapacityLessOrEqualThan { get; set; } + public short? CapacityLessThanOrEqualTo { get; set; } } diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs index 920ea9e..28fe46f 100644 --- a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs @@ -28,12 +28,16 @@ public class GetBusesPageQueryHandler : e => (e.Number.ToLower().Contains(request.Search.ToLower()) || e.Model.ToLower().Contains(request.Search.ToLower())) && - (request.CapacityGreaterOrEqualThan != null - ? e.Capacity >= request.CapacityGreaterOrEqualThan + (request.CompanyGuid != null + ? e.Company.Guid == request.CompanyGuid : true) && - (request.CapacityLessOrEqualThan != null - ? e.Capacity <= request.CapacityLessOrEqualThan + (request.CapacityGreaterThanOrEqualTo != null + ? e.Capacity >= request.CapacityGreaterThanOrEqualTo + : true) && + (request.CapacityLessThanOrEqualTo != null + ? e.Capacity <= request.CapacityLessThanOrEqualTo : true), + e => e.Company, request.PageNumber, request.PageSize, cancellationToken); diff --git a/src/Application/Buses/ViewModels/AddBusViewModel.cs b/src/Application/Buses/ViewModels/AddBusViewModel.cs index 4be8485..0820527 100644 --- a/src/Application/Buses/ViewModels/AddBusViewModel.cs +++ b/src/Application/Buses/ViewModels/AddBusViewModel.cs @@ -7,4 +7,6 @@ public sealed class AddBusViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs index a05dd27..725d0e9 100644 --- a/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs +++ b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs @@ -2,14 +2,9 @@ namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; public sealed class GetBusesPageFilterViewModel { - public string? Number { get; set; } + public Guid? CompanyUuid { get; set; } - public string? Model { get; set; } + public short? CapacityGreaterThanOrEqualTo { 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; } + public short? CapacityLessThanOrEqualTo { get; set; } } diff --git a/src/Application/Buses/ViewModels/UpdateBusViewModel.cs b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs index 6ee2c90..7ce4cde 100644 --- a/src/Application/Buses/ViewModels/UpdateBusViewModel.cs +++ b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs @@ -7,4 +7,6 @@ public sealed class UpdateBusViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Application/Common/FluentValidation/CustomValidators.cs b/src/Application/Common/FluentValidation/CustomValidators.cs new file mode 100644 index 0000000..aa2dadb --- /dev/null +++ b/src/Application/Common/FluentValidation/CustomValidators.cs @@ -0,0 +1,24 @@ +using FluentValidation; + +namespace cuqmbr.TravelGuide.Application.Common.FluentValidation; + +public static class CustomValidators +{ + // According to RFC 5321. + public static IRuleBuilderOptions IsEmail( + this IRuleBuilder ruleBuilder) + { + return + ruleBuilder + .Matches(@"^[\w\.-]{1,64}@[\w\.-]{1,251}\.\w{2,4}$"); + } + + // According to ITU-T E.164, no spaces. + public static IRuleBuilderOptions IsPhoneNumber( + this IRuleBuilder ruleBuilder) + { + return + ruleBuilder + .Matches(@"^\+[0-9]{7,15}$"); + } +} diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/CompanyRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/CompanyRepository.cs new file mode 100644 index 0000000..b65663a --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/CompanyRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface CompanyRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index 5cd0770..6db81b1 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -26,6 +26,8 @@ public interface UnitOfWork : IDisposable RouteAddressRepository RouteAddressRepository { get; } + CompanyRepository CompanyRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs new file mode 100644 index 0000000..5965a59 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; + +public record AddCompanyCommand : IRequest +{ + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } +} diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.cs new file mode 100644 index 0000000..7d77ac2 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandAuthorizer.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.Companies.Commands.AddCompany; + +public class AddCompanyCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddCompanyCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddCompanyCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs new file mode 100644 index 0000000..07b4270 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs @@ -0,0 +1,52 @@ +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.Companies.Commands.AddCompany; + +public class AddCompanyCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddCompanyCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddCompanyCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Name == request.Name, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Company with given name already exists."); + } + + entity = new Company() + { + Name = request.Name, + LegalAddress = request.LegalAddress, + ContactEmail = request.ContactEmail, + ContactPhoneNumber = request.ContactPhoneNumber + }; + + entity = await _unitOfWork.CompanyRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs new file mode 100644 index 0000000..209a093 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs @@ -0,0 +1,58 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; + +public class AddCompanyCommandValidator : AbstractValidator +{ + public AddCompanyCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.LegalAddress) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + + RuleFor(v => v.ContactEmail) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + + RuleFor(v => v.ContactPhoneNumber) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsPhoneNumber() + .WithMessage(localizer["FluentValidation.IsPhoneNumber"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommand.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommand.cs new file mode 100644 index 0000000..a9c6a71 --- /dev/null +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; + +public record DeleteCompanyCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.cs new file mode 100644 index 0000000..bfd16e8 --- /dev/null +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandAuthorizer.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.Companies.Commands.DeleteCompany; + +public class DeleteCompanyCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteCompanyCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteCompanyCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs new file mode 100644 index 0000000..9ceae16 --- /dev/null +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; + +public class DeleteCompanyCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteCompanyCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteCompanyCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.CompanyRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandValidator.cs b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandValidator.cs new file mode 100644 index 0000000..f926330 --- /dev/null +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; + +public class DeleteCompanyCommandValidator : AbstractValidator +{ + public DeleteCompanyCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommand.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommand.cs new file mode 100644 index 0000000..d423b2d --- /dev/null +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommand.cs @@ -0,0 +1,16 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; + +public record UpdateCompanyCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } +} diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs new file mode 100644 index 0000000..5f93add --- /dev/null +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.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.Companies.Commands.UpdateCompany; + +public class UpdateCompanyCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateCompanyCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateCompanyCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs new file mode 100644 index 0000000..78e408e --- /dev/null +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs @@ -0,0 +1,47 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; + +public class UpdateCompanyCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateCompanyCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateCompanyCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + entity.Name = request.Name; + entity.LegalAddress = request.LegalAddress; + entity.ContactEmail = request.ContactEmail; + entity.ContactPhoneNumber = request.ContactPhoneNumber; + + entity = await _unitOfWork.CompanyRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs new file mode 100644 index 0000000..a5bb800 --- /dev/null +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandValidator.cs @@ -0,0 +1,62 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; + +public class UpdateCompanyCommandValidator : AbstractValidator +{ + public UpdateCompanyCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.LegalAddress) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + + RuleFor(v => v.ContactEmail) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + + RuleFor(v => v.ContactPhoneNumber) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsPhoneNumber() + .WithMessage(localizer["FluentValidation.IsPhoneNumber"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Companies/CompanyDto.cs b/src/Application/Companies/CompanyDto.cs new file mode 100644 index 0000000..3bdc207 --- /dev/null +++ b/src/Application/Companies/CompanyDto.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Companies; + +public sealed class CompanyDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQuery.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQuery.cs new file mode 100644 index 0000000..01a8c8f --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQuery.cs @@ -0,0 +1,15 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; + +public record GetCompaniesPageQuery : 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; +} diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs new file mode 100644 index 0000000..979329c --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.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.Companies.Queries.GetCompaniesPage; + +public class GetCompaniesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCompaniesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCompaniesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs new file mode 100644 index 0000000..49724c2 --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs @@ -0,0 +1,49 @@ +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.Companies.Queries.GetCompaniesPage; + +public class GetCompaniesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCompaniesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetCompaniesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.CompanyRepository.GetPageAsync( + e => + e.Name.ToLower().Contains(request.Search.ToLower()) || + e.LegalAddress.ToLower().Contains(request.Search.ToLower()) || + e.ContactEmail.ToLower().Contains(request.Search.ToLower()) || + e.ContactPhoneNumber.ToLower().Contains(request.Search.ToLower()), + 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/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs new file mode 100644 index 0000000..9f9143c --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; + +public class GetCompaniesPageQueryValidator : AbstractValidator +{ + public GetCompaniesPageQueryValidator( + IStringLocalizer localizer, + SessionCultureService 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/Companies/Queries/GetCompany/GetCompanyQuery.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQuery.cs new file mode 100644 index 0000000..95de5b3 --- /dev/null +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; + +public record GetCompanyQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs new file mode 100644 index 0000000..ce96fe7 --- /dev/null +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.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.Companies.Queries.GetCompany; + +public class GetCompanyQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCompanyQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCompanyQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs new file mode 100644 index 0000000..61cc0eb --- /dev/null +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.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.Companies.Queries.GetCompany; + +public class GetCompanyQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCompanyQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetCompanyQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryValidator.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryValidator.cs new file mode 100644 index 0000000..99db2fd --- /dev/null +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; + +public class GetCompanyQueryValidator : AbstractValidator +{ + public GetCompanyQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Companies/ViewModels/AddCompanyViewModel.cs b/src/Application/Companies/ViewModels/AddCompanyViewModel.cs new file mode 100644 index 0000000..2927de8 --- /dev/null +++ b/src/Application/Companies/ViewModels/AddCompanyViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Companies.ViewModels; + +public sealed class AddCompanyViewModel +{ + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } +} diff --git a/src/Application/Companies/ViewModels/UpdateCompanyViewModel.cs b/src/Application/Companies/ViewModels/UpdateCompanyViewModel.cs new file mode 100644 index 0000000..535ba6a --- /dev/null +++ b/src/Application/Companies/ViewModels/UpdateCompanyViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Companies.ViewModels; + +public sealed class UpdateCompanyViewModel +{ + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } +} diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index ff87349..8d104df 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -4,7 +4,9 @@ "NotEmpty": "Must not be empty.", "GreaterThanOrEqualTo": "Must be greater than or equal to {0:G}.", "LessThanOrEqualTo": "Must be less than or equal to {0:G}.", - "MustBeInEnum": "Must be one of the following: {0}." + "MustBeInEnum": "Must be one of the following: {0}.", + "IsEmail": "Must be a valid email address according to RFC 5321.", + "IsPhoneNumber": "Must be a valid phone number according to ITU-T E.164 with no separator characters." }, "Validation": { "DistinctOrder": "Must have distinct order values.", diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs index a5e81d1..6c680e5 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs @@ -9,4 +9,6 @@ public record AddTrainCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs index 8edd60c..4644d93 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs @@ -33,11 +33,22 @@ public class AddTrainCommandHandler : "Train with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + entity = new Train() { Number = request.Number, Model = request.Model, - Capacity = request.Capacity + Capacity = request.Capacity, + CompanyId = parentEntity.Id, + Company = parentEntity }; entity = await _unitOfWork.TrainRepository.AddOneAsync( diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs index ec530dd..092bbc9 100644 --- a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs @@ -33,5 +33,9 @@ public class AddTrainCommandValidator : AbstractValidator RuleFor(v => v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs index 4aa128f..4e40e13 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs @@ -11,4 +11,6 @@ public record UpdateTrainCommand : IRequest public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } } diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs index 70ea0ce..e7be03f 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs @@ -32,7 +32,7 @@ public class UpdateTrainCommandHandler : } var duplicateEntity = await _unitOfWork.TrainRepository.GetOneAsync( - e => e.Number == request.Number, + e => e.Number == request.Number && e.Guid != request.Guid, cancellationToken); if (duplicateEntity != null) @@ -41,9 +41,20 @@ public class UpdateTrainCommandHandler : "Train with given number already exists."); } + var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Guid == request.CompanyGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CompanyGuid} not found."); + } + entity.Number = request.Number; entity.Model = request.Model; entity.Capacity = request.Capacity; + entity.CompanyId = parentEntity.Id; + entity.Company = parentEntity; entity = await _unitOfWork.TrainRepository.UpdateOneAsync( entity, cancellationToken); diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs index b39eac1..b23fb12 100644 --- a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs @@ -37,5 +37,9 @@ public class UpdateTrainCommandValidator : AbstractValidator RuleFor(v => v.Capacity) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs index b0c89fd..afe796b 100644 --- a/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs @@ -24,7 +24,8 @@ public class GetTrainQueryHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.TrainRepository.GetOneAsync( - e => e.Guid == request.Guid, cancellationToken); + e => e.Guid == request.Guid, e => e.Company, + cancellationToken); _unitOfWork.Dispose(); diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs index ba41889..1160f90 100644 --- a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs @@ -13,9 +13,7 @@ public record GetTrainsPageQuery : IRequest> public string Sort { get; set; } = String.Empty; - public string? Number { get; set; } - - public string? Model { get; set; } + public Guid? CompanyGuid { get; set; } public short? CapacityGreaterOrEqualThan { get; set; } diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs index 2690cfa..49a0c7d 100644 --- a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs @@ -28,12 +28,16 @@ public class GetTrainsPageQueryHandler : e => (e.Number.ToLower().Contains(request.Search.ToLower()) || e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.CompanyGuid != null + ? e.Company.Guid == request.CompanyGuid + : true) && (request.CapacityGreaterOrEqualThan != null ? e.Capacity >= request.CapacityGreaterOrEqualThan : true) && (request.CapacityLessOrEqualThan != null ? e.Capacity <= request.CapacityLessOrEqualThan : true), + e => e.Company, request.PageNumber, request.PageSize, cancellationToken); diff --git a/src/Application/Trains/TrainDto.cs b/src/Application/Trains/TrainDto.cs index 2e02c73..2b1cf05 100644 --- a/src/Application/Trains/TrainDto.cs +++ b/src/Application/Trains/TrainDto.cs @@ -13,11 +13,16 @@ public sealed class TrainDto : IMapFrom public short Capacity { get; set; } + public Guid CompanyUuid { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() .ForMember( d => d.Uuid, - opt => opt.MapFrom(s => s.Guid)); + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.CompanyUuid, + opt => opt.MapFrom(s => s.Company.Guid)); } } diff --git a/src/Application/Trains/ViewModels/AddTrainViewModel.cs b/src/Application/Trains/ViewModels/AddTrainViewModel.cs index 998097e..59a75c8 100644 --- a/src/Application/Trains/ViewModels/AddTrainViewModel.cs +++ b/src/Application/Trains/ViewModels/AddTrainViewModel.cs @@ -7,4 +7,6 @@ public sealed class AddTrainViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs b/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs index ef0026b..fed89ef 100644 --- a/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs +++ b/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs @@ -2,14 +2,12 @@ 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 Guid? CompanyUuid { get; set; } - public short? CapacityLessOrEqualThan { get; set; } + public short? CapacityGreaterThanOrEqualTo { get; set; } + + public short? CapacityLessThanOrEqualTo { get; set; } } diff --git a/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs b/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs index 3b7682e..3458e86 100644 --- a/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs +++ b/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs @@ -7,4 +7,6 @@ public sealed class UpdateTrainViewModel public string Model { get; set; } public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } } diff --git a/src/Domain/Entities/Company.cs b/src/Domain/Entities/Company.cs new file mode 100644 index 0000000..a5322fc --- /dev/null +++ b/src/Domain/Entities/Company.cs @@ -0,0 +1,15 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Company : EntityBase +{ + public string Name { get; set; } + + public string LegalAddress { get; set; } + + public string ContactEmail { get; set; } + + public string ContactPhoneNumber { get; set; } + + + public ICollection Vehicles { get; set; } +} diff --git a/src/Domain/Entities/Vehicle.cs b/src/Domain/Entities/Vehicle.cs index 42196e8..f0fb3c9 100644 --- a/src/Domain/Entities/Vehicle.cs +++ b/src/Domain/Entities/Vehicle.cs @@ -7,5 +7,10 @@ public abstract class Vehicle : EntityBase public VehicleType VehicleType { get; set; } + public long CompanyId { get; set; } + + public Company Company { get; set; } + + public ICollection Enrollments { get; set; } } diff --git a/src/HttpApi/Controllers/AircraftsController.cs b/src/HttpApi/Controllers/AircraftsController.cs index 2c040e8..620b581 100644 --- a/src/HttpApi/Controllers/AircraftsController.cs +++ b/src/HttpApi/Controllers/AircraftsController.cs @@ -2,7 +2,6 @@ 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; @@ -51,7 +50,8 @@ public class AircraftsController : ControllerBase { Number = viewModel.Number, Model = viewModel.Model, - Capacity = viewModel.Capacity + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid }, cancellationToken)); } @@ -87,10 +87,11 @@ public class AircraftsController : ControllerBase PageSize = pageQuery.PageSize, Search = searchQuery.Search, Sort = sortQuery.Sort, - CapacityGreaterOrEqualThan = - filterQuery.CapacityGreaterOrEqualThan, - CapacityLessOrEqualThan = - filterQuery.CapacityLessOrEqualThan + CompanyGuid = filterQuery.CompanyUuid, + CapacityGreaterThanOrEqualTo = + filterQuery.CapacityGreaterThanOrEqualTo, + CapacityLessThanOrEqualTo = + filterQuery.CapacityLessThanOrEqualTo }, cancellationToken); } @@ -158,7 +159,8 @@ public class AircraftsController : ControllerBase Guid = uuid, Number = viewModel.Number, Model = viewModel.Model, - Capacity = viewModel.Capacity + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid }, cancellationToken); } diff --git a/src/HttpApi/Controllers/BusesController.cs b/src/HttpApi/Controllers/BusesController.cs index c5d55a5..bd68d37 100644 --- a/src/HttpApi/Controllers/BusesController.cs +++ b/src/HttpApi/Controllers/BusesController.cs @@ -2,7 +2,6 @@ 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; @@ -51,7 +50,8 @@ public class BusesController : ControllerBase { Number = viewModel.Number, Model = viewModel.Model, - Capacity = viewModel.Capacity + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid }, cancellationToken)); } @@ -87,10 +87,11 @@ public class BusesController : ControllerBase PageSize = pageQuery.PageSize, Search = searchQuery.Search, Sort = sortQuery.Sort, - CapacityGreaterOrEqualThan = - filterQuery.CapacityGreaterOrEqualThan, - CapacityLessOrEqualThan = - filterQuery.CapacityLessOrEqualThan + CompanyGuid = filterQuery.CompanyUuid, + CapacityGreaterThanOrEqualTo = + filterQuery.CapacityGreaterThanOrEqualTo, + CapacityLessThanOrEqualTo = + filterQuery.CapacityLessThanOrEqualTo }, cancellationToken); } @@ -158,7 +159,8 @@ public class BusesController : ControllerBase Guid = uuid, Number = viewModel.Number, Model = viewModel.Model, - Capacity = viewModel.Capacity + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid }, cancellationToken); } diff --git a/src/HttpApi/Controllers/CompaniesController.cs b/src/HttpApi/Controllers/CompaniesController.cs new file mode 100644 index 0000000..a8ca669 --- /dev/null +++ b/src/HttpApi/Controllers/CompaniesController.cs @@ -0,0 +1,190 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Companies; +using cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; +using cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; +using cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; +using cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; +using cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany; +using cuqmbr.TravelGuide.Application.Companies.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("companies")] +public class CompaniesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a company")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(CompanyDto))] + [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] AddCompanyViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddCompanyCommand() + { + Name = viewModel.Name, + LegalAddress = viewModel.LegalAddress, + ContactEmail = viewModel.ContactEmail, + ContactPhoneNumber = viewModel.ContactPhoneNumber, + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all companies")] + [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, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetCompaniesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a company by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CompanyDto))] + [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(CompanyDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetCompanyQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a company")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CompanyDto))] + [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(CompanyDto))] + [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] UpdateCompanyViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateCompanyCommand() + { + Guid = uuid, + Name = viewModel.Name, + LegalAddress = viewModel.LegalAddress, + ContactEmail = viewModel.ContactEmail, + ContactPhoneNumber = viewModel.ContactPhoneNumber, + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a company")] + [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(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteCompanyCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/TrainsController.cs b/src/HttpApi/Controllers/TrainsController.cs index fac873a..b1bb992 100644 --- a/src/HttpApi/Controllers/TrainsController.cs +++ b/src/HttpApi/Controllers/TrainsController.cs @@ -88,9 +88,9 @@ public class TrainsController : ControllerBase Search = searchQuery.Search, Sort = sortQuery.Sort, CapacityGreaterOrEqualThan = - filterQuery.CapacityGreaterOrEqualThan, + filterQuery.CapacityGreaterThanOrEqualTo, CapacityLessOrEqualThan = - filterQuery.CapacityLessOrEqualThan + filterQuery.CapacityLessThanOrEqualTo }, cancellationToken); } diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index cde7f75..0773c64 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -26,6 +26,7 @@ public sealed class InMemoryUnitOfWork : UnitOfWork new InMemoryVehicleEnrollmentRepository(_dbContext); RouteAddressRepository = new InMemoryRouteAddressRepository(_dbContext); + CompanyRepository = new InMemoryCompanyRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -50,6 +51,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public RouteAddressRepository RouteAddressRepository { get; init; } + public CompanyRepository CompanyRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs new file mode 100644 index 0000000..66b2516 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.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 InMemoryCompanyRepository : + InMemoryBaseRepository, CompanyRepository +{ + public InMemoryCompanyRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs new file mode 100644 index 0000000..b01666c --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs @@ -0,0 +1,42 @@ +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 CompanyConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("companies"); + + base.Configure(builder); + + + builder + .Property(c => c.Name) + .HasColumnName("name") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(c => c.LegalAddress) + .HasColumnName("legal_address") + .HasColumnType("varchar(256)") + .IsRequired(true); + + builder + .Property(c => c.ContactEmail) + .HasColumnName("contact_email") + .HasColumnType("varchar(256)") + .IsRequired(true); + + builder + .Property(c => c.ContactPhoneNumber) + .HasColumnName("contact_phone_number") + .HasColumnType("varchar(64)") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs b/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs index e95accf..d2c0396 100644 --- a/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs @@ -37,5 +37,29 @@ public class VehicleConfiguration : BaseConfiguration .HasValue(VehicleType.Train); base.Configure(builder); + + + builder + .Property(v => v.CompanyId) + .HasColumnName("company_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(v => v.Company) + .WithMany(c => c.Vehicles) + .HasForeignKey(v => v.CompanyId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(v => v.CompanyId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(v => v.CompanyId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(v => v.CompanyId).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.Designer.cs new file mode 100644 index 0000000..a91ef1f --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.Designer.cs @@ -0,0 +1,706 @@ +// +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("20250515101417_Add_Companies")] + partial class Add_Companies + { + /// + 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("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_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.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "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.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "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("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + 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.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + 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.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + 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.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + 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.Company", b => + { + b.Navigation("Vehicles"); + }); + + 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"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.cs b/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.cs new file mode 100644 index 0000000..501e528 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250515101417_Add_Companies.cs @@ -0,0 +1,110 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Companies : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropCheckConstraint( + name: "ck_vehicle_enrollments_currency", + schema: "application", + table: "vehicle_enrollments"); + + migrationBuilder.CreateSequence( + name: "companies_id_sequence", + schema: "application"); + + migrationBuilder.AddColumn( + name: "company_id", + schema: "application", + table: "vehicles", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.CreateTable( + name: "companies", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.companies_id_sequence')"), + name = table.Column(type: "varchar(64)", nullable: false), + legal_address = table.Column(type: "varchar(256)", nullable: false), + contact_email = table.Column(type: "varchar(256)", nullable: false), + contact_phone_number = table.Column(type: "varchar(64)", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_companies", x => x.id); + table.UniqueConstraint("altk_companies_uuid", x => x.uuid); + }); + + migrationBuilder.CreateIndex( + name: "ix_vehicles_company_id", + schema: "application", + table: "vehicles", + column: "company_id"); + + migrationBuilder.AddCheckConstraint( + name: "ck_vehicle_enrollments_currency", + schema: "application", + table: "vehicle_enrollments", + sql: "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + + migrationBuilder.AddForeignKey( + name: "fk_vehicles_company_id", + schema: "application", + table: "vehicles", + column: "company_id", + principalSchema: "application", + principalTable: "companies", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_vehicles_company_id", + schema: "application", + table: "vehicles"); + + migrationBuilder.DropTable( + name: "companies", + schema: "application"); + + migrationBuilder.DropIndex( + name: "ix_vehicles_company_id", + schema: "application", + table: "vehicles"); + + migrationBuilder.DropCheckConstraint( + name: "ck_vehicle_enrollments_currency", + schema: "application", + table: "vehicle_enrollments"); + + migrationBuilder.DropColumn( + name: "company_id", + schema: "application", + table: "vehicles"); + + migrationBuilder.DropSequence( + name: "companies_id_sequence", + schema: "application"); + + migrationBuilder.AddCheckConstraint( + name: "ck_vehicle_enrollments_currency", + schema: "application", + table: "vehicle_enrollments", + sql: "currency IN ('USD', 'EUR', 'UAH')"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index b480530..2d7ebeb 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -27,6 +27,8 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("cities_id_sequence"); + modelBuilder.HasSequence("companies_id_sequence"); + modelBuilder.HasSequence("countries_id_sequence"); modelBuilder.HasSequence("regions_id_sequence"); @@ -125,6 +127,49 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("cities", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => { b.Property("Id") @@ -327,6 +372,10 @@ namespace Persistence.PostgreSql.Migrations NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + b.Property("Guid") .HasColumnType("uuid") .HasColumnName("uuid"); @@ -342,6 +391,9 @@ namespace Persistence.PostgreSql.Migrations b.HasAlternateKey("Guid") .HasName("altk_vehicles_uuid"); + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + b.ToTable("vehicles", "application", t => { t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); @@ -397,7 +449,7 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("vehicle_enrollments", "application", t => { - t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('USD', 'EUR', 'UAH')"); + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); }); }); @@ -566,6 +618,18 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("VehicleEnrollment"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") @@ -597,6 +661,11 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Addresses"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Vehicles"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => { b.Navigation("Regions"); diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 0e588c2..11eab91 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -26,6 +26,7 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork new PostgreSqlVehicleEnrollmentRepository(_dbContext); RouteAddressRepository = new PostgreSqlRouteAddressRepository(_dbContext); + CompanyRepository = new PostgreSqlCompanyRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -50,6 +51,8 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public RouteAddressRepository RouteAddressRepository { get; init; } + public CompanyRepository CompanyRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs new file mode 100644 index 0000000..ed857cd --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.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 PostgreSqlCompanyRepository : + PostgreSqlBaseRepository, CompanyRepository +{ + public PostgreSqlCompanyRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +}