diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 56207bd..21dede3 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -25,7 +25,6 @@ jobs: cache: true cache-dependency-path: | src/Application/packages.lock.json - src/Identity/packages.lock.json src/Infrastructure/packages.lock.json src/Persistence/packages.lock.json src/Configuration/packages.lock.json diff --git a/TravelGuide.sln b/TravelGuide.sln index f012d1c..75d3e9c 100644 --- a/TravelGuide.sln +++ b/TravelGuide.sln @@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infra EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpApi", "src\HttpApi\HttpApi.csproj", "{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity", "src\Identity\Identity.csproj", "{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Configuration", "src\Configuration\Configuration.csproj", "{1DCFA4EE-A545-42FE-A3BC-A606D2961298}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.IntegrationTests", "tst\Application.IntegrationTests\Application.IntegrationTests.csproj", "{B52B8651-10B8-488D-8ACF-9C4499F8A723}" @@ -48,10 +46,6 @@ Global {4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Debug|Any CPU.Build.0 = Debug|Any CPU {4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.ActiveCfg = Release|Any CPU {4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.Build.0 = Release|Any CPU - {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Release|Any CPU.Build.0 = Release|Any CPU {1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.Build.0 = Debug|Any CPU {1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Application/Addresses/AddressDto.cs b/src/Application/Addresses/AddressDto.cs new file mode 100644 index 0000000..5d936bf --- /dev/null +++ b/src/Application/Addresses/AddressDto.cs @@ -0,0 +1,58 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Addresses; + +public sealed class AddressDto : IMapFrom
+{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public string VehicleType { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.VehicleType, + opt => opt.MapFrom(s => s.VehicleType.Name)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.City.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.City.Region.Country.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.City.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.City.Region.Name)) + .ForMember( + d => d.CityUuid, + opt => opt.MapFrom(s => s.City.Guid)) + .ForMember( + d => d.CityName, + opt => opt.MapFrom(s => s.City.Name)); + } +} diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommand.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommand.cs new file mode 100644 index 0000000..2b1a2f5 --- /dev/null +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommand.cs @@ -0,0 +1,17 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; + +public record AddAddressCommand : IRequest +{ + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public VehicleType VehicleType { get; set; } + + public Guid CityGuid { get; set; } +} diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs new file mode 100644 index 0000000..b4bbe78 --- /dev/null +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; + +public class AddAddressCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddAddressCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddAddressCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs new file mode 100644 index 0000000..189c8d0 --- /dev/null +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs @@ -0,0 +1,64 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; + +public class AddAddressCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddAddressCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddAddressCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AddressRepository.GetOneAsync( + e => e.Name == request.Name && e.City.Guid == request.CityGuid, + cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Address with given name already exists."); + } + + var parentEntity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.CityGuid, e => e.Region.Country, + cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CityGuid} not found."); + } + + entity = new Address() + { + Name = request.Name, + Longitude = request.Longitude, + Latitude = request.Latitude, + VehicleType = request.VehicleType, + CityId = parentEntity.Id + }; + + entity = await _unitOfWork.AddressRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs new file mode 100644 index 0000000..e1c38c8 --- /dev/null +++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs @@ -0,0 +1,65 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; + +public class AddAddressCommandValidator : AbstractValidator +{ + public AddAddressCommandValidator( + 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.Latitude) + .GreaterThanOrEqualTo(-90) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + -90)) + .LessThanOrEqualTo(90) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 90)); + + RuleFor(v => v.Longitude) + .GreaterThanOrEqualTo(-180) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + -180)) + .LessThanOrEqualTo(180) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 180)); + + RuleFor(v => v.VehicleType) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(v => v.CityGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommand.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommand.cs new file mode 100644 index 0000000..87241a4 --- /dev/null +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; + +public record DeleteAddressCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs new file mode 100644 index 0000000..66a9511 --- /dev/null +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; + +public class DeleteAddressCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteAddressCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteAddressCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs new file mode 100644 index 0000000..fc6ea4c --- /dev/null +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; + +public class DeleteAddressCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteAddressCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteAddressCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AddressRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.AddressRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandValidator.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandValidator.cs new file mode 100644 index 0000000..10cec3c --- /dev/null +++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; + +public class DeleteAddressCommandValidator : AbstractValidator +{ + public DeleteAddressCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs new file mode 100644 index 0000000..b44f8cf --- /dev/null +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs @@ -0,0 +1,19 @@ +using MediatR; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; + +public record UpdateAddressCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public VehicleType VehicleType { get; set; } + + public Guid CityGuid { get; set; } +} diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs new file mode 100644 index 0000000..055fd69 --- /dev/null +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; + +public class UpdateAddressCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateAddressCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateAddressCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs new file mode 100644 index 0000000..17ab45f --- /dev/null +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs @@ -0,0 +1,58 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; + +public class UpdateAddressCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateAddressCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateAddressCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AddressRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.City.Region.Country, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var parentEntity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.CityGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CityGuid} not found."); + } + + entity.Name = request.Name; + entity.Longitude = request.Longitude; + entity.Latitude = request.Latitude; + entity.VehicleType = request.VehicleType; + entity.CityId = parentEntity.Id; + + entity = await _unitOfWork.AddressRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs new file mode 100644 index 0000000..adf0322 --- /dev/null +++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; + +public class UpdateAddressCommandValidator : AbstractValidator +{ + public UpdateAddressCommandValidator( + 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.CityGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQuery.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQuery.cs new file mode 100644 index 0000000..fa665c4 --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; + +public record GetAddressQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs new file mode 100644 index 0000000..9bb9784 --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; + +public class GetAddressQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAddressQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAddressQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs new file mode 100644 index 0000000..730b248 --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; + +public class GetAddressQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAddressQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetAddressQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AddressRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.City.Region.Country, + cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryValidator.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryValidator.cs new file mode 100644 index 0000000..5e97a2e --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; + +public class GetAddressQueryValidator : AbstractValidator +{ + public GetAddressQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs new file mode 100644 index 0000000..36ed29a --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; + +public record GetAddressesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? CountryGuid { get; set; } + + public Guid? RegionGuid { get; set; } + + public Guid? CityGuid { get; set; } + + public double? LongitudeGreaterOrEqualThan { get; set; } + + public double? LongitudeLessOrEqualThan { get; set; } + + public double? LatitudeGreaterOrEqualThan { get; set; } + + public double? LatitudeLessOrEqualThan { get; set; } + + public VehicleType? VehicleType { get; set; } +} diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs new file mode 100644 index 0000000..c868ec9 --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; + +public class GetAddressesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAddressesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAddressesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs new file mode 100644 index 0000000..4fcce04 --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs @@ -0,0 +1,74 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; + +public class GetAddressesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAddressesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetAddressesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.AddressRepository.GetPageAsync( + e => + (e.Name.ToLower().Contains(request.Search.ToLower()) || + e.City.Name.ToLower().Contains(request.Search.ToLower()) || + e.City.Region.Name.ToLower().Contains(request.Search.ToLower()) || + e.City.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && + (request.LongitudeGreaterOrEqualThan != null + ? e.Longitude >= request.LongitudeGreaterOrEqualThan + : true) && + (request.LongitudeLessOrEqualThan != null + ? e.Longitude <= request.LongitudeLessOrEqualThan + : true) && + (request.LatitudeGreaterOrEqualThan != null + ? e.Latitude >= request.LatitudeGreaterOrEqualThan + : true) && + (request.LatitudeLessOrEqualThan != null + ? e.Latitude <= request.LatitudeLessOrEqualThan + : true) && + (request.VehicleType != null + ? e.VehicleType == request.VehicleType + : true) && + (request.CityGuid != null + ? e.City.Guid == request.CityGuid + : true) && + (request.RegionGuid != null + ? e.City.Region.Guid == request.RegionGuid + : true) && + (request.CountryGuid != null + ? e.City.Region.Country.Guid == request.CountryGuid + : true), + e => e.City.Region.Country, + 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/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs new file mode 100644 index 0000000..ce7adfe --- /dev/null +++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; + +public class GetAddressesPageQueryValidator : AbstractValidator +{ + public GetAddressesPageQueryValidator( + 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/Addresses/ViewModels/AddAddressViewModel.cs b/src/Application/Addresses/ViewModels/AddAddressViewModel.cs new file mode 100644 index 0000000..904431c --- /dev/null +++ b/src/Application/Addresses/ViewModels/AddAddressViewModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Addresses.ViewModels; + +public sealed class AddAddressViewModel +{ + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public string VehicleType { get; set; } + + public Guid CityUuid { get; set; } +} diff --git a/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs b/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs new file mode 100644 index 0000000..9dc7a20 --- /dev/null +++ b/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs @@ -0,0 +1,20 @@ +namespace cuqmbr.TravelGuide.Application.Addresses.ViewModels; + +public sealed class GetAddressesPageFilterViewModel +{ + public Guid? CountryUuid { get; set; } + + public Guid? RegionUuid { get; set; } + + public Guid? CityUuid { get; set; } + + public double? LongitudeGreaterOrEqualThan { get; set; } + + public double? LongitudeLessOrEqualThan { get; set; } + + public double? LatitudeGreaterOrEqualThan { get; set; } + + public double? LatitudeLessOrEqualThan { get; set; } + + public string? VehicleType { get; set; } +} diff --git a/src/Application/Addresses/ViewModels/UpdateAddressViewModel.cs b/src/Application/Addresses/ViewModels/UpdateAddressViewModel.cs new file mode 100644 index 0000000..6bcb4e4 --- /dev/null +++ b/src/Application/Addresses/ViewModels/UpdateAddressViewModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Addresses.ViewModels; + +public sealed class UpdateAddressViewModel +{ + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public string VehicleType { get; set; } + + public Guid CityUuid { get; set; } +} diff --git a/src/Application/Aircrafts/AircraftDto.cs b/src/Application/Aircrafts/AircraftDto.cs new file mode 100644 index 0000000..58c8866 --- /dev/null +++ b/src/Application/Aircrafts/AircraftDto.cs @@ -0,0 +1,28 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Aircrafts; + +public sealed class AircraftDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + 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 new file mode 100644 index 0000000..3e3a9ff --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public record AddAircraftCommand : IRequest +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs new file mode 100644 index 0000000..69c650f --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public class AddAircraftCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public AddAircraftCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(AddAircraftCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs new file mode 100644 index 0000000..fdb46dd --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs @@ -0,0 +1,63 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public class AddAircraftCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddAircraftCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddAircraftCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Number == request.Number, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Aircraft with given number already exists."); + } + + 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, + CompanyId = parentEntity.Id, + Company = parentEntity + }; + + entity = await _unitOfWork.AircraftRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs new file mode 100644 index 0000000..15253f2 --- /dev/null +++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; + +public class AddAircraftCommandValidator : AbstractValidator +{ + public AddAircraftCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs new file mode 100644 index 0000000..88bd0c7 --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public record DeleteAircraftCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs new file mode 100644 index 0000000..295c08b --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public class DeleteAircraftCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public DeleteAircraftCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(DeleteAircraftCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs new file mode 100644 index 0000000..1d3d867 --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public class DeleteAircraftCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteAircraftCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteAircraftCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.AircraftRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs new file mode 100644 index 0000000..8d43bfc --- /dev/null +++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; + +public class DeleteAircraftCommandValidator : AbstractValidator +{ + public DeleteAircraftCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs new file mode 100644 index 0000000..0de5717 --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs @@ -0,0 +1,16 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public record UpdateAircraftCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs new file mode 100644 index 0000000..a23a8bf --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public class UpdateAircraftCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public UpdateAircraftCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(UpdateAircraftCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs new file mode 100644 index 0000000..9d16e38 --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs @@ -0,0 +1,67 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public class UpdateAircraftCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateAircraftCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateAircraftCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var duplicateEntity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Number == request.Number && e.Guid != request.Guid, + cancellationToken); + + if (duplicateEntity != null) + { + throw new DuplicateEntityException( + "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); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs new file mode 100644 index 0000000..8060418 --- /dev/null +++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs @@ -0,0 +1,45 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; + +public class UpdateAircraftCommandValidator : AbstractValidator +{ + public UpdateAircraftCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs new file mode 100644 index 0000000..60fb5f1 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public record GetAircraftQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs new file mode 100644 index 0000000..aac2bd3 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public class GetAircraftQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetAircraftQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetAircraftQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs new file mode 100644 index 0000000..2977272 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public class GetAircraftQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAircraftQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetAircraftQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.AircraftRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Company, + cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs new file mode 100644 index 0000000..c5a5e09 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; + +public class GetAircraftQueryValidator : AbstractValidator +{ + public GetAircraftQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs new file mode 100644 index 0000000..655a576 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public record GetAircraftsPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? CompanyGuid { get; set; } + + public short? CapacityGreaterThanOrEqualTo { get; set; } + + public short? CapacityLessThanOrEqualTo { get; set; } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs new file mode 100644 index 0000000..2871ed3 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public class GetAircraftsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetAircraftsPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetAircraftsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs new file mode 100644 index 0000000..586d197 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs @@ -0,0 +1,57 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public class GetAircraftsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAircraftsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetAircraftsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.AircraftRepository.GetPageAsync( + e => + (e.Number.ToLower().Contains(request.Search.ToLower()) || + e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.CompanyGuid != null + ? e.Company.Guid == request.CompanyGuid + : true) && + (request.CapacityGreaterThanOrEqualTo != null + ? e.Capacity >= request.CapacityGreaterThanOrEqualTo + : true) && + (request.CapacityLessThanOrEqualTo != null + ? e.Capacity <= request.CapacityLessThanOrEqualTo + : true), + e => e.Company, + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs new file mode 100644 index 0000000..c564c40 --- /dev/null +++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; + +public class GetAircraftsPageQueryValidator : AbstractValidator +{ + public GetAircraftsPageQueryValidator( + 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/Aircrafts/ViewModels/AddAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs new file mode 100644 index 0000000..57b64e0 --- /dev/null +++ b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +public sealed class AddAircraftViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } +} diff --git a/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs new file mode 100644 index 0000000..51994b5 --- /dev/null +++ b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +public sealed class GetAircraftsPageFilterViewModel +{ + public Guid? CompanyUuid { get; set; } + + // TODO: Consider adding strict equals rule although it is not + // necessarily needed to filter with exact capacity + + public short? CapacityGreaterThanOrEqualTo { get; set; } + + public short? CapacityLessThanOrEqualTo { get; set; } +} diff --git a/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs new file mode 100644 index 0000000..52e9374 --- /dev/null +++ b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +public sealed class UpdateAircraftViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } +} diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index e77ed7a..e6484da 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -16,7 +16,12 @@ + + + + + diff --git a/src/Application/Authentication/Commands/Register/RegisterCommand.cs b/src/Application/Authentication/Commands/Register/RegisterCommand.cs index 51edec4..41a1dbc 100644 --- a/src/Application/Authentication/Commands/Register/RegisterCommand.cs +++ b/src/Application/Authentication/Commands/Register/RegisterCommand.cs @@ -4,6 +4,8 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; public record RegisterCommand : IRequest { + public string Username { get; set; } + public string Email { get; set; } public string Password { get; set; } diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandAuthorizer.cs b/src/Application/Authentication/Commands/Register/RegisterCommandAuthorizer.cs new file mode 100644 index 0000000..d3fc40f --- /dev/null +++ b/src/Application/Authentication/Commands/Register/RegisterCommandAuthorizer.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; + +public class RegisterCommandAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy(RegisterCommand request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs b/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs index 99ca0f1..f84b3e5 100644 --- a/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs +++ b/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs @@ -1,21 +1,83 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; public class RegisterCommandHandler : IRequestHandler { - private readonly AuthenticationService _authenticationService; + private readonly IReadOnlyCollection DefaultRoles = + new IdentityRole[] { IdentityRole.User }; - public RegisterCommandHandler(AuthenticationService authenticationService) + private readonly UnitOfWork _unitOfWork; + private readonly PasswordHasherService _passwordHasher; + + public RegisterCommandHandler(UnitOfWork unitOfWork, + PasswordHasherService passwordHasher) { - _authenticationService = authenticationService; + _unitOfWork = unitOfWork; + _passwordHasher = passwordHasher; } - public async Task Handle( - RegisterCommand request, CancellationToken cancellationToken) + public async Task Handle(RegisterCommand request, + CancellationToken cancellationToken) { - await _authenticationService.RegisterAsync( - request.Email, request.Password, cancellationToken); + var datastoreAccount = await _unitOfWork.AccountRepository + .GetOneAsync( + e => + e.Email == request.Email || + e.Username == request.Username, + cancellationToken); + + if (datastoreAccount != null) + { + throw new RegistrationException( + "User with given email or username already registered."); + } + + + var defaultRoleIds = (await _unitOfWork.RoleRepository + .GetPageAsync( + r => DefaultRoles.Contains(r.Value), + 1, DefaultRoles.Count, cancellationToken)) + .Items + .Select(r => r.Id); + + + var password = Encoding.UTF8.GetBytes(request.Password); + + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = await _passwordHasher + .HashAsync(password, salt, cancellationToken); + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + + var newAccount = new Account + { + Username = request.Username, + Email = request.Email, + PasswordHash = hashBase64, + PasswordSalt = saltBase64, + AccountRoles = defaultRoleIds.Select(id => + new AccountRole() + { + RoleId = id + }) + .ToArray() + }; + + + await _unitOfWork.AccountRepository + .AddOneAsync(newAccount, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); } } diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs b/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs index 13afc23..f009284 100644 --- a/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs +++ b/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs @@ -1,31 +1,54 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; +using Microsoft.Extensions.Localization; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; public class RegisterCommandValidator : AbstractValidator { - public RegisterCommandValidator() + public RegisterCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) { + RuleFor(v => v.Username) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 1)) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)) + .IsUsername() + .WithMessage(localizer["FluentValidation.IsUsername"]); + RuleFor(v => v.Email) .NotEmpty() - .WithMessage("Email address is required.") - .Matches(@"\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b") - .WithMessage("Email address is invalid."); + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); RuleFor(v => v.Password) .NotEmpty() - .WithMessage("Password is required.") + .WithMessage(localizer["FluentValidation.NotEmpty"]) .MinimumLength(8) - .WithMessage("Password must be at least 8 characters long.") - .MaximumLength(64) - .WithMessage("Password must be at most 64 characters long.") - .Matches(@"(?=.*[A-Z]).*") - .WithMessage("Password must contain at least one uppercase letter.") - .Matches(@"(?=.*[a-z]).*") - .WithMessage("Password must contain at least one lowercase letter.") - .Matches(@"(?=.*[\d]).*") - .WithMessage("Password must contain at least one digit.") - .Matches(@"(?=.*[!@#$%^&*()]).*") - .WithMessage("Password must contain at least one of the following special charactters: !@#$%^&*()."); + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 8)) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); } } diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs index e5237e1..96f0a24 100644 --- a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs +++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs @@ -1,5 +1,4 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; @@ -7,18 +6,8 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken public class RenewAccessTokenCommandAuthorizer : AbstractRequestAuthorizer { - private readonly SessionUserService _sessionUserService; - - public RenewAccessTokenCommandAuthorizer(SessionUserService currentUserService) - { - _sessionUserService = currentUserService; - } - public override void BuildPolicy(RenewAccessTokenCommand request) { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated = _sessionUserService.IsAuthenticated - }); + UseRequirement(new AllowAllRequirement()); } } diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs index 0cb9016..b6f47c4 100644 --- a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs +++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs @@ -1,22 +1,95 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; using MediatR; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; public class RenewAccessTokenCommandHandler : IRequestHandler { - private readonly AuthenticationService _authenticationService; + private readonly UnitOfWork _unitOfWork; + private readonly JsonWebTokenConfigurationOptions _jwtConfiguration; - public RenewAccessTokenCommandHandler(AuthenticationService authenticationService) + public RenewAccessTokenCommandHandler(UnitOfWork unitOfWork, + IOptions configurationOptions) { - _authenticationService = authenticationService; + _unitOfWork = unitOfWork; + _jwtConfiguration = configurationOptions.Value.JsonWebToken; } public async Task Handle( RenewAccessTokenCommand request, CancellationToken cancellationToken) { - return await _authenticationService.RenewAccessTokenAsync( - request.RefreshToken, cancellationToken); + var refreshToken = (await _unitOfWork.RefreshTokenRepository + .GetOneAsync(e => e.Value == request.RefreshToken, + cancellationToken)); + + if (refreshToken == null) + { + throw new AuthenticationException($"Refresh token was not found."); + } + + if (!refreshToken.IsActive) + { + throw new AuthenticationException("Refresh token is inactive."); + } + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + a => a.RefreshTokens.Contains(refreshToken), + a => a.AccountRoles, cancellationToken); + + var jwtSecurityToken = await CreateJwtAsync(account, cancellationToken); + var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + + return new TokensModel(accessToken, refreshToken.Value); + } + + private async Task CreateJwtAsync( + Account account, CancellationToken cancellationToken) + { + var roleIds = account.AccountRoles.Select(ar => ar.RoleId); + + var roles = (await _unitOfWork.RoleRepository + .GetPageAsync( + r => roleIds.Contains(r.Id), + 1, roleIds.Count(), cancellationToken)) + .Items.Select(r => r.Value); + + var roleClaims = new List(); + foreach (var role in roles) + { + roleClaims.Add(new Claim("roles", role.Name)); + } + + var claims = new List() + { + new Claim(JwtRegisteredClaimNames.Sub, account.Guid.ToString()), + new Claim(JwtRegisteredClaimNames.Nickname, account.Username), + new Claim(JwtRegisteredClaimNames.Email, account.Email) + } + .Union(roleClaims); + + var expirationDateTimeUtc = DateTime.UtcNow.Add( + _jwtConfiguration.AccessTokenValidity); + + var symmetricSecurityKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(_jwtConfiguration.IssuerSigningKey)); + var signingCredentials = new SigningCredentials( + symmetricSecurityKey, SecurityAlgorithms.HmacSha256); + + var jwtSecurityToken = new JwtSecurityToken( + issuer: _jwtConfiguration.Issuer, + audience: _jwtConfiguration.Audience, + claims: claims, + expires: expirationDateTimeUtc, + signingCredentials: signingCredentials); + + return jwtSecurityToken; } } diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs deleted file mode 100644 index 93f2031..0000000 --- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using MediatR; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RenewAccessTokenWithCookie; - -public record RenewAccessTokenWithCookieCommand : IRequest { } diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs deleted file mode 100644 index 481c916..0000000 --- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using MediatR.Behaviors.Authorization; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RenewAccessTokenWithCookie; - -public class RenewAccessTokenWithCookieCommandAuthorizer : - AbstractRequestAuthorizer -{ - private readonly SessionUserService _sessionUserService; - - public RenewAccessTokenWithCookieCommandAuthorizer( - SessionUserService currentUserService) - { - _sessionUserService = currentUserService; - } - - public override void BuildPolicy(RenewAccessTokenWithCookieCommand request) - { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated = _sessionUserService.IsAuthenticated - }); - } -} diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs deleted file mode 100644 index 797b289..0000000 --- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using MediatR; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RenewAccessTokenWithCookie; - -public class RenewAccessTokenWithCookieCommandHandler : - IRequestHandler -{ - private readonly AuthenticationService _authenticationService; - private readonly SessionUserService _sessionUserService; - - public RenewAccessTokenWithCookieCommandHandler( - AuthenticationService authenticationService, - SessionUserService sessionUserService) - { - _authenticationService = authenticationService; - _sessionUserService = sessionUserService; - } - - public async Task Handle( - RenewAccessTokenWithCookieCommand request, - CancellationToken cancellationToken) - { - return await _authenticationService.RenewAccessTokenAsync( - _sessionUserService.RefreshToken, cancellationToken); - } -} diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs deleted file mode 100644 index a49e1ef..0000000 --- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FluentValidation; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RenewAccessTokenWithCookie; - -public class RenewAccessTokenWithCookieCommandValidator : - AbstractValidator -{ - public RenewAccessTokenWithCookieCommandValidator() { } -} diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs index d298795..6ee840d 100644 --- a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs +++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs @@ -1,5 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs index 1c0f322..2e9dee5 100644 --- a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs +++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs @@ -1,4 +1,5 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Persistence; using MediatR; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; @@ -6,17 +7,36 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshTok public class RevokeRefreshTokenCommandHandler : IRequestHandler { - private readonly AuthenticationService _authenticationService; + private readonly UnitOfWork _unitOfWork; - public RevokeRefreshTokenCommandHandler(AuthenticationService authenticationService) + public RevokeRefreshTokenCommandHandler(UnitOfWork unitOfWork) { - _authenticationService = authenticationService; + _unitOfWork = unitOfWork; } public async Task Handle( RevokeRefreshTokenCommand request, CancellationToken cancellationToken) { - await _authenticationService.RevokeRefreshTokenAsync( - request.RefreshToken, cancellationToken); + var refreshToken = (await _unitOfWork.RefreshTokenRepository + .GetOneAsync(e => e.Value == request.RefreshToken, + cancellationToken)); + + if (refreshToken == null) + { + throw new AuthenticationException("Invalid refreshToken"); + } + + if (!refreshToken.IsActive) + { + throw new AuthenticationException("RefreshToken already revoked"); + } + + refreshToken.RevocationTime = DateTimeOffset.UtcNow; + + await _unitOfWork.RefreshTokenRepository + .UpdateOneAsync(refreshToken, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); } } diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs deleted file mode 100644 index f277cc8..0000000 --- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using MediatR; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RevokeRefreshTokenWithCookie; - -public record RevokeRefreshTokenWithCookieCommand : IRequest { } diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs deleted file mode 100644 index eb2f8e2..0000000 --- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using MediatR.Behaviors.Authorization; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RevokeRefreshTokenWithCookie; - -public class RevokeRefreshTokenWithCookieCommandAuthorizer : - AbstractRequestAuthorizer -{ - private readonly SessionUserService _sessionUserService; - - public RevokeRefreshTokenWithCookieCommandAuthorizer( - SessionUserService currentUserService) - { - _sessionUserService = currentUserService; - } - - public override void BuildPolicy(RevokeRefreshTokenWithCookieCommand request) - { - UseRequirement(new MustBeAuthenticatedRequirement - { - IsAuthenticated = _sessionUserService.IsAuthenticated - }); - } -} diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs deleted file mode 100644 index 27a8339..0000000 --- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using MediatR; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RevokeRefreshTokenWithCookie; - -public class RevokeRefreshTokenWithCookieCommandHandler : - IRequestHandler -{ - private readonly AuthenticationService _authenticationService; - private readonly SessionUserService _sessionUserService; - - public RevokeRefreshTokenWithCookieCommandHandler( - AuthenticationService authenticationService, - SessionUserService sessionUserService) - { - _authenticationService = authenticationService; - _sessionUserService = sessionUserService; - } - - public async Task Handle( - RevokeRefreshTokenWithCookieCommand request, - CancellationToken cancellationToken) - { - await _authenticationService.RevokeRefreshTokenAsync( - _sessionUserService.RefreshToken, cancellationToken); - } -} diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs deleted file mode 100644 index c378206..0000000 --- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FluentValidation; - -namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RevokeRefreshTokenWithCookie; - -public class RevokeRefreshTokenWithCookieCommandValidator : - AbstractValidator -{ - public RevokeRefreshTokenWithCookieCommandValidator() { } -} diff --git a/src/Application/Authentication/Queries/Login/LoginQuery.cs b/src/Application/Authentication/Queries/Login/LoginQuery.cs index 905347c..3a2c4dd 100644 --- a/src/Application/Authentication/Queries/Login/LoginQuery.cs +++ b/src/Application/Authentication/Queries/Login/LoginQuery.cs @@ -4,7 +4,7 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; public record LoginQuery : IRequest { - public string Email { get; set; } + public string EmailOrUsername { get; set; } public string Password { get; set; } } diff --git a/src/Application/Authentication/Queries/Login/LoginQueryAuthorizer.cs b/src/Application/Authentication/Queries/Login/LoginQueryAuthorizer.cs new file mode 100644 index 0000000..eb5ab38 --- /dev/null +++ b/src/Application/Authentication/Queries/Login/LoginQueryAuthorizer.cs @@ -0,0 +1,12 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; + +public class LoginQueryAuthorizer : AbstractRequestAuthorizer +{ + public override void BuildPolicy(LoginQuery request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs b/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs index 693a5bb..00e360b 100644 --- a/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs +++ b/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs @@ -1,21 +1,140 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Entities; using MediatR; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; public class LoginQueryHandler : IRequestHandler { - private readonly AuthenticationService _authenticationService; + private readonly UnitOfWork _unitOfWork; + private readonly PasswordHasherService _passwordHasher; + private readonly JsonWebTokenConfigurationOptions _jwtConfiguration; - public LoginQueryHandler(AuthenticationService authenticationService) + public LoginQueryHandler(UnitOfWork unitOfWork, + PasswordHasherService passwordHasher, + IOptions configurationOptions) { - _authenticationService = authenticationService; + _unitOfWork = unitOfWork; + _passwordHasher = passwordHasher; + _jwtConfiguration = configurationOptions.Value.JsonWebToken; } public async Task Handle( LoginQuery request, CancellationToken cancellationToken) { - return await _authenticationService.LoginAsync( - request.Email, request.Password, cancellationToken); + var account = await _unitOfWork.AccountRepository + .GetOneAsync( + a => + a.Email == request.EmailOrUsername || + a.Username == request.EmailOrUsername, + a => a.AccountRoles, cancellationToken); + + if (account == null) + { + throw new LoginException("No users registered with given email."); + } + + var hash = Convert.FromBase64String(account.PasswordHash); + var salt = Convert.FromBase64String(account.PasswordSalt); + var password = Encoding.UTF8.GetBytes(request.Password); + + var isValidPassword = await _passwordHasher + .IsValidHashAsync(hash, password, salt, cancellationToken); + + if (!isValidPassword) + { + throw new LoginException("Given password is incorrect."); + } + + var jwtSecurityToken = await CreateJwtAsync(account, cancellationToken); + var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + + var refreshToken = (await _unitOfWork.RefreshTokenRepository + .GetPageAsync( + e => + e.AccountId == account.Id && + e.RevocationTime == null && + e.ExpirationTime > DateTimeOffset.UtcNow, + 1, int.MaxValue, cancellationToken)) + .Items.FirstOrDefault(); + + if (refreshToken == null) + { + refreshToken = CreateRefreshToken(); + refreshToken.AccountId = account.Id; + + await _unitOfWork.RefreshTokenRepository + .AddOneAsync(refreshToken, cancellationToken); + } + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return new TokensModel(accessToken, refreshToken.Value); + } + + private async Task CreateJwtAsync( + Account account, CancellationToken cancellationToken) + { + var roleIds = account.AccountRoles.Select(ar => ar.RoleId); + + var roles = (await _unitOfWork.RoleRepository + .GetPageAsync( + r => roleIds.Contains(r.Id), + 1, roleIds.Count(), cancellationToken)) + .Items.Select(r => r.Value); + + var roleClaims = new List(); + foreach (var role in roles) + { + roleClaims.Add(new Claim("roles", role.Name)); + } + + var claims = new List() + { + new Claim(JwtRegisteredClaimNames.Sub, account.Guid.ToString()), + new Claim(JwtRegisteredClaimNames.Nickname, account.Username), + new Claim(JwtRegisteredClaimNames.Email, account.Email) + } + .Union(roleClaims); + + var expirationDateTimeUtc = DateTime.UtcNow.Add( + _jwtConfiguration.AccessTokenValidity); + + var symmetricSecurityKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(_jwtConfiguration.IssuerSigningKey)); + var signingCredentials = new SigningCredentials( + symmetricSecurityKey, SecurityAlgorithms.HmacSha256); + + var jwtSecurityToken = new JwtSecurityToken( + issuer: _jwtConfiguration.Issuer, + audience: _jwtConfiguration.Audience, + claims: claims, + expires: expirationDateTimeUtc, + signingCredentials: signingCredentials); + + return jwtSecurityToken; + } + + private RefreshToken CreateRefreshToken() + { + var token = RandomNumberGenerator.GetBytes(128 / 8); + + return new RefreshToken + { + Guid = Guid.NewGuid(), + Value = Convert.ToBase64String(token), + CreationTime = DateTimeOffset.UtcNow, + ExpirationTime = DateTimeOffset.UtcNow.Add( + _jwtConfiguration.RefreshTokenValidity) + }; } } diff --git a/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs b/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs index 9f3fb38..7a0ed0b 100644 --- a/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs +++ b/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs @@ -1,15 +1,18 @@ using FluentValidation; +using Microsoft.Extensions.Localization; namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; public class LoginQueryValidator : AbstractValidator { - public LoginQueryValidator() + public LoginQueryValidator(IStringLocalizer localizer) { - RuleFor(v => v.Email) - .NotEmpty().WithMessage("Email address is required."); + RuleFor(v => v.EmailOrUsername) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); RuleFor(v => v.Password) - .NotEmpty().WithMessage("Password is required."); + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); } } diff --git a/src/Application/Buses/BusDto.cs b/src/Application/Buses/BusDto.cs new file mode 100644 index 0000000..1be10a6 --- /dev/null +++ b/src/Application/Buses/BusDto.cs @@ -0,0 +1,28 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Buses; + +public sealed class BusDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + 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 new file mode 100644 index 0000000..00f0405 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public record AddBusCommand : IRequest +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs new file mode 100644 index 0000000..ad47b23 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public class AddBusCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public AddBusCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(AddBusCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs new file mode 100644 index 0000000..3d5c276 --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs @@ -0,0 +1,62 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public class AddBusCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddBusCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddBusCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Number == request.Number, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Bus with given number already exists."); + } + + 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, + CompanyId = parentEntity.Id, + Company = parentEntity + }; + + entity = await _unitOfWork.BusRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs new file mode 100644 index 0000000..f16532a --- /dev/null +++ b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; + +public class AddBusCommandValidator : AbstractValidator +{ + public AddBusCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs new file mode 100644 index 0000000..32ea1d6 --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public record DeleteBusCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs new file mode 100644 index 0000000..24e4d72 --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public class DeleteBusCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public DeleteBusCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(DeleteBusCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs new file mode 100644 index 0000000..1755ddd --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public class DeleteBusCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteBusCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteBusCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.BusRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs new file mode 100644 index 0000000..c4e71a5 --- /dev/null +++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; + +public class DeleteBusCommandValidator : AbstractValidator +{ + public DeleteBusCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs new file mode 100644 index 0000000..4a5f18a --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs @@ -0,0 +1,16 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public record UpdateBusCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs new file mode 100644 index 0000000..11be53a --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public class UpdateBusCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public UpdateBusCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(UpdateBusCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs new file mode 100644 index 0000000..64e74b8 --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs @@ -0,0 +1,67 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public class UpdateBusCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateBusCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateBusCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var duplicateEntity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Number == request.Number && e.Guid != request.Guid, + cancellationToken); + + if (duplicateEntity != null) + { + throw new DuplicateEntityException( + "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); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs new file mode 100644 index 0000000..4a11102 --- /dev/null +++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs @@ -0,0 +1,45 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; + +public class UpdateBusCommandValidator : AbstractValidator +{ + public UpdateBusCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQuery.cs b/src/Application/Buses/Queries/GetBus/GetBusQuery.cs new file mode 100644 index 0000000..8419f16 --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public record GetBusQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs new file mode 100644 index 0000000..1a898b1 --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public class GetBusQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetBusQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetBusQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs new file mode 100644 index 0000000..55c1b86 --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public class GetBusQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetBusQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetBusQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.BusRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Company, + cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs new file mode 100644 index 0000000..b97a62f --- /dev/null +++ b/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; + +public class GetBusQueryValidator : AbstractValidator +{ + public GetBusQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs new file mode 100644 index 0000000..343e576 --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public record GetBusesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? CompanyGuid { get; set; } + + public short? CapacityGreaterThanOrEqualTo { get; set; } + + public short? CapacityLessThanOrEqualTo { get; set; } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs new file mode 100644 index 0000000..74b0c99 --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public class GetBusesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetBusesPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetBusesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs new file mode 100644 index 0000000..9d29d12 --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs @@ -0,0 +1,57 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public class GetBusesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetBusesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetBusesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.BusRepository.GetPageAsync( + e => + (e.Number.ToLower().Contains(request.Search.ToLower()) || + e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.CompanyGuid != null + ? e.Company.Guid == request.CompanyGuid + : true) && + (request.CapacityGreaterThanOrEqualTo != null + ? e.Capacity >= request.CapacityGreaterThanOrEqualTo + : true) && + (request.CapacityLessThanOrEqualTo != null + ? e.Capacity <= request.CapacityLessThanOrEqualTo + : true), + e => e.Company, + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs new file mode 100644 index 0000000..432f9e3 --- /dev/null +++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; + +public class GetBusesPageQueryValidator : AbstractValidator +{ + public GetBusesPageQueryValidator( + 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/Buses/ViewModels/AddBusViewModel.cs b/src/Application/Buses/ViewModels/AddBusViewModel.cs new file mode 100644 index 0000000..0820527 --- /dev/null +++ b/src/Application/Buses/ViewModels/AddBusViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; + +public sealed class AddBusViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } +} diff --git a/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs new file mode 100644 index 0000000..725d0e9 --- /dev/null +++ b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; + +public sealed class GetBusesPageFilterViewModel +{ + public Guid? CompanyUuid { get; set; } + + public short? CapacityGreaterThanOrEqualTo { get; set; } + + public short? CapacityLessThanOrEqualTo { get; set; } +} diff --git a/src/Application/Buses/ViewModels/UpdateBusViewModel.cs b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs new file mode 100644 index 0000000..7ce4cde --- /dev/null +++ b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Buses.ViewModels; + +public sealed class UpdateBusViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } +} diff --git a/src/Application/Cities/CityDto.cs b/src/Application/Cities/CityDto.cs new file mode 100644 index 0000000..f3a47ff --- /dev/null +++ b/src/Application/Cities/CityDto.cs @@ -0,0 +1,39 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Cities; + +public sealed class CityDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.Region.Country.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.Region.Name)); + } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommand.cs b/src/Application/Cities/Commands/AddCity/AddCityCommand.cs new file mode 100644 index 0000000..72152a3 --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommand.cs @@ -0,0 +1,10 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public record AddCityCommand : IRequest +{ + public string Name { get; set; } + + public Guid RegionGuid { get; set; } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs new file mode 100644 index 0000000..32ff5c8 --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public class AddCityCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddCityCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddCityCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs new file mode 100644 index 0000000..13d7708 --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs @@ -0,0 +1,61 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public class AddCityCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddCityCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddCityCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Name == request.Name && e.Region.Guid == request.RegionGuid, + cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "City with given name already exists."); + } + + var parentEntity = await _unitOfWork.RegionRepository.GetOneAsync( + e => e.Guid == request.RegionGuid, e => e.Country, + cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.RegionGuid} not found."); + } + + entity = new City() + { + Name = request.Name, + RegionId = parentEntity.Id + }; + + entity = await _unitOfWork.CityRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs new file mode 100644 index 0000000..4dd216c --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs @@ -0,0 +1,27 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public class AddCityCommandValidator : AbstractValidator +{ + public AddCityCommandValidator( + 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.RegionGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs new file mode 100644 index 0000000..94dbf43 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public record DeleteCityCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs new file mode 100644 index 0000000..9295ca7 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public class DeleteCityCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteCityCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteCityCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs new file mode 100644 index 0000000..c5ebf61 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public class DeleteCityCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteCityCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteCityCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.CityRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs new file mode 100644 index 0000000..582aa0b --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public class DeleteCityCommandValidator : AbstractValidator +{ + public DeleteCityCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs new file mode 100644 index 0000000..acafa27 --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public record UpdateCityCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Name { get; set; } + + public Guid RegionGuid { get; set; } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs new file mode 100644 index 0000000..376f57d --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public class UpdateCityCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateCityCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateCityCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs new file mode 100644 index 0000000..8e71fb3 --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs @@ -0,0 +1,55 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public class UpdateCityCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateCityCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateCityCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Region.Country, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var parentEntity = await _unitOfWork.RegionRepository.GetOneAsync( + e => e.Guid == request.RegionGuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.RegionGuid} not found."); + } + + entity.Name = request.Name; + entity.RegionId = parentEntity.Id; + + entity = await _unitOfWork.CityRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs new file mode 100644 index 0000000..6d1ef78 --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public class UpdateCityCommandValidator : AbstractValidator +{ + public UpdateCityCommandValidator( + 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.RegionGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs new file mode 100644 index 0000000..2fdcac5 --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs @@ -0,0 +1,19 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public record GetCitiesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? CountryGuid { get; set; } + + public Guid? RegionGuid { get; set; } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs new file mode 100644 index 0000000..a14f95d --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public class GetCitiesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCitiesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCitiesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs new file mode 100644 index 0000000..f3b45af --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs @@ -0,0 +1,55 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public class GetCitiesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCitiesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetCitiesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.CityRepository.GetPageAsync( + e => + (e.Name.ToLower().Contains(request.Search.ToLower()) || + e.Region.Name.ToLower().Contains(request.Search.ToLower()) || + e.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && + (request.RegionGuid != null + ? e.Region.Guid == request.RegionGuid + : true) && + (request.CountryGuid != null + ? e.Region.Country.Guid == request.CountryGuid + : true), + e => e.Region.Country, + 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/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs new file mode 100644 index 0000000..9bc82fa --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public class GetCitiesPageQueryValidator : AbstractValidator +{ + public GetCitiesPageQueryValidator( + 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/Cities/Queries/GetCity/GetCityQuery.cs b/src/Application/Cities/Queries/GetCity/GetCityQuery.cs new file mode 100644 index 0000000..4f2c898 --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public record GetCityQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs new file mode 100644 index 0000000..d811478 --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public class GetCityQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCityQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCityQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs new file mode 100644 index 0000000..49827e2 --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public class GetCityQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCityQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetCityQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Region.Country, + cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs new file mode 100644 index 0000000..1c7fbb5 --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public class GetCityQueryValidator : AbstractValidator +{ + public GetCityQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/ViewModels/AddCityViewModel.cs b/src/Application/Cities/ViewModels/AddCityViewModel.cs new file mode 100644 index 0000000..ad60681 --- /dev/null +++ b/src/Application/Cities/ViewModels/AddCityViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Cities.ViewModels; + +public sealed class AddCityViewModel +{ + public string Name { get; set; } + + public Guid RegionGuid { get; set; } +} diff --git a/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs b/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs new file mode 100644 index 0000000..68ec4d9 --- /dev/null +++ b/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Cities.ViewModels; + +public sealed class GetCitiesPageFilterViewModel +{ + public Guid? CountryUuid { get; set; } + + public Guid? RegionUuid { get; set; } +} diff --git a/src/Application/Cities/ViewModels/UpdateCityViewModel.cs b/src/Application/Cities/ViewModels/UpdateCityViewModel.cs new file mode 100644 index 0000000..05f7e11 --- /dev/null +++ b/src/Application/Cities/ViewModels/UpdateCityViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Cities.ViewModels; + +public sealed class UpdateCityViewModel +{ + public string Name { get; set; } + + public Guid RegionUuid { get; set; } +} diff --git a/src/Application/Common/Authorization/AllowAllRequirement.cs b/src/Application/Common/Authorization/AllowAllRequirement.cs new file mode 100644 index 0000000..05c7d78 --- /dev/null +++ b/src/Application/Common/Authorization/AllowAllRequirement.cs @@ -0,0 +1,17 @@ +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Common.Authorization; + +public class AllowAllRequirement : IAuthorizationRequirement +{ + class MustBeAuthenticatedRequirementHandler : + IAuthorizationHandler + { + public Task Handle( + AllowAllRequirement request, + CancellationToken cancellationToken) + { + return Task.FromResult(AuthorizationResult.Succeed()); + } + } +} diff --git a/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs b/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs index 809c638..685e587 100644 --- a/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs +++ b/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs @@ -1,4 +1,3 @@ -// using cuqmbr.TravelGuide.Application.Common.Exceptions; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Common.Authorization; @@ -17,8 +16,6 @@ public class MustBeAuthenticatedRequirement : IAuthorizationRequirement if (!request.IsAuthenticated) { return Task.FromResult(AuthorizationResult.Fail()); - // TODO: Remove UnAuthorizedException, isn't used - // throw new UnAuthorizedException(); } return Task.FromResult(AuthorizationResult.Succeed()); diff --git a/src/Application/Common/Authorization/MustBeInRolesRequirement.cs b/src/Application/Common/Authorization/MustBeInAnyOfRolesRequirement.cs similarity index 54% rename from src/Application/Common/Authorization/MustBeInRolesRequirement.cs rename to src/Application/Common/Authorization/MustBeInAnyOfRolesRequirement.cs index e1368d1..4a293fb 100644 --- a/src/Application/Common/Authorization/MustBeInRolesRequirement.cs +++ b/src/Application/Common/Authorization/MustBeInAnyOfRolesRequirement.cs @@ -1,31 +1,22 @@ using MediatR.Behaviors.Authorization; -using Microsoft.Extensions.Localization; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application.Common.Authorization; -public class MustBeInRolesRequirement : IAuthorizationRequirement +public class MustBeInAnyOfRolesRequirement : IAuthorizationRequirement { public ICollection UserRoles { get; init; } public ICollection RequiredRoles { get; init; } - class MustBeInRolesRequirementHandler : - IAuthorizationHandler + class MustBeInAnyOfRolesRequirementHandler : + IAuthorizationHandler { - private readonly IStringLocalizer _localizer; - - public MustBeInRolesRequirementHandler(IStringLocalizer localizer) - { - _localizer = localizer; - } - public Task Handle( - MustBeInRolesRequirement request, + MustBeInAnyOfRolesRequirement request, CancellationToken cancellationToken) { - var isUserInRequiredRoles = - request.UserRoles?.Any(ur => request.RequiredRoles.Contains(ur)) - ?? false; + var isUserInRequiredRoles = request.UserRoles + .Any(ur => request.RequiredRoles.Contains(ur)); if (!isUserInRequiredRoles) { diff --git a/src/Application/Common/Authorization/MustBeObjectOwnerOrAdminRequirement.cs b/src/Application/Common/Authorization/MustBeObjectOwnerOrAdminRequirement.cs new file mode 100644 index 0000000..d33ffc3 --- /dev/null +++ b/src/Application/Common/Authorization/MustBeObjectOwnerOrAdminRequirement.cs @@ -0,0 +1,42 @@ +using MediatR.Behaviors.Authorization; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Authorization; + +public class MustBeObjectOwnerOrAdminRequirement : IAuthorizationRequirement +{ + public ICollection? UserRoles { get; init; } + + public Guid? UserGuid { get; init; } + public Guid? RequiredGuid { get; init; } + + class MustBeObjectOwnerOrAdminRequirementHandler : + IAuthorizationHandler + { + public Task Handle( + MustBeObjectOwnerOrAdminRequirement request, + CancellationToken cancellationToken) + { + var isAdmin = request?.UserRoles + ?.Any(ur => ur.Equals(IdentityRole.Administrator)) ?? + false; + + if (isAdmin) + { + return Task.FromResult(AuthorizationResult.Succeed()); + } + + if (request?.UserGuid == null || request?.RequiredGuid == null) + { + return Task.FromResult(AuthorizationResult.Fail()); + } + + if (request.UserGuid == request.RequiredGuid) + { + return Task.FromResult(AuthorizationResult.Succeed()); + } + + return Task.FromResult(AuthorizationResult.Fail()); + } + } +} diff --git a/src/Application/Common/Exceptions/ValidationException.cs b/src/Application/Common/Exceptions/ValidationException.cs index 7c1df67..0ed4b17 100644 --- a/src/Application/Common/Exceptions/ValidationException.cs +++ b/src/Application/Common/Exceptions/ValidationException.cs @@ -10,9 +10,24 @@ public class ValidationException : Exception Errors = new Dictionary(); } + public ValidationException(string message) : base(message) + { + Errors = new Dictionary(); + } + public ValidationException(IEnumerable failures) : this() { + // TODO: Make serialized dictionary look more like this + // "errors": { + // "viewModel": [ + // "The viewModel field is required." + // ], + // "$.addresses[0].order": [ + // "The JSON value could not be converted to System.Int16. Path: $.addresses[0].order | LineNumber: 5 | BytePositionInLine: 26." + // ] + // }, + Errors = failures .GroupBy(f => f.PropertyName, f => f.ErrorMessage) .ToDictionary(fg => fg.Key, fg => fg.ToArray()); diff --git a/src/Application/Common/FluentValidation/CustomValidators.cs b/src/Application/Common/FluentValidation/CustomValidators.cs new file mode 100644 index 0000000..aa7bb49 --- /dev/null +++ b/src/Application/Common/FluentValidation/CustomValidators.cs @@ -0,0 +1,63 @@ +using FluentValidation; + +namespace cuqmbr.TravelGuide.Application.Common.FluentValidation; + +public static class CustomValidators +{ + public static IRuleBuilderOptions IsUsername( + this IRuleBuilder ruleBuilder) + { + return + ruleBuilder + .Matches(@"^[a-z0-9-_\.]*$"); + } + + // According to RFC 5321. + public static IRuleBuilderOptions IsEmail( + this IRuleBuilder ruleBuilder) + { + return + ruleBuilder + .Matches(@"^[a-z0-9-_\.]{1,64}@[a-z0-9-_\.]{1,251}\.[a-z0-9-_]{2,4}$"); + } + + // According to ITU-T E.164, no spaces. + public static IRuleBuilderOptions IsPhoneNumber( + this IRuleBuilder ruleBuilder) + { + return + ruleBuilder + .Matches(@"^\+[0-9]{7,15}$"); + } + + public static IRuleBuilderOptions> + IsUnique( + this IRuleBuilder> ruleBuilder, + Func selector) + { + if (selector == null) + { + throw new ArgumentNullException( + nameof(selector), + "Cannot pass a null selector."); + } + + return + ruleBuilder + .Must(x => x.IsDistinct(selector)); + } + + public static bool IsDistinct( + this IEnumerable elements, Func selector) + { + var hashSet = new HashSet(); + foreach (var element in elements.Select(selector)) + { + if (!hashSet.Contains(element)) + hashSet.Add(element); + else + return false; + } + return true; + } +} diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs deleted file mode 100644 index b98a962..0000000 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ /dev/null @@ -1,12 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; - -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; - -public interface UnitOfWork : IDisposable -{ - CountryRepository CountryRepository { get; } - RegionRepository RegionRepository { get; } - - int Save(); - Task SaveAsync(CancellationToken cancellationToken); -} diff --git a/src/Application/Common/Interfaces/Services/AuthenticationService.cs b/src/Application/Common/Interfaces/Services/AuthenticationService.cs deleted file mode 100644 index 6f8932b..0000000 --- a/src/Application/Common/Interfaces/Services/AuthenticationService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using cuqmbr.TravelGuide.Application.Authenticaion; - -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; - -public interface AuthenticationService -{ - Task RegisterAsync(string email, string password, - CancellationToken cancellationToken); - - Task LoginAsync(string email, string password, - CancellationToken cancellationToken); - - Task RenewAccessTokenAsync(string refreshToken, - CancellationToken cancellationToken); - - Task RevokeRefreshTokenAsync(string refreshToken, - CancellationToken cancellationToken); -} diff --git a/src/Application/Common/Interfaces/Services/CultureService.cs b/src/Application/Common/Interfaces/Services/CultureService.cs deleted file mode 100644 index 3ee1ee8..0000000 --- a/src/Application/Common/Interfaces/Services/CultureService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Globalization; - -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; - -public interface CultureService -{ - public CultureInfo Culture { get; } -} diff --git a/src/Application/Common/Interfaces/Services/SessionUserService.cs b/src/Application/Common/Interfaces/Services/SessionUserService.cs deleted file mode 100644 index 0ffb78d..0000000 --- a/src/Application/Common/Interfaces/Services/SessionUserService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using cuqmbr.TravelGuide.Application.Common.Models; - -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; - -public interface SessionUserService -{ - public int? Id { get; } - - public Guid? Uuid { get; } - - public string? Email { get; } - - public ICollection Roles { get; } - - - public bool IsAuthenticated => Id != null; - - - public string? AccessToken { get; } - - public string? RefreshToken { get; } -} diff --git a/src/Application/Common/Interfaces/Services/TimeZoneService.cs b/src/Application/Common/Interfaces/Services/TimeZoneService.cs deleted file mode 100644 index 6ec775c..0000000 --- a/src/Application/Common/Interfaces/Services/TimeZoneService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; - -public interface TimeZoneService -{ - public TimeZoneInfo TimeZone { get; } -} diff --git a/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs b/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs index 679eb28..8f7797f 100644 --- a/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs +++ b/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs @@ -1,14 +1,14 @@ using AutoMapper; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; namespace cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers; public class DateTimeOffsetToLocalResolver : IMemberValueResolver { - private readonly TimeZoneService _timeZoneService; + private readonly SessionTimeZoneService _timeZoneService; - public DateTimeOffsetToLocalResolver(TimeZoneService timeZoneService) + public DateTimeOffsetToLocalResolver(SessionTimeZoneService timeZoneService) { _timeZoneService = timeZoneService; } diff --git a/src/Application/Common/Persistence/Repositories/AccountRepository.cs b/src/Application/Common/Persistence/Repositories/AccountRepository.cs new file mode 100644 index 0000000..57ab40e --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/AccountRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface AccountRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/AccountRoleRepository.cs b/src/Application/Common/Persistence/Repositories/AccountRoleRepository.cs new file mode 100644 index 0000000..42ce1c1 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/AccountRoleRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface AccountRoleRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/AddressRepository.cs b/src/Application/Common/Persistence/Repositories/AddressRepository.cs new file mode 100644 index 0000000..0280f38 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/AddressRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface AddressRepository : BaseRepository
{ } diff --git a/src/Application/Common/Persistence/Repositories/AircraftRepository.cs b/src/Application/Common/Persistence/Repositories/AircraftRepository.cs new file mode 100644 index 0000000..3d82992 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/AircraftRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface AircraftRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/BaseRepository.cs b/src/Application/Common/Persistence/Repositories/BaseRepository.cs similarity index 94% rename from src/Application/Common/Interfaces/Persistence/Repositories/BaseRepository.cs rename to src/Application/Common/Persistence/Repositories/BaseRepository.cs index e3b78d5..a89c127 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/BaseRepository.cs +++ b/src/Application/Common/Persistence/Repositories/BaseRepository.cs @@ -2,8 +2,7 @@ using System.Linq.Expressions; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface BaseRepository where TEntity : EntityBase diff --git a/src/Application/Common/Persistence/Repositories/BusRepository.cs b/src/Application/Common/Persistence/Repositories/BusRepository.cs new file mode 100644 index 0000000..d06a5f3 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/BusRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface BusRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/CityRepository.cs b/src/Application/Common/Persistence/Repositories/CityRepository.cs new file mode 100644 index 0000000..4db10a0 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/CityRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface CityRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/CompanyRepository.cs b/src/Application/Common/Persistence/Repositories/CompanyRepository.cs new file mode 100644 index 0000000..23be434 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/CompanyRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common + .Persistence.Repositories; + +public interface CompanyRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/CountryRepository.cs b/src/Application/Common/Persistence/Repositories/CountryRepository.cs similarity index 54% rename from src/Application/Common/Interfaces/Persistence/Repositories/CountryRepository.cs rename to src/Application/Common/Persistence/Repositories/CountryRepository.cs index 2be573d..1e27047 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/CountryRepository.cs +++ b/src/Application/Common/Persistence/Repositories/CountryRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface CountryRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/EmployeeRepository.cs b/src/Application/Common/Persistence/Repositories/EmployeeRepository.cs new file mode 100644 index 0000000..5941838 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/EmployeeRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common + .Persistence.Repositories; + +public interface EmployeeRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/RefreshTokenRepository.cs b/src/Application/Common/Persistence/Repositories/RefreshTokenRepository.cs new file mode 100644 index 0000000..b44ebec --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/RefreshTokenRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface RefreshTokenRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/RegionRepository.cs b/src/Application/Common/Persistence/Repositories/RegionRepository.cs similarity index 54% rename from src/Application/Common/Interfaces/Persistence/Repositories/RegionRepository.cs rename to src/Application/Common/Persistence/Repositories/RegionRepository.cs index 29ecdfe..29ba027 100644 --- a/src/Application/Common/Interfaces/Persistence/Repositories/RegionRepository.cs +++ b/src/Application/Common/Persistence/Repositories/RegionRepository.cs @@ -1,6 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; -namespace cuqmbr.TravelGuide.Application.Common.Interfaces - .Persistence.Repositories; +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; public interface RegionRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/RoleRepository.cs b/src/Application/Common/Persistence/Repositories/RoleRepository.cs new file mode 100644 index 0000000..72d3640 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/RoleRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface RoleRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/RouteAddressDetailRepository.cs b/src/Application/Common/Persistence/Repositories/RouteAddressDetailRepository.cs new file mode 100644 index 0000000..b0588ba --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/RouteAddressDetailRepository.cs @@ -0,0 +1,7 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common + .Persistence.Repositories; + +public interface RouteAddressDetailRepository : + BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/RouteAddressRepository.cs b/src/Application/Common/Persistence/Repositories/RouteAddressRepository.cs new file mode 100644 index 0000000..8670b9b --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/RouteAddressRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface RouteAddressRepository : + BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/RouteRepository.cs b/src/Application/Common/Persistence/Repositories/RouteRepository.cs new file mode 100644 index 0000000..866a0a3 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/RouteRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface RouteRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/TicketGroupRepository.cs b/src/Application/Common/Persistence/Repositories/TicketGroupRepository.cs new file mode 100644 index 0000000..9dfe535 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/TicketGroupRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common + .Persistence.Repositories; + +public interface TicketGroupRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/TicketRepository.cs b/src/Application/Common/Persistence/Repositories/TicketRepository.cs new file mode 100644 index 0000000..881008c --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/TicketRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common + .Persistence.Repositories; + +public interface TicketRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/TrainRepository.cs b/src/Application/Common/Persistence/Repositories/TrainRepository.cs new file mode 100644 index 0000000..9358316 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/TrainRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface TrainRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs b/src/Application/Common/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs new file mode 100644 index 0000000..599dd71 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/VehicleEnrollmentEmployeeRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface VehicleEnrollmentEmployeeRepository : + BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/VehicleEnrollmentRepository.cs b/src/Application/Common/Persistence/Repositories/VehicleEnrollmentRepository.cs new file mode 100644 index 0000000..763cff0 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/VehicleEnrollmentRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface VehicleEnrollmentRepository : + BaseRepository { } diff --git a/src/Application/Common/Persistence/Repositories/VehicleRepository.cs b/src/Application/Common/Persistence/Repositories/VehicleRepository.cs new file mode 100644 index 0000000..7a04b65 --- /dev/null +++ b/src/Application/Common/Persistence/Repositories/VehicleRepository.cs @@ -0,0 +1,5 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +public interface VehicleRepository : BaseRepository { } diff --git a/src/Application/Common/Persistence/UnitOfWork.cs b/src/Application/Common/Persistence/UnitOfWork.cs new file mode 100644 index 0000000..41bd0aa --- /dev/null +++ b/src/Application/Common/Persistence/UnitOfWork.cs @@ -0,0 +1,56 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; + +namespace cuqmbr.TravelGuide.Application.Common.Persistence; + +public interface UnitOfWork : IDisposable +{ + // Application Logic + + CountryRepository CountryRepository { get; } + + RegionRepository RegionRepository { get; } + + CityRepository CityRepository { get; } + + AddressRepository AddressRepository { get; } + + RouteRepository RouteRepository { get; } + + VehicleRepository VehicleRepository { get; } + + BusRepository BusRepository { get; } + + AircraftRepository AircraftRepository { get; } + + TrainRepository TrainRepository { get; } + + VehicleEnrollmentRepository VehicleEnrollmentRepository { get; } + + RouteAddressRepository RouteAddressRepository { get; } + + CompanyRepository CompanyRepository { get; } + + EmployeeRepository EmployeeRepository { get; } + + TicketGroupRepository TicketGroupRepository { get; } + + TicketRepository TicketRepository { get; } + + RouteAddressDetailRepository RouteAddressDetailRepository { get; } + + VehicleEnrollmentEmployeeRepository VehicleEnrollmentEmployeeRepository { get; } + + // Identity + + AccountRepository AccountRepository { get; } + + RoleRepository RoleRepository { get; } + + AccountRoleRepository AccountRoleRepository { get; } + + RefreshTokenRepository RefreshTokenRepository { get; } + + int Save(); + + Task SaveAsync(CancellationToken cancellationToken); +} diff --git a/src/Application/Common/Services/CurrencyConverterService.cs b/src/Application/Common/Services/CurrencyConverterService.cs new file mode 100644 index 0000000..e8ae6a0 --- /dev/null +++ b/src/Application/Common/Services/CurrencyConverterService.cs @@ -0,0 +1,12 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface CurrencyConverterService +{ + Task ConvertAsync(decimal amount, Currency from, Currency to, + CancellationToken cancellationToken); + + Task ConvertAsync(decimal amount, Currency from, Currency to, + DateTimeOffset time, CancellationToken cancellationToken); +} diff --git a/src/Application/Common/Services/EmailSenderService.cs b/src/Application/Common/Services/EmailSenderService.cs new file mode 100644 index 0000000..706d96d --- /dev/null +++ b/src/Application/Common/Services/EmailSenderService.cs @@ -0,0 +1,7 @@ +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface EmailSenderService +{ + Task SendAsync(string[] addresses, string subject, string body, + CancellationToken cancellationToken); +} diff --git a/src/Application/Common/Services/LiqPayPaymentService.cs b/src/Application/Common/Services/LiqPayPaymentService.cs new file mode 100644 index 0000000..238bb84 --- /dev/null +++ b/src/Application/Common/Services/LiqPayPaymentService.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface LiqPayPaymentService +{ + string GetPaymentLink( + decimal amount, Currency currency, + string orderId, TimeSpan validity, string description, + string resultPath, string callbackPath); + + bool IsValidSignature(string postData, string postSignature); +} diff --git a/src/Application/Common/Services/PasswordHasherService.cs b/src/Application/Common/Services/PasswordHasherService.cs new file mode 100644 index 0000000..a3dc22c --- /dev/null +++ b/src/Application/Common/Services/PasswordHasherService.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface PasswordHasherService +{ + Task HashAsync(byte[] password, byte[] salt, + CancellationToken cancellationToken); + + Task IsValidHashAsync(byte[] hash, byte[] password, + byte[] salt, CancellationToken cancellationToken); +} diff --git a/src/Application/Common/Services/SessionCurrencyService.cs b/src/Application/Common/Services/SessionCurrencyService.cs new file mode 100644 index 0000000..15e7ce0 --- /dev/null +++ b/src/Application/Common/Services/SessionCurrencyService.cs @@ -0,0 +1,8 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface SessionCurrencyService +{ + public Currency Currency { get; } +} diff --git a/src/Application/Common/Services/SessionTimeZoneService.cs b/src/Application/Common/Services/SessionTimeZoneService.cs new file mode 100644 index 0000000..4b2faaa --- /dev/null +++ b/src/Application/Common/Services/SessionTimeZoneService.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface SessionTimeZoneService +{ + public TimeZoneInfo TimeZone { get; } +} diff --git a/src/Application/Common/Services/SessionUserService.cs b/src/Application/Common/Services/SessionUserService.cs new file mode 100644 index 0000000..7a868ab --- /dev/null +++ b/src/Application/Common/Services/SessionUserService.cs @@ -0,0 +1,22 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface SessionUserService +{ + public Guid? Guid { get; } + + public string? Username { get; } + + public string? Email { get; } + + public ICollection Roles { get; } + + + public bool IsAuthenticated => Guid != null; + + + public string? AccessToken { get; } + + public string? RefreshToken { get; } +} diff --git a/src/Application/Common/Services/SessoionCultureService.cs b/src/Application/Common/Services/SessoionCultureService.cs new file mode 100644 index 0000000..32a1aa0 --- /dev/null +++ b/src/Application/Common/Services/SessoionCultureService.cs @@ -0,0 +1,8 @@ +using System.Globalization; + +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface SessionCultureService +{ + public CultureInfo Culture { get; } +} diff --git a/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs b/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs new file mode 100644 index 0000000..82aed33 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommand.cs @@ -0,0 +1,21 @@ +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; } + + + public string Username { get; set; } + + public string Email { get; set; } + + public string Password { 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..d7f9f45 --- /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.Services; +using cuqmbr.TravelGuide.Domain.Enums; +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 MustBeInAnyOfRolesRequirement + { + 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..8665252 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandHandler.cs @@ -0,0 +1,96 @@ +using MediatR; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using System.Security.Cryptography; +using System.Text; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany; + +public class AddCompanyCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly PasswordHasherService _passwordHasher; + + public AddCompanyCommandHandler(UnitOfWork unitOfWork, IMapper mapper, + PasswordHasherService passwordHasher) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _passwordHasher = passwordHasher; + } + + 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."); + } + + + // Create new account for employee + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Email == request.Email, + cancellationToken); + + if (account != null) + { + throw new DuplicateEntityException(); + } + + var role = (await _unitOfWork.RoleRepository.GetPageAsync( + 1, IdentityRole.Enumerations.Count(), cancellationToken)) + .Items + .First(r => r.Value.Equals(IdentityRole.CompanyOwner)); + + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = await _passwordHasher.HashAsync( + Encoding.UTF8.GetBytes(request.Password), + salt, cancellationToken); + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + account = new Account() + { + Username = request.Username, + Email = request.Email, + PasswordHash = hashBase64, + PasswordSalt = saltBase64, + AccountRoles = new AccountRole[] { new() { RoleId = role.Id } } + }; + + account = await _unitOfWork.AccountRepository.AddOneAsync( + account, cancellationToken); + + + entity = new Company() + { + Name = request.Name, + LegalAddress = request.LegalAddress, + ContactEmail = request.ContactEmail, + ContactPhoneNumber = request.ContactPhoneNumber, + Account = account + }; + + 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..8718e95 --- /dev/null +++ b/src/Application/Companies/Commands/AddCompany/AddCompanyCommandValidator.cs @@ -0,0 +1,99 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.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)); + + + RuleFor(v => v.Username) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 1)) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)) + .IsUsername() + .WithMessage(localizer["FluentValidation.IsUsername"]); + + RuleFor(v => v.Email) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); + + RuleFor(v => v.Password) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(8) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 8)) + .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..d71fa4e --- /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.Services; +using cuqmbr.TravelGuide.Domain.Enums; +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 MustBeInAnyOfRolesRequirement + { + 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..ec65227 --- /dev/null +++ b/src/Application/Companies/Commands/DeleteCompany/DeleteCompanyCommandHandler.cs @@ -0,0 +1,37 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.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, e => e.Account, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.CompanyRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.AccountRepository.DeleteOneAsync( + entity.Account, 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..b1b41da --- /dev/null +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany; + +public class UpdateCompanyCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public UpdateCompanyCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(UpdateCompanyCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs new file mode 100644 index 0000000..bcab8c5 --- /dev/null +++ b/src/Application/Companies/Commands/UpdateCompany/UpdateCompanyCommandHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.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(); + } + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + a => a.Id == entity.AccountId, cancellationToken); + + entity.Name = request.Name; + entity.LegalAddress = request.LegalAddress; + entity.ContactEmail = request.ContactEmail; + entity.ContactPhoneNumber = request.ContactPhoneNumber; + entity.Account = account; + + 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..c79afb9 --- /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.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/CompanyAccountDto.cs b/src/Application/Companies/CompanyAccountDto.cs new file mode 100644 index 0000000..a428a7d --- /dev/null +++ b/src/Application/Companies/CompanyAccountDto.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Companies; + +public sealed class CompanyAccountDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Username { get; set; } + + public string Email { 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/CompanyDto.cs b/src/Application/Companies/CompanyDto.cs new file mode 100644 index 0000000..47fd659 --- /dev/null +++ b/src/Application/Companies/CompanyDto.cs @@ -0,0 +1,27 @@ +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 CompanyAccountDto Account { 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..d449f07 --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryAuthorizer.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage; + +public class GetCompaniesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy(GetCompaniesPageQuery request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs new file mode 100644 index 0000000..62ea670 --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryHandler.cs @@ -0,0 +1,62 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.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); + + // Hydrate companies + + var accountIds = paginatedList.Items.Select(e => e.AccountId); + var accounts = await _unitOfWork.AccountRepository.GetPageAsync( + e => accountIds.Contains(e.Id), + 1, paginatedList.Items.Count, cancellationToken); + + foreach (var company in paginatedList.Items) + { + company.Account = + accounts.Items.First(a => a.Id == company.AccountId); + } + + 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..5fb0088 --- /dev/null +++ b/src/Application/Companies/Queries/GetCompaniesPage/GetCompaniesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.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..eadbd4d --- /dev/null +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryAuthorizer.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany; + +public class GetCompanyQueryAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy(GetCompanyQuery request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs new file mode 100644 index 0000000..9388c14 --- /dev/null +++ b/src/Application/Companies/Queries/GetCompany/GetCompanyQueryHandler.cs @@ -0,0 +1,43 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.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); + + if (entity == null) + { + throw new NotFoundException(); + } + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Id == entity.AccountId, cancellationToken); + + entity.Account = account; + + _unitOfWork.Dispose(); + + 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..253696e --- /dev/null +++ b/src/Application/Companies/ViewModels/AddCompanyViewModel.cs @@ -0,0 +1,19 @@ +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; } + + + public string Username { get; set; } + + public string Email { get; set; } + + public string Password { 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/ConfigurationOptions.cs b/src/Application/ConfigurationOptions.cs index e80d048..8944ec8 100644 --- a/src/Application/ConfigurationOptions.cs +++ b/src/Application/ConfigurationOptions.cs @@ -2,11 +2,13 @@ namespace cuqmbr.TravelGuide.Application; public sealed class ConfigurationOptions { - public static string SectionName { get; } = "Application"; + public static string SectionName { get; } = ""; public LocalizationConfigurationOptions Localization { get; set; } = new(); public LoggingConfigurationOptions Logging { get; set; } = new(); + + public JsonWebTokenConfigurationOptions JsonWebToken { get; set; } = new(); } public sealed class LocalizationConfigurationOptions @@ -26,3 +28,16 @@ public sealed class LoggingConfigurationOptions public bool UseUtcTimestamp { get; set; } = true; } + +public sealed class JsonWebTokenConfigurationOptions +{ + public string Issuer { get; set; } = "localhost"; + + public string Audience { get; set; } = "localhost"; + + public string IssuerSigningKey { get; set; } = "change-me"; + + public TimeSpan AccessTokenValidity { get; set; } = TimeSpan.FromMinutes(15); + + public TimeSpan RefreshTokenValidity { get; set; } = TimeSpan.FromDays(3); +} diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs index 3ba50fd..832728e 100644 --- a/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; @@ -22,7 +22,7 @@ public class AddCountryCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs index 91ad72c..fc7399b 100644 --- a/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs index 51e03ce..34fcdae 100644 --- a/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; @@ -8,7 +8,7 @@ public class AddCountryCommandValidator : AbstractValidator { public AddCountryCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Name) .NotEmpty() diff --git a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs index 73b8422..8492015 100644 --- a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs +++ b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; @@ -22,7 +22,7 @@ public class DeleteCountryCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs index 8f3b55a..39fc2db 100644 --- a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs +++ b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs index dc4994c..1279663 100644 --- a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry; @@ -22,7 +22,7 @@ public class UpdateCountryCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs index 7d3d822..22fe5fd 100644 --- a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs index d2d0612..52e7d12 100644 --- a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; @@ -8,7 +8,7 @@ public class UpdateCountryCommandValidator : AbstractValidator v.Guid) .NotEmpty() diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs index 67796d8..5b527aa 100644 --- a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; @@ -19,12 +19,13 @@ public class GetCountriesPageQueryAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs index 2ab565d..74b3eeb 100644 --- a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs index 59d2866..00ac279 100644 --- a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; @@ -9,7 +9,7 @@ public class GetCountriesPageQueryValidator : { public GetCountriesPageQueryValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs b/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs index 699f958..a57b9d9 100644 --- a/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs +++ b/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry; @@ -19,12 +19,13 @@ public class GetCountryQueryAuthorizer : { UseRequirement(new MustBeAuthenticatedRequirement { - IsAuthenticated= _sessionUserService.IsAuthenticated + IsAuthenticated = _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs b/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs index 49851a9..cdd06a2 100644 --- a/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs +++ b/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; diff --git a/src/Application/Countries/ViewModels/AddCountryViewModel.cs b/src/Application/Countries/ViewModels/AddCountryViewModel.cs new file mode 100644 index 0000000..f244738 --- /dev/null +++ b/src/Application/Countries/ViewModels/AddCountryViewModel.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Countries.ViewModels; + +public sealed class AddCountryViewModel +{ + public string Name { get; set; } +} diff --git a/src/Application/Countries/ViewModels/UpdateCountryViewModel.cs b/src/Application/Countries/ViewModels/UpdateCountryViewModel.cs new file mode 100644 index 0000000..20fe7f5 --- /dev/null +++ b/src/Application/Countries/ViewModels/UpdateCountryViewModel.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Countries.ViewModels; + +public sealed class UpdateCountryViewModel +{ + public string Name { get; set; } +} diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs new file mode 100644 index 0000000..073c181 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs @@ -0,0 +1,30 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; +using cuqmbr.TravelGuide.Application.Employees.Models; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; + +public record AddEmployeeCommand : IRequest +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public Sex Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public Guid CompanyGuid { get; set; } + + public ICollection Documents { get; set; } + + + public string Username { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } +} diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs new file mode 100644 index 0000000..cb39878 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; + +public class AddEmployeeCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public AddEmployeeCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(AddEmployeeCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs new file mode 100644 index 0000000..63857c4 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs @@ -0,0 +1,124 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Domain.Enums; +using System.Security.Cryptography; +using cuqmbr.TravelGuide.Application.Common.Services; +using System.Text; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; + +public class AddEmployeeCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly IStringLocalizer _localizer; + private readonly PasswordHasherService _passwordHasher; + + public AddEmployeeCommandHandler(UnitOfWork unitOfWork, IMapper mapper, + IStringLocalizer localizer, PasswordHasherService passwordHasher) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + _passwordHasher = passwordHasher; + } + + public async Task Handle( + AddEmployeeCommand request, + CancellationToken cancellationToken) + { + 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."); + } + + var entity = await _unitOfWork.EmployeeRepository.GetOneAsync( + e => + e.FirstName == request.FirstName && + e.LastName == request.LastName && + e.Patronymic == request.Patronymic && + e.Sex == request.Sex && + e.BirthDate == request.BirthDate && + e.CompanyId == parentEntity.Id, + cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException(); + } + + + // Create new account for employee + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Email == request.Email, + cancellationToken); + + if (account != null) + { + throw new DuplicateEntityException(); + } + + var role = (await _unitOfWork.RoleRepository.GetPageAsync( + 1, IdentityRole.Enumerations.Count(), cancellationToken)) + .Items + .First(r => r.Value.Equals(IdentityRole.CompanyEmployee)); + + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = await _passwordHasher.HashAsync( + Encoding.UTF8.GetBytes(request.Password), + salt, cancellationToken); + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + account = new Account() + { + Username = request.Username, + Email = request.Email, + PasswordHash = hashBase64, + PasswordSalt = saltBase64, + AccountRoles = new AccountRole[] { new() { RoleId = role.Id } } + }; + + account = await _unitOfWork.AccountRepository.AddOneAsync( + account, cancellationToken); + + + entity = new Employee() + { + FirstName = request.FirstName, + LastName = request.LastName, + Patronymic = request.Patronymic, + Sex = request.Sex, + BirthDate = request.BirthDate, + Documents = request.Documents.Select( + d => new EmployeeDocument() + { + DocumentType = d.DocumentType, + Information = d.Information + }) + .ToArray(), + Company = parentEntity, + Account = account + }; + + entity = await _unitOfWork.EmployeeRepository.AddOneAsync( + entity, cancellationToken); + + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs new file mode 100644 index 0000000..5e7a87a --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs @@ -0,0 +1,125 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; + +public class AddEmployeeCommandValidator : AbstractValidator +{ + public AddEmployeeCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(e => e.FirstName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.LastName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.Patronymic) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.Sex) + .Must((e, s) => Sex.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Sex.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(e => e.BirthDate) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))); + + RuleForEach(e => e.Documents).ChildRules(d => + { + d.RuleFor(d => d.DocumentType) + .Must(dt => DocumentType.Enumerations.ContainsValue(dt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + DocumentType.Enumerations.Values.Select(e => e.Name)))); + + d.RuleFor(d => d.Information) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + }); + + + RuleFor(v => v.Username) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 1)) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)) + .IsUsername() + .WithMessage(localizer["FluentValidation.IsUsername"]); + + RuleFor(v => v.Email) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); + + RuleFor(v => v.Password) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(8) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 8)) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs new file mode 100644 index 0000000..4a1d1be --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; + +public record DeleteEmployeeCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs new file mode 100644 index 0000000..9d07ec0 --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; + +public class DeleteEmployeeCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public DeleteEmployeeCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(DeleteEmployeeCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var employee = _unitOfWork.EmployeeRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = employee?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs new file mode 100644 index 0000000..a4f6233 --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs @@ -0,0 +1,40 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; + +public class DeleteEmployeeCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteEmployeeCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteEmployeeCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.EmployeeRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Account, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // TODO: Check for Vehicles that using this employee in Enrollments + // Delete if there are no such Vehicles + + await _unitOfWork.EmployeeRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.AccountRepository.DeleteOneAsync( + entity.Account, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs new file mode 100644 index 0000000..9509930 --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; + +public class DeleteEmployeeCommandValidator : AbstractValidator +{ + public DeleteEmployeeCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs new file mode 100644 index 0000000..883a6a4 --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs @@ -0,0 +1,25 @@ +using MediatR; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Employees.Models; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; + +public record UpdateEmployeeCommand : IRequest +{ + public Guid Guid { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public Sex Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public Guid CompanyGuid { get; set; } + + public ICollection Documents { get; set; } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs new file mode 100644 index 0000000..dcc01e3 --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; + +public class UpdateEmployeeCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public UpdateEmployeeCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(UpdateEmployeeCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs new file mode 100644 index 0000000..081327a --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs @@ -0,0 +1,112 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; + +public class UpdateEmployeeCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + public IStringLocalizer _localizer { get; set; } + + public UpdateEmployeeCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + UpdateEmployeeCommand request, + CancellationToken cancellationToken) + { + 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."); + } + + + var employee = await _unitOfWork.EmployeeRepository.GetOneAsync( + e => + e.FirstName == request.FirstName && + e.LastName == request.LastName && + e.Patronymic == request.Patronymic && + e.Sex == request.Sex && + e.BirthDate == request.BirthDate && + e.CompanyId == parentEntity.Id && + e.Guid != request.Guid, + cancellationToken); + + if (employee != null) + { + throw new DuplicateEntityException(); + } + + + employee = await _unitOfWork.EmployeeRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Documents, cancellationToken); + + if (employee == null) + { + throw new NotFoundException(); + } + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + a => a.Id == employee.AccountId, cancellationToken); + + + employee.Guid = request.Guid; + employee.FirstName = request.FirstName; + employee.LastName = request.LastName; + employee.Patronymic = request.Patronymic; + employee.Sex = request.Sex; + employee.BirthDate = request.BirthDate; + employee.CompanyId = parentEntity.Id; + + employee.Company = parentEntity; + employee.Account = account; + + + var requestEmployeeDocuments = request.Documents.Select( + d => new EmployeeDocument() + { + DocumentType = d.DocumentType, + Information = d.Information + }); + + var commonEmployeeDocuments = employee.Documents.IntersectBy( + requestEmployeeDocuments.Select( + ed => (ed.DocumentType, ed.Information)), + ed => (ed.DocumentType, ed.Information)); + + var newEmployeeDocuments = requestEmployeeDocuments.ExceptBy( + employee.Documents.Select(ed => (ed.DocumentType, ed.Information)), + ed => (ed.DocumentType, ed.Information)); + + var combinedEmployeeDocuments = commonEmployeeDocuments.UnionBy( + newEmployeeDocuments, ed => (ed.DocumentType, ed.Information)); + + employee.Documents = combinedEmployeeDocuments.ToList(); + + + employee = await _unitOfWork.EmployeeRepository.UpdateOneAsync( + employee, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(employee); + } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs new file mode 100644 index 0000000..ac4fdb0 --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs @@ -0,0 +1,87 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; + +public class UpdateEmployeeCommandValidator : AbstractValidator +{ + public UpdateEmployeeCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(e => e.FirstName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.LastName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.Patronymic) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(e => e.Sex) + .Must((e, s) => Sex.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Sex.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(e => e.BirthDate) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))); + + RuleForEach(e => e.Documents).ChildRules(d => + { + d.RuleFor(d => d.DocumentType) + .Must(dt => DocumentType.Enumerations.ContainsValue(dt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + DocumentType.Enumerations.Values.Select(e => e.Name)))); + + d.RuleFor(d => d.Information) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(256) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 256)); + }); + } +} diff --git a/src/Application/Employees/EmployeeAccountDto.cs b/src/Application/Employees/EmployeeAccountDto.cs new file mode 100644 index 0000000..ced3c91 --- /dev/null +++ b/src/Application/Employees/EmployeeAccountDto.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Employees; + +public sealed class EmployeeAccountDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Username { get; set; } + + public string Email { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Employees/EmployeeDocumentDto.cs b/src/Application/Employees/EmployeeDocumentDto.cs new file mode 100644 index 0000000..8865f14 --- /dev/null +++ b/src/Application/Employees/EmployeeDocumentDto.cs @@ -0,0 +1,19 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Employees; + +public sealed class EmployeeDocumentDto : IMapFrom +{ + public string DocumentType { get; set; } + + public string Information { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.DocumentType, + opt => opt.MapFrom(s => s.DocumentType.Name)); + } +} diff --git a/src/Application/Employees/EmployeeDto.cs b/src/Application/Employees/EmployeeDto.cs new file mode 100644 index 0000000..ff20575 --- /dev/null +++ b/src/Application/Employees/EmployeeDto.cs @@ -0,0 +1,40 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Employees; + +public sealed class EmployeeDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public string Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public Guid CompanyUuid { get; set; } + + public ICollection Documents { get; set; } + + public EmployeeAccountDto Account { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Sex, + opt => opt.MapFrom(s => s.Sex.Name)) + .ForMember( + d => d.CompanyUuid, + opt => opt.MapFrom(s => s.Company.Guid)); + } +} diff --git a/src/Application/Employees/Models/EmployeeDocumentModel.cs b/src/Application/Employees/Models/EmployeeDocumentModel.cs new file mode 100644 index 0000000..2992dfa --- /dev/null +++ b/src/Application/Employees/Models/EmployeeDocumentModel.cs @@ -0,0 +1,10 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Employees.Models; + +public sealed class EmployeeDocumentModel +{ + public DocumentType DocumentType { get; set; } + + public string Information { get; set; } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs new file mode 100644 index 0000000..67a4024 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; + +public record GetEmployeeQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs new file mode 100644 index 0000000..00535f8 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; + +public class GetEmployeeQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetEmployeeQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetEmployeeQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var employee = _unitOfWork.EmployeeRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = employee?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs new file mode 100644 index 0000000..75eba22 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs @@ -0,0 +1,53 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; + +public class GetEmployeeQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetEmployeeQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetEmployeeQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.EmployeeRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Documents, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + + // Hydrate employee + + var company = await _unitOfWork.CompanyRepository.GetOneAsync( + e => e.Id == entity.CompanyId, cancellationToken); + + entity.Company = company; + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Id == entity.AccountId, cancellationToken); + + entity.Account = account; + + + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs new file mode 100644 index 0000000..da6a0ef --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; + +public class GetEmployeeQueryValidator : AbstractValidator +{ + public GetEmployeeQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs new file mode 100644 index 0000000..209fcc2 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs @@ -0,0 +1,24 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; + +public record GetEmployeesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Sex? Sex { get; set; } + + public DateOnly? BirthDateGreaterThanOrEqualTo { get; set; } + + public DateOnly? BirthDateLessThanOrEqualTo { get; set; } + + public Guid? CompanyGuid { get; set; } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs new file mode 100644 index 0000000..12b2cae --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; + +public class GetEmployeesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetEmployeesPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetEmployeesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs new file mode 100644 index 0000000..20a2e66 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs @@ -0,0 +1,89 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; + +public class GetEmployeesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetEmployeesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetEmployeesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.EmployeeRepository.GetPageAsync( + e => + (e.FirstName.ToLower().Contains(request.Search.ToLower()) || + e.LastName.ToLower().Contains(request.Search.ToLower()) || + e.Patronymic.ToLower().Contains(request.Search.ToLower()) || + e.Documents + .Select(d => d.Information.ToLower()) + .Contains(request.Search.ToLower())) && + (request.CompanyGuid != null + ? e.Company.Guid == request.CompanyGuid + : true) && + (request.Sex != null + ? e.Sex == request.Sex + : true) && + (request.BirthDateLessThanOrEqualTo != null + ? e.BirthDate <= request.BirthDateLessThanOrEqualTo + : true) && + (request.BirthDateGreaterThanOrEqualTo != null + ? e.BirthDate >= request.BirthDateGreaterThanOrEqualTo + : true), + e => e.Documents, + request.PageNumber, request.PageSize, + cancellationToken); + + + // Hydrate employees + + var companies = await _unitOfWork.CompanyRepository.GetPageAsync( + e => paginatedList.Items.Select(e => e.CompanyId).Contains(e.Id), + 1, paginatedList.Items.Count, cancellationToken); + + foreach (var employee in paginatedList.Items) + { + employee.Company = + companies.Items.First(c => c.Id == employee.CompanyId); + } + + var accountIds = paginatedList.Items.Select(e => e.AccountId); + var accounts = await _unitOfWork.AccountRepository.GetPageAsync( + e => accountIds.Contains(e.Id), + 1, paginatedList.Items.Count, cancellationToken); + + foreach (var employee in paginatedList.Items) + { + employee.Account = + accounts.Items.First(a => a.Id == employee.AccountId); + } + + + 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/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs new file mode 100644 index 0000000..5b3e464 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; + +public class GetEmployeesPageQueryValidator : AbstractValidator +{ + public GetEmployeesPageQueryValidator( + 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/Employees/ViewModels/AddEmployeeViewModel.cs b/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs new file mode 100644 index 0000000..c058d22 --- /dev/null +++ b/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs @@ -0,0 +1,26 @@ +namespace cuqmbr.TravelGuide.Application.Employees.ViewModels; + +public sealed class AddEmployeeViewModel +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public string Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public Guid CompanyUuid { get; set; } + + public ICollection Documents { get; set; } + + + public string Username { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } +} diff --git a/src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs b/src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs new file mode 100644 index 0000000..bb1155e --- /dev/null +++ b/src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Employees.ViewModels; + +public sealed class EmployeeDocumentViewModel +{ + public string DocumentType { get; set; } + + public string Information { get; set; } +} diff --git a/src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs b/src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs new file mode 100644 index 0000000..8a6e59d --- /dev/null +++ b/src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Employees.ViewModels; + +public sealed class GetEmployeesPageFilterViewModel +{ + public string? Sex { get; set; } + + public DateOnly? BirthDateGreaterThanOrEqualTo { get; set; } + + public DateOnly? BirthDateLessThanOrEqualTo { get; set; } + + public Guid? CompanyUuid { get; set; } +} diff --git a/src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs b/src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs new file mode 100644 index 0000000..0d2f4ed --- /dev/null +++ b/src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs @@ -0,0 +1,19 @@ +namespace cuqmbr.TravelGuide.Application.Employees.ViewModels; + +public sealed class UpdateEmployeeViewModel +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public string Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public Guid CompanyUuid { get; set; } + + public ICollection Documents { get; set; } +} diff --git a/src/Application/Identity/Accounts/AccountDto.cs b/src/Application/Identity/Accounts/AccountDto.cs new file mode 100644 index 0000000..38dfe57 --- /dev/null +++ b/src/Application/Identity/Accounts/AccountDto.cs @@ -0,0 +1,27 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Application.Common.Mappings; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts; + +public sealed class AccountDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Username { get; set; } + + public string Email { get; set; } + + public ICollection Roles { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Roles, + opt => opt.MapFrom(s => + s.AccountRoles.Select(ar => ar.Role.Value.Name))); + } +} diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs new file mode 100644 index 0000000..eff0d6d --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommand.cs @@ -0,0 +1,16 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Identity + .Accounts.Commands.AddAccount; + +public record AddAccountCommand : IRequest +{ + public string Username { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } + + public ICollection Roles { get; set; } +} diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs new file mode 100644 index 0000000..5cf1ed3 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.AddAccount; + +public class AddAccountCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionAccountService; + + public AddAccountCommandAuthorizer(SessionUserService sessionAccountService) + { + _sessionAccountService = sessionAccountService; + } + + public override void BuildPolicy(AddAccountCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionAccountService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionAccountService.Roles + }); + } +} diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs new file mode 100644 index 0000000..149baca --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandHandler.cs @@ -0,0 +1,77 @@ +using MediatR; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Application.Common.Services; +using System.Security.Cryptography; +using System.Text; + +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.AddAccount; + +public class AddAccountCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly PasswordHasherService _passwordHasher; + + public AddAccountCommandHandler(UnitOfWork unitOfWork, + IMapper mapper, PasswordHasherService passwordHasher) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _passwordHasher = passwordHasher; + } + + public async Task Handle( + AddAccountCommand request, + CancellationToken cancellationToken) + { + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Email == request.Email, + cancellationToken); + + if (account != null) + { + throw new DuplicateEntityException(); + } + + + var roles = (await _unitOfWork.RoleRepository + .GetPageAsync( + e => request.Roles.Contains(e.Value), + 1, request.Roles.Count, cancellationToken)) + .Items; + + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = await _passwordHasher.HashAsync( + Encoding.UTF8.GetBytes(request.Password), + salt, cancellationToken); + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + account = new Account() + { + Username = request.Username, + Email = request.Email, + PasswordHash = hashBase64, + PasswordSalt = saltBase64, + AccountRoles = roles.Select(r => new AccountRole() + { + RoleId = r.Id + }) + .ToArray() + }; + + account = await _unitOfWork.AccountRepository.AddOneAsync( + account, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(account); + } +} diff --git a/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs new file mode 100644 index 0000000..85e91c5 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/AddAccount/AddAccountCommandValidator.cs @@ -0,0 +1,70 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.AddAccount; + +public class AddAccountCommandValidator : + AbstractValidator +{ + public AddAccountCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Username) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 1)) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)) + .IsUsername() + .WithMessage(localizer["FluentValidation.IsUsername"]); + + RuleFor(v => v.Email) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); + + RuleFor(v => v.Password) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MinimumLength(8) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 8)) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Roles ?? new IdentityRole[0]) + .IsUnique(r => r) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(v => v.Roles) + .Must(r => IdentityRole.Enumerations.ContainsValue(r)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + IdentityRole.Enumerations.Values.Select(e => e.Name)))); + } +} diff --git a/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommand.cs b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommand.cs new file mode 100644 index 0000000..0d673af --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommand.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Identity + .Accounts.Commands.DeleteAccount; + +public record DeleteAccountCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs new file mode 100644 index 0000000..8898f22 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity + .Accounts.Commands.DeleteAccount; + +public class DeleteAccountCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteAccountCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteAccountCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandHandler.cs b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandHandler.cs new file mode 100644 index 0000000..26dec51 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.DeleteAccount; + +public class DeleteAccountCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteAccountCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteAccountCommand request, + CancellationToken cancellationToken) + { + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (account == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.AccountRepository.DeleteOneAsync( + account, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandValidator.cs b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandValidator.cs new file mode 100644 index 0000000..1baf7f2 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/DeleteAccount/DeleteAccountCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.DeleteAccount; + +public class DeleteAccountCommandValidator : AbstractValidator +{ + public DeleteAccountCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommand.cs b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommand.cs new file mode 100644 index 0000000..b5ed1e2 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommand.cs @@ -0,0 +1,18 @@ +using MediatR; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.UpdateAccount; + +public record UpdateAccountCommand : IRequest +{ + public Guid Guid { get; set; } + + public string? Username { get; set; } + + public string? Email { get; set; } + + public string? Password { get; set; } + + public ICollection? Roles { get; set; } +} diff --git a/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs new file mode 100644 index 0000000..023bceb --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.UpdateAccount; + +public class UpdateAccountCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateAccountCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateAccountCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandHandler.cs b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandHandler.cs new file mode 100644 index 0000000..8a64931 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandHandler.cs @@ -0,0 +1,109 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.UpdateAccount; + +public class UpdateAccountCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly PasswordHasherService _passwordHasher; + + public UpdateAccountCommandHandler(UnitOfWork unitOfWork, + IMapper mapper, PasswordHasherService passwordHasher) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _passwordHasher = passwordHasher; + } + + public async Task Handle( + UpdateAccountCommand request, + CancellationToken cancellationToken) + { + var account = await _unitOfWork.AccountRepository + .GetOneAsync(e => e.Guid == request.Guid, + e => e.AccountRoles, cancellationToken); + + if (account == null) + { + throw new NotFoundException(); + } + + + account.Username = request.Username ?? account.Username; + account.Email = request.Email ?? account.Email; + + if (request.Password != null) + { + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = await _passwordHasher.HashAsync( + Encoding.UTF8.GetBytes(request.Password), + salt, cancellationToken); + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + account.PasswordHash = hashBase64; + account.PasswordSalt = saltBase64; + } + + + if (request.Roles != null) + { + var requestRoleIds = (await _unitOfWork.RoleRepository + .GetPageAsync( + r => request.Roles.Contains(r.Value), + 1, request.Roles.Count, cancellationToken)) + .Items + .Select(r => r.Id); + + var accountRoles = account.AccountRoles; + var accountRoleIds = accountRoles.Select(ar => ar.RoleId); + + var commonRoleIds = requestRoleIds.Intersect(accountRoleIds); + + var newRoleIds = requestRoleIds.Except(accountRoleIds); + + var combinedRoleIds = commonRoleIds.Union(newRoleIds); + + account.AccountRoles = combinedRoleIds.Select(rId => + new AccountRole() + { + Id = accountRoles.FirstOrDefault(ar => + ar.RoleId == rId)?.Id ?? default, + RoleId = rId + }) + .ToList(); + } + else + { + var accountRoleIds = account.AccountRoles.Select(ar => ar.RoleId); + var accountRoles = (await _unitOfWork.AccountRoleRepository + .GetPageAsync( + ar => accountRoleIds.Contains(ar.RoleId), + ar => ar.Role, + 1, accountRoleIds.Count(), cancellationToken)) + .Items; + + account.AccountRoles = accountRoles.ToList(); + } + + + account = await _unitOfWork.AccountRepository.UpdateOneAsync( + account, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(account); + } +} diff --git a/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandValidator.cs b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandValidator.cs new file mode 100644 index 0000000..ab00393 --- /dev/null +++ b/src/Application/Identity/Accounts/Commands/UpdateAccount/UpdateAccountCommandValidator.cs @@ -0,0 +1,68 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application + .Identity.Accounts.Commands.UpdateAccount; + +public class UpdateAccountCommandValidator : + AbstractValidator +{ + public UpdateAccountCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Username) + .MinimumLength(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 1)) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)) + .IsUsername() + .WithMessage(localizer["FluentValidation.IsUsername"]); + + RuleFor(v => v.Email) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); + + RuleFor(v => v.Password) + .MinimumLength(8) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MinimumLength"], + 8)) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Roles ?? new IdentityRole[0]) + .IsUnique(r => r) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(v => v.Roles) + .Must(r => IdentityRole.Enumerations.ContainsValue(r)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + IdentityRole.Enumerations.Values.Select(e => e.Name)))); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQuery.cs b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQuery.cs new file mode 100644 index 0000000..0031fcc --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount; + +public record GetAccountQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs new file mode 100644 index 0000000..99ec778 --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount; + +public class GetAccountQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAccountQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAccountQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryHandler.cs b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryHandler.cs new file mode 100644 index 0000000..afbf47a --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount; + +public class GetAccountQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAccountQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetAccountQuery request, + CancellationToken cancellationToken) + { + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.AccountRoles, + cancellationToken); + + if (account == null) + { + throw new NotFoundException(); + } + + + var accountRoleIds = account.AccountRoles.Select(ar => ar.RoleId); + var accountRoles = (await _unitOfWork.AccountRoleRepository + .GetPageAsync( + ar => accountRoleIds.Contains(ar.RoleId), + ar => ar.Role, + 1, accountRoleIds.Count(), cancellationToken)) + .Items; + + account.AccountRoles = accountRoles.ToList(); + + + _unitOfWork.Dispose(); + + return _mapper.Map(account); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryValidator.cs b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryValidator.cs new file mode 100644 index 0000000..d0b26ab --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccount/GetAccountQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount; + +public class GetAccountQueryValidator : AbstractValidator +{ + public GetAccountQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQuery.cs b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQuery.cs new file mode 100644 index 0000000..252f9c5 --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQuery.cs @@ -0,0 +1,18 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage; + +public record GetAccountsPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public ICollection? Roles { get; set; } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs new file mode 100644 index 0000000..53a7a30 --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage; + +public class GetAccountsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetAccountsPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetAccountsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryHandler.cs b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryHandler.cs new file mode 100644 index 0000000..dc5f975 --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryHandler.cs @@ -0,0 +1,81 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage; + +public class GetAccountsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetAccountsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetAccountsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.AccountRepository.GetPageAsync( + a => + (a.Username.ToLower().Contains(request.Search.ToLower()) || + a.Email.ToLower().Contains(request.Search.ToLower())) && + (request.Roles != null + ? request.Roles.All(r => a.AccountRoles.Any(ar => ar.Role.Value == r)) + : true), + a => a.AccountRoles, + request.PageNumber, request.PageSize, cancellationToken); + + + var accounts = paginatedList.Items; + + var accountsRoleIds = accounts + .SelectMany(a => a.AccountRoles) + .Select(ar => ar.RoleId) + .Distinct(); + + var roles = (await _unitOfWork.RoleRepository + .GetPageAsync( + r => accountsRoleIds.Contains(r.Id), + 1, accountsRoleIds.Count(), cancellationToken)) + .Items; + + foreach (var account in accounts) + { + account.AccountRoles = account.AccountRoles.Select(ar => + new AccountRole() + { + RoleId = ar.RoleId, + Role = roles.Single(r => r.Id == ar.RoleId), + AccountId = account.Id, + Account = account + }) + .ToArray(); + } + + + var mappedItems = _mapper + .ProjectTo(accounts.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + + throw new NotImplementedException(); + } +} diff --git a/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryValidator.cs b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryValidator.cs new file mode 100644 index 0000000..d42a9ac --- /dev/null +++ b/src/Application/Identity/Accounts/Queries/GetAccountsPage/GetAccountsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage; + +public class GetAccountsPageQueryValidator : AbstractValidator +{ + public GetAccountsPageQueryValidator( + 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/Identity/Accounts/ViewModels/AddAccountViewModel.cs b/src/Application/Identity/Accounts/ViewModels/AddAccountViewModel.cs new file mode 100644 index 0000000..5ff18bc --- /dev/null +++ b/src/Application/Identity/Accounts/ViewModels/AddAccountViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; + +public sealed class AddAccountViewModel +{ + public string Username { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } + + public ICollection Roles { get; set; } +} diff --git a/src/Application/Identity/Accounts/ViewModels/GetAccountsPageFilterViewModel.cs b/src/Application/Identity/Accounts/ViewModels/GetAccountsPageFilterViewModel.cs new file mode 100644 index 0000000..d95a4a6 --- /dev/null +++ b/src/Application/Identity/Accounts/ViewModels/GetAccountsPageFilterViewModel.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; + +public sealed class GetAccountsPageFilterViewModel +{ + public ICollection? Roles { get; set; } +} diff --git a/src/Application/Identity/Accounts/ViewModels/UpdateAccountViewModel.cs b/src/Application/Identity/Accounts/ViewModels/UpdateAccountViewModel.cs new file mode 100644 index 0000000..4ae0732 --- /dev/null +++ b/src/Application/Identity/Accounts/ViewModels/UpdateAccountViewModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; + +public sealed class UpdateAccountViewModel +{ + public Guid Uuid { get; set; } + + public string? Username { get; set; } + + public string? Email { get; set; } + + public string? Password { get; set; } + + public ICollection? Roles { get; set; } +} diff --git a/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQuery.cs b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQuery.cs new file mode 100644 index 0000000..47936af --- /dev/null +++ b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQuery.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage; + +public record GetRolesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; +} diff --git a/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs new file mode 100644 index 0000000..c21d365 --- /dev/null +++ b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage; + +public class GetRolesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetRolesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetRolesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryHandler.cs b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryHandler.cs new file mode 100644 index 0000000..718ccd0 --- /dev/null +++ b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryHandler.cs @@ -0,0 +1,25 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage; + +public class GetRolesPageQueryHandler : + IRequestHandler> +{ + public async Task> Handle( + GetRolesPageQuery request, + CancellationToken cancellationToken) + { + var roleStrings = IdentityRole.Enumerations.Select(e => e.Value.Name); + + var roleCount = roleStrings.Count(); + + var filteredRoles = roleStrings + .Where(r => r.Contains(request.Search)) + .ToArray(); + + return new PaginatedList( + filteredRoles, roleCount, request.PageNumber, request.PageSize); + } +} diff --git a/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryValidator.cs b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryValidator.cs new file mode 100644 index 0000000..cbcd22b --- /dev/null +++ b/src/Application/Identity/Roles/Queries/GetRolesPage/GetRolesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage; + +public class GetRolesPageQueryValidator : AbstractValidator +{ + public GetRolesPageQueryValidator( + 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/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs new file mode 100644 index 0000000..66f6ce8 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs @@ -0,0 +1,27 @@ +using cuqmbr.TravelGuide.Application.Payments.LiqPay.TicketGroups.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public record GetPaymentLinkCommand : IRequest +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public Sex PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public string? PassangerEmail { get; set; } + + + public ICollection Tickets { get; set; } + + + public string ResultPath { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs new file mode 100644 index 0000000..b1c8fb0 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public class GetPaymentLinkCommandAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy(GetPaymentLinkCommand request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs new file mode 100644 index 0000000..123c931 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs @@ -0,0 +1,513 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation.Results; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public class GetPaymentLinkCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + private readonly CurrencyConverterService _currencyConverterService; + + private readonly LiqPayPaymentService _liqPayPaymentService; + + private readonly IStringLocalizer _localizer; + + private readonly EmailSenderService _emailSender; + + private readonly SessionTimeZoneService _sessionTimeZoneService; + private readonly SessionCultureService _sessionCultureService; + private readonly SessionUserService _sessionUserService; + + public GetPaymentLinkCommandHandler( + UnitOfWork unitOfWork, + CurrencyConverterService currencyConverterService, + LiqPayPaymentService liqPayPaymentService, + IStringLocalizer localizer, + EmailSenderService emailSender, + SessionTimeZoneService SessionTimeZoneService, + SessionCultureService sessionCultureService, + SessionUserService sessionUserService) + { + _unitOfWork = unitOfWork; + _currencyConverterService = currencyConverterService; + _liqPayPaymentService = liqPayPaymentService; + _localizer = localizer; + _emailSender = emailSender; + _sessionTimeZoneService = SessionTimeZoneService; + _sessionCultureService = sessionCultureService; + _sessionUserService = sessionUserService; + } + + public async Task Handle( + GetPaymentLinkCommand request, + CancellationToken cancellationToken) + { + // Check whether provided vehicle enrollments are present in datastore. + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + if (vehicleEnrollmentGuids.Count() > vehicleEnrollments.Count) + { + throw new NotFoundException(); + } + } + + + // Check whether provided arrival and departure address guids + // are used in provided vehicle enrollment and + // and are in the correct order. + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + routeAddressGuids.Contains(e.Guid), + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + foreach (var t in request.Tickets) + { + var departureRouteAddress = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid); + var arrivalRouteAddress = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid); + + var ve = vehicleEnrollments.First( + e => e.Guid == t.VehicleEnrollmentGuid); + + if (departureRouteAddress.RouteId != ve.RouteId || + arrivalRouteAddress.RouteId != ve.RouteId) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + + if (departureRouteAddress.Order > arrivalRouteAddress.Order) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Check availability of free places. + { + // Get all tickets for vehicle enrollments requested in ticket group. + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var unavailableTicketStatuses = new TicketStatus[] + { + TicketStatus.Reserved, + TicketStatus.Purchased + }; + + var ticketGroupTickets = (await _unitOfWork.TicketRepository + .GetPageAsync( + e => + vehicleEnrollmentGuids.Contains(e.VehicleEnrollment.Guid) && + unavailableTicketStatuses.Contains(e.TicketGroup.Status), + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Get all vehicle enrollments requested in ticket group + // together with vehicles. + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + e => e.Vehicle, + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + // Get all route addresses of vehicle enrollments + // requested in ticket group. + var routeIds = vehicleEnrollments.Select(e => e.RouteId); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => routeIds.Contains(e.RouteId), + 1, int.MaxValue, cancellationToken)) + .Items; + + // For each ticket in request. + foreach (var requestTicket in request.Tickets) + { + // Get vehicle enrollment of requested ticket. + var requestVehicleEnrollment = vehicleEnrollments.First(e => + e.Guid == requestTicket.VehicleEnrollmentGuid); + + // Get bought tickets of vehicle enrollment of requested ticket. + var tickets = ticketGroupTickets.Where(t => + t.VehicleEnrollmentId == requestVehicleEnrollment.Id); + + // Get route addresses of vehicle enrollment. + var ticketRouteAddresses = routeAddresses + .Where(e => e.RouteId == requestVehicleEnrollment.RouteId) + .OrderBy(e => e.Order); + + + // Count available capacity. + + // Get total capacity in requested vehicle. + int totalCapacity; + var vehicle = vehicleEnrollments.First(e => + e.Guid == requestTicket.VehicleEnrollmentGuid) + .Vehicle; + if (vehicle.VehicleType.Equals(VehicleType.Bus)) + { + totalCapacity = ((Bus)vehicle).Capacity; + } + else if (vehicle.VehicleType.Equals(VehicleType.Aircraft)) + { + totalCapacity = ((Aircraft)vehicle).Capacity; + } + else if (vehicle.VehicleType.Equals(VehicleType.Train)) + { + totalCapacity = ((Train)vehicle).Capacity; + } + else + { + throw new NotImplementedException(); + } + + int takenCapacity = 0; + + // For each bought ticket. + foreach (var ticket in tickets) + { + // Get departure and arrival route address + // of requested ticket. + var requestDepartureRouteAddress = ticketRouteAddresses + .Single(e => + e.Guid == requestTicket.DepartureRouteAddressGuid); + var requestArrivalRouteAddress = ticketRouteAddresses + .Single(e => + e.Guid == requestTicket.ArrivalRouteAddressGuid); + + // Get departure and arrival route address + // of bought ticket. + var departureRouteAddress = ticketRouteAddresses + .Single(e => + e.Id == ticket.DepartureRouteAddressId); + var arrivalRouteAddress = ticketRouteAddresses + .Single(e => + e.Id == ticket.ArrivalRouteAddressId); + + + // Count taken capacity in requested vehicle + // accounting for requested ticket + // departure and arrival route addresses. + // The algorithm is the same as vehicle enrollment + // time overlap check. + if ((requestDepartureRouteAddress.Order >= + departureRouteAddress.Order && + requestDepartureRouteAddress.Order < + arrivalRouteAddress.Order) || + (requestArrivalRouteAddress.Order <= + arrivalRouteAddress.Order && + requestArrivalRouteAddress.Order > + departureRouteAddress.Order) || + (requestDepartureRouteAddress.Order <= + departureRouteAddress.Order && + requestArrivalRouteAddress.Order >= + arrivalRouteAddress.Order)) + { + takenCapacity++; + } + } + + var availableCapacity = totalCapacity - takenCapacity; + + if (availableCapacity <= 0) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Calculate travel time and cost. + + var ticketsDetails = new List<(short order, DateTimeOffset departureTime, + DateTimeOffset arrivalTime, decimal cost, Currency currency)>(); + + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + routeAddressGuids.Contains(e.Guid), + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + var vehicleEnrollmentIds = vehicleEnrollments.Select(ve => ve.Id); + + var allRouteAddressDetails = (await _unitOfWork + .RouteAddressDetailRepository.GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.RouteAddress, + 1, int.MaxValue, cancellationToken)) + .Items; + + + foreach (var t in request.Tickets.OrderBy(t => t.Order)) + { + var ve = vehicleEnrollments.First( + e => e.Guid == t.VehicleEnrollmentGuid); + + var departureRouteAddressId = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid) + .Id; + var arrivalRouteAddressId = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid) + .Id; + + var verad = allRouteAddressDetails + .Where(arad => arad.VehicleEnrollmentId == ve.Id) + .OrderBy(rad => rad.RouteAddress.Order) + .TakeWhile(rad => rad.Id != arrivalRouteAddressId); + + + // TODO: This counts departure address stop time which is + // not wrong but may be not desired. + var timeToDeparture = verad + .TakeWhile(rad => rad.RouteAddressId != departureRouteAddressId) + .Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + var departureTime = ve.DepartureTime.Add(timeToDeparture); + + + var timeToArrival = verad.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + var arrivalTime = ve.DepartureTime.Add(timeToArrival); + + + var costToDeparture = verad + .TakeWhile(rad => rad.RouteAddressId != departureRouteAddressId) + .Aggregate((decimal)0, (sum, next) => + sum + next.CostToNextAddress); + + var costToArrival = verad + .Aggregate((decimal)0, (sum, next) => + sum + next.CostToNextAddress); + + var cost = costToArrival - costToDeparture; + + + ticketsDetails.Add( + (t.Order, departureTime, arrivalTime, cost, ve.Currency)); + } + } + + // Check whether there are overlaps in ticket departure/arrival times. + { + for (int i = 1; i < ticketsDetails.Count; i++) + { + var previousTd = ticketsDetails[i - 1]; + var currentTd = ticketsDetails[i]; + + if (previousTd.arrivalTime >= currentTd.departureTime) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Create entity and insert into a datastore. + + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => routeAddressGuids.Contains(e.Guid), + e => e.Address.City.Region.Country, + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + e => e.Guid == _sessionUserService.Guid, cancellationToken); + + + var travelTime = + ticketsDetails.OrderBy(td => td.order).Last().arrivalTime - + ticketsDetails.OrderBy(td => td.order).First().departureTime; + + var entity = new TicketGroup() + { + PassangerFirstName = request.PassangerFirstName, + PassangerLastName = request.PassangerLastName, + PassangerPatronymic = request.PassangerPatronymic, + PassangerSex = request.PassangerSex, + PassangerBirthDate = request.PassangerBirthDate, + PurchaseTime = DateTimeOffset.UtcNow, + Status = TicketStatus.Reserved, + TravelTime = travelTime, + PassangerEmail = request.PassangerEmail, + Tickets = request.Tickets.Select( + t => + { + var ve = vehicleEnrollments.First( + ve => ve.Guid == t.VehicleEnrollmentGuid); + + + var departureRouteAddress = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid); + var arrivalRouteAddress = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid); + + + var detail = ticketsDetails + .SingleOrDefault(td => td.order == t.Order); + + return new Ticket() + { + DepartureRouteAddressId = departureRouteAddress.Id, + DepartureRouteAddress = departureRouteAddress, + ArrivalRouteAddressId = arrivalRouteAddress.Id, + ArrivalRouteAddress = arrivalRouteAddress, + Order = t.Order, + Cost = detail.cost, + Currency = detail.currency, + VehicleEnrollmentId = ve.Id + }; + }).ToArray(), + AccountId = account?.Id + }; + + entity = await _unitOfWork.TicketGroupRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + + var amount = entity.Tickets.Sum(e => + _currencyConverterService + .ConvertAsync( + e.Cost, e.Currency, Currency.UAH, + cancellationToken).Result); + var guid = entity.Guid; + var validity = TimeSpan.FromMinutes(10); + var resultPath = request.ResultPath; + var callbackPath = "/payments/liqPay/ticket/callback"; + + var paymentLink = _liqPayPaymentService + .GetPaymentLink( + amount, Currency.UAH, guid.ToString(), validity, + _localizer["PaymentProcessing.Ticket.PaymentDescription"], + resultPath, callbackPath); + + if (request.PassangerEmail != null) + { + var validUntil = DateTimeOffset.UtcNow + .Add(validity) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset); + + var subject = + _localizer["PaymentProcessing.Ticket" + + ".Email.PaymentCreated.Subject"]; + + var body = String.Format( + _sessionCultureService.Culture, + _localizer["PaymentProcessing.Ticket" + + ".Email.PaymentCreated.Body"], + Currency.UAH.Round(amount), Currency.UAH.Name, + validUntil, paymentLink); + + await _emailSender.SendAsync( + new[] { request.PassangerEmail }, subject, + body, cancellationToken); + } + + return new PaymentLinkDto() { PaymentLink = paymentLink }; + } + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs new file mode 100644 index 0000000..0662c2f --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs @@ -0,0 +1,110 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public class GetPaymentLinkCommandValidator : + AbstractValidator +{ + public GetPaymentLinkCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(tg => tg.PassangerFirstName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerLastName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerPatronymic) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerSex) + .Must((tg, s) => Sex.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Sex.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(tg => tg.PassangerBirthDate) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))); + + When(tg => tg.PassangerEmail != null, () => + { + RuleFor(v => v.PassangerEmail) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .IsEmail() + .WithMessage(localizer["FluentValidation.IsEmail"]); + }); + + RuleFor(tg => tg.Tickets) + .IsUnique(t => t.VehicleEnrollmentGuid) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleFor(tg => tg.Tickets) + .IsUnique(t => t.Order) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(tg => tg.Tickets).ChildRules(t => + { + t.RuleFor(t => t.DepartureRouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + t.RuleFor(t => t.ArrivalRouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + t.RuleFor(t => t.Order) + .GreaterThanOrEqualTo(short.MinValue) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + short.MinValue)) + .LessThanOrEqualTo(short.MaxValue) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + short.MaxValue)); + + t.RuleFor(t => t.VehicleEnrollmentGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + }); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs new file mode 100644 index 0000000..60b6068 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public record ProcessCallbackCommand : IRequest +{ + public string Data { get; set; } + + public string Signature { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs new file mode 100644 index 0000000..200d9db --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public class ProcessCallbackCommandAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy(ProcessCallbackCommand request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs new file mode 100644 index 0000000..4064da6 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs @@ -0,0 +1,307 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using System.Text; +using Newtonsoft.Json; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public class ProcessCallbackCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + private readonly LiqPayPaymentService _liqPayPaymentService; + + private readonly IStringLocalizer _localizer; + + private readonly EmailSenderService _emailSender; + + public ProcessCallbackCommandHandler( + UnitOfWork unitOfWork, + LiqPayPaymentService liqPayPaymentService, + IStringLocalizer localizer, + EmailSenderService emailSender) + { + _unitOfWork = unitOfWork; + _liqPayPaymentService = liqPayPaymentService; + _localizer = localizer; + _emailSender = emailSender; + } + + public async Task Handle( + ProcessCallbackCommand request, + CancellationToken cancellationToken) + { + // Validate signature. + + var isSignatureValid = _liqPayPaymentService + .IsValidSignature(request.Data, request.Signature); + + if (!isSignatureValid) + { + throw new ForbiddenException(); + } + + + // Parse request data. + + var dataBytes = Convert.FromBase64String(request.Data); + var dataJson = Encoding.UTF8.GetString(dataBytes); + + var data = JsonConvert.DeserializeObject(dataJson); + + string status = data.status; + + + var ticketGroupGuid = Guid.Parse((string)data.order_id); + var ticketGroup = await _unitOfWork.TicketGroupRepository + .GetOneAsync(e => e.Guid == ticketGroupGuid, + e => e.Tickets, cancellationToken); + + if (ticketGroup == null || + ticketGroup.Status == TicketStatus.Purchased) + { + throw new ForbiddenException(); + } + + + // Process callback status + + if (status.Equals("error") || status.Equals("failure")) + { + await _unitOfWork.TicketGroupRepository + .DeleteOneAsync(ticketGroup, cancellationToken); + } + else if (status.Equals("success")) + { + // Update ticket status + + ticketGroup.Status = TicketStatus.Purchased; + await _unitOfWork.TicketGroupRepository + .UpdateOneAsync(ticketGroup, cancellationToken); + + + // Hydrate ticket group + + var vehicleEnrollmentIds = + ticketGroup.Tickets.Select(t => t.VehicleEnrollmentId); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + ve => vehicleEnrollmentIds.Contains(ve.Id), + ve => ve.Route.RouteAddresses, + 1, vehicleEnrollmentIds.Count(), cancellationToken)) + .Items; + + var routeAddressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + rad => routeAddressIds.Contains(rad.RouteAddressId), + 1, routeAddressIds.Count(), cancellationToken)) + .Items; + + var addressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.AddressId); + var addresses = (await _unitOfWork.AddressRepository + .GetPageAsync( + a => addressIds.Contains(a.Id), + a => a.City.Region.Country, + 1, addressIds.Count(), cancellationToken)) + .Items; + + var vehicleIds = vehicleEnrollments + .Select(ve => ve.VehicleId); + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + v => vehicleIds.Contains(v.Id), + v => v.Company, + 1, vehicleIds.Count(), cancellationToken)) + .Items; + + foreach (var ve in vehicleEnrollments) + { + ve.Vehicle = vehicles.Single(v => v.Id == ve.VehicleId); + + foreach (var ra in ve.Route.RouteAddresses) + { + ra.Address = addresses.Single(a => a.Id == ra.AddressId); + ra.Details = routeAddressDetails + .Where(rad => rad.RouteAddressId == ra.Id) + .ToArray(); + } + } + + foreach (var t in ticketGroup.Tickets) + { + t.VehicleEnrollment = vehicleEnrollments + .Single(ve => ve.Id == t.VehicleEnrollmentId); + } + + + // Send email + + if (ticketGroup.PassangerEmail != null) + { + var subject = + _localizer["PaymentProcessing.Ticket" + + ".Email.PaymentCompleted.Subject"]; + + var ticketDetails = GetTicketDetails(ticketGroup); + + var body = String.Format( + _localizer["PaymentProcessing.Ticket" + + ".Email.PaymentCompleted.Body"], + ticketDetails); + + await _emailSender.SendAsync( + new[] { ticketGroup.PassangerEmail }, subject, + body, cancellationToken); + } + } + + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } + + private string GetTicketDetails(TicketGroup ticketGroup) + { + var sb = new StringBuilder(); + + sb.AppendLine("General:"); + sb.AppendLine(); + sb.AppendLine($"Ticket uuid: {ticketGroup.Guid}"); + sb.AppendLine($"Purchase Time: {ticketGroup.PurchaseTime}"); + sb.AppendLine(); + + var departureRouteAddressId = + ticketGroup.Tickets.First().DepartureRouteAddressId; + var arrivalRouteAddressId = + ticketGroup.Tickets.Last().ArrivalRouteAddressId; + + var departureTime = + ticketGroup.Tickets.First() + .VehicleEnrollment.GetDepartureTime(departureRouteAddressId); + var arrivalTime = + ticketGroup.Tickets.Last() + .VehicleEnrollment.GetArrivalTime(arrivalRouteAddressId); + + var departureAddress = + ticketGroup.Tickets.First() + .VehicleEnrollment.Route.RouteAddresses + .Single(ra => ra.Id == departureRouteAddressId) + .Address; + var arrivalAddress = + ticketGroup.Tickets.Last() + .VehicleEnrollment.Route.RouteAddresses + .Single(ra => ra.Id == arrivalRouteAddressId) + .Address; + + var departureAddressName = + $"{departureAddress.City.Region.Country.Name}, " + + $"{departureAddress.City.Region.Name}, " + + $"{departureAddress.City.Name}, " + + $"{departureAddress.Name}"; + var arrivalAddressName = + $"{arrivalAddress.City.Region.Country.Name}, " + + $"{arrivalAddress.City.Region.Name}, " + + $"{arrivalAddress.City.Name}, " + + $"{arrivalAddress.Name}"; + + sb.AppendLine($"Departure: {departureAddressName} at {departureTime}."); + sb.AppendLine($"Arrival: {arrivalAddressName} at {arrivalTime}."); + sb.AppendLine(); + sb.AppendLine(); + + sb.AppendLine($"Passanger details:"); + sb.AppendLine(); + sb.AppendLine($"First Name: {ticketGroup.PassangerFirstName}."); + sb.AppendLine($"Last Name: {ticketGroup.PassangerLastName}."); + sb.AppendLine($"Patronymic: {ticketGroup.PassangerPatronymic}."); + sb.AppendLine($"Sex: {ticketGroup.PassangerSex}."); + sb.AppendLine($"Birth Date: {ticketGroup.PassangerBirthDate}."); + sb.AppendLine($"Email: {ticketGroup.PassangerEmail}."); + sb.AppendLine(); + sb.AppendLine(); + + + sb.AppendLine("Vehicle enrollments' details:"); + sb.AppendLine(); + + foreach (var t in ticketGroup.Tickets) + { + departureRouteAddressId = t.DepartureRouteAddressId; + arrivalRouteAddressId = t.ArrivalRouteAddressId; + + departureTime = + t.VehicleEnrollment.GetDepartureTime(departureRouteAddressId); + arrivalTime = + t.VehicleEnrollment.GetArrivalTime(arrivalRouteAddressId); + + departureAddress = + t.VehicleEnrollment.Route.RouteAddresses + .Single(ra => ra.Id == departureRouteAddressId) + .Address; + arrivalAddress = + t.VehicleEnrollment.Route.RouteAddresses + .Single(ra => ra.Id == arrivalRouteAddressId) + .Address; + + departureAddressName = + $"{departureAddress.City.Region.Country.Name}, " + + $"{departureAddress.City.Region.Name}, " + + $"{departureAddress.City.Name}, " + + $"{departureAddress.Name}"; + arrivalAddressName = + $"{arrivalAddress.City.Region.Country.Name}, " + + $"{arrivalAddress.City.Region.Name}, " + + $"{arrivalAddress.City.Name}, " + + $"{arrivalAddress.Name}"; + + var vehicle = t.VehicleEnrollment.Vehicle; + var company = vehicle.Company; + + sb.AppendLine($"Departure: {departureAddressName} at {departureTime}."); + sb.AppendLine($"Arrival: {arrivalAddressName} at {arrivalTime}."); + + if (vehicle is Bus) + { + sb.AppendLine($"Vehicle: Bus, {((Bus)vehicle).Model}, " + + $"{((Bus)vehicle).Number}."); + } + else if (vehicle is Aircraft) + { + sb.AppendLine($"Vehicle: Aircraft, {((Aircraft)vehicle).Model}, " + + $"{((Aircraft)vehicle).Number}."); + } + else if (vehicle is Train) + { + sb.AppendLine($"Vehicle: Train, {((Train)vehicle).Model}, " + + $"{((Train)vehicle).Number}."); + } + else + { + throw new NotImplementedException(); + } + + sb.AppendLine($"Company: {company.Name}, ({company.ContactEmail}, " + + $"{company.ContactPhoneNumber})."); + + var cost = t.Currency.Round( + t.VehicleEnrollment + .GetCost(departureRouteAddressId,arrivalRouteAddressId)); + sb.AppendLine($"Cost: {cost} {t.Currency.Name}"); + sb.AppendLine(); + } + + return sb.ToString(); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs new file mode 100644 index 0000000..ccec5f5 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public class ProcessCallbackCommandValidator : + AbstractValidator +{ + public ProcessCallbackCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Data) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Signature) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs b/src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs new file mode 100644 index 0000000..4bc5a3b --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Models; + +public sealed class TicketGroupPaymentTicketModel +{ + public Guid DepartureRouteAddressGuid { get; set; } + + public Guid ArrivalRouteAddressGuid { get; set; } + + public short Order { get; set; } + + + public Guid VehicleEnrollmentGuid { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs new file mode 100644 index 0000000..ca696c0 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs @@ -0,0 +1,9 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; + +public sealed class CallbackViewModel +{ + public string Data { get; set; } + + public string Signature { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs new file mode 100644 index 0000000..0e4f447 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs @@ -0,0 +1,23 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; + +public sealed class TicketGroupPaymentViewModel +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public string PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public string? PassangerEmail { get; set; } + + + public ICollection Tickets { get; set; } + + + public string ResultPath { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs new file mode 100644 index 0000000..f30684f --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; + +public sealed class TicketPaymentViewModel +{ + public Guid DepartureRouteAddressUuid { get; set; } + + public Guid ArrivalRouteAddressUuid { get; set; } + + public short Order { get; set; } + + + public Guid VehicleEnrollmentUuid { get; set; } +} diff --git a/src/Application/Payments/PaymentLinkDto.cs b/src/Application/Payments/PaymentLinkDto.cs new file mode 100644 index 0000000..d47bde9 --- /dev/null +++ b/src/Application/Payments/PaymentLinkDto.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Payments; + +public sealed class PaymentLinkDto +{ + public string PaymentLink { get; set; } +} diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs index cc9c48f..e3369a0 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs @@ -6,5 +6,5 @@ public record AddRegionCommand : IRequest { public string Name { get; set; } - public Guid CountryUuid { get; set; } + public Guid CountryGuid { get; set; } } diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs index 05be471..f2b0ec6 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; @@ -22,7 +22,7 @@ public class AddRegionCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs index 87357bd..a2acfcc 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Domain.Entities; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; @@ -25,7 +25,7 @@ public class AddRegionCommandHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.RegionRepository.GetOneAsync( - e => e.Name == request.Name && e.Country.Guid == request.CountryUuid, + e => e.Name == request.Name && e.Country.Guid == request.CountryGuid, cancellationToken); if (entity != null) @@ -35,12 +35,12 @@ public class AddRegionCommandHandler : } var parentEntity = await _unitOfWork.CountryRepository.GetOneAsync( - e => e.Guid == request.CountryUuid, cancellationToken); + e => e.Guid == request.CountryGuid, cancellationToken); if (parentEntity == null) { throw new NotFoundException( - $"Parent entity with Guid: {request.CountryUuid} not found."); + $"Parent entity with Guid: {request.CountryGuid} not found."); } entity = new Region() diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs index 6cbc97d..d94ecf2 100644 --- a/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; @@ -8,7 +8,7 @@ public class AddRegionCommandValidator : AbstractValidator { public AddRegionCommandValidator( IStringLocalizer localizer, - CultureService cultureService) + SessionCultureService cultureService) { RuleFor(v => v.Name) .NotEmpty() @@ -20,7 +20,7 @@ public class AddRegionCommandValidator : AbstractValidator localizer["FluentValidation.MaximumLength"], 64)); - RuleFor(v => v.CountryUuid) + RuleFor(v => v.CountryGuid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs index b6839cd..2e43800 100644 --- a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs @@ -4,5 +4,5 @@ namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; public record DeleteRegionCommand : IRequest { - public Guid Uuid { get; set; } + public Guid Guid { get; set; } } diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs index 593eb3b..3d5a52c 100644 --- a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; @@ -22,7 +22,7 @@ public class DeleteRegionCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs index b52384b..2688c20 100644 --- a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; @@ -18,7 +18,7 @@ public class DeleteRegionCommandHandler : IRequestHandler CancellationToken cancellationToken) { var entity = await _unitOfWork.RegionRepository.GetOneAsync( - e => e.Guid == request.Uuid, cancellationToken); + e => e.Guid == request.Guid, cancellationToken); if (entity == null) { diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs index 5bfaf0b..1b62b86 100644 --- a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs @@ -7,7 +7,7 @@ public class DeleteRegionCommandValidator : AbstractValidator v.Uuid) + RuleFor(v => v.Guid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs index 1ff6fb2..2bbe7c3 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs @@ -4,9 +4,9 @@ namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; public record UpdateRegionCommand : IRequest { - public Guid Uuid { get; set; } + public Guid Guid { get; set; } public string Name { get; set; } - public Guid CountryUuid { get; set; } + public Guid CountryGuid { get; set; } } diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs index d36cd2d..7f7d408 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; @@ -22,7 +22,7 @@ public class UpdateRegionCommandAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { RequiredRoles = [IdentityRole.Administrator], UserRoles = _sessionUserService.Roles diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs index f5fd163..44de767 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Exceptions; @@ -24,7 +24,7 @@ public class UpdateRegionCommandHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.RegionRepository.GetOneAsync( - e => e.Guid == request.Uuid, cancellationToken); + e => e.Guid == request.Guid, cancellationToken); if (entity == null) { @@ -32,12 +32,12 @@ public class UpdateRegionCommandHandler : } var parentEntity = await _unitOfWork.CountryRepository.GetOneAsync( - e => e.Guid == request.CountryUuid, cancellationToken); + e => e.Guid == request.CountryGuid, cancellationToken); if (parentEntity == null) { throw new NotFoundException( - $"Parent entity with Guid: {request.CountryUuid} not found."); + $"Parent entity with Guid: {request.CountryGuid} not found."); } entity.Name = request.Name; diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs index a259cd5..dc726ec 100644 --- a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; @@ -8,9 +8,9 @@ public class UpdateRegionCommandValidator : AbstractValidator v.Uuid) + RuleFor(v => v.Guid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); @@ -24,7 +24,7 @@ public class UpdateRegionCommandValidator : AbstractValidator v.CountryUuid) + RuleFor(v => v.CountryGuid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs index 32b5f5d..eb0431e 100644 --- a/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs @@ -4,5 +4,5 @@ namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; public record GetRegionQuery : IRequest { - public Guid Uuid { get; set; } + public Guid Guid { get; set; } } diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs index 55ebab2..2d56d94 100644 --- a/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; @@ -22,9 +22,10 @@ public class GetRegionQueryAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs index 1af903e..d7c71e2 100644 --- a/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Application.Common.Exceptions; using AutoMapper; @@ -24,7 +24,7 @@ public class GetRegionQueryHandler : CancellationToken cancellationToken) { var entity = await _unitOfWork.RegionRepository.GetOneAsync( - e => e.Guid == request.Uuid, e => e.Country, + e => e.Guid == request.Guid, e => e.Country, cancellationToken); _unitOfWork.Dispose(); diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs index 90e7f09..e6fc08d 100644 --- a/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs @@ -7,7 +7,7 @@ public class GetRegionQueryValidator : AbstractValidator { public GetRegionQueryValidator(IStringLocalizer localizer) { - RuleFor(v => v.Uuid) + RuleFor(v => v.Guid) .NotEmpty() .WithMessage(localizer["FluentValidation.NotEmpty"]); } diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs index d618dd6..9d8ad93 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs @@ -13,5 +13,5 @@ public record GetRegionsPageQuery : IRequest> public string Sort { get; set; } = String.Empty; - public Guid? CountryUuid { get; set; } + public Guid? CountryGuid { get; set; } } diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs index e742d58..1f9fb10 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs @@ -1,6 +1,6 @@ using cuqmbr.TravelGuide.Application.Common.Authorization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; @@ -22,9 +22,10 @@ public class GetRegionsPageQueryAuthorizer : IsAuthenticated= _sessionUserService.IsAuthenticated }); - UseRequirement(new MustBeInRolesRequirement + UseRequirement(new MustBeInAnyOfRolesRequirement { - RequiredRoles = [IdentityRole.Administrator], + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], UserRoles = _sessionUserService.Roles }); } diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs index 1e40663..a14c545 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using AutoMapper; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Extensions; @@ -28,8 +28,8 @@ public class GetRegionsPageQueryHandler : e => (e.Name.ToLower().Contains(request.Search.ToLower()) || e.Country.Name.ToLower().Contains(request.Search.ToLower())) && - (request.CountryUuid != null - ? e.Country.Guid == request.CountryUuid + (request.CountryGuid != null + ? e.Country.Guid == request.CountryGuid : true), e => e.Country, request.PageNumber, request.PageSize, diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs index e075ef5..8f0ba44 100644 --- a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using FluentValidation; using Microsoft.Extensions.Localization; @@ -8,7 +8,7 @@ public class GetRegionsPageQueryValidator : AbstractValidator v.PageNumber) .GreaterThanOrEqualTo(1) diff --git a/src/Application/Regions/RegionDto.cs b/src/Application/Regions/RegionDto.cs index c4ce975..66e8b87 100644 --- a/src/Application/Regions/RegionDto.cs +++ b/src/Application/Regions/RegionDto.cs @@ -11,6 +11,8 @@ public sealed class RegionDto : IMapFrom public Guid CountryUuid { get; set; } + public string CountryName { get; set; } + public void Mapping(MappingProfile profile) { profile.CreateMap() @@ -19,6 +21,9 @@ public sealed class RegionDto : IMapFrom opt => opt.MapFrom(s => s.Guid)) .ForMember( d => d.CountryUuid, - opt => opt.MapFrom(s => s.Country.Guid)); + opt => opt.MapFrom(s => s.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.Country.Name)); } } diff --git a/src/Application/Regions/ViewModels/AddRegionViewModel.cs b/src/Application/Regions/ViewModels/AddRegionViewModel.cs new file mode 100644 index 0000000..4b1273c --- /dev/null +++ b/src/Application/Regions/ViewModels/AddRegionViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Regions.ViewModels; + +public sealed class AddRegionViewModel +{ + public string Name { get; set; } + + public Guid CountryUuid { get; set; } +} diff --git a/src/Application/Regions/ViewModels/UpdateRegionViewModel.cs b/src/Application/Regions/ViewModels/UpdateRegionViewModel.cs new file mode 100644 index 0000000..e3025df --- /dev/null +++ b/src/Application/Regions/ViewModels/UpdateRegionViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Regions.ViewModels; + +public sealed class UpdateRegionViewModel +{ + public string Name { get; set; } + + public Guid CountryUuid { get; set; } +} diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index 481cb0f..af06306 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -1,9 +1,27 @@ { "FluentValidation": { - "MaximumLength": "Must less than {0:G} characters.", "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}." + "GreaterThanOrEqualTo": "Must be greater than or equal to {0}.", + "LessThanOrEqualTo": "Must be less than or equal to {0}.", + "MinimumLength": "Length must greater than or equal to {0} characters.", + "MaximumLength": "Length must less than or equal to {0} characters.", + "MustBeInEnum": "Must be one of the following: {0}.", + "IsUsername": "May contain lowercase latin characters (a-z), numbers (0-9), hyphens (-), underscores (_) and dots (.).", + "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.", + "IsUnique": "Elements of the collection must be unique." + }, + "Validation": { + "DistinctOrder": "Must have distinct order values.", + "SameVehicleType": "Must have the same vehicle type.", + "DateTimeOffset": { + "GreaterThanOrEqualTo": "Must be greater or equal to {0:U}" + }, + "VehicleEnrollments": { + "OverlapWithOther": "Provided vehicle enrollment overlapping in schedule with other one.", + "NegativeTime": "Specified time must be positive time span.", + "NegativeCost": "Specified cost must be positive value." + } }, "ExceptionHandling": { "ValidationException": { @@ -18,7 +36,7 @@ "Title": "Unauthenticated access prevented.", "Detail": "Request lacks valid authentication credentials for the target resource." }, - "AithenticationException": { + "AuthenticationException": { "Title": "Authentication failed.", "Detail": "Check provided credentials validity." }, @@ -42,5 +60,20 @@ "Title": "One or more internal server errors occurred.", "Detail": "Report this error to service's support team." } + }, + "PaymentProcessing": { + "Ticket": { + "PaymentDescription": "Ticket purchase.", + "Email": { + "PaymentCreated": { + "Subject": "Ticket purchase payment link.", + "Body": "You have reserved a ticket. Payment amount is {0} {1} Payment link is valid until {2}.\n\nLink: {3}" + }, + "PaymentCompleted": { + "Subject": "Ticket purchase complete.", + "Body": "Payment is succeeded.\n\n\nTicket details:\n\n{0}" + } + } + } } } diff --git a/src/Application/Resources/Localization/uk-UA.json b/src/Application/Resources/Localization/uk-UA.json index 740082d..7c98d20 100644 --- a/src/Application/Resources/Localization/uk-UA.json +++ b/src/Application/Resources/Localization/uk-UA.json @@ -1,46 +1,79 @@ { "FluentValidation": { - "MaximumLength": "Повинно бути менше ніж {0:G} символів.", "NotEmpty": "Не повинно бути порожнім.", - "GreaterThanOrEqualTo": "Повинно бути більше або дорівнювати {0:G}.", - "LessThanOrEqualTo": "Повинно бути менше або дорівнювати {0:G}." + "GreaterThanOrEqualTo": "Повинно бути більше або дорівнювати {0}.", + "LessThanOrEqualTo": "Повинно бути менше або дорівнювати {0}.", + "MinimumLength": "Довжина повинна бути більшою або дорівнювати {0} символам.", + "MaximumLength": "Довжина повинна бути меншою або дорівнювати {0} символам.", + "MustBeInEnum": "Повинно бути одним із наступних: {0}.", + "IsUsername": "Може містити латинські літери у нижньому регістрі (a-z), цифри (0-9), дефіси (-), підкреслення (_) та крапки (.).", + "IsEmail": "Повинно бути дійсною електронною адресою відповідно до RFC 5321.", + "IsPhoneNumber": "Повинно бути дійсним номером телефону відповідно до ITU-T E.164 без роздільних символів.", + "IsUnique": "Елементи колекції повинні бути унікальними." + }, + "Validation": { + "DistinctOrder": "Повинно мати унікальні значення порядку.", + "SameVehicleType": "Повинно мати однаковий тип транспортного засобу.", + "DateTimeOffset": { + "GreaterThanOrEqualTo": "Повинно бути більше або дорівнювати {0:U}" + }, + "VehicleEnrollments": { + "OverlapWithOther": "Наданий запис транспортного засобу перетинається за розкладом з іншим.", + "NegativeTime": "Вказаний час повинен бути додатнім проміжком часу.", + "NegativeCost": "Вказана вартість повинна бути додатнім значенням." + } }, "ExceptionHandling": { "ValidationException": { - "Title": "Виникла одна або декілька помилок валідації.", - "Detail": "Надані дані не задовольняють вимогам валідації." + "Title": "Виникла одна або кілька помилок валідації.", + "Detail": "Надані дані не відповідають вимогам валідації." }, "RegistrationException": { "Title": "Реєстрація не вдалася.", "Detail": "Електронна пошта вже зареєстрована." }, "UnAuthorizedException": { - "Title": "Доступ без автентифікації заблоковано.", + "Title": "Неавтентифікований доступ заблоковано.", "Detail": "Запит не містить дійсних автентифікаційних даних для цільового ресурсу." }, - "AithenticationException": { + "AuthenticationException": { "Title": "Автентифікація не вдалася.", "Detail": "Перевірте правильність наданих облікових даних." }, "LoginException": { "Title": "Вхід не вдалий.", - "Detail": "Надані електронна пошта та/або пароль недійсні." + "Detail": "Надана електронна пошта та/або пароль недійсні." }, "ForbiddenException": { - "Title": "Доступ заборонено.", + "Title": "Неавторизований доступ заблоковано.", "Detail": "У вас недостатньо прав для виконання запиту." }, "DuplicateEntityException": { - "Title": "Об’єкт вже існує.", - "Detail": "Дублювання не дозволяється." + "Title": "Об'єкт вже існує.", + "Detail": "Дублікати заборонені." }, "NotFoundException": { - "Title": "Один або декілька ресурсів не знайдено.", + "Title": "Один або кілька ресурсів не знайдено.", "Detail": "Перевірте правильність вхідних даних." }, "UnhandledException": { - "Title": "Виникла одна або декілька внутрішніх помилок сервера.", - "Detail": "Повідомте про цю помилку службі підтримки сервісу." + "Title": "Виникла одна або кілька внутрішніх помилок сервера.", + "Detail": "Повідомте про цю помилку команді підтримки сервісу." + } + }, + "PaymentProcessing": { + "Ticket": { + "PaymentDescription": "Придбання квитка.", + "Email": { + "PaymentCreated": { + "Subject": "Посилання для оплати придбання квитка.", + "Body": "Ви забронювали квиток. Сума оплати становить {0} {1}. Посилання дійсне до {2}.\n\nПосилання: {3}" + }, + "PaymentCompleted": { + "Subject": "Придбання квитка завершено.", + "Body": "Оплата пройшла успішно.\n\n\nОсь деталі вашого квитка:\n\n{0}" + } + } } } } diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommand.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommand.cs new file mode 100644 index 0000000..01180a3 --- /dev/null +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommand.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; +using cuqmbr.TravelGuide.Application.Routes.Models; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; + +public record AddRouteCommand : IRequest +{ + public string Name { get; set; } + + public VehicleType VehicleType { get; set; } + + public ICollection Addresses { get; set; } +} diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs new file mode 100644 index 0000000..5240e74 --- /dev/null +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; + +public class AddRouteCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddRouteCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddRouteCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs new file mode 100644 index 0000000..7b83fcf --- /dev/null +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandHandler.cs @@ -0,0 +1,84 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using Microsoft.Extensions.Localization; +using FluentValidation.Results; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; + +public class AddRouteCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly IStringLocalizer _localizer; + + public AddRouteCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + AddRouteCommand request, + CancellationToken cancellationToken) + { + var page = await _unitOfWork.AddressRepository.GetPageAsync( + e => request.Addresses.Select(a => a.Guid).Contains(e.Guid), + e => e.City.Region.Country, + 1, request.Addresses.Count, cancellationToken); + + var invalidVehicleTypeAddress = + page.Items.FirstOrDefault(a => a.VehicleType != request.VehicleType); + if (invalidVehicleTypeAddress != null) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Addresses), + ErrorMessage = _localizer["Validation.SameVehicleType"] + } + }); + } + + var pageContainsAllRequestedAddresses = + request.Addresses.Select(e => e.Guid) + .All(e => page.Items.Select(e => e.Guid).Contains(e)); + if (!pageContainsAllRequestedAddresses) + { + var notFoundCount = request.Addresses.Count - page.TotalCount; + throw new NotFoundException( + $"{notFoundCount} addresses was not found."); + } + + var entity = new Route() + { + Name = request.Name, + VehicleType = request.VehicleType, + RouteAddresses = request.Addresses.Select( + e => new RouteAddress() + { + Order = e.Order, + AddressId = page.Items.Single(i => i.Guid == e.Guid).Id + }) + .OrderBy(e => e.Order) + .ToArray() + }; + + entity = await _unitOfWork.RouteRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs b/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs new file mode 100644 index 0000000..7c33c13 --- /dev/null +++ b/src/Application/Routes/Commands/AddRoute/AddRouteCommandValidator.cs @@ -0,0 +1,45 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; + +public class AddRouteCommandValidator : AbstractValidator +{ + public AddRouteCommandValidator( + 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.VehicleType) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(v => v.Addresses.Count) + .GreaterThanOrEqualTo(2) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 2)); + + RuleFor(v => v.Addresses) + .Must((v, a) => a.DistinctBy(e => e.Order).Count() == a.Count()) + .WithMessage(localizer["Validation.DistinctOrder"]); + } +} diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommand.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommand.cs new file mode 100644 index 0000000..dbf98bc --- /dev/null +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; + +public record DeleteRouteCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs new file mode 100644 index 0000000..fd47ef4 --- /dev/null +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; + +public class DeleteRouteCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteRouteCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteRouteCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs new file mode 100644 index 0000000..bc14a6c --- /dev/null +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandHandler.cs @@ -0,0 +1,37 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; + +public class DeleteRouteCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteRouteCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteRouteCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.RouteRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // TODO: Check for Vehicles that using this route in Enrollments + // Delete if there are no such Vehicles + + await _unitOfWork.RouteRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandValidator.cs b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandValidator.cs new file mode 100644 index 0000000..3c655d2 --- /dev/null +++ b/src/Application/Routes/Commands/DeleteRoute/DeleteRouteCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; + +public class DeleteRouteCommandValidator : AbstractValidator +{ + public DeleteRouteCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommand.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommand.cs new file mode 100644 index 0000000..4fd3797 --- /dev/null +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommand.cs @@ -0,0 +1,16 @@ +using MediatR; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Routes.Models; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; + +public record UpdateRouteCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Name { get; set; } + + public VehicleType VehicleType { get; set; } + + public ICollection Addresses { get; set; } +} diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs new file mode 100644 index 0000000..1f714da --- /dev/null +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; + +public class UpdateRouteCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateRouteCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateRouteCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs new file mode 100644 index 0000000..f4e1cea --- /dev/null +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandHandler.cs @@ -0,0 +1,108 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using FluentValidation.Results; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; + +public class UpdateRouteCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + public IStringLocalizer _localizer { get; set; } + + public UpdateRouteCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + UpdateRouteCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.RouteRepository.GetOneAsync( + e => e.Guid == request.Guid, + e => e.RouteAddresses, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var page = await _unitOfWork.AddressRepository.GetPageAsync( + e => request.Addresses.Select(a => a.Guid).Contains(e.Guid), + e => e.City.Region.Country, + 1, request.Addresses.Count, cancellationToken); + + var invalidVehicleTypeAddress = + page.Items.FirstOrDefault(a => a.VehicleType != request.VehicleType); + if (invalidVehicleTypeAddress != null) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Addresses), + ErrorMessage = _localizer["Validation.SameVehicleType"] + } + }); + } + + var pageContainsAllRequestedAddresses = + request.Addresses.Select(e => e.Guid) + .All(e => page.Items.Select(e => e.Guid).Contains(e)); + if (!pageContainsAllRequestedAddresses) + { + var notFoundCount = request.Addresses.Count - page.TotalCount; + throw new NotFoundException( + $"{notFoundCount} addresses was not found."); + } + + + entity.Guid = request.Guid; + entity.Name = request.Name; + entity.VehicleType = request.VehicleType; + + + var requestRouteAddresses = request.Addresses.Select( + e => new RouteAddress() + { + Order = e.Order, + AddressId = page.Items.Single(i => i.Guid == e.Guid).Id + }); + + var commonRouteAddresses = entity.RouteAddresses.IntersectBy( + requestRouteAddresses.Select(ra => (ra.Order, ra.AddressId)), + ra => (ra.Order, ra.AddressId)); + + var newRouteAddresses = requestRouteAddresses.ExceptBy( + entity.RouteAddresses.Select(ra => (ra.Order, ra.AddressId)), + ra => (ra.Order, ra.AddressId)); + + var combinedRouteAddresses = commonRouteAddresses.UnionBy( + newRouteAddresses, ra => (ra.Order, ra.AddressId)); + + entity.RouteAddresses = combinedRouteAddresses + .OrderBy(e => e.Order).ToList(); + + + entity = await _unitOfWork.RouteRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs new file mode 100644 index 0000000..49314d8 --- /dev/null +++ b/src/Application/Routes/Commands/UpdateRoute/UpdateRouteCommandValidator.cs @@ -0,0 +1,49 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; + +public class UpdateRouteCommandValidator : AbstractValidator +{ + public UpdateRouteCommandValidator( + 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.VehicleType) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(v => v.Addresses.Count) + .GreaterThanOrEqualTo(2) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 2)); + + RuleFor(v => v.Addresses) + .Must((v, a) => a.DistinctBy(e => e.Order).Count() == a.Count()) + .WithMessage(localizer["Validation.DistinctOrder"]); + } +} diff --git a/src/Application/Routes/Models/RouteAddressModel.cs b/src/Application/Routes/Models/RouteAddressModel.cs new file mode 100644 index 0000000..2bcde99 --- /dev/null +++ b/src/Application/Routes/Models/RouteAddressModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Routes.Models; + +public sealed class RouteAddressModel +{ + public short Order { get; set; } + + public Guid Guid { get; set; } +} diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQuery.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQuery.cs new file mode 100644 index 0000000..4d8ad72 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; + +public record GetRouteQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs new file mode 100644 index 0000000..eba39e0 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQueryAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; + +public class GetRouteQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetRouteQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetRouteQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs new file mode 100644 index 0000000..07e1f16 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQueryHandler.cs @@ -0,0 +1,60 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; + +public class GetRouteQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetRouteQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetRouteQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.RouteRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.RouteAddresses, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // TODO: Find a way to include through lists + var addresses = await _unitOfWork.AddressRepository.GetPageAsync( + e => entity.RouteAddresses.Select(ra => ra.AddressId).Contains(e.Id), + e => e.City.Region.Country, + 1, entity.RouteAddresses.Count, cancellationToken); + + entity.RouteAddresses = entity.RouteAddresses.Select( + e => new RouteAddress() + { + Id = e.Id, + Guid = e.Guid, + Order = e.Order, + RouteId = e.RouteId, + Route = e.Route, + AddressId = e.AddressId, + Address = addresses.Items.First(a => a.Id == e.AddressId) + }) + .OrderBy(e => e.Order) + .ToArray(); + + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Routes/Queries/GetRoute/GetRouteQueryValidator.cs b/src/Application/Routes/Queries/GetRoute/GetRouteQueryValidator.cs new file mode 100644 index 0000000..30b065a --- /dev/null +++ b/src/Application/Routes/Queries/GetRoute/GetRouteQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; + +public class GetRouteQueryValidator : AbstractValidator +{ + public GetRouteQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQuery.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQuery.cs new file mode 100644 index 0000000..9680329 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQuery.cs @@ -0,0 +1,18 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; + +public record GetRoutesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public VehicleType? VehicleType { get; set; } +} diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs new file mode 100644 index 0000000..62ae9a3 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryAuthorizer.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; + +public class GetRoutesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetRoutesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetRoutesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = + [IdentityRole.Administrator, IdentityRole.CompanyOwner], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs new file mode 100644 index 0000000..38faa54 --- /dev/null +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryHandler.cs @@ -0,0 +1,74 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; + +public class GetRoutesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetRoutesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetRoutesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.RouteRepository.GetPageAsync( + e => + e.Name.ToLower().Contains(request.Search.ToLower()) && + (request.VehicleType != null + ? e.VehicleType == request.VehicleType + : true), + e => e.RouteAddresses, + request.PageNumber, request.PageSize, + cancellationToken); + + foreach (var route in paginatedList.Items) + { + // TODO: Find a way to include through lists + var addresses = await _unitOfWork.AddressRepository.GetPageAsync( + e => route.RouteAddresses.Select(ra => ra.AddressId).Contains(e.Id), + e => e.City.Region.Country, + 1, route.RouteAddresses.Count, cancellationToken); + + route.RouteAddresses = route.RouteAddresses.Select( + e => new RouteAddress() + { + Id = e.Id, + Guid = e.Guid, + Order = e.Order, + RouteId = e.RouteId, + Route = e.Route, + AddressId = e.AddressId, + Address = addresses.Items.First(a => a.Id == e.AddressId) + }) + .OrderBy(e => e.Order) + .ToArray(); + } + + 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/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs new file mode 100644 index 0000000..945bebc --- /dev/null +++ b/src/Application/Routes/Queries/GetRoutesPage/GetRoutesPageQueryValidator.cs @@ -0,0 +1,44 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; + +public class GetRoutesPageQueryValidator : AbstractValidator +{ + public GetRoutesPageQueryValidator( + 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/Routes/RouteAddressDto.cs b/src/Application/Routes/RouteAddressDto.cs new file mode 100644 index 0000000..8fcd4ec --- /dev/null +++ b/src/Application/Routes/RouteAddressDto.cs @@ -0,0 +1,75 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Routes; + +public sealed class RouteAddressDto : IMapFrom +{ + public Guid RouteAddressUuid { get; set; } + + public short Order { get; set; } + + + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public string VehicleType { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.RouteAddressUuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Address.Guid)) + .ForMember( + d => d.Name, + opt => opt.MapFrom(s => s.Address.Name)) + .ForMember( + d => d.Longitude, + opt => opt.MapFrom(s => s.Address.Longitude)) + .ForMember( + d => d.Latitude, + opt => opt.MapFrom(s => s.Address.Latitude)) + .ForMember( + d => d.VehicleType, + opt => opt.MapFrom(s => s.Address.VehicleType.Name)) + .ForMember( + d => d.CityUuid, + opt => opt.MapFrom(s => s.Address.City.Guid)) + .ForMember( + d => d.CityName, + opt => opt.MapFrom(s => s.Address.City.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.Address.City.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.Address.City.Region.Name)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.Address.City.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.Address.City.Region.Country.Name)); + } +} diff --git a/src/Application/Routes/RouteDto.cs b/src/Application/Routes/RouteDto.cs new file mode 100644 index 0000000..8b97da9 --- /dev/null +++ b/src/Application/Routes/RouteDto.cs @@ -0,0 +1,29 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Routes; + +public sealed class RouteDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public string VehicleType { get; set; } + + public ICollection Addresses { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.VehicleType, + opt => opt.MapFrom(s => s.VehicleType.Name)) + .ForMember( + d => d.Addresses, + opt => opt.MapFrom(s => s.RouteAddresses)); + } +} diff --git a/src/Application/Routes/ViewModels/AddRouteViewModel.cs b/src/Application/Routes/ViewModels/AddRouteViewModel.cs new file mode 100644 index 0000000..1370a5c --- /dev/null +++ b/src/Application/Routes/ViewModels/AddRouteViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Routes.ViewModels; + +public sealed class AddRouteViewModel +{ + public string Name { get; set; } + + public string VehicleType { get; set; } + + public ICollection Addresses { get; set; } +} diff --git a/src/Application/Routes/ViewModels/GetRoutesPageFilterViewModel.cs b/src/Application/Routes/ViewModels/GetRoutesPageFilterViewModel.cs new file mode 100644 index 0000000..d3e3a4a --- /dev/null +++ b/src/Application/Routes/ViewModels/GetRoutesPageFilterViewModel.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Routes.ViewModels; + +public sealed class GetRoutesPageFilterViewModel +{ + public string? VehicleType { get; set; } +} diff --git a/src/Application/Routes/ViewModels/RouteAddressViewModel.cs b/src/Application/Routes/ViewModels/RouteAddressViewModel.cs new file mode 100644 index 0000000..922665d --- /dev/null +++ b/src/Application/Routes/ViewModels/RouteAddressViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Routes.ViewModels; + +public sealed class RouteAddressViewModel +{ + public short Order { get; set; } + + public Guid Uuid { get; set; } +} diff --git a/src/Application/Routes/ViewModels/UpdateRouteViewModel.cs b/src/Application/Routes/ViewModels/UpdateRouteViewModel.cs new file mode 100644 index 0000000..5245b17 --- /dev/null +++ b/src/Application/Routes/ViewModels/UpdateRouteViewModel.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Routes.ViewModels; + +public sealed class UpdateRouteViewModel +{ + public string Name { get; set; } + + public string VehicleType { get; set; } + + public ICollection Addresses { get; set; } +} diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs new file mode 100644 index 0000000..a77bfee --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; +using cuqmbr.TravelGuide.Application.TicketGroups.Models; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; + +public record AddTicketGroupCommand : IRequest +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public Sex PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public DateTimeOffset PurchaseTime { get; set; } + + public TicketStatus Status { get; set; } + + + public ICollection Tickets { get; set; } +} diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs new file mode 100644 index 0000000..192bf72 --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; + +public class AddTicketGroupCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddTicketGroupCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddTicketGroupCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInAnyOfRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs new file mode 100644 index 0000000..9854582 --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs @@ -0,0 +1,482 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation.Results; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; + +// TODO: Add descriptions and proper property names to validation errors + +public class AddTicketGroupCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly IStringLocalizer _localizer; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + + public AddTicketGroupCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; + } + + public async Task Handle( + AddTicketGroupCommand request, + CancellationToken cancellationToken) + { + // Check whether provided vehicle enrollments are present in datastore. + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + if (vehicleEnrollmentGuids.Count() > vehicleEnrollments.Count) + { + throw new NotFoundException(); + } + } + + + // Check whether provided arrival and departure address guids + // are used in provided vehicle enrollment and + // and are in the correct order. + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + routeAddressGuids.Contains(e.Guid), + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + foreach (var t in request.Tickets) + { + var departureRouteAddress = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid); + var arrivalRouteAddress = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid); + + var ve = vehicleEnrollments.First( + e => e.Guid == t.VehicleEnrollmentGuid); + + if (departureRouteAddress.RouteId != ve.RouteId || + arrivalRouteAddress.RouteId != ve.RouteId) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + + if (departureRouteAddress.Order > arrivalRouteAddress.Order) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Check availability of free places. + { + // Get all tickets for vehicle enrollments requested in ticket group. + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var unavailableTicketStatuses = new TicketStatus[] + { + TicketStatus.Reserved, + TicketStatus.Purchased + }; + + var ticketGroupTickets = (await _unitOfWork.TicketRepository + .GetPageAsync( + e => + vehicleEnrollmentGuids.Contains(e.VehicleEnrollment.Guid) && + unavailableTicketStatuses.Contains(e.TicketGroup.Status), + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Get all vehicle enrollments requested in ticket group + // together with vehicles. + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + e => e.Vehicle.Company, + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + // Get all route addresses of vehicle enrollments + // requested in ticket group. + var routeIds = vehicleEnrollments.Select(e => e.RouteId); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => routeIds.Contains(e.RouteId), + 1, int.MaxValue, cancellationToken)) + .Items; + + // For each ticket in request. + foreach (var requestTicket in request.Tickets) + { + // Get vehicle enrollment of requested ticket. + var requestVehicleEnrollment = vehicleEnrollments.First(e => + e.Guid == requestTicket.VehicleEnrollmentGuid); + + // Get bought tickets of vehicle enrollment of requested ticket. + var tickets = ticketGroupTickets.Where(t => + t.VehicleEnrollmentId == requestVehicleEnrollment.Id); + + // Get route addresses of vehicle enrollment. + var ticketRouteAddresses = routeAddresses + .Where(e => e.RouteId == requestVehicleEnrollment.RouteId) + .OrderBy(e => e.Order); + + + // Count available capacity. + + // Get total capacity in requested vehicle. + int totalCapacity; + var vehicle = vehicleEnrollments.First(e => + e.Guid == requestTicket.VehicleEnrollmentGuid) + .Vehicle; + if (vehicle.VehicleType.Equals(VehicleType.Bus)) + { + totalCapacity = ((Bus)vehicle).Capacity; + } + else if (vehicle.VehicleType.Equals(VehicleType.Aircraft)) + { + totalCapacity = ((Aircraft)vehicle).Capacity; + } + else if (vehicle.VehicleType.Equals(VehicleType.Train)) + { + totalCapacity = ((Train)vehicle).Capacity; + } + else + { + throw new NotImplementedException(); + } + + int takenCapacity = 0; + + // For each bought ticket. + foreach (var ticket in tickets) + { + // Get departure and arrival route address + // of requested ticket. + var requestDepartureRouteAddress = ticketRouteAddresses + .Single(e => + e.Guid == requestTicket.DepartureRouteAddressGuid); + var requestArrivalRouteAddress = ticketRouteAddresses + .Single(e => + e.Guid == requestTicket.ArrivalRouteAddressGuid); + + // Get departure and arrival route address + // of bought ticket. + var departureRouteAddress = ticketRouteAddresses + .Single(e => + e.Id == ticket.DepartureRouteAddressId); + var arrivalRouteAddress = ticketRouteAddresses + .Single(e => + e.Id == ticket.ArrivalRouteAddressId); + + + // Count taken capacity in requested vehicle + // accounting for requested ticket + // departure and arrival route addresses. + // The algorithm is the same as vehicle enrollment + // time overlap check. + if ((requestDepartureRouteAddress.Order >= + departureRouteAddress.Order && + requestDepartureRouteAddress.Order < + arrivalRouteAddress.Order) || + (requestArrivalRouteAddress.Order <= + arrivalRouteAddress.Order && + requestArrivalRouteAddress.Order > + departureRouteAddress.Order) || + (requestDepartureRouteAddress.Order <= + departureRouteAddress.Order && + requestArrivalRouteAddress.Order >= + arrivalRouteAddress.Order)) + { + takenCapacity++; + } + } + + var availableCapacity = totalCapacity - takenCapacity; + + if (availableCapacity <= 0) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Calculate travel time and cost. + + var ticketsDetails = new List<(short order, DateTimeOffset departureTime, + DateTimeOffset arrivalTime, decimal cost, Currency currency)>(); + + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + routeAddressGuids.Contains(e.Guid), + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + var vehicleEnrollmentIds = vehicleEnrollments.Select(ve => ve.Id); + + var allRouteAddressDetails = (await _unitOfWork + .RouteAddressDetailRepository.GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.RouteAddress, + 1, int.MaxValue, cancellationToken)) + .Items; + + + foreach (var t in request.Tickets.OrderBy(t => t.Order)) + { + var ve = vehicleEnrollments.First( + e => e.Guid == t.VehicleEnrollmentGuid); + + var departureRouteAddressId = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid) + .Id; + var arrivalRouteAddressId = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid) + .Id; + + var verad = allRouteAddressDetails + .Where(arad => arad.VehicleEnrollmentId == ve.Id) + .OrderBy(rad => rad.RouteAddress.Order) + .TakeWhile(rad => rad.Id != arrivalRouteAddressId); + + + // TODO: This counts departure address stop time which is + // not wrong but may be not desired. + var timeToDeparture = verad + .TakeWhile(rad => rad.RouteAddressId != departureRouteAddressId) + .Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + var departureTime = ve.DepartureTime.Add(timeToDeparture); + + + var timeToArrival = verad.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + var arrivalTime = ve.DepartureTime.Add(timeToArrival); + + + var costToDeparture = verad + .TakeWhile(rad => rad.RouteAddressId != departureRouteAddressId) + .Aggregate((decimal)0, (sum, next) => + sum + next.CostToNextAddress); + + var costToArrival = verad + .Aggregate((decimal)0, (sum, next) => + sum + next.CostToNextAddress); + + var cost = costToArrival - costToDeparture; + + + ticketsDetails.Add( + (t.Order, departureTime, arrivalTime, cost, ve.Currency)); + } + } + + // Check whether there are overlaps in ticket departure/arrival times. + { + for (int i = 1; i < ticketsDetails.Count; i++) + { + var previousTd = ticketsDetails[i - 1]; + var currentTd = ticketsDetails[i]; + + if (previousTd.arrivalTime >= currentTd.departureTime) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + } + } + + // Create entity and insert into a datastore. + + var ticketsCostDetails = new List<(short order, + decimal cost, Currency currency)>(); + + TimeSpan travelTime; + + { + travelTime = + ticketsDetails.OrderBy(td => td.order).Last().arrivalTime - + ticketsDetails.OrderBy(td => td.order).First().departureTime; + + foreach (var td in ticketsDetails) + { + var initialCurrency = td.currency; + var convertedCurrency = + _sessionCurrencyService.Currency != Currency.Default ? + _sessionCurrencyService.Currency : + initialCurrency; + + var cost = td.cost; + + var convertedCost = await _currencyConverterService + .ConvertAsync(cost, initialCurrency, + convertedCurrency, cancellationToken); + + ticketsCostDetails.Add((td.order, convertedCost, convertedCurrency)); + } + } + + { + var vehicleEnrollmentGuids = + request.Tickets.Select(t => t.VehicleEnrollmentGuid); + + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => vehicleEnrollmentGuids.Contains(e.Guid), + 1, vehicleEnrollmentGuids.Count(), cancellationToken)) + .Items; + + var routeAddressGuids = + request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat( + request.Tickets.Select(t => t.ArrivalRouteAddressGuid)); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => routeAddressGuids.Contains(e.Guid), + e => e.Address.City.Region.Country, + 1, routeAddressGuids.Count(), cancellationToken)) + .Items; + + + var entity = new TicketGroup() + { + PassangerFirstName = request.PassangerFirstName, + PassangerLastName = request.PassangerLastName, + PassangerPatronymic = request.PassangerPatronymic, + PassangerSex = request.PassangerSex, + PassangerBirthDate = request.PassangerBirthDate, + PurchaseTime = request.PurchaseTime, + Status = request.Status, + TravelTime = travelTime, + Tickets = request.Tickets.Select( + t => + { + var ve = vehicleEnrollments.First( + ve => ve.Guid == t.VehicleEnrollmentGuid); + + + var departureRouteAddress = routeAddresses.First( + ra => ra.Guid == t.DepartureRouteAddressGuid); + var arrivalRouteAddress = routeAddresses.First( + ra => ra.Guid == t.ArrivalRouteAddressGuid); + + + var costDetail = ticketsCostDetails + .SingleOrDefault(td => td.order == t.Order); + + + return new Ticket() + { + DepartureRouteAddressId = departureRouteAddress.Id, + DepartureRouteAddress = departureRouteAddress, + ArrivalRouteAddressId = arrivalRouteAddress.Id, + ArrivalRouteAddress = arrivalRouteAddress, + Order = t.Order, + Cost = costDetail.cost, + Currency = costDetail.currency, + VehicleEnrollmentId = ve.Id + }; + }) + .ToArray() + }; + + entity = await _unitOfWork.TicketGroupRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } + } +} diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs new file mode 100644 index 0000000..03c95c6 --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs @@ -0,0 +1,116 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; + +public class AddTicketGroupCommandValidator : AbstractValidator +{ + public AddTicketGroupCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(tg => tg.PassangerFirstName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerLastName) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerPatronymic) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(tg => tg.PassangerSex) + .Must((tg, s) => Sex.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Sex.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(tg => tg.PassangerBirthDate) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))); + + RuleFor(tg => tg.PurchaseTime) + .GreaterThanOrEqualTo(DateTimeOffset.UtcNow) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateTimeOffset.UtcNow)); + + RuleFor(tg => tg.Status) + .Must((tg, s) => TicketStatus.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + TicketStatus.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(tg => tg.Tickets) + .IsUnique(t => t.VehicleEnrollmentGuid) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleFor(tg => tg.Tickets) + .IsUnique(t => t.Order) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(tg => tg.Tickets).ChildRules(t => + { + t.RuleFor(t => t.DepartureRouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + t.RuleFor(t => t.ArrivalRouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + t.RuleFor(t => t.Order) + .GreaterThanOrEqualTo(short.MinValue) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + short.MinValue)) + .LessThanOrEqualTo(short.MaxValue) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + short.MaxValue)); + + t.RuleFor(t => t.VehicleEnrollmentGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + }); + } +} diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs new file mode 100644 index 0000000..9284fd0 --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.RemoveOldReservedTicketGroups; + +public record RemoveOldReservedTicketGroupsCommand : IRequest +{ + public TimeSpan ReservedFor { get; set; } +} diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs new file mode 100644 index 0000000..b73e99f --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.RemoveOldReservedTicketGroups; + +public class RemoveOldReservedTicketGroupsCommandAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy( + RemoveOldReservedTicketGroupsCommand request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs new file mode 100644 index 0000000..e14af8a --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs @@ -0,0 +1,44 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.TicketGroups + .Commands.RemoveOldReservedTicketGroups; + +public class RemoveOldReservedTicketGroupsCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public RemoveOldReservedTicketGroupsCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + RemoveOldReservedTicketGroupsCommand request, + CancellationToken cancellationToken) + { + var statusesToRemove = new TicketStatus[] + { + TicketStatus.Reserved + }; + + var entities = (await _unitOfWork.TicketGroupRepository + .GetPageAsync( + e => + statusesToRemove.Contains(e.Status) && + DateTimeOffset.UtcNow - e.PurchaseTime > request.ReservedFor, + 1, int.MaxValue, cancellationToken)) + .Items; + + foreach (var entity in entities) + { + await _unitOfWork.TicketGroupRepository + .DeleteOneAsync(entity, cancellationToken); + } + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs new file mode 100644 index 0000000..71a17d2 --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups + .Commands.RemoveOldReservedTicketGroups; + +public class RemoveOldReservedTicketGroupsCommandValidator : + AbstractValidator +{ + public RemoveOldReservedTicketGroupsCommandValidator( + IStringLocalizer localizer) + { + RuleFor(v => v.ReservedFor) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + } +} diff --git a/src/Application/TicketGroups/Models/TicketModel.cs b/src/Application/TicketGroups/Models/TicketModel.cs new file mode 100644 index 0000000..2cd2960 --- /dev/null +++ b/src/Application/TicketGroups/Models/TicketModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.TicketGroups.Models; + +public sealed class TicketModel +{ + public Guid DepartureRouteAddressGuid { get; set; } + + public Guid ArrivalRouteAddressGuid { get; set; } + + public short Order { get; set; } + + + public Guid VehicleEnrollmentGuid { get; set; } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQuery.cs b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQuery.cs new file mode 100644 index 0000000..b06e83a --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; + +public record GetTicketGroupQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs new file mode 100644 index 0000000..b9b0782 --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; + +public class GetTicketGroupQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetTicketGroupQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetTicketGroupQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var ticketGroup = _unitOfWork.TicketGroupRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Account!, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = ticketGroup?.Account?.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs new file mode 100644 index 0000000..e8e50e0 --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryHandler.cs @@ -0,0 +1,183 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; + +public class GetTicketGroupQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly CurrencyConverterService _currencyConverter; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly SessionTimeZoneService _sessionTimeZoneService; + + private readonly object _lock = new(); + + public GetTicketGroupQueryHandler(UnitOfWork unitOfWork, + IMapper mapper, CurrencyConverterService currencyConverterService, + SessionCurrencyService sessionCurrencyService, + SessionTimeZoneService sessionTimeZoneService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _currencyConverter = currencyConverterService; + _sessionCurrencyService = sessionCurrencyService; + _sessionTimeZoneService = sessionTimeZoneService; + } + + public async Task Handle( + GetTicketGroupQuery request, + CancellationToken cancellationToken) + { + var ticketGroup = await _unitOfWork.TicketGroupRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Tickets, + cancellationToken); + + if (ticketGroup == null) + { + throw new NotFoundException(); + } + + + // Hydrate + + var vehicleEnrollmentIds = + ticketGroup.Tickets.Select(t => t.VehicleEnrollmentId); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + ve => vehicleEnrollmentIds.Contains(ve.Id), + ve => ve.Route.RouteAddresses, + 1, vehicleEnrollmentIds.Count(), cancellationToken)) + .Items; + + var routeAddressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + rad => routeAddressIds.Contains(rad.RouteAddressId), + 1, routeAddressIds.Count(), cancellationToken)) + .Items; + + var addressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.AddressId); + var addresses = (await _unitOfWork.AddressRepository + .GetPageAsync( + a => addressIds.Contains(a.Id), + a => a.City.Region.Country, + 1, addressIds.Count(), cancellationToken)) + .Items; + + var vehicleIds = vehicleEnrollments + .Select(ve => ve.VehicleId); + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + v => vehicleIds.Contains(v.Id), + v => v.Company, + 1, vehicleIds.Count(), cancellationToken)) + .Items; + + var account = await _unitOfWork.AccountRepository.GetOneAsync( + a => a.Id == ticketGroup.AccountId, cancellationToken); + + if (ticketGroup.AccountId != null) + { + ticketGroup.Account = account; + } + + foreach (var ve in vehicleEnrollments) + { + ve.Vehicle = vehicles.Single(v => v.Id == ve.VehicleId); + + foreach (var ra in ve.Route.RouteAddresses) + { + ra.Address = addresses.Single(a => a.Id == ra.AddressId); + ra.Details = routeAddressDetails + .Where(rad => rad.RouteAddressId == ra.Id) + .ToArray(); + } + } + + + // TODO: Replace with AutoMapper resolvers + // Convert currency and apply session time zone + + var convertTasks = new List(); + + foreach (var t in ticketGroup.Tickets) + { + convertTasks.Add(Task.Factory.StartNew(() => + { + t.VehicleEnrollment.DepartureTime = + TimeZoneInfo.ConvertTime(t.VehicleEnrollment.DepartureTime, + _sessionTimeZoneService.TimeZone); + })); + + if (_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + break; + } + + convertTasks.Add(Task.Factory.StartNew(() => + { + lock (_lock) + { + var convertedCost = _currencyConverter.ConvertAsync(t.Cost, + t.Currency, _sessionCurrencyService.Currency, + cancellationToken) + .Result; + + t.Cost = _sessionCurrencyService + .Currency.Round(convertedCost); + } + })); + + foreach (var rad in t.VehicleEnrollment.RouteAddressDetails) + { + convertTasks.Add(Task.Factory.StartNew(() => + { + lock (_lock) + { + var convertedCost = _currencyConverter.ConvertAsync( + rad.CostToNextAddress, t.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken) + .Result; + + rad.CostToNextAddress = _sessionCurrencyService + .Currency.Round(convertedCost); + } + })); + } + } + + Task.WaitAll(convertTasks); + + foreach (var t in ticketGroup.Tickets) + { + if (_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + break; + } + + t.Currency = _sessionCurrencyService.Currency; + t.VehicleEnrollment.Currency = _sessionCurrencyService.Currency; + } + + + _unitOfWork.Dispose(); + + var dto = _mapper.Map(ticketGroup); + + dto.Currency = _sessionCurrencyService.Currency.Name; + + return dto; + } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryValidator.cs b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryValidator.cs new file mode 100644 index 0000000..937166a --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroup/GetTicketGroupQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; + +public class GetTicketGroupQueryValidator : AbstractValidator +{ + public GetTicketGroupQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs new file mode 100644 index 0000000..4f85a19 --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQuery.cs @@ -0,0 +1,55 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; + +public record GetTicketGroupsPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public HashSet? PassangerSex { get; set; } + + public DateOnly? PassangerBirthDateGreaterThanOrEqualTo { get; set; } + + public DateOnly? PassangerBirthDateLessThanOrEqualTo { get; set; } + + public DateTimeOffset? PurchaseTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? PurchaseTimeLessThanOrEqualTo { get; set; } + + public HashSet? Statuses { get; set; } + + public HashSet? VehicleTypes { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + + public Guid? AccountGuid { get; set; } + + // TODO: Add filtering parametetrs listed below. It is hard to + // be done because of pagination. + + // public decimal? CostGreaterThanOrEqualTo { get; set; } + // + // public decimal? CostLessThanOrEqualTo { get; set; } + // + // public short? NumberOfTransfersGreaterThanOrEqualTo { get; set; } + // + // public short? NumberOfTransfersLessThanOrEqualTo { get; set; } + // + // public DateTimeOffset? DepartureTimeGreaterThanOrEqualTo { get; set; } + // + // public DateTimeOffset? DepartureTimeLessThanOrEqualTo { get; set; } + // + // public DateTimeOffset? ArrivalTimeGreaterThanOrEqualTo { get; set; } + // + // public DateTimeOffset? ArrivalTimeLessThanOrEqualTo { get; set; } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs new file mode 100644 index 0000000..0a105d6 --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryAuthorizer.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; + +public class GetTicketGroupsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetTicketGroupsPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetTicketGroupsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var account = _unitOfWork.AccountRepository + .GetOneAsync( + e => e.Guid == request.AccountGuid, CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = account?.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs new file mode 100644 index 0000000..e350e7d --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryHandler.cs @@ -0,0 +1,251 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; + +public class GetTicketGroupsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly CurrencyConverterService _currencyConverter; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly SessionTimeZoneService _sessionTimeZoneService; + + private readonly object _lock = new(); + + public GetTicketGroupsPageQueryHandler(UnitOfWork unitOfWork, + IMapper mapper, CurrencyConverterService currencyConverterService, + SessionCurrencyService sessionCurrencyService, + SessionTimeZoneService sessionTimeZoneService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _currencyConverter = currencyConverterService; + _sessionCurrencyService = sessionCurrencyService; + _sessionTimeZoneService = sessionTimeZoneService; + } + + public async Task> Handle( + GetTicketGroupsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.TicketGroupRepository.GetPageAsync( + e => + (e.PassangerFirstName.ToLower().Contains(request.Search.ToLower()) || + e.PassangerLastName.ToLower().Contains(request.Search.ToLower()) || + e.PassangerPatronymic.ToLower().Contains(request.Search.ToLower()) || + (e.PassangerEmail != null ? + e.PassangerEmail.ToLower().Contains(request.Search.ToLower()) : + false)) && + (request.PassangerSex != null + ? request.PassangerSex.Contains(e.PassangerSex) + : true) && + (request.PassangerBirthDateGreaterThanOrEqualTo != null + ? e.PassangerBirthDate >= request.PassangerBirthDateGreaterThanOrEqualTo + : true) && + (request.PassangerBirthDateLessThanOrEqualTo != null + ? e.PassangerBirthDate <= request.PassangerBirthDateLessThanOrEqualTo + : true) && + (request.PurchaseTimeGreaterThanOrEqualTo != null + ? e.PurchaseTime >= request.PurchaseTimeGreaterThanOrEqualTo + : true) && + (request.PurchaseTimeLessThanOrEqualTo != null + ? e.PurchaseTime <= request.PurchaseTimeLessThanOrEqualTo + : true) && + (request.PassangerSex != null + ? request.PassangerSex.Contains(e.PassangerSex) + : true) && + (request.Statuses != null + ? request.Statuses.Contains(e.Status) + : true) && + (request.VehicleTypes != null + ? e.Tickets + .Select(t => t.VehicleEnrollment.Vehicle.VehicleType) + .Any(vt => request.VehicleTypes.Contains(vt)) + : true) && + (request.TravelTimeGreaterThanOrEqualTo != null + ? e.TravelTime >= request.TravelTimeGreaterThanOrEqualTo + : true) && + (request.TravelTimeLessThanOrEqualTo != null + ? e.TravelTime <= request.TravelTimeLessThanOrEqualTo + : true) && + (request.AccountGuid != null + ? e.Account.Guid == request.AccountGuid + : true), + e => e.Tickets, + request.PageNumber, request.PageSize, cancellationToken); + + var ticketGroups = paginatedList.Items; + + + // Hydrate + + var vehicleEnrollmentIds = + ticketGroups.SelectMany(tg => tg.Tickets) + .Select(t => t.VehicleEnrollmentId); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + ve => vehicleEnrollmentIds.Contains(ve.Id), + ve => ve.Route.RouteAddresses, + 1, vehicleEnrollmentIds.Count(), cancellationToken)) + .Items; + + var routeAddressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + rad => routeAddressIds.Contains(rad.RouteAddressId), + 1, routeAddressIds.Count(), cancellationToken)) + .Items; + + var addressIds = vehicleEnrollments + .SelectMany(ve => ve.Route.RouteAddresses) + .Select(ra => ra.AddressId); + var addresses = (await _unitOfWork.AddressRepository + .GetPageAsync( + a => addressIds.Contains(a.Id), + a => a.City.Region.Country, + 1, addressIds.Count(), cancellationToken)) + .Items; + + var vehicleIds = vehicleEnrollments + .Select(ve => ve.VehicleId); + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + v => vehicleIds.Contains(v.Id), + v => v.Company, + 1, vehicleIds.Count(), cancellationToken)) + .Items; + + var accountIds = + ticketGroups.Select(tg => tg.AccountId); + var accounts = (await _unitOfWork.AccountRepository + .GetPageAsync( + a => accountIds.Contains(a.Id), + 1, accountIds.Count(), cancellationToken)) + .Items; + + foreach (var tg in ticketGroups) + { + if (tg.AccountId != null) + { + tg.Account = accounts.Single(a => a.Id == tg.AccountId); + } + } + + foreach (var ve in vehicleEnrollments) + { + ve.Vehicle = vehicles.Single(v => v.Id == ve.VehicleId); + + foreach (var ra in ve.Route.RouteAddresses) + { + ra.Address = addresses.Single(a => a.Id == ra.AddressId); + ra.Details = routeAddressDetails + .Where(rad => rad.RouteAddressId == ra.Id) + .ToArray(); + } + } + + + // TODO: Replace with AutoMapper resolvers. + // Convert currency and apply session time zone. + + var convertTasks = new List(); + var processedRouteAddressDetailIds = new HashSet(); + + foreach (var t in ticketGroups.SelectMany(tg => tg.Tickets)) + { + convertTasks.Add(Task.Factory.StartNew(() => + { + t.VehicleEnrollment.DepartureTime = + TimeZoneInfo.ConvertTime(t.VehicleEnrollment.DepartureTime, + _sessionTimeZoneService.TimeZone); + })); + + if (_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + break; + } + + convertTasks.Add(Task.Factory.StartNew(() => + { + lock (_lock) + { + var convertedCost = _currencyConverter.ConvertAsync(t.Cost, + t.Currency, _sessionCurrencyService.Currency, + t.TicketGroup.PurchaseTime, cancellationToken) + .Result; + + t.Cost = _sessionCurrencyService + .Currency.Round(convertedCost); + } + })); + + foreach (var rad in t.VehicleEnrollment.RouteAddressDetails) + { + convertTasks.Add(Task.Factory.StartNew(() => + { + lock (_lock) + { + if (processedRouteAddressDetailIds.Contains(rad.Id)) + { + return; + } + + var convertedCost = _currencyConverter.ConvertAsync( + rad.CostToNextAddress, t.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + t.TicketGroup.PurchaseTime, cancellationToken) + .Result; + + rad.CostToNextAddress = _sessionCurrencyService + .Currency.Round(convertedCost); + + processedRouteAddressDetailIds.Add(rad.Id); + } + })); + } + } + + Task.WaitAll(convertTasks); + + foreach (var t in ticketGroups.SelectMany(tg => tg.Tickets)) + { + if (_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + break; + } + + t.Currency = _sessionCurrencyService.Currency; + t.VehicleEnrollment.Currency = _sessionCurrencyService.Currency; + } + + + var mappedItems = + _mapper.Map>(ticketGroups); + + foreach (var item in mappedItems) + { + item.Currency = _sessionCurrencyService.Currency.Name; + } + + mappedItems = QueryableExtension + .ApplySort(mappedItems.AsQueryable(), request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryValidator.cs b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryValidator.cs new file mode 100644 index 0000000..ed16b23 --- /dev/null +++ b/src/Application/TicketGroups/Queries/GetTicketGroupsPage/GetTicketGroupsPageQueryValidator.cs @@ -0,0 +1,80 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; + +public class GetTicketGroupsPageQueryValidator : AbstractValidator +{ + public GetTicketGroupsPageQueryValidator( + 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)); + + When(v => v.PassangerSex != null, () => + { + RuleForEach(v => v.PassangerSex) + .Must((v, s) => Sex.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Sex.Enumerations.Values.Select(e => e.Name)))); + }); + + When(v => v.Statuses != null, () => + { + RuleForEach(v => v.Statuses) + .Must((v, s) => TicketStatus.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + TicketStatus.Enumerations.Values.Select(e => e.Name)))); + }); + + When(v => v.VehicleTypes != null, () => + { + RuleForEach(v => v.VehicleTypes) + .Must((v, s) => VehicleType.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + }); + } +} diff --git a/src/Application/TicketGroups/TicketGroupAccountDto.cs b/src/Application/TicketGroups/TicketGroupAccountDto.cs new file mode 100644 index 0000000..c439d72 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupAccountDto.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupAccountDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Username { get; set; } + + public string Email { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/TicketGroups/TicketGroupAddressDto.cs b/src/Application/TicketGroups/TicketGroupAddressDto.cs new file mode 100644 index 0000000..dd86935 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupAddressDto.cs @@ -0,0 +1,87 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupAddressDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + public short Order { get; set; } + + public Guid RouteAddressUuid { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.RouteAddress.Address.Guid)) + .ForMember( + d => d.Name, + opt => opt.MapFrom(s => s.RouteAddress.Address.Name)) + .ForMember( + d => d.Longitude, + opt => opt.MapFrom(s => s.RouteAddress.Address.Longitude)) + .ForMember( + d => d.Latitude, + opt => opt.MapFrom(s => s.RouteAddress.Address.Latitude)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Region.Country.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Region.Name)) + .ForMember( + d => d.CityUuid, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Guid)) + .ForMember( + d => d.CityName, + opt => opt.MapFrom(s => s.RouteAddress.Address.City.Name)) + .ForMember( + d => d.TimeToNextAddress, + opt => opt.MapFrom(s => s.TimeToNextAddress)) + .ForMember( + d => d.CostToNextAddress, + opt => opt.MapFrom(s => s.CostToNextAddress)) + .ForMember( + d => d.CurrentAddressStopTime, + opt => opt.MapFrom(s => s.CurrentAddressStopTime)) + .ForMember( + d => d.Order, + opt => opt.MapFrom(s => s.RouteAddress.Order)) + .ForMember( + d => d.RouteAddressUuid, + opt => opt.MapFrom(s => s.RouteAddress.Guid)); + } +} diff --git a/src/Application/TicketGroups/TicketGroupCompanyDto.cs b/src/Application/TicketGroups/TicketGroupCompanyDto.cs new file mode 100644 index 0000000..d0e4163 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupCompanyDto.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupCompanyDto : IMapFrom +{ + public string 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/TicketGroups/TicketGroupDto.cs b/src/Application/TicketGroups/TicketGroupDto.cs new file mode 100644 index 0000000..bdbbae0 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupDto.cs @@ -0,0 +1,163 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public string PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public string? PassangerEmail { get; set; } + + public DateTimeOffset PurchaseTime { get; set; } + + public string Status { get; set; } + + + public DateTimeOffset DepartureTime { get; set; } + + public DateTimeOffset ArrivalTime { get; set; } + + public TimeSpan TravelTime { get; set; } + + public TimeSpan TimeInStops { get; set; } + + public int NumberOfTransfers { get; set; } + + public string Currency { get; set; } + + public decimal Cost { get; set; } + + public TicketGroupAccountDto? Account { get; set; } + + public ICollection Enrollments { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.PassangerSex, + opt => opt.MapFrom(s => s.PassangerSex.Name)) + .ForMember( + d => d.PurchaseTime, + opt => opt + .MapFrom( + s => s.PurchaseTime)) + .ForMember( + d => d.Status, + opt => opt.MapFrom(s => s.Status.Name)) + .ForMember( + d => d.DepartureTime, + opt => opt.MapFrom( + (s, d) => + { + var departureRouteAddressId = + s.Tickets + .OrderBy(t => t.Order) + .First() + .DepartureRouteAddressId; + return + s.Tickets + .OrderBy(t => t.Order) + .First().VehicleEnrollment + .GetDepartureTime(departureRouteAddressId); + })) + .ForMember( + d => d.ArrivalTime, + opt => opt.MapFrom( + (s, d) => + { + var arrivalRouteAddressId = + s.Tickets + .OrderBy(t => t.Order) + .First() + .ArrivalRouteAddressId; + return + s.Tickets + .OrderBy(t => t.Order) + .First().VehicleEnrollment + .GetArrivalTime(arrivalRouteAddressId); + })) + .ForMember( + d => d.TravelTime, + opt => opt.MapFrom( + (s, d) => + { + var departureRouteAddressId = + s.Tickets + .OrderBy(t => t.Order) + .First() + .DepartureRouteAddressId; + var arrivalRouteAddressId = + s.Tickets + .OrderBy(t => t.Order) + .First() + .ArrivalRouteAddressId; + var departureTime = + s.Tickets + .OrderBy(t => t.Order) + .First().VehicleEnrollment + .GetDepartureTime(departureRouteAddressId); + var arrivalTime = + s.Tickets + .OrderBy(t => t.Order) + .First().VehicleEnrollment + .GetArrivalTime(departureRouteAddressId); + return arrivalTime - departureTime; + })) + .ForMember( + d => d.TimeInStops, + opt => opt.MapFrom( + (s, d) => + { + var timePeriodsInStops = + s.Tickets.Select(t => + { + var departureRouteAddressId = + t.DepartureRouteAddressId; + var arrivalRouteAddressId = + t.ArrivalRouteAddressId; + return + t.VehicleEnrollment.GetTimeInStops( + departureRouteAddressId, + arrivalRouteAddressId); + }); + return + timePeriodsInStops + .Aggregate(TimeSpan.Zero, + (sum, next) => sum += next); + })) + .ForMember( + d => d.NumberOfTransfers, + opt => opt.MapFrom(s => s.Tickets.Count() - 1)) + .ForMember( + d => d.Cost, + opt => opt.MapFrom( + (s, d) => + { + var costs = + s.Tickets.Select(t => t.Currency.Round(t.Cost)); + return + costs + .Aggregate((decimal)0, + (sum, next) => sum += next); + })) + .ForMember( + d => d.Enrollments, + opt => opt.MapFrom(s => s.Tickets)); + } +} diff --git a/src/Application/TicketGroups/TicketGroupVehicleDto.cs b/src/Application/TicketGroups/TicketGroupVehicleDto.cs new file mode 100644 index 0000000..006d245 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupVehicleDto.cs @@ -0,0 +1,95 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupVehicleDto : IMapFrom +{ + public string Uuid { get; set; } + + public string Type { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Type, + opt => opt.MapFrom(s => s.VehicleType.Name)) + .ForMember( + d => d.Number, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Number; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Number; + } + else if (s is Train) + { + return ((Train)s).Number; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Model, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Model; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Model; + } + else if (s is Train) + { + return ((Train)s).Model; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Capacity, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Capacity; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Capacity; + } + else if (s is Train) + { + return ((Train)s).Capacity; + } + else + { + throw new NotImplementedException(); + } + })); + } +} + diff --git a/src/Application/TicketGroups/TicketGroupVehicleEnrollmentDto.cs b/src/Application/TicketGroups/TicketGroupVehicleEnrollmentDto.cs new file mode 100644 index 0000000..ac1ac64 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupVehicleEnrollmentDto.cs @@ -0,0 +1,99 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.TicketGroups; + +public sealed class TicketGroupVehicleEnrollmentDto : IMapFrom +{ + public DateTimeOffset DepartureTime { get; set; } + + public DateTimeOffset ArrivalTime { get; set; } + + public TimeSpan TravelTime { get; set; } + + public TimeSpan TimeMoving { get; set; } + + public TimeSpan TimeInStops { get; set; } + + public int NumberOfStops { get; set; } + + public string Currency { get; set; } + + public decimal Cost { get; set; } + + public Guid Uuid { get; set; } + + public short Order { get; set; } + + public TicketGroupCompanyDto Company { get; set; } + + public TicketGroupVehicleDto Vehicle { get; set; } + + public ICollection Addresses { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.VehicleEnrollment.Guid)) + .ForMember( + d => d.DepartureTime, + opt => opt.MapFrom( + (s, d) => + { + return s.VehicleEnrollment + .GetDepartureTime(s.DepartureRouteAddressId); + })) + .ForMember( + d => d.ArrivalTime, + opt => opt.MapFrom( + (s, d) => + { + return s.VehicleEnrollment + .GetArrivalTime(s.ArrivalRouteAddressId); + })) + .ForMember( + d => d.TravelTime, + opt => opt.MapFrom( + (s, d) => + { + var departureTime = s.VehicleEnrollment + .GetDepartureTime(s.DepartureRouteAddressId); + var arrivalTime = s.VehicleEnrollment + .GetArrivalTime(s.ArrivalRouteAddressId); + return arrivalTime - departureTime; + })) + .ForMember( + d => d.TimeInStops, + opt => opt.MapFrom( + (s, d) => + { + return s.VehicleEnrollment.GetTimeInStops( + s.DepartureRouteAddressId, s.ArrivalRouteAddressId); + })) + .ForMember( + d => d.NumberOfStops, + opt => opt.MapFrom( + (s, d) => + { + return s.VehicleEnrollment.GetNumberOfStops( + s.DepartureRouteAddressId, s.ArrivalRouteAddressId); + })) + .ForMember( + d => d.Currency, + opt => opt.MapFrom(s => s.Currency)) + .ForMember( + d => d.Cost, + opt => opt.MapFrom(s => s.Currency.Round(s.Cost))) + .ForMember( + d => d.Company, + opt => opt.MapFrom(s => s.VehicleEnrollment.Vehicle.Company)) + .ForMember( + d => d.Vehicle, + opt => opt.MapFrom(s => s.VehicleEnrollment.Vehicle)) + .ForMember( + d => d.Addresses, + opt => opt.MapFrom(s => s.VehicleEnrollment.RouteAddressDetails)); + } +} diff --git a/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs b/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs new file mode 100644 index 0000000..9cac564 --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs @@ -0,0 +1,21 @@ +namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; + +public sealed class AddTicketGroupViewModel +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public string PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public DateTimeOffset PurchaseTime { get; set; } + + public string Status { get; set; } + + + public ICollection Tickets { get; set; } +} diff --git a/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs b/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs new file mode 100644 index 0000000..f60135d --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs @@ -0,0 +1,24 @@ +namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; + +public sealed class GetTicketGroupsPageFilterViewModel +{ + public HashSet? PassangerSex { get; set; } + + public DateOnly? PassangerBirthDateGreaterThanOrEqualTo { get; set; } + + public DateOnly? PassangerBirthDateLessThanOrEqualTo { get; set; } + + public DateTimeOffset? PurchaseTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? PurchaseTimeLessThanOrEqualTo { get; set; } + + public HashSet? Statuses { get; set; } + + public HashSet? VehicleTypes { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + + public Guid? AccountUuid { get; set; } +} diff --git a/src/Application/TicketGroups/ViewModels/TicketViewModel.cs b/src/Application/TicketGroups/ViewModels/TicketViewModel.cs new file mode 100644 index 0000000..15560d1 --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/TicketViewModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; + +public sealed class TicketViewModel +{ + public Guid DepartureRouteAddressUuid { get; set; } + + public Guid ArrivalRouteAddressUuid { get; set; } + + public short Order { get; set; } + + + public Guid VehicleEnrollmentUuid { get; set; } +} diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs new file mode 100644 index 0000000..6c680e5 --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public record AddTrainCommand : IRequest +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } +} diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs new file mode 100644 index 0000000..ad27294 --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public class AddTrainCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public AddTrainCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(AddTrainCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs new file mode 100644 index 0000000..b420c50 --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandHandler.cs @@ -0,0 +1,62 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public class AddTrainCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddTrainCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddTrainCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Number == request.Number, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Train with given number already exists."); + } + + 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, + CompanyId = parentEntity.Id, + Company = parentEntity + }; + + entity = await _unitOfWork.TrainRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs new file mode 100644 index 0000000..52e38e8 --- /dev/null +++ b/src/Application/Trains/Commands/AddTrain/AddTrainCommandValidator.cs @@ -0,0 +1,41 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; + +public class AddTrainCommandValidator : AbstractValidator +{ + public AddTrainCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs new file mode 100644 index 0000000..6d714c7 --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public record DeleteTrainCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs new file mode 100644 index 0000000..8b3be6d --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public class DeleteTrainCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public DeleteTrainCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(DeleteTrainCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs new file mode 100644 index 0000000..b3cc70b --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public class DeleteTrainCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteTrainCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteTrainCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.TrainRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs new file mode 100644 index 0000000..f1008cd --- /dev/null +++ b/src/Application/Trains/Commands/DeleteTrain/DeleteTrainCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; + +public class DeleteTrainCommandValidator : AbstractValidator +{ + public DeleteTrainCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs new file mode 100644 index 0000000..4e40e13 --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommand.cs @@ -0,0 +1,16 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public record UpdateTrainCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyGuid { get; set; } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs new file mode 100644 index 0000000..01985f6 --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public class UpdateTrainCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public UpdateTrainCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(UpdateTrainCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs new file mode 100644 index 0000000..0607955 --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandHandler.cs @@ -0,0 +1,67 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public class UpdateTrainCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateTrainCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateTrainCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var duplicateEntity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Number == request.Number && e.Guid != request.Guid, + cancellationToken); + + if (duplicateEntity != null) + { + throw new DuplicateEntityException( + "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); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs new file mode 100644 index 0000000..6a2c65e --- /dev/null +++ b/src/Application/Trains/Commands/UpdateTrain/UpdateTrainCommandValidator.cs @@ -0,0 +1,45 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; + +public class UpdateTrainCommandValidator : AbstractValidator +{ + public UpdateTrainCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Number) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(32) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 32)); + + RuleFor(v => v.Model) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.Capacity) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.CompanyGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs new file mode 100644 index 0000000..9285936 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public record GetTrainQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs new file mode 100644 index 0000000..e471e0d --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public class GetTrainQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetTrainQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetTrainQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicel = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicel?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs new file mode 100644 index 0000000..fe3788e --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public class GetTrainQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetTrainQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetTrainQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.TrainRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.Company, + cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs b/src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs new file mode 100644 index 0000000..f3bcd7e --- /dev/null +++ b/src/Application/Trains/Queries/GetTrain/GetTrainQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; + +public class GetTrainQueryValidator : AbstractValidator +{ + public GetTrainQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs new file mode 100644 index 0000000..1160f90 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQuery.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public record GetTrainsPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? CompanyGuid { get; set; } + + public short? CapacityGreaterOrEqualThan { get; set; } + + public short? CapacityLessOrEqualThan { get; set; } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs new file mode 100644 index 0000000..a8acb71 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public class GetTrainsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetTrainsPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetTrainsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var company = _unitOfWork.CompanyRepository + .GetOneAsync( + e => e.Guid == request.CompanyGuid, e => e.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = company?.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs new file mode 100644 index 0000000..39892ae --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryHandler.cs @@ -0,0 +1,57 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public class GetTrainsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetTrainsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetTrainsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.TrainRepository.GetPageAsync( + e => + (e.Number.ToLower().Contains(request.Search.ToLower()) || + e.Model.ToLower().Contains(request.Search.ToLower())) && + (request.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); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs new file mode 100644 index 0000000..087a2b1 --- /dev/null +++ b/src/Application/Trains/Queries/GetTrainsPage/GetTrainsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; + +public class GetTrainsPageQueryValidator : AbstractValidator +{ + public GetTrainsPageQueryValidator( + 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/Trains/TrainDto.cs b/src/Application/Trains/TrainDto.cs new file mode 100644 index 0000000..2b1cf05 --- /dev/null +++ b/src/Application/Trains/TrainDto.cs @@ -0,0 +1,28 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Trains; + +public sealed class TrainDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + 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 new file mode 100644 index 0000000..59a75c8 --- /dev/null +++ b/src/Application/Trains/ViewModels/AddTrainViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Trains.ViewModels; + +public sealed class AddTrainViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } +} diff --git a/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs b/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs new file mode 100644 index 0000000..fed89ef --- /dev/null +++ b/src/Application/Trains/ViewModels/GetTrainsPageFilterViewModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.Trains.ViewModels; + +public sealed class GetTrainsPageFilterViewModel +{ + // TODO: Consider adding strict equals rule although it is not + // necessarily needed to filter with exact capacity + + public Guid? CompanyUuid { 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 new file mode 100644 index 0000000..3458e86 --- /dev/null +++ b/src/Application/Trains/ViewModels/UpdateTrainViewModel.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Application.Trains.ViewModels; + +public sealed class UpdateTrainViewModel +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public Guid CompanyUuid { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs new file mode 100644 index 0000000..5f71e49 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs @@ -0,0 +1,39 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +public record SearchAllQuery : + IRequest> +{ + public Guid DepartureAddressGuid { get; set; } + + public Guid ArrivalAddressGuid { get; set; } + + public DateOnly DepartureDate { get; set; } + + public HashSet VehicleTypes { get; set; } + + public string Sort { get; set; } = String.Empty; + + public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + + public decimal? CostGreaterThanOrEqualTo { get; set; } + + public decimal? CostLessThanOrEqualTo { get; set; } + + public short? NumberOfTransfersGreaterThanOrEqualTo { get; set; } + + public short? NumberOfTransfersLessThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeLessThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeLessThanOrEqualTo { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs new file mode 100644 index 0000000..4032b4b --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +public class SearchAllQueryAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy(SearchAllQuery request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs new file mode 100644 index 0000000..02d83a3 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs @@ -0,0 +1,545 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using QuikGraph; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +// TODO: Add configurable time between transfers. +// TODO: Refactor DTO creation code to use mapper as much as possible. +public class SearchAllQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + + private readonly SessionTimeZoneService _sessionTimeZoneService; + + public SearchAllQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService, + SessionTimeZoneService sessionTimeZoneService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; + _sessionTimeZoneService = sessionTimeZoneService; + } + + public async Task> Handle( + SearchAllQuery request, + CancellationToken cancellationToken) + { + // Get related data + + var zeroTime = TimeOnly.FromTimeSpan(TimeSpan.Zero); + var departureDate = + new DateTimeOffset(request.DepartureDate, zeroTime, TimeSpan.Zero); + + var range = TimeSpan.FromDays(3); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => + e.DepartureTime >= departureDate.Subtract(range) && + e.DepartureTime <= departureDate.Add(range) && + request.VehicleTypes.Contains(e.Vehicle.VehicleType), + e => e.Route, + 1, int.MaxValue, cancellationToken)) + .Items; + + if (vehicleEnrollments.Count == 0) + { + throw new NotFoundException(); + } + + var vehicleEnrollmentIds = vehicleEnrollments.Select(e => e.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.RouteAddress.Address.City.Region.Country, + 1, int.MaxValue, cancellationToken)) + .Items; + + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + e => e.Enrollments.All(e => vehicleEnrollmentIds.Contains(e.Id)), + 1, int.MaxValue, cancellationToken)) + .Items; + + var companyIds = vehicles.Select(e => e.CompanyId); + var companies = (await _unitOfWork.CompanyRepository + .GetPageAsync( + e => companyIds.Contains(e.Id), + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Hydrate vehicle enrollments with: + // - route address details; + // - vehicle info; + // - comapny info. + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + vehicleEnrollment.RouteAddressDetails = routeAddressDetails + .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) + .OrderBy(e => e.RouteAddress.Order) + .ToArray(); + + vehicleEnrollment.Vehicle = vehicles + .Single(e => e.Id == vehicleEnrollment.VehicleId); + + vehicleEnrollment.Vehicle.Company = companies + .Single(e => e.Id == vehicleEnrollment.Vehicle.CompanyId); + } + + + // Creat and fill graph data structure + + var graph = new AdjacencyGraph< + Address, TaggedEdge>(); + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + var vehicleEnrollmentRouteAddressDetails = routeAddressDetails + .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) + .OrderBy(e => e.RouteAddress.Order); + + for (int i = 1; i < vehicleEnrollmentRouteAddressDetails.Count(); i++) + { + var sourceRouteAddressDetail = + vehicleEnrollmentRouteAddressDetails.ElementAt(i - 1); + var targetRouteAddressDetail = + vehicleEnrollmentRouteAddressDetails.ElementAt(i); + + var sourceAddress = + sourceRouteAddressDetail.RouteAddress.Address; + var targetAddress = + targetRouteAddressDetail.RouteAddress.Address; + + var weight = sourceRouteAddressDetail.CostToNextAddress; + + graph.AddVerticesAndEdge( + new TaggedEdge( + sourceAddress, targetAddress, sourceRouteAddressDetail)); + } + } + + + // Find paths + + var departureAddress = routeAddressDetails + .First(e => e.RouteAddress.Address.Guid == request.DepartureAddressGuid) + .RouteAddress.Address; + var arrivalAddress = routeAddressDetails + .First(e => e.RouteAddress.Address.Guid == request.ArrivalAddressGuid) + .RouteAddress.Address; + + + var paths = new List>>(); + var queue = new Queue<(TaggedEdge edge, List> path)>(); + + foreach (var edge in graph.OutEdges(departureAddress)) + { + queue.Enqueue((edge, new List>() { edge })); + } + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + + if (current.edge.Target.Equals(arrivalAddress)) + { + paths.Add(current.path); + continue; + } + + foreach (var edge in graph.OutEdges(current.edge.Target)) + { + var neighbor = edge; + if (!current.path.Contains(neighbor)) + { + var newPath = new List>(current.path) { neighbor }; + queue.Enqueue((neighbor, newPath)); + } + } + } + + + // Create DTO object + + var result = new List(); + + foreach (var path in paths) + { + var vehicleEnrollmentDtos = + new List(); + + var addressDtos = new List(); + + var firstRouteAddressId = path.First().Tag.RouteAddressId; + + Guid lastRouteAddressGuid; + long lastRouteAddressId; + + decimal vehicleEnrollmentCost; + Currency vehicleEnrollmentCurrency; + + decimal costToNextAddress; + + short addressOrder = 1; + short enrollmentOrder = 1; + + Address source; + Address nextSource; + + Address target; + Address nextTarget; + + RouteAddressDetail tag; + RouteAddressDetail nextTag; + + for (int i = 0; i < path.Count - 1; i++) + { + var edge = path[i]; + var nextEdge = path[i+1]; + + source = edge.Source; + nextSource = nextEdge.Source; + + tag = edge.Tag; + nextTag = nextEdge.Tag; + + + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = source.Guid, + Name = source.Name, + Longitude = source.Latitude, + Latitude = source.Longitude, + CountryUuid = source.City.Region.Country.Guid, + CountryName = source.City.Region.Country.Name, + RegionUuid = source.City.Region.Guid, + RegionName = source.City.Region.Name, + CityUuid = source.City.Guid, + CityName = source.City.Name, + TimeToNextAddress = tag.TimeToNextAddress, + CostToNextAddress = costToNextAddress, + CurrentAddressStopTime = tag.CurrentAddressStopTime, + Order = addressOrder, + RouteAddressUuid = tag.RouteAddress.Guid + }); + + addressOrder++; + + + if (tag.VehicleEnrollmentId != nextTag.VehicleEnrollmentId) + { + target = edge.Target; + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = target.Guid, + Name = target.Name, + Longitude = target.Latitude, + Latitude = target.Longitude, + CountryUuid = target.City.Region.Country.Guid, + CountryName = target.City.Region.Country.Name, + RegionUuid = target.City.Region.Guid, + RegionName = target.City.Region.Name, + CityUuid = target.City.Guid, + CityName = target.City.Name, + TimeToNextAddress = TimeSpan.Zero, + CostToNextAddress = 0, + CurrentAddressStopTime = TimeSpan.Zero, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + + lastRouteAddressId = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Id; + + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + + vehicleEnrollmentDtos.Add( + new VehicleEnrollmentSearchVehicleEnrollmentDto() + { + DepartureTime = tag.VehicleEnrollment + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = tag.VehicleEnrollment + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = tag.VehicleEnrollment + .GetTravelTime(firstRouteAddressId, + lastRouteAddressId), + TimeMoving = tag.VehicleEnrollment + .GetTimeMoving(firstRouteAddressId, + lastRouteAddressId), + TimeInStops = tag.VehicleEnrollment + .GetTimeInStops(firstRouteAddressId, + lastRouteAddressId), + NumberOfStops = tag.VehicleEnrollment + .GetNumberOfStops(firstRouteAddressId, + lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, + VehicleType = source.VehicleType.Name, + Uuid = tag.VehicleEnrollment.Guid, + Order = enrollmentOrder, + Addresses = addressDtos, + Company = _mapper + .Map( + tag.VehicleEnrollment.Vehicle.Company), + Vehicle = _mapper + .Map( + tag.VehicleEnrollment.Vehicle) + }); + + + firstRouteAddressId = nextTag.RouteAddressId; + addressDtos = new List(); + addressOrder = 1; + enrollmentOrder++; + } + } + + // --------------- + + source = path.Select(e => e.Source).Last(); + target = path.Select(e => e.Target).Last(); + tag = path.Select(e => e.Tag).Last(); + + + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = source.Guid, + Name = source.Name, + Longitude = source.Latitude, + Latitude = source.Longitude, + CountryUuid = source.City.Region.Country.Guid, + CountryName = source.City.Region.Country.Name, + RegionUuid = source.City.Region.Guid, + RegionName = source.City.Region.Name, + CityUuid = source.City.Guid, + CityName = source.City.Name, + TimeToNextAddress = tag.TimeToNextAddress, + CostToNextAddress = 0, + CurrentAddressStopTime = tag.CurrentAddressStopTime, + Order = addressOrder, + RouteAddressUuid = tag.RouteAddress.Guid + }); + + + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = target.Guid, + Name = target.Name, + Longitude = target.Latitude, + Latitude = target.Longitude, + CountryUuid = target.City.Region.Country.Guid, + CountryName = target.City.Region.Country.Name, + RegionUuid = target.City.Region.Guid, + RegionName = target.City.Region.Name, + CityUuid = target.City.Guid, + CityName = target.City.Name, + TimeToNextAddress = TimeSpan.Zero, + CostToNextAddress = 0, + CurrentAddressStopTime = TimeSpan.Zero, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + + lastRouteAddressId = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Id; + + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + + vehicleEnrollmentDtos.Add( + new VehicleEnrollmentSearchVehicleEnrollmentDto() + { + DepartureTime = tag.VehicleEnrollment + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = tag.VehicleEnrollment + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = tag.VehicleEnrollment + .GetTravelTime(firstRouteAddressId, + lastRouteAddressId), + TimeMoving = tag.VehicleEnrollment + .GetTimeMoving(firstRouteAddressId, + lastRouteAddressId), + TimeInStops = tag.VehicleEnrollment + .GetTimeInStops(firstRouteAddressId, + lastRouteAddressId), + NumberOfStops = tag.VehicleEnrollment + .GetNumberOfStops(firstRouteAddressId, + lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, + VehicleType = source.VehicleType.Name, + Uuid = tag.VehicleEnrollment.Guid, + Order = enrollmentOrder, + Addresses = addressDtos, + Company = _mapper.Map( + tag.VehicleEnrollment.Vehicle.Company), + Vehicle = _mapper.Map( + tag.VehicleEnrollment.Vehicle) + }); + + // --------------- + + + var departureTime = vehicleEnrollmentDtos + .OrderBy(e => e.Order).First().DepartureTime; + var arrivalTime = vehicleEnrollmentDtos + .OrderBy(e => e.Order).Last().ArrivalTime; + var timeInStops = vehicleEnrollmentDtos + .Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops); + var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1; + var cost = vehicleEnrollmentDtos + .Aggregate((decimal)0, (sum, next) => sum += next.Cost); + + result.Add(new VehicleEnrollmentSearchDto() + { + DepartureTime = departureTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = arrivalTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = arrivalTime - departureTime, + TimeInStops = timeInStops, + NumberOfTransfers = numberOfTransfers, + Currency = _sessionCurrencyService.Currency.Name, + Cost = cost, + Enrollments = vehicleEnrollmentDtos + }); + } + + + if (result.Count == 0) + { + throw new NotFoundException(); + } + + + var filteredResult = result.Where(e => + (request.TravelTimeGreaterThanOrEqualTo != null + ? e.TravelTime >= request.TravelTimeGreaterThanOrEqualTo + : true) && + (request.TravelTimeLessThanOrEqualTo != null + ? e.TravelTime <= request.TravelTimeLessThanOrEqualTo + : true) && + (request.CostGreaterThanOrEqualTo != null + ? e.Cost >= request.CostGreaterThanOrEqualTo + : true) && + (request.CostLessThanOrEqualTo != null + ? e.Cost <= request.CostLessThanOrEqualTo + : true) && + (request.NumberOfTransfersGreaterThanOrEqualTo != null + ? e.NumberOfTransfers >= request.NumberOfTransfersGreaterThanOrEqualTo + : true) && + (request.NumberOfTransfersLessThanOrEqualTo != null + ? e.NumberOfTransfers <= request.NumberOfTransfersLessThanOrEqualTo + : true) && + (request.DepartureTimeGreaterThanOrEqualTo != null + ? e.DepartureTime >= request.DepartureTimeGreaterThanOrEqualTo + : true) && + (request.DepartureTimeLessThanOrEqualTo != null + ? e.DepartureTime <= request.DepartureTimeLessThanOrEqualTo + : true) && + (request.ArrivalTimeGreaterThanOrEqualTo != null + ? e.ArrivalTime >= request.ArrivalTimeGreaterThanOrEqualTo + : true) && + (request.ArrivalTimeLessThanOrEqualTo != null + ? e.ArrivalTime <= request.ArrivalTimeLessThanOrEqualTo + : true)); + + var sortedResult = QueryableExtension + .ApplySort(filteredResult.AsQueryable(), request.Sort); + + + return sortedResult; + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs new file mode 100644 index 0000000..9ed2b6f --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +public class SearchAllQueryValidator : + AbstractValidator +{ + public SearchAllQueryValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.DepartureAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.ArrivalAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.DepartureDate) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow)) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow))); + + RuleForEach(v => v.VehicleTypes) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQuery.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQuery.cs new file mode 100644 index 0000000..4bdfae6 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQuery.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchShortest; + +public record SearchShortestQuery : + IRequest +{ + public Guid DepartureAddressGuid { get; set; } + + public Guid ArrivalAddressGuid { get; set; } + + public DateOnly DepartureDate { get; set; } + + public HashSet VehicleTypes { get; set; } + + public bool ShortestByCost { get; set; } + + public bool ShortestByTime { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs new file mode 100644 index 0000000..0141b59 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryAuthorizer.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchShortest; + +public class SearchShortestQueryAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy(SearchShortestQuery request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs new file mode 100644 index 0000000..17318b5 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs @@ -0,0 +1,491 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using QuikGraph; +using QuikGraph.Algorithms; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchShortest; + +// TODO: Add configurable time between transfers. +// TODO: Refactor DTO creation code to use mapper as much as possible. +public class SearchShortestQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + + private readonly SessionTimeZoneService _sessionTimeZoneService; + + public SearchShortestQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService, + SessionTimeZoneService sessionTimeZoneService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; + _sessionTimeZoneService = sessionTimeZoneService; + } + + public async Task Handle( + SearchShortestQuery request, + CancellationToken cancellationToken) + { + // Get related data + + var zeroTime = TimeOnly.FromTimeSpan(TimeSpan.Zero); + var departureDate = + new DateTimeOffset(request.DepartureDate, zeroTime, TimeSpan.Zero); + + var range = TimeSpan.FromDays(3); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => + e.DepartureTime >= departureDate.Subtract(range) && + e.DepartureTime <= departureDate.Add(range) && + request.VehicleTypes.Contains(e.Vehicle.VehicleType), + e => e.Route, + 1, int.MaxValue, cancellationToken)) + .Items; + + if (vehicleEnrollments.Count == 0) + { + throw new NotFoundException(); + } + + var vehicleEnrollmentIds = vehicleEnrollments.Select(e => e.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.RouteAddress.Address.City.Region.Country, + 1, int.MaxValue, cancellationToken)) + .Items; + + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + e => e.Enrollments.All(e => vehicleEnrollmentIds.Contains(e.Id)), + 1, int.MaxValue, cancellationToken)) + .Items; + + var companyIds = vehicles.Select(e => e.CompanyId); + var companies = (await _unitOfWork.CompanyRepository + .GetPageAsync( + e => companyIds.Contains(e.Id), + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Hydrate vehicle enrollments with: + // - route address details; + // - vehicle info; + // - comapny info. + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + vehicleEnrollment.RouteAddressDetails = routeAddressDetails + .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) + .OrderBy(e => e.RouteAddress.Order) + .ToArray(); + + vehicleEnrollment.Vehicle = vehicles + .Single(e => e.Id == vehicleEnrollment.VehicleId); + + vehicleEnrollment.Vehicle.Company = companies + .Single(e => e.Id == vehicleEnrollment.Vehicle.CompanyId); + } + + + // Creat and fill graph data structure + + var graph = new AdjacencyGraph< + Address, TaggedEdge>(); + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + var vehicleEnrollmentRouteAddressDetails = routeAddressDetails + .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) + .OrderBy(e => e.RouteAddress.Order); + + for (int i = 1; i < vehicleEnrollmentRouteAddressDetails.Count(); i++) + { + var sourceRouteAddressDetail = + vehicleEnrollmentRouteAddressDetails.ElementAt(i-1); + var targetRouteAddressDetail = + vehicleEnrollmentRouteAddressDetails.ElementAt(i); + + var sourceAddress = + sourceRouteAddressDetail.RouteAddress.Address; + var targetAddress = + targetRouteAddressDetail.RouteAddress.Address; + + var weight = sourceRouteAddressDetail.CostToNextAddress; + + graph.AddVerticesAndEdge( + new TaggedEdge( + sourceAddress, targetAddress, sourceRouteAddressDetail)); + } + } + + + // Find paths + + var departureAddress = routeAddressDetails + .First(e => e.RouteAddress.Address.Guid == request.DepartureAddressGuid) + .RouteAddress.Address; + var arrivalAddress = routeAddressDetails + .First(e => e.RouteAddress.Address.Guid == request.ArrivalAddressGuid) + .RouteAddress.Address; + + + Func, double> weightByCost = + edge => (double)edge.Tag.CostToNextAddress; + + Func, double> weightByTime = + edge => + edge.Tag.TimeToNextAddress.Ticks + + edge.Tag.CurrentAddressStopTime.Ticks; + + Func, double> edgeWeight = + _ => 0; + + if (request.ShortestByCost && request.ShortestByTime) + { + edgeWeight = edge => weightByCost(edge) + weightByTime(edge); + } + else if (request.ShortestByCost) + { + edgeWeight = edge => weightByCost(edge); + } + else if (request.ShortestByTime) + { + edgeWeight = edge => weightByTime(edge); + } + + + var tryGetPaths = graph.ShortestPathsDijkstra(edgeWeight, departureAddress); + + + // Create and hydrate a DTO object + + var vehicleEnrollmentDtos = + new List(); + + var totalTravelTime = TimeSpan.Zero; + var totalCost = (decimal)0; + + if (tryGetPaths(arrivalAddress, out var path)) + { + var firstRouteAddressId = path.First().Tag.RouteAddressId; + long lastRouteAddressId; + Guid lastRouteAddressGuid; + + decimal vehicleEnrollmentCost; + Currency vehicleEnrollmentCurrency; + + decimal costToNextAddress; + + var addressDtos = new List(); + + var addressOrder = (short)1; + var enrollmentOrder = (short)1; + + Address source; + Address target; + RouteAddressDetail tag; + RouteAddressDetail nextTag; + + for (int i = 0; i < path.Count() - 1; i++) + { + source = path.Select(e => e.Source).ElementAt(i); + tag = path.Select(e => e.Tag).ElementAt(i); + nextTag = path.Select(e => e.Tag).ElementAt(i+1); + + + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = source.Guid, + Name = source.Name, + Longitude = source.Latitude, + Latitude = source.Longitude, + CountryUuid = source.City.Region.Country.Guid, + CountryName = source.City.Region.Country.Name, + RegionUuid = source.City.Region.Guid, + RegionName = source.City.Region.Name, + CityUuid = source.City.Guid, + CityName = source.City.Name, + TimeToNextAddress = tag.TimeToNextAddress, + CostToNextAddress = costToNextAddress, + CurrentAddressStopTime = tag.CurrentAddressStopTime, + Order = addressOrder, + RouteAddressUuid = tag.RouteAddress.Guid + }); + + addressOrder++; + + + // First address after transfer + if (nextTag.VehicleEnrollmentId != tag.VehicleEnrollmentId) + { + target = path.Select(e => e.Target).ElementAt(i); + + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = target.Guid, + Name = target.Name, + Longitude = target.Latitude, + Latitude = target.Longitude, + CountryUuid = target.City.Region.Country.Guid, + CountryName = target.City.Region.Country.Name, + RegionUuid = target.City.Region.Guid, + RegionName = target.City.Region.Name, + CityUuid = target.City.Guid, + CityName = target.City.Name, + TimeToNextAddress = TimeSpan.Zero, + CostToNextAddress = 0, + CurrentAddressStopTime = TimeSpan.Zero, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + lastRouteAddressId = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Id; + + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + + vehicleEnrollmentDtos.Add( + new VehicleEnrollmentSearchVehicleEnrollmentDto() + { + DepartureTime = tag.VehicleEnrollment + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = tag.VehicleEnrollment + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = tag.VehicleEnrollment + .GetTravelTime(firstRouteAddressId, + lastRouteAddressId), + TimeMoving = tag.VehicleEnrollment + .GetTimeMoving(firstRouteAddressId, + lastRouteAddressId), + TimeInStops = tag.VehicleEnrollment + .GetTimeInStops(firstRouteAddressId, + lastRouteAddressId), + NumberOfStops = tag.VehicleEnrollment + .GetNumberOfStops(firstRouteAddressId, + lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, + VehicleType = source.VehicleType.Name, + Uuid = tag.VehicleEnrollment.Guid, + Order = enrollmentOrder, + Addresses = addressDtos, + Company = _mapper + .Map( + tag.VehicleEnrollment.Vehicle.Company), + Vehicle = _mapper + .Map( + tag.VehicleEnrollment.Vehicle) + }); + + firstRouteAddressId = nextTag.RouteAddressId; + + addressDtos = new List(); + addressOrder = (short)1; + enrollmentOrder++; + } + } + + source = path.Select(e => e.Source).Last(); + target = path.Select(e => e.Target).Last(); + tag = path.Select(e => e.Tag).Last(); + nextTag = path.Select(e => e.Tag).Last(); + + + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = source.Guid, + Name = source.Name, + Longitude = source.Latitude, + Latitude = source.Longitude, + CountryUuid = source.City.Region.Country.Guid, + CountryName = source.City.Region.Country.Name, + RegionUuid = source.City.Region.Guid, + RegionName = source.City.Region.Name, + CityUuid = source.City.Guid, + CityName = source.City.Name, + TimeToNextAddress = tag.TimeToNextAddress, + CostToNextAddress = costToNextAddress, + CurrentAddressStopTime = tag.CurrentAddressStopTime, + Order = addressOrder, + RouteAddressUuid = tag.RouteAddress.Guid + }); + + addressOrder++; + + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = target.Guid, + Name = target.Name, + Longitude = target.Latitude, + Latitude = target.Longitude, + CountryUuid = target.City.Region.Country.Guid, + CountryName = target.City.Region.Country.Name, + RegionUuid = target.City.Region.Guid, + RegionName = target.City.Region.Name, + CityUuid = target.City.Guid, + CityName = target.City.Name, + TimeToNextAddress = TimeSpan.Zero, + CostToNextAddress = 0, + CurrentAddressStopTime = TimeSpan.Zero, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + lastRouteAddressId = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Id; + + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + + vehicleEnrollmentDtos.Add( + new VehicleEnrollmentSearchVehicleEnrollmentDto() + { + DepartureTime = tag.VehicleEnrollment + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = tag.VehicleEnrollment + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = tag.VehicleEnrollment + .GetTravelTime(firstRouteAddressId, + lastRouteAddressId), + TimeMoving = tag.VehicleEnrollment + .GetTimeMoving(firstRouteAddressId, + lastRouteAddressId), + TimeInStops = tag.VehicleEnrollment + .GetTimeInStops(firstRouteAddressId, + lastRouteAddressId), + NumberOfStops = tag.VehicleEnrollment + .GetNumberOfStops(firstRouteAddressId, + lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, + VehicleType = source.VehicleType.Name, + Uuid = tag.VehicleEnrollment.Guid, + Order = enrollmentOrder, + Addresses = addressDtos, + Company = _mapper.Map( + tag.VehicleEnrollment.Vehicle.Company), + Vehicle = _mapper.Map( + tag.VehicleEnrollment.Vehicle) + }); + } + else + { + throw new NotFoundException(); + } + + + var departureTime = vehicleEnrollmentDtos + .OrderBy(e => e.Order).First().DepartureTime; + var arrivalTime = vehicleEnrollmentDtos + .OrderBy(e => e.Order).Last().ArrivalTime; + var timeInStops = vehicleEnrollmentDtos + .Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops); + var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1; + var cost = vehicleEnrollmentDtos + .Aggregate((decimal)0, (sum, next) => sum += next.Cost); + + return new VehicleEnrollmentSearchDto() + { + DepartureTime = departureTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = arrivalTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = arrivalTime - departureTime, + TimeInStops = timeInStops, + NumberOfTransfers = numberOfTransfers, + Currency = _sessionCurrencyService.Currency.Name, + Cost = cost, + Enrollments = vehicleEnrollmentDtos + }; + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs new file mode 100644 index 0000000..674e636 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchShortest; + +public class SearchShortestQueryValidator : + AbstractValidator +{ + public SearchShortestQueryValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.DepartureAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.ArrivalAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.DepartureDate) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow)) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow))); + + RuleForEach(v => v.VehicleTypes) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchAddressDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchAddressDto.cs new file mode 100644 index 0000000..01b2602 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchAddressDto.cs @@ -0,0 +1,34 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; + +public sealed class VehicleEnrollmentSearchAddressDto +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + public short Order { get; set; } + + public Guid RouteAddressUuid { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchCompanyDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchCompanyDto.cs new file mode 100644 index 0000000..40e5155 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchCompanyDto.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; + +public sealed class VehicleEnrollmentSearchCompanyDto : IMapFrom +{ + public string 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/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs new file mode 100644 index 0000000..e8fc53b --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs @@ -0,0 +1,21 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; + +public sealed class VehicleEnrollmentSearchDto +{ + public DateTimeOffset DepartureTime { get; set; } + + public DateTimeOffset ArrivalTime { get; set; } + + public TimeSpan TravelTime { get; set; } + + public TimeSpan TimeInStops { get; set; } + + public int NumberOfTransfers { get; set; } + + public string Currency { get; set; } + + public decimal Cost { get; set; } + + public ICollection + Enrollments { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleDto.cs new file mode 100644 index 0000000..acc05f4 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleDto.cs @@ -0,0 +1,94 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; + +public sealed class VehicleEnrollmentSearchVehicleDto : IMapFrom +{ + public string Uuid { get; set; } + + public string Type { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Type, + opt => opt.MapFrom(s => s.VehicleType.Name)) + .ForMember( + d => d.Number, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Number; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Number; + } + else if (s is Train) + { + return ((Train)s).Number; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Model, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Model; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Model; + } + else if (s is Train) + { + return ((Train)s).Model; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Capacity, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Capacity; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Capacity; + } + else if (s is Train) + { + return ((Train)s).Capacity; + } + else + { + throw new NotImplementedException(); + } + })); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs new file mode 100644 index 0000000..37c959d --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchVehicleEnrollmentDto.cs @@ -0,0 +1,33 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; + +public sealed class VehicleEnrollmentSearchVehicleEnrollmentDto +{ + public string VehicleType { get; set; } + + public DateTimeOffset DepartureTime { get; set; } + + public DateTimeOffset ArrivalTime { get; set; } + + public TimeSpan TravelTime { get; set; } + + public TimeSpan TimeMoving { get; set; } + + public TimeSpan TimeInStops { get; set; } + + public int NumberOfStops { get; set; } + + public string Currency { get; set; } + + public decimal Cost { get; set; } + + public Guid Uuid { get; set; } + + public short Order { get; set; } + + public VehicleEnrollmentSearchCompanyDto Company { get; set; } + + public VehicleEnrollmentSearchVehicleDto Vehicle { get; set; } + + public ICollection + Addresses { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs b/src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs new file mode 100644 index 0000000..d072655 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs @@ -0,0 +1,32 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels; + +public sealed class SearchAllViewModel +{ + public Guid DepartureAddressUuid { get; set; } + + public Guid ArrivalAddressUuid { get; set; } + + public DateOnly DepartureDate { get; set; } + + public HashSet VehicleTypes { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + + public decimal? CostGreaterThanOrEqualTo { get; set; } + + public decimal? CostLessThanOrEqualTo { get; set; } + + public short? NumberOfTransfersGreaterThanOrEqualTo { get; set; } + + public short? NumberOfTransfersLessThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeLessThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeLessThanOrEqualTo { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/ViewModels/SearchShortestViewModel.cs b/src/Application/VehicleEnrollmentSearch/ViewModels/SearchShortestViewModel.cs new file mode 100644 index 0000000..aa9813a --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/ViewModels/SearchShortestViewModel.cs @@ -0,0 +1,16 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels; + +public sealed class SearchShortestViewModel +{ + public Guid DepartureAddressUuid { get; set; } + + public Guid ArrivalAddressUuid { get; set; } + + public DateOnly DepartureDate { get; set; } + + public HashSet VehicleTypes { get; set; } + + public bool ShortestByCost { get; set; } + + public bool ShortestByTime { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs new file mode 100644 index 0000000..72ff69b --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommand.cs @@ -0,0 +1,22 @@ +using cuqmbr.TravelGuide.Application.VehicleEnrollments.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.AddVehicleEnrollment; + +public record AddVehicleEnrollmentCommand : IRequest +{ + public DateTimeOffset DepartureTime { get; set; } + + public Currency Currency { get; set; } + + + public Guid VehicleGuid { get; set; } + + public Guid RouteGuid { get; set; } + + public ICollection RouteAddressDetails { get; set; } + + public ICollection EmployeeGuids { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs new file mode 100644 index 0000000..cf7e8db --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandAuthorizer.cs @@ -0,0 +1,60 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.AddVehicleEnrollment; + +public class AddVehicleEnrollmentCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public AddVehicleEnrollmentCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(AddVehicleEnrollmentCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicle = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.VehicleGuid, e => e.Company.Account, + CancellationToken.None) + .Result; + + var employees = _unitOfWork.EmployeeRepository + .GetPageAsync( + e => request.EmployeeGuids.Contains(e.Guid), + e => e.Company.Account, + 1, request.EmployeeGuids.Count, CancellationToken.None) + .Result.Items; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicle?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + + foreach (var employee in employees) + { + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = employee.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } + } +} diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs new file mode 100644 index 0000000..dc82b55 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs @@ -0,0 +1,236 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using FluentValidation.Results; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.AddVehicleEnrollment; + +public class AddVehicleEnrollmentCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly IStringLocalizer _localizer; + + public AddVehicleEnrollmentCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + AddVehicleEnrollmentCommand request, + CancellationToken cancellationToken) + { + // Check if the vehicle exists. + + var vehicle = await _unitOfWork.VehicleRepository.GetOneAsync( + e => e.Guid == request.VehicleGuid, cancellationToken); + + if (vehicle == null) + { + throw new NotFoundException( + $"Vehicle with Guid: {request.VehicleGuid} not found."); + } + + + // Check if the route exists. + + var route = await _unitOfWork.RouteRepository.GetOneAsync( + e => e.Guid == request.RouteGuid, e => e.RouteAddresses, + cancellationToken); + + if (route == null) + { + throw new NotFoundException( + $"Route with Guid: {request.RouteGuid} not found."); + } + + + // Check if specified vehicle and route compatible. + + if (vehicle.VehicleType != route.VehicleType) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.VehicleGuid), + ErrorMessage = _localizer["Validation.SameVehicleType"] + } + }); + } + + + // Check that request has the same Route Addresses + // as a route data from datastore. + + var sameRouteAddresses = route.RouteAddresses.All( + ra => request.RouteAddressDetails.Any( + rad => rad.RouteAddressGuid == ra.Guid)); + + if (!sameRouteAddresses) + { + throw new NotFoundException( + $"Not all route addresses are found in a datastore."); + } + + // Check vehicle enrollments that might overlap with new one. + + var requestDepartureTime = request.DepartureTime; + + var requestTravelTime = + request.RouteAddressDetails.Aggregate( + TimeSpan.Zero, (sum, rad) => sum + + rad.TimeToNextAddress + rad.CurrentAddressStopTime); + + var requestArrivalTime = requestDepartureTime + requestTravelTime; + + var enrollmentHistory = + await _unitOfWork.VehicleEnrollmentRepository.GetPageAsync( + e => + e.Vehicle.Guid == request.VehicleGuid && + e.DepartureTime >= DateTimeOffset.UtcNow.AddDays(-7), + e => e.RouteAddressDetails, + 1, 200, cancellationToken); + + // Three cases are included: + // + // ---SD---------RD----------SA---> + // time + // + // ---SD---------RA----------SA---> + // time + // + // ---RD-----SD-------SA-----RA---> + // time + // Where: + // RD - request enrollment departure time + // RA - request enrollment arrival time + // SD - datastore enrollment (S for store) departure time + // SA - datastore enrollment (S for store) arrival time + + var overlappingWithOtherEnrollments = enrollmentHistory.Items + .Where(ve => + { + var departureTime = ve.DepartureTime; + + var arrivalTime = + ve.DepartureTime + + ve.RouteAddressDetails + .Aggregate( + TimeSpan.Zero, + (sum, rad) => sum + + rad.TimeToNextAddress + + rad.CurrentAddressStopTime); + + return + (requestDepartureTime >= departureTime && + requestDepartureTime <= arrivalTime) || + (requestArrivalTime >= departureTime && + requestArrivalTime <= arrivalTime) || + (requestDepartureTime <= departureTime && + requestArrivalTime >= arrivalTime); + }) + .Any(); + + if (overlappingWithOtherEnrollments) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.DepartureTime), + ErrorMessage = _localizer["Validation." + + "VehicleEnrollments.OverlapWithOther"] + } + }); + } + + + // Check if employee guids are valid + + var employees = (await _unitOfWork.EmployeeRepository + .GetPageAsync( + e => request.EmployeeGuids.Contains(e.Guid), + 1, request.EmployeeGuids.Count, cancellationToken)) + .Items; + + if (employees.Count < request.EmployeeGuids.Count) + { + throw new NotFoundException(); + } + + + // Create entity and add to datastore. + + var entity = new VehicleEnrollment() + { + DepartureTime = request.DepartureTime, + Currency = request.Currency, + VehicleId = vehicle.Id, + RouteId = route.Id, + Route = route, + Vehicle = vehicle, + RouteAddressDetails = route.RouteAddresses + .OrderBy(ra => ra.Order) + .Select(ra => new RouteAddressDetail() + { + TimeToNextAddress = request.RouteAddressDetails + .First(rad => rad.RouteAddressGuid == ra.Guid) + .TimeToNextAddress, + CostToNextAddress = request.RouteAddressDetails + .First(rad => rad.RouteAddressGuid == ra.Guid) + .CostToNextAddress, + CurrentAddressStopTime = request.RouteAddressDetails + .First(rad => rad.RouteAddressGuid == ra.Guid) + .CurrentAddressStopTime, + RouteAddressId = ra.Id + }) + .ToArray(), + VehicleEnrollmentEmployees = request.EmployeeGuids.Select(g => + new VehicleEnrollmentEmployee() + { + EmployeeId = employees.Single(e => e.Guid == g).Id + }) + .ToArray() + }; + + entity = await _unitOfWork.VehicleEnrollmentRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + + // Hydrate vehicle enrollment with address information + + var routeAddresses = await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + request.RouteAddressDetails + .Select(rad => rad.RouteAddressGuid) + .Contains(e.Guid), + e => e.Address.City.Region.Country, + 1, entity.RouteAddressDetails.Count(), + cancellationToken); + + foreach (var rad in entity.RouteAddressDetails) + { + rad.RouteAddress = routeAddresses.Items + .First(ra => ra.Id == rad.RouteAddressId); + } + + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs new file mode 100644 index 0000000..415a15d --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandValidator.cs @@ -0,0 +1,100 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.AddVehicleEnrollment; + +public class AddVehicleEnrollmentCommandValidator : + AbstractValidator +{ + public AddVehicleEnrollmentCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService, + SessionTimeZoneService timeZoneService) + { + RuleFor(v => v.DepartureTime) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .GreaterThanOrEqualTo(DateTimeOffset.Now) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateTimeOffset.Now.ToOffset(timeZoneService.TimeZone.BaseUtcOffset))); + + RuleFor(v => v.Currency) + .Must(c => Currency.Enumerations.ContainsValue(c)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Currency.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(v => v.RouteAddressDetails.Count) + .GreaterThanOrEqualTo(2) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 2)); + + RuleForEach(v => v.RouteAddressDetails).ChildRules(rad => + { + rad.RuleFor(rad => rad.RouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + rad.RuleFor(rad => rad.TimeToNextAddress) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + + rad.RuleFor(rad => rad.CostToNextAddress) + .GreaterThanOrEqualTo(0) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 0)); + + rad.RuleFor(rad => rad.CurrentAddressStopTime) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + }); + + RuleFor(v => v.VehicleGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.RouteGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.EmployeeGuids.Count) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.EmployeeGuids) + .IsUnique(g => g) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(v => v.EmployeeGuids) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommand.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommand.cs new file mode 100644 index 0000000..a11cbd2 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; + +public record DeleteVehicleEnrollmentCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs new file mode 100644 index 0000000..d880e73 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; + +public class DeleteVehicleEnrollmentCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public DeleteVehicleEnrollmentCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(DeleteVehicleEnrollmentCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicelEnrollment = _unitOfWork.VehicleEnrollmentRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Vehicle.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicelEnrollment?.Vehicle.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs new file mode 100644 index 0000000..b743321 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; + +public class DeleteVehicleEnrollmentCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteVehicleEnrollmentCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteVehicleEnrollmentCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.VehicleEnrollmentRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // TODO: Check for tickets bought for this enrollment. + // Decide whether to cancel tickets or do not allow deletion. + + await _unitOfWork.VehicleEnrollmentRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandValidator.cs new file mode 100644 index 0000000..0126b0b --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/DeleteVehicleEnrollment/DeleteVehicleEnrollmentCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Commands.DeleteVehicleEnrollment; + +public class DeleteVehicleEnrollmentCommandValidator : AbstractValidator +{ + public DeleteVehicleEnrollmentCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs new file mode 100644 index 0000000..3fff097 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommand.cs @@ -0,0 +1,20 @@ +using cuqmbr.TravelGuide.Application.VehicleEnrollments.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.UpdateVehicleEnrollment; + +public record UpdateVehicleEnrollmentCommand : IRequest +{ + public Guid Guid { get; set; } + + public DateTimeOffset DepartureTime { get; set; } + + public Currency Currency { get; set; } + + + public ICollection RouteAddressDetails { get; set; } + + public ICollection EmployeeGuids { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs new file mode 100644 index 0000000..ddc7bef --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandAuthorizer.cs @@ -0,0 +1,60 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.UpdateVehicleEnrollment; + +public class UpdateVehicleEnrollmentCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public UpdateVehicleEnrollmentCommandAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(UpdateVehicleEnrollmentCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicleEnrollment = _unitOfWork.VehicleEnrollmentRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Vehicle.Company.Account, + CancellationToken.None) + .Result; + + var employees = _unitOfWork.EmployeeRepository + .GetPageAsync( + e => request.EmployeeGuids.Contains(e.Guid), + e => e.Company.Account, + 1, request.EmployeeGuids.Count, CancellationToken.None) + .Result.Items; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicleEnrollment?.Vehicle.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + + foreach (var employee in employees) + { + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = employee.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } + } +} diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs new file mode 100644 index 0000000..0251fef --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs @@ -0,0 +1,239 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using FluentValidation.Results; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.UpdateVehicleEnrollment; + +public class UpdateVehicleEnrollmentCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly IStringLocalizer _localizer; + + public UpdateVehicleEnrollmentCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _localizer = localizer; + } + + public async Task Handle( + UpdateVehicleEnrollmentCommand request, + CancellationToken cancellationToken) + { + // TODO: Check for tickets bought for this enrollment. + // Decide whether allow or not to perform update action. + + var entity = await _unitOfWork.VehicleEnrollmentRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.RouteAddressDetails, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // Check that request has the same Route Addresses + // as a route data from datastore. + + var route = await _unitOfWork.RouteRepository.GetOneAsync( + e => e.Id == entity.RouteId, e => e.RouteAddresses, + cancellationToken); + + var sameRouteAddresses = route.RouteAddresses.All( + ra => request.RouteAddressDetails.Any( + rad => rad.RouteAddressGuid == ra.Guid)); + + if (!sameRouteAddresses) + { + throw new NotFoundException( + $"Not all route addresses are found in a datastore."); + } + + + // Check vehicle enrollments that might overlap with updated one. + // Exclude this vehicle enrollment. + + var requestDepartureTime = request.DepartureTime; + + var requestTravelTime = + request.RouteAddressDetails.Aggregate( + TimeSpan.Zero, (sum, rad) => sum + + rad.TimeToNextAddress + rad.CurrentAddressStopTime); + + var requestArrivalTime = requestDepartureTime + requestTravelTime; + + var enrollmentHistory = + await _unitOfWork.VehicleEnrollmentRepository.GetPageAsync( + e => + e.Vehicle.Id == entity.VehicleId && + e.Id != entity.Id && + e.DepartureTime >= DateTimeOffset.UtcNow.AddDays(-7), + e => e.RouteAddressDetails, + 1, 200, cancellationToken); + + // Three cases are included: + // + // ---SD---------RD----------SA---> + // time + // + // ---SD---------RA----------SA---> + // time + // + // ---RD-----SD-------SA-----RA---> + // time + // Where: + // RD - request enrollment departure time + // RA - request enrollment arrival time + // SD - datastore enrollment (S for store) departure time + // SA - datastore enrollment (S for store) arrival time + + var overlappingWithOtherEnrollments = enrollmentHistory.Items + .Where(ve => + { + var departureTime = ve.DepartureTime; + + var arrivalTime = + ve.DepartureTime + + ve.RouteAddressDetails + .Aggregate( + TimeSpan.Zero, + (sum, rad) => sum + + rad.TimeToNextAddress + + rad.CurrentAddressStopTime); + + return + (requestDepartureTime >= departureTime && + requestDepartureTime <= arrivalTime) || + (requestArrivalTime >= departureTime && + requestArrivalTime <= arrivalTime) || + (requestDepartureTime <= departureTime && + requestArrivalTime >= arrivalTime); + }) + .Any(); + + if (overlappingWithOtherEnrollments) + { + throw new ValidationException( + new List + { + new() + { + PropertyName = nameof(request.DepartureTime), + ErrorMessage = _localizer["Validation." + + "VehicleEnrollments.OverlapWithOther"] + } + }); + } + + + // Check if employee guids are valid + + var employees = (await _unitOfWork.EmployeeRepository + .GetPageAsync( + e => request.EmployeeGuids.Contains(e.Guid), + 1, request.EmployeeGuids.Count, cancellationToken)) + .Items; + + if (employees.Count < request.EmployeeGuids.Count) + { + throw new NotFoundException(); + } + + + // Get vehicle and hydrate vehicle enrollment + + var vehicle = await _unitOfWork.VehicleRepository + .GetOneAsync(e => e.Id == entity.VehicleId, cancellationToken); + entity.Vehicle = vehicle; + + // Update entity and add to datastore. + + var requestEmployeeGuids = request.EmployeeGuids; + var datastoreEmployeeGuids = (await _unitOfWork + .VehicleEnrollmentEmployeeRepository.GetPageAsync( + e => e.VehicleEnrollmentId == entity.Id, + e => e.Employee, + 1, int.MaxValue, cancellationToken)) + .Items + .Select(e => e.Employee.Guid); + + var commonEmployeeGuids = datastoreEmployeeGuids + .Intersect(requestEmployeeGuids); + + var newEmployeeGuids = requestEmployeeGuids + .Except(datastoreEmployeeGuids); + + var combinedEmployeeGuids = commonEmployeeGuids + .Union(newEmployeeGuids); + + entity.VehicleEnrollmentEmployees = combinedEmployeeGuids + .Select(g => + new VehicleEnrollmentEmployee() + { + EmployeeId = employees.Single(e => e.Guid == g).Id, + VehicleEnrollmentId = entity.Id + }) + .ToList(); + + + entity.DepartureTime = request.DepartureTime; + entity.Currency = request.Currency; + + foreach (var rad in entity.RouteAddressDetails) + { + var correspondingRouteAddress = route.RouteAddresses + .First(ra => ra.Id == rad.RouteAddressId); + + rad.TimeToNextAddress = request.RouteAddressDetails + .First(rrad => rrad.RouteAddressGuid == rad.RouteAddress.Guid) + .TimeToNextAddress; + rad.CostToNextAddress = request.RouteAddressDetails + .First(rrad => rrad.RouteAddressGuid == rad.RouteAddress.Guid) + .CostToNextAddress; + rad.CurrentAddressStopTime = request.RouteAddressDetails + .First(rrad => rrad.RouteAddressGuid == rad.RouteAddress.Guid) + .CurrentAddressStopTime; + } + + entity = await _unitOfWork.VehicleEnrollmentRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + + // Hydrate vehicle enrollment with address information + + var routeAddresses = await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + entity.RouteAddressDetails + .Select(rad => rad.RouteAddressId) + .Contains(e.Id), + e => e.Address.City.Region.Country, + 1, entity.RouteAddressDetails.Count(), + cancellationToken); + + foreach (var rad in entity.RouteAddressDetails) + { + rad.RouteAddress = routeAddresses.Items + .First(ra => ra.Id == rad.RouteAddressId); + } + + entity.RouteAddressDetails = entity.RouteAddressDetails + .OrderBy(rad => rad.RouteAddress.Order) + .ToArray(); + + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs new file mode 100644 index 0000000..9e33153 --- /dev/null +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandValidator.cs @@ -0,0 +1,96 @@ +using cuqmbr.TravelGuide.Application.Common.FluentValidation; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.UpdateVehicleEnrollment; + +public class UpdateVehicleEnrollmentCommandValidator : + AbstractValidator +{ + public UpdateVehicleEnrollmentCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService, + SessionTimeZoneService timeZoneService) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.DepartureTime) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .GreaterThanOrEqualTo(DateTimeOffset.Now) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateTimeOffset.Now.ToOffset(timeZoneService.TimeZone.BaseUtcOffset))); + + RuleFor(v => v.Currency) + .Must(c => Currency.Enumerations.ContainsValue(c)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + Currency.Enumerations.Values.Select(e => e.Name)))); + + RuleFor(v => v.RouteAddressDetails.Count) + .GreaterThanOrEqualTo(2) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 2)); + + RuleForEach(v => v.RouteAddressDetails).ChildRules(rad => + { + rad.RuleFor(rad => rad.RouteAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + rad.RuleFor(rad => rad.TimeToNextAddress) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + + rad.RuleFor(rad => rad.CostToNextAddress) + .GreaterThanOrEqualTo(0) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 0)); + + rad.RuleFor(rad => rad.CurrentAddressStopTime) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + }); + + RuleFor(v => v.EmployeeGuids.Count) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.EmployeeGuids) + .IsUnique(g => g) + .WithMessage(localizer["FluentValidation.IsUnique"]); + + RuleForEach(v => v.EmployeeGuids) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/VehicleEnrollments/Models/RouteAddressDetailModel.cs b/src/Application/VehicleEnrollments/Models/RouteAddressDetailModel.cs new file mode 100644 index 0000000..e3d972c --- /dev/null +++ b/src/Application/VehicleEnrollments/Models/RouteAddressDetailModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Models; + +public sealed class RouteAddressDetailModel +{ + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + + public Guid RouteAddressGuid { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQuery.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQuery.cs new file mode 100644 index 0000000..4220c6f --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQuery.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollment; + +public record GetVehicleEnrollmentQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs new file mode 100644 index 0000000..32e3d60 --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryAuthorizer.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollment; + +public class GetVehicleEnrollmentQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetVehicleEnrollmentQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetVehicleEnrollmentQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicelEnrollment = _unitOfWork.VehicleEnrollmentRepository + .GetOneAsync( + e => e.Guid == request.Guid, e => e.Vehicle.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicelEnrollment?.Vehicle.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs new file mode 100644 index 0000000..f41760e --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryHandler.cs @@ -0,0 +1,104 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollment; + +public class GetVehicleEnrollmentQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + + public GetVehicleEnrollmentQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; + } + + public async Task Handle( + GetVehicleEnrollmentQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.VehicleEnrollmentRepository.GetOneAsync( + e => e.Guid == request.Guid, e => e.RouteAddressDetails, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + // Hydrate vehicle enrollment with address, + // vehicle, route and employee information. + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + entity.RouteAddressDetails + .Select(rad => rad.RouteAddressId) + .Contains(e.Id), + e => e.Address.City.Region.Country, + 1, entity.RouteAddressDetails.Count(), cancellationToken)) + .Items; + + foreach (var rad in entity.RouteAddressDetails) + { + rad.RouteAddress = routeAddresses + .First(ra => ra.Id == rad.RouteAddressId); + } + + var vehicle = await _unitOfWork.VehicleRepository + .GetOneAsync(e => e.Id == entity.VehicleId, cancellationToken); + + var route = await _unitOfWork.RouteRepository + .GetOneAsync(e => e.Id == entity.RouteId, cancellationToken); + + var vehicleEnrollmentEmployees = + (await _unitOfWork.VehicleEnrollmentEmployeeRepository + .GetPageAsync( + e => e.VehicleEnrollmentId == entity.Id, + e => e.Employee, + 1, int.MaxValue, cancellationToken)) + .Items; + + entity.Vehicle = vehicle; + entity.Route = route; + entity.VehicleEnrollmentEmployees = + vehicleEnrollmentEmployees.ToArray(); + + + // Convert currency + + // TODO: Replace with AutoMapper Resolver + + if (!_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + foreach (var rad in entity.RouteAddressDetails) + { + rad.CostToNextAddress = await _currencyConverterService + .ConvertAsync(rad.CostToNextAddress, entity.Currency, + _sessionCurrencyService.Currency, cancellationToken); + } + entity.Currency = _sessionCurrencyService.Currency; + } + + + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryValidator.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryValidator.cs new file mode 100644 index 0000000..8a5931f --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollment/GetVehicleEnrollmentQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Queries.GetVehicleEnrollment; + +public class GetVehicleEnrollmentQueryValidator : AbstractValidator +{ + public GetVehicleEnrollmentQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs new file mode 100644 index 0000000..1253cd9 --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQuery.cs @@ -0,0 +1,54 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollmentsPage; + +public record GetVehicleEnrollmentsPageQuery : + IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + // public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? RouteGuid { get; set; } + + public Guid? VehicleGuid { get; set; } + + public int? NumberOfAddressesGreaterThanOrEqual { get; set; } + + public int? NumberOfAddressesLessThanOrEqual { get; set; } + + public DateTimeOffset? DepartureTimeGreaterThanOrEqual { get; set; } + + public DateTimeOffset? DepartureTimeLessThanOrEqual { get; set; } + + public DateTimeOffset? ArrivalTimeGreaterThanOrEqual { get; set; } + + public DateTimeOffset? ArrivalTimeLessThanOrEqual { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqual { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqual { get; set; } + + public TimeSpan? TimeMovingGreaterThanOrEqual { get; set; } + + public TimeSpan? TimeMovingLessThanOrEqual { get; set; } + + public TimeSpan? TimeInStopsGreaterThanOrEqual { get; set; } + + public TimeSpan? TimeInStopsLessThanOrEqual { get; set; } + + public decimal? CostGreaterThanOrEqual { get; set; } + + public decimal? CostLessThanOrEqual { get; set; } + + public Currency? Currency { get; set; } + + public ICollection? EmployeeGuids { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs new file mode 100644 index 0000000..ec0cbaa --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryAuthorizer.cs @@ -0,0 +1,42 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Queries.GetVehicleEnrollmentsPage; + +public class GetVehicleEnrollmentsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + private readonly UnitOfWork _unitOfWork; + + public GetVehicleEnrollmentsPageQueryAuthorizer( + SessionUserService sessionUserService, + UnitOfWork unitOfWork) + { + _sessionUserService = sessionUserService; + _unitOfWork = unitOfWork; + } + + public override void BuildPolicy(GetVehicleEnrollmentsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + + var vehicles = _unitOfWork.VehicleRepository + .GetOneAsync( + e => e.Guid == request.VehicleGuid, e => e.Company.Account, + CancellationToken.None) + .Result; + + UseRequirement(new MustBeObjectOwnerOrAdminRequirement + { + UserRoles = _sessionUserService.Roles, + RequiredGuid = vehicles?.Company.Account.Guid, + UserGuid = _sessionUserService.Guid + }); + } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs new file mode 100644 index 0000000..af32246 --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryHandler.cs @@ -0,0 +1,230 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollmentsPage; + +public class GetVehicleEnrollmentsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + + public GetVehicleEnrollmentsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; + } + + public async Task> Handle( + GetVehicleEnrollmentsPageQuery request, + CancellationToken cancellationToken) + { + // TODO: Add search functionality or remove it + var pagedList = await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => + // (e.Name.ToLower().Contains(request.Search.ToLower()) || + // e.City.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && + (request.RouteGuid != null + ? e.Route.Guid == request.RouteGuid + : true) && + (request.VehicleGuid != null + ? e.Vehicle.Guid >= request.VehicleGuid + : true) && + (request.NumberOfAddressesGreaterThanOrEqual != null + ? + e.RouteAddressDetails.Count() >= + request.NumberOfAddressesGreaterThanOrEqual + : true) && + (request.NumberOfAddressesLessThanOrEqual != null + ? + e.RouteAddressDetails.Count() <= + request.NumberOfAddressesLessThanOrEqual + : true) && + (request.DepartureTimeGreaterThanOrEqual != null + ? e.DepartureTime >= request.DepartureTimeGreaterThanOrEqual + : true) && + (request.DepartureTimeLessThanOrEqual != null + ? e.DepartureTime <= request.DepartureTimeLessThanOrEqual + : true) && + (request.ArrivalTimeGreaterThanOrEqual != null + ? + e.DepartureTime.AddSeconds(e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds + + rad.CurrentAddressStopTime.TotalSeconds)) >= + request.ArrivalTimeGreaterThanOrEqual + : true) && + (request.ArrivalTimeLessThanOrEqual != null + ? + e.DepartureTime.AddSeconds(e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds + + rad.CurrentAddressStopTime.TotalSeconds)) <= + request.ArrivalTimeLessThanOrEqual + : true) && + (request.TravelTimeGreaterThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) >= + request.TravelTimeGreaterThanOrEqual.Value.TotalSeconds + : true) && + (request.TravelTimeLessThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) <= + request.TravelTimeLessThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeMovingGreaterThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) >= + request.TimeMovingGreaterThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeMovingLessThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.TimeToNextAddress.TotalSeconds) <= + request.TimeMovingLessThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeInStopsGreaterThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.CurrentAddressStopTime.TotalSeconds) >= + request.TimeInStopsGreaterThanOrEqual.Value.TotalSeconds + : true) && + (request.TimeInStopsLessThanOrEqual != null + ? + e.RouteAddressDetails + .Sum(rad => rad.CurrentAddressStopTime.TotalSeconds) <= + request.TimeInStopsLessThanOrEqual.Value.TotalSeconds + : true) && + (request.CostGreaterThanOrEqual != null + ? e.RouteAddressDetails.Sum(rad => rad.CostToNextAddress) >= + request.CostGreaterThanOrEqual + : true) && + (request.CostLessThanOrEqual != null + ? e.RouteAddressDetails.Sum(rad => rad.CostToNextAddress) <= + request.CostLessThanOrEqual + : true) && + (request.Currency != null + ? e.Currency == request.Currency + : true) && + (request.EmployeeGuids != null + ? e.VehicleEnrollmentEmployees.Any(e => request.EmployeeGuids.Contains(e.Employee.Guid)) + : true), + e => e.RouteAddressDetails, + request.PageNumber, request.PageSize, cancellationToken); + + var vehicleEnrollments = pagedList.Items; + + var vehicleIds = vehicleEnrollments.Select(e => e.VehicleId); + var vehicles = (await _unitOfWork.VehicleRepository + .GetPageAsync( + e => vehicleIds.Contains(e.Id), + 1, vehicleIds.Count(), cancellationToken)) + .Items; + + var routeIds = vehicleEnrollments.Select(e => e.RouteId); + var routes = (await _unitOfWork.RouteRepository + .GetPageAsync( + e => routeIds.Contains(e.Id), + 1, routeIds.Count(), cancellationToken)) + .Items; + + + // Hydrate vehicle enrollment with address, + // vehicle, route and employee information. + + var routeAddressIds = vehicleEnrollments + .SelectMany(ve => ve.RouteAddressDetails) + .Select(rad => rad.RouteAddressId); + + var routeAddresses = (await _unitOfWork.RouteAddressRepository + .GetPageAsync( + e => + routeAddressIds.Contains(e.Id), + e => e.Address.City.Region.Country, + 1, routeAddressIds.Count(), cancellationToken)) + .Items; + + var vehicleEnrollmentIds = vehicleEnrollments + .Select(ve => ve.Id); + + var vehicleEnrollmentEmployees = + (await _unitOfWork.VehicleEnrollmentEmployeeRepository + .GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.Employee, + 1, int.MaxValue, cancellationToken)) + .Items; + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + foreach (var routeAddressDetail in + vehicleEnrollment.RouteAddressDetails) + { + routeAddressDetail.RouteAddress = routeAddresses + .First(ra => ra.Id == routeAddressDetail.RouteAddressId); + } + + vehicleEnrollment.Route = routes + .Single(e => e.Id == vehicleEnrollment.RouteId); + + vehicleEnrollment.Vehicle = vehicles + .Single(e => e.Id == vehicleEnrollment.VehicleId); + + vehicleEnrollment.VehicleEnrollmentEmployees = + vehicleEnrollmentEmployees + .Where(vee => vee.VehicleEnrollmentId == vehicleEnrollment.Id) + .ToArray(); + } + + + // Convert currency + + // TODO: Replace with AutoMapper Resolver + + if (!_sessionCurrencyService.Currency.Equals(Currency.Default)) + { + foreach (var ve in vehicleEnrollments) + { + foreach (var rad in ve.RouteAddressDetails) + { + rad.CostToNextAddress = await _currencyConverterService + .ConvertAsync(rad.CostToNextAddress, ve.Currency, + _sessionCurrencyService.Currency, cancellationToken); + } + ve.Currency = _sessionCurrencyService.Currency; + } + } + + + var vehicleEnrollmentsDto = _mapper + .Map>(vehicleEnrollments) + .AsQueryable(); + + vehicleEnrollmentsDto = QueryableExtension + .ApplySort(vehicleEnrollmentsDto, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + vehicleEnrollmentsDto.ToList(), + pagedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs new file mode 100644 index 0000000..2d1d71b --- /dev/null +++ b/src/Application/VehicleEnrollments/Queries/GetVehicleEnrollmentsPage/GetVehicleEnrollmentsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.Queries.GetVehicleEnrollmentsPage; + +public class GetVehicleEnrollmentsPageQueryValidator : AbstractValidator +{ + public GetVehicleEnrollmentsPageQueryValidator( + 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/VehicleEnrollments/RouteAddressDetailDto.cs b/src/Application/VehicleEnrollments/RouteAddressDetailDto.cs new file mode 100644 index 0000000..fe8d741 --- /dev/null +++ b/src/Application/VehicleEnrollments/RouteAddressDetailDto.cs @@ -0,0 +1,29 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments; + +public sealed class RouteAddressDetailDto : IMapFrom +{ + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + public Guid RouteAddressUuid { get; set; } + + + public VehicleEnrollmentRouteAddressDto RouteAddress { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.RouteAddressUuid, + opt => opt.MapFrom(s => s.RouteAddress.Guid)) + .ForMember( + d => d.RouteAddress, + opt => opt.MapFrom(s => s.RouteAddress)); + } +} diff --git a/src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs b/src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs new file mode 100644 index 0000000..40ca6a3 --- /dev/null +++ b/src/Application/VehicleEnrollments/VehicleEnrollmentDto.cs @@ -0,0 +1,69 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments; + +public sealed class VehicleEnrollmentDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public DateTimeOffset DepartureTime { get; set; } + + public DateTimeOffset ArrivalTime => + DepartureTime + + RouteAddressDetails.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + public TimeSpan TravelTime => + RouteAddressDetails.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress + next.CurrentAddressStopTime); + + public TimeSpan TimeMoving => + RouteAddressDetails.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.TimeToNextAddress); + + public TimeSpan TimeInStops => + RouteAddressDetails.Aggregate(TimeSpan.Zero, (sum, next) => + sum + next.CurrentAddressStopTime); + + public decimal TotalCost => + RouteAddressDetails.Aggregate((decimal)0, (sum, next) => + sum + next.CostToNextAddress); + + public string Currency { get; set; } + + public Guid VehicleUuid { get; set; } + + public VehicleEnrollmentVehicleDto Vehicle { get; set; } + + public Guid RouteUuid { get; set; } + + public ICollection RouteAddressDetails { get; set; } + + // TODO: Add collection of employee dto objects + public ICollection EmployeeUuids { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.DepartureTime, + opt => opt + .MapFrom( + s => s.DepartureTime)) + .ForMember( + d => d.VehicleUuid, + opt => opt.MapFrom(s => s.Vehicle.Guid)) + .ForMember( + d => d.RouteUuid, + opt => opt.MapFrom(s => s.Route.Guid)) + .ForMember( + d => d.EmployeeUuids, + opt => opt.MapFrom(s => + s.VehicleEnrollmentEmployees.Select(e => e.Employee.Guid))); + } +} diff --git a/src/Application/VehicleEnrollments/VehicleEnrollmentRouteAddressDto.cs b/src/Application/VehicleEnrollments/VehicleEnrollmentRouteAddressDto.cs new file mode 100644 index 0000000..dae8127 --- /dev/null +++ b/src/Application/VehicleEnrollments/VehicleEnrollmentRouteAddressDto.cs @@ -0,0 +1,75 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments; + +public sealed class VehicleEnrollmentRouteAddressDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public short Order { get; set; } + + + public Guid AddressUuid { get; set; } + + public string AddressName { get; set; } + + public double AddressLongitude { get; set; } + + public double AddressLatitude { get; set; } + + public string AddressVehicleType { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public Guid CityUuid { get; set; } + + public string CityName { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.AddressUuid, + opt => opt.MapFrom(s => s.Address.Guid)) + .ForMember( + d => d.AddressName, + opt => opt.MapFrom(s => s.Address.Name)) + .ForMember( + d => d.AddressLongitude, + opt => opt.MapFrom(s => s.Address.Longitude)) + .ForMember( + d => d.AddressLatitude, + opt => opt.MapFrom(s => s.Address.Latitude)) + .ForMember( + d => d.AddressVehicleType, + opt => opt.MapFrom(s => s.Address.VehicleType.Name)) + .ForMember( + d => d.CityUuid, + opt => opt.MapFrom(s => s.Address.City.Guid)) + .ForMember( + d => d.CityName, + opt => opt.MapFrom(s => s.Address.City.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.Address.City.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.Address.City.Region.Name)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.Address.City.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.Address.City.Region.Country.Name)); + } +} diff --git a/src/Application/VehicleEnrollments/VehicleEnrollmentVehicleDto.cs b/src/Application/VehicleEnrollments/VehicleEnrollmentVehicleDto.cs new file mode 100644 index 0000000..a810f0a --- /dev/null +++ b/src/Application/VehicleEnrollments/VehicleEnrollmentVehicleDto.cs @@ -0,0 +1,94 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments; + +public sealed class VehicleEnrollmentVehicleDto : IMapFrom +{ + public string Uuid { get; set; } + + public string Type { get; set; } + + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.Type, + opt => opt.MapFrom(s => s.VehicleType.Name)) + .ForMember( + d => d.Number, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Number; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Number; + } + else if (s is Train) + { + return ((Train)s).Number; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Model, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Model; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Model; + } + else if (s is Train) + { + return ((Train)s).Model; + } + else + { + throw new NotImplementedException(); + } + })) + .ForMember( + d => d.Capacity, + opt => opt.MapFrom( + (s, d) => + { + if (s is Bus) + { + return ((Bus)s).Capacity; + } + else if (s is Aircraft) + { + return ((Aircraft)s).Capacity; + } + else if (s is Train) + { + return ((Train)s).Capacity; + } + else + { + throw new NotImplementedException(); + } + })); + } +} diff --git a/src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs new file mode 100644 index 0000000..deeb095 --- /dev/null +++ b/src/Application/VehicleEnrollments/ViewModels/AddVehicleEnrollmentViewModel.cs @@ -0,0 +1,17 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.ViewModels; + +public sealed class AddVehicleEnrollmentViewModel +{ + public DateTimeOffset DepartureTime { get; set; } + + public string Currency { get; set; } + + + public Guid VehicleUuid { get; set; } + + public Guid RouteUuid { get; set; } + + public ICollection RouteAddressDetails { get; set; } + + public ICollection EmployeeUuids { get; set; } +} diff --git a/src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs new file mode 100644 index 0000000..511799e --- /dev/null +++ b/src/Application/VehicleEnrollments/ViewModels/GetVehicleEnrollmentsPageFilterViewModel.cs @@ -0,0 +1,48 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.ViewModels; + +public sealed class GetVehicleEnrollmentsPageFilterViewModel +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? RouteGuid { get; set; } + + public Guid? VehicleGuid { get; set; } + + public int? NumberOfAddressesGreaterThanOrEqual { get; set; } + + public int? NumberOfAddressesLessThanOrEqual { get; set; } + + public DateTimeOffset? DepartureTimeGreaterThanOrEqual { get; set; } + + public DateTimeOffset? DepartureTimeLessThanOrEqual { get; set; } + + public DateTimeOffset? ArrivalTimeGreaterThanOrEqual { get; set; } + + public DateTimeOffset? ArrivalTimeLessThanOrEqual { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqual { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqual { get; set; } + + public TimeSpan? TimeMovingGreaterThanOrEqual { get; set; } + + public TimeSpan? TimeMovingLessThanOrEqual { get; set; } + + public TimeSpan? TimeInStopsGreaterThanOrEqual { get; set; } + + public TimeSpan? TimeInStopsLessThanOrEqual { get; set; } + + public decimal? CostGreaterThanOrEqual { get; set; } + + public decimal? CostLessThanOrEqual { get; set; } + + public string? Currency { get; set; } + + public ICollection? EmployeeUuids { get; set; } +} diff --git a/src/Application/VehicleEnrollments/ViewModels/RouteAddressDetailViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/RouteAddressDetailViewModel.cs new file mode 100644 index 0000000..e1aaa6f --- /dev/null +++ b/src/Application/VehicleEnrollments/ViewModels/RouteAddressDetailViewModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.ViewModels; + +public sealed class RouteAddressDetailViewModel +{ + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + + public Guid RouteAddressUuid { get; set; } +} diff --git a/src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs b/src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs new file mode 100644 index 0000000..e58befd --- /dev/null +++ b/src/Application/VehicleEnrollments/ViewModels/UpdateVehicleEnrollmentViewModel.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollments.ViewModels; + +public sealed class UpdateVehicleEnrollmentViewModel +{ + public DateTimeOffset DepartureTime { get; set; } + + public string Currency { get; set; } + + + public ICollection RouteAddressDetails { get; set; } + + public ICollection EmployeeUuids { get; set; } +} diff --git a/src/Application/packages.lock.json b/src/Application/packages.lock.json index 0718004..43c1312 100644 --- a/src/Application/packages.lock.json +++ b/src/Application/packages.lock.json @@ -48,6 +48,15 @@ "MediatR.Contracts": "2.0.1" } }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Direct", + "requested": "[9.0.5, )", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[9.0.4, )", @@ -59,11 +68,36 @@ "Microsoft.Extensions.Options": "9.0.4" } }, - "Microsoft.NET.ILLink.Tasks": { + "Microsoft.IdentityModel.JsonWebTokens": { "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" + "requested": "[8.11.0, )", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.11.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Direct", + "requested": "[8.11.0, )", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" + } + }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "QuikGraph": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" }, "System.Linq.Dynamic.Core": { "type": "Direct", @@ -176,10 +210,48 @@ "resolved": "9.0.4", "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.11.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, "domain": { "type": "Project" } - }, - "net9.0/linux-x64": {} + } } } \ No newline at end of file diff --git a/src/Configuration/Application/Configuration.cs b/src/Configuration/Application/Configuration.cs index cfbd70c..4f078cb 100644 --- a/src/Configuration/Application/Configuration.cs +++ b/src/Configuration/Application/Configuration.cs @@ -10,6 +10,8 @@ using MediatR.Behaviors.Authorization.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using cuqmbr.TravelGuide.Application.Common.Authorization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; namespace cuqmbr.TravelGuide.Configuration.Application; @@ -18,15 +20,16 @@ public static class Configuration public static IServiceCollection ConfigureApplication( this IServiceCollection services) { - var configurationOptions = services.BuildServiceProvider().GetService< + var configuration = services.BuildServiceProvider().GetService< IOptions>() .Value; return services - .AddLocalization(configurationOptions.Localization) + .AddLocalization(configuration.Localization) .AddFluentValidation() .AddAutoMapper() - .AddMediatR(); + .AddMediatR() + .AddAuthentication(configuration.JsonWebToken); } private static IServiceCollection AddFluentValidation( @@ -91,4 +94,42 @@ public static class Configuration .Assembly); }); } + + private static IServiceCollection AddAuthentication( + this IServiceCollection services, + JsonWebTokenConfigurationOptions configuration) + { + services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = + JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = + JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = + JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.IncludeErrorDetails = true; + options.SaveToken = true; + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = + new TokenValidationParameters() + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidAudience = configuration.Audience, + ValidIssuer = configuration.Issuer, + ClockSkew = TimeSpan.Zero, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes( + configuration.IssuerSigningKey)) + }; + }); + + return services; + } } diff --git a/src/Configuration/Configuration/Configuration.cs b/src/Configuration/Configuration/Configuration.cs index 0c6eaa9..7264d13 100644 --- a/src/Configuration/Configuration/Configuration.cs +++ b/src/Configuration/Configuration/Configuration.cs @@ -4,8 +4,8 @@ using PersistenceConfigurationOptions = cuqmbr.TravelGuide.Persistence.ConfigurationOptions; using ApplicationConfigurationOptions = cuqmbr.TravelGuide.Application.ConfigurationOptions; -using IdentityConfigurationOptions = - cuqmbr.TravelGuide.Identity.ConfigurationOptions; +using InfrastructureConfigurationOptions = + cuqmbr.TravelGuide.Infrastructure.ConfigurationOptions; namespace cuqmbr.TravelGuide.Configuration.Configuration; @@ -29,14 +29,12 @@ public static class Configuration configuration.GetSection( PersistenceConfigurationOptions.SectionName)); - services.AddOptions().Bind( - configuration.GetSection( - ApplicationConfigurationOptions.SectionName)); + services.AddOptions() + .Bind(configuration); + + services.AddOptions() + .Bind(configuration); - services.AddOptions().Bind( - configuration.GetSection( - IdentityConfigurationOptions.SectionName)); - return services; } } diff --git a/src/Configuration/Identity/Configuration.cs b/src/Configuration/Identity/Configuration.cs deleted file mode 100644 index d9c1d2a..0000000 --- a/src/Configuration/Identity/Configuration.cs +++ /dev/null @@ -1,96 +0,0 @@ -using cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; -using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; -using Microsoft.EntityFrameworkCore; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Identity.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; -using System.Text; -using cuqmbr.TravelGuide.Identity.Exceptions; - -namespace cuqmbr.TravelGuide.Configuration.Identity; - -public static class Configuration -{ - public static IServiceCollection ConfigureIdentity( - this IServiceCollection services) - { - using var configurationServiceProvider = services.BuildServiceProvider(); - var configuration = configurationServiceProvider.GetService< - IOptions>() - .Value; - - // TODO: Make enum from available datastore types - - if (configuration.Datastore.Type.ToLower().Equals("postgresql")) - { - services.AddDbContext(options => - { - options.UseNpgsql( - configuration.Datastore.ConnectionString, - options => - { - options.MigrationsHistoryTable( - "ef_migrations_history", - configuration.Datastore.PartitionName); - }); - }); - - services - .AddIdentity() - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - - if (configuration.Datastore.Initialize) - { - using var dbContextServiceProvider = services.BuildServiceProvider(); - PostgreSqlInitializer.Initialize(dbContextServiceProvider); - } - } - else - { - throw new UnSupportedDatastoreException( - $"{configuration.Datastore.Type} datastore is not supported."); - } - - services - .AddScoped(); - - services - .AddAuthentication(options => - { - options.DefaultAuthenticateScheme = - JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = - JwtBearerDefaults.AuthenticationScheme; - options.DefaultScheme = - JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(options => - { - options.IncludeErrorDetails = true; - options.SaveToken = true; - options.RequireHttpsMetadata = false; - options.TokenValidationParameters = - new TokenValidationParameters() - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidAudience = configuration.JsonWebToken.Audience, - ValidIssuer = configuration.JsonWebToken.Issuer, - ClockSkew = TimeSpan.Zero, - IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes( - configuration.JsonWebToken.IssuerSigningKey)) - }; - }); - - return services; - } -} diff --git a/src/Configuration/Infrastructure/Configuration.cs b/src/Configuration/Infrastructure/Configuration.cs index ebbe6ce..3dd3f4b 100644 --- a/src/Configuration/Infrastructure/Configuration.cs +++ b/src/Configuration/Infrastructure/Configuration.cs @@ -1,19 +1,31 @@ -// using Microsoft.Extensions.Configuration; -// using Microsoft.Extensions.DependencyInjection; -// -// namespace cuqmbr.TravelGuide.Configuration.Infrastructure; -// -// public static class Configuration -// { -// public static IServiceCollection ConfigureInfrastructure( -// this IServiceCollection services, -// IConfiguration configuration) -// { -// services -// .AddIdentity(configuration) -// .AddAuthenticationWithJwt(configuration) -// .AddServices(); -// -// return services; -// } -// } +using cuqmbr.TravelGuide.Infrastructure.Services; +using cuqmbr.TravelGuide.Application.Common.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace cuqmbr.TravelGuide.Configuration.Infrastructure; + +public static class Configuration +{ + public static IServiceCollection ConfigureInfrastructure( + this IServiceCollection services) + { + services + .AddHttpClient(); + + services + .AddScoped< + PasswordHasherService, + Pbkdf2PasswordHasherService>() + .AddScoped< + CurrencyConverterService, + ExchangeApiCurrencyConverterService>() + .AddScoped< + cuqmbr.TravelGuide.Application.Common.Services.LiqPayPaymentService, + cuqmbr.TravelGuide.Infrastructure.Services.LiqPayPaymentService>() + .AddScoped< + EmailSenderService, + MailKitEmailSenderService>(); + + return services; + } +} diff --git a/src/Configuration/Persistence/Configuration.cs b/src/Configuration/Persistence/Configuration.cs index 5aacbf3..4ab16fb 100644 --- a/src/Configuration/Persistence/Configuration.cs +++ b/src/Configuration/Persistence/Configuration.cs @@ -1,11 +1,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence; using cuqmbr.TravelGuide.Persistence; using cuqmbr.TravelGuide.Persistence.Exceptions; using Microsoft.EntityFrameworkCore; using cuqmbr.TravelGuide.Persistence.PostgreSql; using cuqmbr.TravelGuide.Persistence.InMemory; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace cuqmbr.TravelGuide.Configuration.Persistence; @@ -29,10 +30,13 @@ public static class Configuration configuration.ConnectionString, options => { + // TODO: Move to persistence project options.MigrationsHistoryTable( "ef_migrations_history", configuration.PartitionName); }); + options.ConfigureWarnings(w => w.Ignore( + RelationalEventId.PendingModelChangesWarning)); }); services @@ -41,10 +45,10 @@ public static class Configuration if (configuration.Migrate) { - using var dbContextServiceProvider = + using var serviceProvider = services.BuildServiceProvider(); var dbContext = - dbContextServiceProvider.GetService(); + serviceProvider.GetService(); PostgreSqlDbInitializer.Initialize(dbContext); } } @@ -72,9 +76,11 @@ public static class Configuration $"{configuration.Type} datastore is not supported."); } - // using var serviceProvider = services.BuildServiceProvider(); - // var unitOfWork = serviceProvider.GetService(); - // DbSeeder.Seed(unitOfWork); + if (configuration.Seed) + { + using var serviceProvider = services.BuildServiceProvider(); + DbSeeder.Seed(serviceProvider); + } return services; } diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json index e591490..aed0299 100644 --- a/src/Configuration/packages.lock.json +++ b/src/Configuration/packages.lock.json @@ -150,12 +150,6 @@ "Microsoft.Extensions.Primitives": "9.0.4" } }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" - }, "System.Text.Json": { "type": "Direct", "requested": "[9.0.4, )", @@ -170,11 +164,25 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "FluentValidation": { "type": "Transitive", "resolved": "11.11.0", "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" }, + "MailKit": { + "type": "Transitive", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "MediatR": { "type": "Transitive", "resolved": "12.4.1", @@ -242,8 +250,8 @@ }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } @@ -274,15 +282,15 @@ }, "Microsoft.AspNetCore.Cryptography.Internal": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" + "resolved": "2.3.0", + "contentHash": "/qy5r0CD40OccajzDmX3gBfqqxpAJkcXoqlVz0YR70x3gTRq/VuseDU/lZ5eh8vM+KCdmPFAtyGcRWxTyXxuYg==" }, "Microsoft.AspNetCore.Cryptography.KeyDerivation": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", + "resolved": "2.3.0", + "contentHash": "S7pph0JuBkgNqtyiIdLtQ5icZxmpX502zxxvHuMtM5W7IR3CKl1r/Cup+i6+E6B7IF3BeZYF4O3RbcA108syig==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" + "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0" } }, "Microsoft.AspNetCore.DataProtection": { @@ -365,15 +373,6 @@ "Microsoft.Extensions.Primitives": "8.0.0" } }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "9.0.4", - "Microsoft.Extensions.Identity.Stores": "9.0.4" - } - }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", "resolved": "9.0.0", @@ -501,13 +500,23 @@ "resolved": "9.0.4", "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", + "resolved": "9.0.4", + "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -545,24 +554,28 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, - "Microsoft.Extensions.Identity.Core": { + "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "9.0.4", - "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", + "contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Diagnostics": "9.0.4", "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", "Microsoft.Extensions.Options": "9.0.4" } }, - "Microsoft.Extensions.Identity.Stores": { + "Microsoft.Extensions.Identity.Core": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", + "resolved": "2.3.0", + "contentHash": "yR0eFnUbAM2k+q5QsX0NKinfShIe1B/aiHXEywiNT5Cs2MvEhxQIbIn5rWXnEAfmwW+i+t5D8odPSEHz/taIyQ==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.4", - "Microsoft.Extensions.Identity.Core": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", + "Microsoft.Extensions.Logging": "8.0.1", + "Microsoft.Extensions.Options": "8.0.2", + "System.ComponentModel.Annotations": "5.0.0" } }, "Microsoft.Extensions.Localization": { @@ -634,23 +647,23 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.8.0" + "Microsoft.IdentityModel.Tokens": "8.11.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.8.0" + "Microsoft.IdentityModel.Abstractions": "8.11.0" } }, "Microsoft.IdentityModel.Protocols": { @@ -672,11 +685,11 @@ }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.IdentityModel.Logging": "8.8.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" } }, "Microsoft.Net.Http.Headers": { @@ -702,6 +715,20 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "Npgsql": { "type": "Transitive", "resolved": "9.0.3", @@ -720,6 +747,11 @@ "Npgsql": "9.0.3" } }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.10", @@ -755,6 +787,16 @@ "resolved": "4.6.0", "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" + }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -815,30 +857,25 @@ "FluentValidation": "[11.11.0, )", "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.5, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.11.0, )", + "Microsoft.IdentityModel.Tokens": "[8.11.0, )", + "Newtonsoft.Json": "[13.0.3, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, "domain": { "type": "Project" }, - "identity": { - "type": "Project", - "dependencies": { - "Application": "[1.0.0, )", - "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.4, )", - "Microsoft.AspNetCore.Identity": "[2.3.1, )", - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[9.0.4, )", - "Microsoft.Extensions.Options": "[9.0.4, )", - "Microsoft.IdentityModel.JsonWebTokens": "[8.8.0, )", - "Microsoft.IdentityModel.Tokens": "[8.8.0, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" - } - }, "infrastructure": { "type": "Project", "dependencies": { - "Application": "[1.0.0, )" + "Application": "[1.0.0, )", + "MailKit": "[4.12.1, )", + "Microsoft.Extensions.Http": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )" } }, "persistence": { @@ -852,46 +889,6 @@ "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" } } - }, - "net9.0/linux-x64": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", - "dependencies": { - "System.Security.AccessControl": "4.5.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.10", - "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - } } } } \ No newline at end of file diff --git a/src/Domain/Entities/Account.cs b/src/Domain/Entities/Account.cs new file mode 100644 index 0000000..983203d --- /dev/null +++ b/src/Domain/Entities/Account.cs @@ -0,0 +1,23 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Account : EntityBase +{ + public string Username { get; set; } + + public string Email { get; set; } + + public string PasswordHash { get; set; } + + public string PasswordSalt { get; set; } + + public ICollection AccountRoles { get; set; } + + public ICollection RefreshTokens { get; set; } + + + public Employee? Employee { get; set; } + + public Company? Company { get; set; } + + public ICollection TicketGroups { get; set; } +} diff --git a/src/Domain/Entities/AccountRole.cs b/src/Domain/Entities/AccountRole.cs new file mode 100644 index 0000000..fd28b0d --- /dev/null +++ b/src/Domain/Entities/AccountRole.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class AccountRole : EntityBase +{ + public long AccountId { get; set; } + + public Account Account { get; set; } + + public long RoleId { get; set; } + + public Role Role { get; set; } +} diff --git a/src/Domain/Entities/Address.cs b/src/Domain/Entities/Address.cs index 81e017b..5d32156 100644 --- a/src/Domain/Entities/Address.cs +++ b/src/Domain/Entities/Address.cs @@ -7,11 +7,11 @@ public sealed class Address : EntityBase public string Name { get; set; } // TODO: Implement coordinates using NetTopologySuite - // public double Longitude { get; set; } - // - // public double Latitude { get; set; } + public double Longitude { get; set; } - // public VehicleType VehicleType { get; set; } + public double Latitude { get; set; } + + public VehicleType VehicleType { get; set; } public long CityId { get; set; } @@ -21,4 +21,3 @@ public sealed class Address : EntityBase public ICollection AddressRoutes { get; set; } } - diff --git a/src/Domain/Entities/Aircraft.cs b/src/Domain/Entities/Aircraft.cs new file mode 100644 index 0000000..4401999 --- /dev/null +++ b/src/Domain/Entities/Aircraft.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class Aircraft : Vehicle +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + // TODO: Add more properties to describe aircraft's capabilities +} diff --git a/src/Domain/Entities/Bus.cs b/src/Domain/Entities/Bus.cs new file mode 100644 index 0000000..eed23c9 --- /dev/null +++ b/src/Domain/Entities/Bus.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class Bus : Vehicle +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + // TODO: Add more properties to describe bus' capabilities +} diff --git a/src/Domain/Entities/Company.cs b/src/Domain/Entities/Company.cs new file mode 100644 index 0000000..553a601 --- /dev/null +++ b/src/Domain/Entities/Company.cs @@ -0,0 +1,22 @@ +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 Employees { get; set; } + + public ICollection Vehicles { get; set; } + + + public long AccountId { get; set; } + + public Account Account { get; set; } +} diff --git a/src/Domain/Entities/Employee.cs b/src/Domain/Entities/Employee.cs new file mode 100644 index 0000000..d89d2ce --- /dev/null +++ b/src/Domain/Entities/Employee.cs @@ -0,0 +1,30 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Employee : EntityBase +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Patronymic { get; set; } + + public Sex Sex { get; set; } + + public DateOnly BirthDate { get; set; } + + + public long CompanyId { get; set; } + + public Company Company { get; set; } + + public ICollection Documents { get; set; } + + public ICollection VehicleEnrollmentEmployees { get; set; } + + + public long AccountId { get; set; } + + public Account Account { get; set; } +} diff --git a/src/Domain/Entities/EmployeeDocument.cs b/src/Domain/Entities/EmployeeDocument.cs new file mode 100644 index 0000000..861c33c --- /dev/null +++ b/src/Domain/Entities/EmployeeDocument.cs @@ -0,0 +1,15 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class EmployeeDocument : EntityBase +{ + public DocumentType DocumentType { get; set; } + + public string Information { get; set; } + + + public long EmployeeId { get; set; } + + public Employee Employee { get; set; } +} diff --git a/src/Domain/Entities/RefreshToken.cs b/src/Domain/Entities/RefreshToken.cs new file mode 100644 index 0000000..833d0e0 --- /dev/null +++ b/src/Domain/Entities/RefreshToken.cs @@ -0,0 +1,20 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class RefreshToken : EntityBase +{ + public string Value { get; set; } = null!; + + public DateTimeOffset CreationTime { get; set; } + + public DateTimeOffset ExpirationTime { get; set; } + + public DateTimeOffset? RevocationTime { get; set; } + + public bool IsExpired => DateTimeOffset.UtcNow >= ExpirationTime; + + public bool IsActive => RevocationTime == null && !IsExpired; + + public long AccountId { get; set; } + + public Account Account { get; set; } +} diff --git a/src/Domain/Entities/Role.cs b/src/Domain/Entities/Role.cs new file mode 100644 index 0000000..a0b4539 --- /dev/null +++ b/src/Domain/Entities/Role.cs @@ -0,0 +1,10 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Role : EntityBase +{ + public IdentityRole Value { get; set; } + + public ICollection AccountRoles { get; set; } +} diff --git a/src/Domain/Entities/Route.cs b/src/Domain/Entities/Route.cs index 8199c35..c4a0920 100644 --- a/src/Domain/Entities/Route.cs +++ b/src/Domain/Entities/Route.cs @@ -6,9 +6,10 @@ public sealed class Route : EntityBase { public string Name { get; set; } - // public VehicleType VehicleType { get; set; } + public VehicleType VehicleType { get; set; } public ICollection RouteAddresses { get; set; } -} + public ICollection VehicleEnrollments { get; set; } +} diff --git a/src/Domain/Entities/RouteAddress.cs b/src/Domain/Entities/RouteAddress.cs index f216cca..d383ff5 100644 --- a/src/Domain/Entities/RouteAddress.cs +++ b/src/Domain/Entities/RouteAddress.cs @@ -13,4 +13,7 @@ public sealed class RouteAddress : EntityBase public long RouteId { get; set; } public Route Route { get; set; } + + + public ICollection Details { get; set; } } diff --git a/src/Domain/Entities/RouteAddressDetail.cs b/src/Domain/Entities/RouteAddressDetail.cs new file mode 100644 index 0000000..3c4a6e4 --- /dev/null +++ b/src/Domain/Entities/RouteAddressDetail.cs @@ -0,0 +1,20 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class RouteAddressDetail : EntityBase +{ + public TimeSpan TimeToNextAddress { get; set; } + + public decimal CostToNextAddress { get; set; } + + public TimeSpan CurrentAddressStopTime { get; set; } + + + public long VehicleEnrollmentId { get; set; } + + public VehicleEnrollment VehicleEnrollment { get; set; } + + + public long RouteAddressId { get; set; } + + public RouteAddress RouteAddress { get; set; } +} diff --git a/src/Domain/Entities/Ticket.cs b/src/Domain/Entities/Ticket.cs new file mode 100644 index 0000000..3865d77 --- /dev/null +++ b/src/Domain/Entities/Ticket.cs @@ -0,0 +1,30 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Ticket : EntityBase +{ + public long DepartureRouteAddressId { get; set; } + + public RouteAddress DepartureRouteAddress { get; set; } + + public long ArrivalRouteAddressId { get; set; } + + public RouteAddress ArrivalRouteAddress { get; set; } + + public short Order { get; set; } + + public Currency Currency { get; set; } + + public decimal Cost { get; set; } + + + public long TicketGroupId { get; set; } + + public TicketGroup TicketGroup { get; set; } + + + public long VehicleEnrollmentId { get; set; } + + public VehicleEnrollment VehicleEnrollment { get; set; } +} diff --git a/src/Domain/Entities/TicketGroup.cs b/src/Domain/Entities/TicketGroup.cs new file mode 100644 index 0000000..66ac0ba --- /dev/null +++ b/src/Domain/Entities/TicketGroup.cs @@ -0,0 +1,32 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class TicketGroup : EntityBase +{ + public string PassangerFirstName { get; set; } + + public string PassangerLastName { get; set; } + + public string PassangerPatronymic { get; set; } + + public Sex PassangerSex { get; set; } + + public DateOnly PassangerBirthDate { get; set; } + + public string? PassangerEmail { get; set; } + + public DateTimeOffset PurchaseTime { get; set; } + + public TicketStatus Status { get; set; } + + public TimeSpan TravelTime { get; set; } + + + public ICollection Tickets { get; set; } + + + public long? AccountId { get; set; } + + public Account? Account { get; set; } +} diff --git a/src/Domain/Entities/Train.cs b/src/Domain/Entities/Train.cs new file mode 100644 index 0000000..215c21b --- /dev/null +++ b/src/Domain/Entities/Train.cs @@ -0,0 +1,12 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class Train : Vehicle +{ + public string Number { get; set; } + + public string Model { get; set; } + + public short Capacity { get; set; } + + // TODO: Add more properties to describe train's capabilities +} diff --git a/src/Domain/Entities/Vehicle.cs b/src/Domain/Entities/Vehicle.cs new file mode 100644 index 0000000..f0fb3c9 --- /dev/null +++ b/src/Domain/Entities/Vehicle.cs @@ -0,0 +1,16 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +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/Domain/Entities/VehicleEnrollment.cs b/src/Domain/Entities/VehicleEnrollment.cs new file mode 100644 index 0000000..21470de --- /dev/null +++ b/src/Domain/Entities/VehicleEnrollment.cs @@ -0,0 +1,220 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class VehicleEnrollment : EntityBase +{ + public DateTimeOffset DepartureTime { get; set; } + + public Currency Currency { get; set; } + + + public long VehicleId { get; set; } + + public Vehicle Vehicle { get; set; } + + + public long RouteId { get; set; } + + public Route Route { get; set; } + + + public ICollection RouteAddressDetails { get; set; } + + public ICollection Tickets { get; set; } + + public ICollection VehicleEnrollmentEmployees { get; set; } + + + public DateTimeOffset GetDepartureTime(long DepartureRouteAddressId) + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + + if (DepartureRouteAddressId == firstRouteAddressId) + { + return DepartureTime; + } + + + var orderedRouteAddressDetails = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order); + + var timeToDeparture = TimeSpan.Zero; + foreach (var routeAddressDetail in orderedRouteAddressDetails) + { + timeToDeparture = + timeToDeparture + routeAddressDetail.CurrentAddressStopTime; + + if (routeAddressDetail.Id == DepartureRouteAddressId) + { + break; + } + + timeToDeparture = + timeToDeparture += routeAddressDetail.TimeToNextAddress; + } + + return DepartureTime + timeToDeparture; + } + + public DateTimeOffset GetDepartureTime() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + + return GetDepartureTime(firstRouteAddressId); + } + + public DateTimeOffset GetArrivalTime(long ArrivalRouteAddressId) + { + var orderedRouteAddressDetails = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order); + + var timeToDeparture = TimeSpan.Zero; + foreach (var routeAddressDetail in orderedRouteAddressDetails) + { + if (routeAddressDetail.Id == ArrivalRouteAddressId) + { + break; + } + + timeToDeparture = + timeToDeparture + + routeAddressDetail.TimeToNextAddress + + routeAddressDetail.CurrentAddressStopTime; + } + + return DepartureTime + timeToDeparture; + } + + public DateTimeOffset GetArrivalTime() + { + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetArrivalTime(lastRouteAddressId); + } + + public TimeSpan GetTravelTime( + long DepartureRouteAddressId, long ArrivalRouteAddressId) + { + return + GetArrivalTime(ArrivalRouteAddressId) - + GetDepartureTime(DepartureRouteAddressId); + } + + public TimeSpan GetTravelTime() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetTravelTime(firstRouteAddressId, lastRouteAddressId); + } + + public TimeSpan GetTimeInStops( + long DepartureRouteAddressId, long ArrivalRouteAddressId) + { + var orderedRouteAddressDetails = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order); + + var departureRouteAddressDetail = orderedRouteAddressDetails + .Single(e => e.RouteAddressId == DepartureRouteAddressId); + + var timeInStops = TimeSpan.Zero; + foreach (var routeAddressDetail in orderedRouteAddressDetails) + { + if (routeAddressDetail.RouteAddress.Order <= + departureRouteAddressDetail.RouteAddress.Order) + { + continue; + } + + if (routeAddressDetail.Id == ArrivalRouteAddressId) + { + break; + } + + timeInStops += routeAddressDetail.CurrentAddressStopTime; + } + + return timeInStops; + } + + public TimeSpan GetTimeInStops() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetTimeInStops(firstRouteAddressId, lastRouteAddressId); + } + + public int GetNumberOfStops( + long DepartureRouteAddressId, long ArrivalRouteAddressId) + { + return + RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order) + .SkipWhile(e => e.RouteAddressId != DepartureRouteAddressId) + .TakeWhile(e => e.RouteAddressId != ArrivalRouteAddressId) + .Count() - 1; + } + + public TimeSpan GetNumberOfStops() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetTimeInStops(firstRouteAddressId, lastRouteAddressId); + } + + public TimeSpan GetTimeMoving( + long DepartureRouteAddressId, long ArrivalRouteAddressId) + { + return + RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order) + .SkipWhile(e => e.RouteAddressId != DepartureRouteAddressId) + .TakeWhile(e => e.RouteAddressId != ArrivalRouteAddressId) + .Aggregate(TimeSpan.Zero, + (sum, next) => sum += next.TimeToNextAddress); + } + + public TimeSpan GetTimeMoving() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetTimeMoving(firstRouteAddressId, lastRouteAddressId); + } + + public decimal GetCost( + long DepartureRouteAddressId, long ArrivalRouteAddressId) + { + return + RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order) + .SkipWhile(e => e.RouteAddressId != DepartureRouteAddressId) + .TakeWhile(e => e.RouteAddressId != ArrivalRouteAddressId) + .Aggregate((decimal)0, + (sum, next) => sum += next.CostToNextAddress); + } + + public decimal GetCost() + { + var firstRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).First().RouteAddressId; + var lastRouteAddressId = RouteAddressDetails + .OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId; + + return GetCost(firstRouteAddressId, lastRouteAddressId); + } +} diff --git a/src/Domain/Entities/VehicleEnrollmentEmployee.cs b/src/Domain/Entities/VehicleEnrollmentEmployee.cs new file mode 100644 index 0000000..7ac51df --- /dev/null +++ b/src/Domain/Entities/VehicleEnrollmentEmployee.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public class VehicleEnrollmentEmployee : EntityBase +{ + public long EmployeeId { get; set; } + + public Employee Employee { get; set; } + + + public long VehicleEnrollmentId { get; set; } + + public VehicleEnrollment VehicleEnrollment { get; set; } +} diff --git a/src/Domain/Enums/Currency.cs b/src/Domain/Enums/Currency.cs new file mode 100644 index 0000000..cf30549 --- /dev/null +++ b/src/Domain/Enums/Currency.cs @@ -0,0 +1,52 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +// Do not forget to update the schema of your database when changing +// this class (if you use it with a database) + +// ISO-4217 Currency Codes dated 2025-03-31 + +public abstract class Currency : Enumeration +{ + public static readonly Currency Default = new DefaultCurrency(); + public static readonly Currency USD = new USDCurrency(); + public static readonly Currency EUR = new EURCurrency(); + public static readonly Currency UAH = new UAHCurrency(); + + protected Currency(int value, string name) : base(value, name) { } + + protected virtual byte DecimalDigits { get; } = byte.MaxValue; + + public decimal Round(decimal amount) + { + return Math.Round(amount, DecimalDigits); + } + + // When no currency is specified + private sealed class DefaultCurrency : Currency + { + public DefaultCurrency() : base(Int32.MaxValue, "DEFAULT") { } + + protected override byte DecimalDigits => 2; + } + + private sealed class USDCurrency : Currency + { + public USDCurrency() : base(840, "USD") { } + + protected override byte DecimalDigits => 2; + } + + private sealed class EURCurrency : Currency + { + public EURCurrency() : base(978, "EUR") { } + + protected override byte DecimalDigits => 2; + } + + private sealed class UAHCurrency : Currency + { + public UAHCurrency() : base(980, "UAH") { } + + protected override byte DecimalDigits => 2; + } +} diff --git a/src/Domain/Enums/DocumentType.cs b/src/Domain/Enums/DocumentType.cs new file mode 100644 index 0000000..bde01d7 --- /dev/null +++ b/src/Domain/Enums/DocumentType.cs @@ -0,0 +1,16 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +// Do not forget to update the schema of your database when changing +// this class (if you use it with a database) + +public abstract class DocumentType : Enumeration +{ + public static readonly DocumentType Passport = new PassportDocumentType(); + + protected DocumentType(int value, string name) : base(value, name) { } + + private sealed class PassportDocumentType : DocumentType + { + public PassportDocumentType() : base(0, "passport") { } + } +} diff --git a/src/Application/Common/Models/IdentityRole.cs b/src/Domain/Enums/IdentityRole.cs similarity index 52% rename from src/Application/Common/Models/IdentityRole.cs rename to src/Domain/Enums/IdentityRole.cs index 5636e3b..4424fb0 100644 --- a/src/Application/Common/Models/IdentityRole.cs +++ b/src/Domain/Enums/IdentityRole.cs @@ -1,11 +1,11 @@ -using cuqmbr.TravelGuide.Domain.Enums; - -namespace cuqmbr.TravelGuide.Application.Common.Models; +namespace cuqmbr.TravelGuide.Domain.Enums; public abstract class IdentityRole : Enumeration { public static readonly IdentityRole Administrator = new AdministratorRole(); public static readonly IdentityRole User = new UserRole(); + public static readonly IdentityRole CompanyOwner = new CompanyOwnerRole(); + public static readonly IdentityRole CompanyEmployee = new CompanyEmployeeRole(); protected IdentityRole(int value, string name) : base(value, name) { } @@ -18,4 +18,14 @@ public abstract class IdentityRole : Enumeration { public UserRole() : base(1, "user") { } } + + private sealed class CompanyOwnerRole : IdentityRole + { + public CompanyOwnerRole() : base(2, "company_owner") { } + } + + private sealed class CompanyEmployeeRole : IdentityRole + { + public CompanyEmployeeRole() : base(3, "company_employee") { } + } } diff --git a/src/Domain/Enums/Sex.cs b/src/Domain/Enums/Sex.cs new file mode 100644 index 0000000..f1a8460 --- /dev/null +++ b/src/Domain/Enums/Sex.cs @@ -0,0 +1,22 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +// Do not forget to update the schema of your database when changing +// this class (if you use it with a database) + +public abstract class Sex : Enumeration +{ + public static readonly Sex Male = new MaleSex(); + public static readonly Sex Female = new FemaleSex(); + + protected Sex(int value, string name) : base(value, name) { } + + private sealed class MaleSex : Sex + { + public MaleSex() : base(Int32.MaxValue, "male") { } + } + + private sealed class FemaleSex : Sex + { + public FemaleSex() : base(Int32.MinValue, "female") { } + } +} diff --git a/src/Domain/Enums/TicketStatus.cs b/src/Domain/Enums/TicketStatus.cs new file mode 100644 index 0000000..b3c0b3a --- /dev/null +++ b/src/Domain/Enums/TicketStatus.cs @@ -0,0 +1,25 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +public abstract class TicketStatus : Enumeration +{ + public static readonly TicketStatus Reserved = new ReservedTicketStatus(); + public static readonly TicketStatus Returned = new ReturnedTicketStatus(); + public static readonly TicketStatus Purchased = new PurchasedTicketStatus(); + + protected TicketStatus(int value, string name) : base(value, name) { } + + private sealed class ReservedTicketStatus : TicketStatus + { + public ReservedTicketStatus() : base(0, "reserved") { } + } + + private sealed class ReturnedTicketStatus : TicketStatus + { + public ReturnedTicketStatus() : base(1, "returned") { } + } + + private sealed class PurchasedTicketStatus : TicketStatus + { + public PurchasedTicketStatus() : base(2, "purchased") { } + } +} diff --git a/src/Domain/Enums/VehicleType.cs b/src/Domain/Enums/VehicleType.cs index a107370..30402d1 100644 --- a/src/Domain/Enums/VehicleType.cs +++ b/src/Domain/Enums/VehicleType.cs @@ -5,24 +5,24 @@ namespace cuqmbr.TravelGuide.Domain.Enums; public abstract class VehicleType : Enumeration { - public static readonly VehicleType Bus = new BusVehicleType(); - public static readonly VehicleType Train = new TrainVehicleType(); - public static readonly VehicleType Aircraft = new AircraftVehicleType(); + public static readonly VehicleType Bus = new BusVehicleType(); + public static readonly VehicleType Train = new TrainVehicleType(); + public static readonly VehicleType Aircraft = new AircraftVehicleType(); - protected VehicleType(int value, string name) : base(value, name) { } + protected VehicleType(int value, string name) : base(value, name) { } - private sealed class BusVehicleType : VehicleType - { - public BusVehicleType() : base(0, "bus") { } - } + private sealed class BusVehicleType : VehicleType + { + public BusVehicleType() : base(0, "bus") { } + } - private sealed class TrainVehicleType : VehicleType - { - public TrainVehicleType() : base(1, "train") { } - } + private sealed class TrainVehicleType : VehicleType + { + public TrainVehicleType() : base(1, "train") { } + } - private sealed class AircraftVehicleType : VehicleType - { - public AircraftVehicleType() : base(2, "aircraft") { } - } + private sealed class AircraftVehicleType : VehicleType + { + public AircraftVehicleType() : base(2, "aircraft") { } + } } diff --git a/src/HttpApi/Controllers/AddressesController.cs b/src/HttpApi/Controllers/AddressesController.cs new file mode 100644 index 0000000..2d8c8c1 --- /dev/null +++ b/src/HttpApi/Controllers/AddressesController.cs @@ -0,0 +1,206 @@ +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.Addresses; +using cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress; +using cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage; +using cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress; +using cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress; +using cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress; +using cuqmbr.TravelGuide.Application.Addresses.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("addresses")] +public class AddressesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add an address")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(AddressDto))] + [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] AddAddressViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddAddressCommand() + { + Name = viewModel.Name, + Longitude = viewModel.Longitude, + Latitude = viewModel.Latitude, + VehicleType = VehicleType.FromName(viewModel.VehicleType), + CityGuid = viewModel.CityUuid + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all addresses")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetAddressesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetAddressesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + LongitudeGreaterOrEqualThan = + filterQuery.LongitudeGreaterOrEqualThan, + LongitudeLessOrEqualThan = + filterQuery.LongitudeLessOrEqualThan, + LatitudeGreaterOrEqualThan = + filterQuery.LatitudeGreaterOrEqualThan, + LatitudeLessOrEqualThan = + filterQuery.LatitudeLessOrEqualThan, + VehicleType = VehicleType.FromName(filterQuery.VehicleType), + CountryGuid = filterQuery.CountryUuid, + RegionGuid = filterQuery.RegionUuid, + CityGuid = filterQuery.CityUuid + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get an address by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AddressDto))] + [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(AddressDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetAddressQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update an address")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AddressDto))] + [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(AddressDto))] + [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] UpdateAddressViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateAddressCommand() + { + Guid = uuid, + Name = viewModel.Name, + Longitude = viewModel.Longitude, + Latitude = viewModel.Latitude, + VehicleType = VehicleType.FromName(viewModel.VehicleType), + CityGuid = viewModel.CityUuid + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete an address")] + [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 DeleteAddressCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/AircraftsController.cs b/src/HttpApi/Controllers/AircraftsController.cs new file mode 100644 index 0000000..620b581 --- /dev/null +++ b/src/HttpApi/Controllers/AircraftsController.cs @@ -0,0 +1,196 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Aircrafts; +using cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage; +using cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft; +using cuqmbr.TravelGuide.Application.Aircrafts.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("aircrafts")] +public class AircraftsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a aircraft")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddAircraftViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddAircraftCommand() + { + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all aircrafts")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetAircraftsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetAircraftsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CompanyGuid = filterQuery.CompanyUuid, + CapacityGreaterThanOrEqualTo = + filterQuery.CapacityGreaterThanOrEqualTo, + CapacityLessThanOrEqualTo = + filterQuery.CapacityLessThanOrEqualTo + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a aircraft by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetAircraftQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a aircraft")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(AircraftDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateAircraftViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateAircraftCommand() + { + Guid = uuid, + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a aircraft")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteAircraftCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/AuthenticationController.cs b/src/HttpApi/Controllers/AuthenticationController.cs index ecb2a62..6ee61b4 100644 --- a/src/HttpApi/Controllers/AuthenticationController.cs +++ b/src/HttpApi/Controllers/AuthenticationController.cs @@ -4,10 +4,6 @@ using cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; using cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; using cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; using cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; -using cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RenewAccessTokenWithCookie; -using cuqmbr.TravelGuide.Application.Authenticaion.Commands - .RevokeRefreshTokenWithCookie; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -30,28 +26,6 @@ public class AuthenticationController : ControllerBase return await Mediator.Send(query, cancellationToken); } - // [HttpPost("loginWithCookie")] - // public async Task LoginWithCookie( - // [FromBody] LoginQuery query, - // CancellationToken cancellationToken) - // { - // var tokens = await Mediator.Send(query, cancellationToken); - // - // HttpContext.Response.Cookies.Delete("refreshToken"); - // - // var cookieOptions = new CookieOptions() - // { - // Path = "/", - // Expires = DateTimeOffset.MaxValue, - // HttpOnly = true - // }; - // - // HttpContext.Response.Cookies.Append( - // "refreshToken", tokens.RefreshToken, cookieOptions); - // - // return tokens; - // } - [HttpPost("renewAccessToken")] public async Task RenewAccessToken( [FromBody] RenewAccessTokenCommand command, @@ -60,14 +34,6 @@ public class AuthenticationController : ControllerBase return await Mediator.Send(command, cancellationToken); } - // [HttpPost("renewAccessTokenWithCookie")] - // public async Task RenewAccessTokenWithCookie( - // [FromBody] RenewAccessTokenWithCookieCommand command, - // CancellationToken cancellationToken) - // { - // return await Mediator.Send(command, cancellationToken); - // } - [HttpPost("revokeRefreshToken")] public async Task RevokeRefreshToken( [FromBody] RevokeRefreshTokenCommand command, @@ -75,13 +41,4 @@ public class AuthenticationController : ControllerBase { await Mediator.Send(command, cancellationToken); } - - // [HttpPost("revokeRefreshTokenWithCookie")] - // public async Task RevokeRefreshTokenWithCookie( - // [FromBody] RevokeRefreshTokenWithCookieCommand command, - // CancellationToken cancellationToken) - // { - // await Mediator.Send(command, cancellationToken); - // HttpContext.Response.Cookies.Delete("refreshToken"); - // } } diff --git a/src/HttpApi/Controllers/BusesController.cs b/src/HttpApi/Controllers/BusesController.cs new file mode 100644 index 0000000..bd68d37 --- /dev/null +++ b/src/HttpApi/Controllers/BusesController.cs @@ -0,0 +1,196 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Buses; +using cuqmbr.TravelGuide.Application.Buses.Commands.AddBus; +using cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage; +using cuqmbr.TravelGuide.Application.Buses.Queries.GetBus; +using cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus; +using cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus; +using cuqmbr.TravelGuide.Application.Buses.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("buses")] +public class BusesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a bus")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddBusViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddBusCommand() + { + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all buses")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetBusesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetBusesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CompanyGuid = filterQuery.CompanyUuid, + CapacityGreaterThanOrEqualTo = + filterQuery.CapacityGreaterThanOrEqualTo, + CapacityLessThanOrEqualTo = + filterQuery.CapacityLessThanOrEqualTo + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a bus by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetBusQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a bus")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(BusDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateBusViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateBusCommand() + { + Guid = uuid, + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity, + CompanyGuid = viewModel.CompanyUuid + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a bus")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteBusCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/CitiesController.cs b/src/HttpApi/Controllers/CitiesController.cs new file mode 100644 index 0000000..73bb49d --- /dev/null +++ b/src/HttpApi/Controllers/CitiesController.cs @@ -0,0 +1,189 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Cities; +using cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; +using cuqmbr.TravelGuide.Application.Cities.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("cities")] +public class CitiesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a city")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(CityDto))] + [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] AddCityViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddCityCommand() + { + Name = viewModel.Name, + RegionGuid = viewModel.RegionGuid + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all cities")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetCitiesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CountryGuid = filterQuery.CountryUuid, + RegionGuid = filterQuery.RegionUuid + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a city by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CityDto))] + [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(CityDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetCityQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a city")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CityDto))] + [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(CityDto))] + [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] UpdateCityViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateCityCommand() + { + Guid = uuid, + Name= viewModel.Name, + RegionGuid = viewModel.RegionUuid + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a city")] + [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 DeleteCityCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/CompaniesController.cs b/src/HttpApi/Controllers/CompaniesController.cs new file mode 100644 index 0000000..617f3ff --- /dev/null +++ b/src/HttpApi/Controllers/CompaniesController.cs @@ -0,0 +1,193 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.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, + Username = viewModel.Username, + Email = viewModel.Email, + Password = viewModel.Password + }, + 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/CountriesController.cs b/src/HttpApi/Controllers/CountriesController.cs index d40fa01..056245e 100644 --- a/src/HttpApi/Controllers/CountriesController.cs +++ b/src/HttpApi/Controllers/CountriesController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Countries.ViewModels; using cuqmbr.TravelGuide.Application.Countries; using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; using cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; @@ -15,7 +16,7 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class CountriesController : ControllerBase { [HttpPost] - [SwaggerOperation("Create a country")] + [SwaggerOperation("Add a country")] [SwaggerResponse( StatusCodes.Status201Created, "Object successfuly created", typeof(CountryDto))] @@ -36,12 +37,17 @@ public class CountriesController : ControllerBase StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] public async Task> Add( - [FromBody] AddCountryCommand command, + [FromBody] AddCountryViewModel viewModel, CancellationToken cancellationToken) { return StatusCode( StatusCodes.Status201Created, - await Mediator.Send(command, cancellationToken)); + await Mediator.Send( + new AddCountryCommand() + { + Name = viewModel.Name + }, + cancellationToken)); } [HttpGet] @@ -78,7 +84,7 @@ public class CountriesController : ControllerBase } [HttpGet("{uuid:guid}")] - [SwaggerOperation("Get country by uuid")] + [SwaggerOperation("Get a country by uuid")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(CountryDto))] [SwaggerResponse( @@ -105,7 +111,7 @@ public class CountriesController : ControllerBase } [HttpPut("{uuid:guid}")] - [SwaggerOperation("Update country")] + [SwaggerOperation("Update a country")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(CountryDto))] [SwaggerResponse( @@ -128,15 +134,20 @@ public class CountriesController : ControllerBase typeof(ProblemDetails))] public async Task Update( [FromRoute] Guid uuid, - [FromBody] UpdateCountryCommand command, + [FromBody] UpdateCountryViewModel viewModel, CancellationToken cancellationToken) { - command.Guid = uuid; - return await Mediator.Send(command, cancellationToken); + return await Mediator.Send( + new UpdateCountryCommand() + { + Guid = uuid, + Name = viewModel.Name + }, + cancellationToken); } [HttpDelete("{uuid:guid}")] - [SwaggerOperation("Delete country")] + [SwaggerOperation("Delete a country")] [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", @@ -149,7 +160,8 @@ public class CountriesController : ControllerBase "Not enough privileges to perform an action", typeof(ProblemDetails))] [SwaggerResponse( - StatusCodes.Status404NotFound, "Object not found", typeof(CountryDto))] + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] diff --git a/src/HttpApi/Controllers/EmployeesController.cs b/src/HttpApi/Controllers/EmployeesController.cs new file mode 100644 index 0000000..2af1e28 --- /dev/null +++ b/src/HttpApi/Controllers/EmployeesController.cs @@ -0,0 +1,215 @@ +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.Employees; +using cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; +using cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; +using cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; +using cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; +using cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; +using cuqmbr.TravelGuide.Application.Employees.ViewModels; +using cuqmbr.TravelGuide.Application.Employees.Models; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("employees")] +public class EmployeesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add an employee")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(EmployeeDto))] + [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, "One or more addresses was not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddEmployeeViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddEmployeeCommand() + { + FirstName = viewModel.FirstName, + LastName = viewModel.LastName, + Patronymic = viewModel.Patronymic, + Sex = Sex.FromName(viewModel.Sex), + BirthDate = viewModel.BirthDate, + Documents = viewModel.Documents.Select( + e => new EmployeeDocumentModel() + { + DocumentType = DocumentType.FromName(e.DocumentType), + Information = e.Information + + }).ToArray(), + CompanyGuid = viewModel.CompanyUuid, + Username = viewModel.Username, + Email = viewModel.Email, + Password = viewModel.Password + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all employees")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetEmployeesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetEmployeesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + Sex = Sex.FromName(filterQuery.Sex), + BirthDateLessThanOrEqualTo = + filterQuery.BirthDateLessThanOrEqualTo, + BirthDateGreaterThanOrEqualTo = + filterQuery.BirthDateGreaterThanOrEqualTo, + CompanyGuid = filterQuery.CompanyUuid + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get an employee by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(EmployeeDto))] + [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(EmployeeDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetEmployeeQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update an employee")] + [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(EmployeeDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "One or more addresses was not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateEmployeeViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateEmployeeCommand() + { + Guid = uuid, + FirstName = viewModel.FirstName, + LastName = viewModel.LastName, + Patronymic = viewModel.Patronymic, + Sex = Sex.FromName(viewModel.Sex), + BirthDate = viewModel.BirthDate, + Documents = viewModel.Documents.Select( + e => new EmployeeDocumentModel() + { + DocumentType = DocumentType.FromName(e.DocumentType), + Information = e.Information + + }).ToArray(), + CompanyGuid = viewModel.CompanyUuid + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete an employee")] + [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 DeleteEmployeeCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/IdentityController.cs b/src/HttpApi/Controllers/IdentityController.cs new file mode 100644 index 0000000..967da73 --- /dev/null +++ b/src/HttpApi/Controllers/IdentityController.cs @@ -0,0 +1,238 @@ +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.Identity.Roles.Queries.GetRolesPage; +using cuqmbr.TravelGuide.Application.Identity.Accounts; +using cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; +using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; +using cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage; +using cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount; +using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.UpdateAccount; +using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.DeleteAccount; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("identity")] +public class IdentityController : ControllerBase +{ + [HttpGet("roles")] + [SwaggerOperation("Get a list of all roles")] + [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> GetRolesPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetRolesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search + }, + cancellationToken); + } + + + + + [HttpPost("accounts")] + [SwaggerOperation("Add an account")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(AccountDto))] + [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> AddAccount( + [FromBody] AddAccountViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddAccountCommand() + { + Username = viewModel.Username, + Email = viewModel.Email, + Password = viewModel.Password, + Roles = viewModel.Roles + .Select(s => IdentityRole.FromName(s)) + .ToArray() + }, + cancellationToken)); + } + + [HttpGet("accounts")] + [SwaggerOperation("Get a list of all accounts")] + [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> GetAccountsPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetAccountsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetAccountsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + Roles = filterQuery.Roles == null ? null : + filterQuery.Roles + .Select(s => IdentityRole.FromName(s)) + .ToArray() + }, + cancellationToken); + } + + [HttpGet("accounts/{uuid:guid}")] + [SwaggerOperation("Get an account by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AccountDto))] + [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 GetAccount( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetAccountQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("accounts/{uuid:guid}")] + [SwaggerOperation("Update an account")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(AccountDto))] + [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(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task UpdateAccount( + [FromRoute] Guid uuid, + [FromBody] UpdateAccountViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateAccountCommand() + { + Guid = uuid, + Username = viewModel.Username, + Email = viewModel.Email, + Password = viewModel.Password, + Roles = viewModel.Roles == null ? null : + viewModel.Roles + .Select(s => IdentityRole.FromName(s)) + .ToArray() + }, + cancellationToken); + } + + [HttpDelete("accounts/{uuid:guid}")] + [SwaggerOperation("Delete an account")] + [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 DeleteAccount( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteAccountCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/PaymentController.cs b/src/HttpApi/Controllers/PaymentController.cs new file mode 100644 index 0000000..04c28e8 --- /dev/null +++ b/src/HttpApi/Controllers/PaymentController.cs @@ -0,0 +1,93 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Payments; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Models; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("payments")] +public class PaymentController : ControllerBase +{ + [HttpPost("liqPay/ticket/getLink")] + [SwaggerOperation("Get payment link for provided ticket")] + [SwaggerResponse( + StatusCodes.Status200OK, "Successfuly created", + typeof(PaymentLinkDto))] + [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> LiqPayTicketGetLink( + [FromBody] TicketGroupPaymentViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status200OK, + await Mediator.Send( + new GetPaymentLinkCommand() + { + PassangerFirstName = viewModel.PassangerFirstName, + PassangerLastName = viewModel.PassangerLastName, + PassangerPatronymic = viewModel.PassangerPatronymic, + PassangerSex = Sex.FromName(viewModel.PassangerSex), + PassangerBirthDate = viewModel.PassangerBirthDate, + PassangerEmail = viewModel.PassangerEmail, + Tickets = viewModel.Tickets.Select(e => + new TicketGroupPaymentTicketModel() + { + DepartureRouteAddressGuid = e.DepartureRouteAddressUuid, + ArrivalRouteAddressGuid = e.ArrivalRouteAddressUuid, + Order = e.Order, + VehicleEnrollmentGuid = e.VehicleEnrollmentUuid + }) + .ToArray(), + ResultPath = viewModel.ResultPath + }, + cancellationToken)); + } + + [Consumes("application/x-www-form-urlencoded")] + [HttpPost("liqPay/ticket/callback")] + [SwaggerOperation("Process LiqPay callback for ticket")] + [SwaggerResponse( + StatusCodes.Status200OK, "Successfuly processed")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task LiqPayTicketCallback( + [FromForm] CallbackViewModel viewModel, + CancellationToken cancellationToken) + { + await Mediator.Send( + new ProcessCallbackCommand() + { + Data = viewModel.Data, + Signature = viewModel.Signature + }, + cancellationToken); + } +} diff --git a/src/HttpApi/Controllers/RegionsController.cs b/src/HttpApi/Controllers/RegionsController.cs index 1306916..92d1d4b 100644 --- a/src/HttpApi/Controllers/RegionsController.cs +++ b/src/HttpApi/Controllers/RegionsController.cs @@ -2,13 +2,13 @@ using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Regions.ViewModels; using cuqmbr.TravelGuide.Application.Regions; using cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; using cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; using cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; -using cuqmbr.TravelGuide.Application.Regions.ViewModels; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -16,7 +16,7 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class RegionsController : ControllerBase { [HttpPost] - [SwaggerOperation("Create a region")] + [SwaggerOperation("Add a region")] [SwaggerResponse( StatusCodes.Status201Created, "Object successfuly created", typeof(RegionDto))] @@ -40,12 +40,18 @@ public class RegionsController : ControllerBase StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] public async Task> Add( - [FromBody] AddRegionCommand command, + [FromBody] AddRegionViewModel viewModel, CancellationToken cancellationToken) { return StatusCode( StatusCodes.Status201Created, - await Mediator.Send(command, cancellationToken)); + await Mediator.Send( + new AddRegionCommand() + { + Name = viewModel.Name, + CountryGuid = viewModel.CountryUuid + }, + cancellationToken)); } [HttpGet] @@ -53,9 +59,6 @@ public class RegionsController : ControllerBase [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(PaginatedList))] - [SwaggerResponse( - StatusCodes.Status400BadRequest, "Object already exists", - typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", typeof(HttpValidationProblemDetails))] @@ -82,18 +85,15 @@ public class RegionsController : ControllerBase PageSize = pageQuery.PageSize, Search = searchQuery.Search, Sort = sortQuery.Sort, - CountryUuid = filterQuery.CountryUuid + CountryGuid = filterQuery.CountryUuid }, cancellationToken); } [HttpGet("{uuid:guid}")] - [SwaggerOperation("Get region by uuid")] + [SwaggerOperation("Get a region by uuid")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(RegionDto))] - [SwaggerResponse( - StatusCodes.Status400BadRequest, "Object already exists", - typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", typeof(HttpValidationProblemDetails))] @@ -113,12 +113,12 @@ public class RegionsController : ControllerBase [FromRoute] Guid uuid, CancellationToken cancellationToken) { - return await Mediator.Send(new GetRegionQuery() { Uuid = uuid }, + return await Mediator.Send(new GetRegionQuery() { Guid = uuid }, cancellationToken); } [HttpPut("{uuid:guid}")] - [SwaggerOperation("Update region")] + [SwaggerOperation("Update a region")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(RegionDto))] [SwaggerResponse( @@ -144,15 +144,21 @@ public class RegionsController : ControllerBase typeof(ProblemDetails))] public async Task Update( [FromRoute] Guid uuid, - [FromBody] UpdateRegionCommand command, + [FromBody] UpdateRegionViewModel viewModel, CancellationToken cancellationToken) { - command.Uuid = uuid; - return await Mediator.Send(command, cancellationToken); + return await Mediator.Send( + new UpdateRegionCommand() + { + Guid = uuid, + Name = viewModel.Name, + CountryGuid = viewModel.CountryUuid + }, + cancellationToken); } [HttpDelete("{uuid:guid}")] - [SwaggerOperation("Delete region")] + [SwaggerOperation("Delete a region")] [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", @@ -165,7 +171,8 @@ public class RegionsController : ControllerBase "Not enough privileges to perform an action", typeof(ProblemDetails))] [SwaggerResponse( - StatusCodes.Status404NotFound, "Object not found", typeof(RegionDto))] + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] @@ -174,7 +181,7 @@ public class RegionsController : ControllerBase CancellationToken cancellationToken) { await Mediator.Send( - new DeleteRegionCommand() { Uuid = uuid }, + new DeleteRegionCommand() { Guid = uuid }, cancellationToken); return StatusCode(StatusCodes.Status204NoContent); } diff --git a/src/HttpApi/Controllers/RoutesController.cs b/src/HttpApi/Controllers/RoutesController.cs new file mode 100644 index 0000000..7eb6850 --- /dev/null +++ b/src/HttpApi/Controllers/RoutesController.cs @@ -0,0 +1,199 @@ +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.Routes; +using cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute; +using cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage; +using cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute; +using cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute; +using cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute; +using cuqmbr.TravelGuide.Application.Routes.ViewModels; +using cuqmbr.TravelGuide.Application.Routes.Models; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("routes")] +public class RoutesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a route")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(RouteDto))] + [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, "One or more addresses was not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddRouteViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddRouteCommand() + { + Name = viewModel.Name, + VehicleType = VehicleType.FromName(viewModel.VehicleType), + Addresses = viewModel.Addresses.Select( + e => new RouteAddressModel() + { + Order = e.Order, + Guid = e.Uuid + + }).ToArray() + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all routes")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetRoutesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetRoutesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + VehicleType = VehicleType.FromName(filterQuery.VehicleType) + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a route by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(RouteDto))] + [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(RouteDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetRouteQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a route")] + [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(RouteDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "One or more addresses was not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateRouteViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateRouteCommand() + { + Guid = uuid, + Name = viewModel.Name, + VehicleType = VehicleType.FromName(viewModel.VehicleType), + Addresses = viewModel.Addresses.Select( + e => new RouteAddressModel() + { + Order = e.Order, + Guid = e.Uuid + + }).ToArray() + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a route")] + [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 DeleteRouteCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/TestsController.cs b/src/HttpApi/Controllers/TestsController.cs index 4533d4e..73b7134 100644 --- a/src/HttpApi/Controllers/TestsController.cs +++ b/src/HttpApi/Controllers/TestsController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -8,19 +8,35 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class TestsController : ControllerBase { private readonly IStringLocalizer _localizer; + private readonly EmailSenderService _emailSender; - public TestsController( - CultureService cultureService, - IStringLocalizer localizer) + public TestsController(SessionCultureService cultureService, + IStringLocalizer localizer, EmailSenderService emailSender) { _localizer = localizer; + _emailSender = emailSender; } [HttpGet("getLocalizedString/{inputString}")] - public Task getLocalizedString( + public Task GetLocalizedString( [FromRoute] string inputString, CancellationToken cancellationToken) { return Task.FromResult(_localizer[inputString]); } + + [HttpGet("trigger")] + public async Task Trigger(CancellationToken cancellationToken) + { + var body = +@"Hello, friend! + +This is my email message for you. + +-- +Travel Guide Service +"; + + await _emailSender.SendAsync(new string[] { "cuqmbr@ya.ru" }, "Test subject", body, cancellationToken); + } } diff --git a/src/HttpApi/Controllers/TicketGroupsController.cs b/src/HttpApi/Controllers/TicketGroupsController.cs new file mode 100644 index 0000000..a1291d7 --- /dev/null +++ b/src/HttpApi/Controllers/TicketGroupsController.cs @@ -0,0 +1,155 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.TicketGroups; +using cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup; +using cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup; +using cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage; +using cuqmbr.TravelGuide.Application.TicketGroups.ViewModels; +using cuqmbr.TravelGuide.Application.TicketGroups.Models; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("ticketGroups")] +public class TicketGroupsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a ticket group")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(TicketGroupDto))] + [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] AddTicketGroupViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddTicketGroupCommand() + { + PassangerFirstName = viewModel.PassangerFirstName, + PassangerLastName = viewModel.PassangerLastName, + PassangerPatronymic = viewModel.PassangerPatronymic, + PassangerSex = Sex.FromName(viewModel.PassangerSex), + PassangerBirthDate = viewModel.PassangerBirthDate, + PurchaseTime = viewModel.PurchaseTime, + Status = TicketStatus.FromName(viewModel.Status), + Tickets = viewModel.Tickets.Select(e => + new TicketModel() + { + DepartureRouteAddressGuid = + e.DepartureRouteAddressUuid, + ArrivalRouteAddressGuid = + e.ArrivalRouteAddressUuid, + Order = e.Order, + VehicleEnrollmentGuid = + e.VehicleEnrollmentUuid, + }) + .ToArray() + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all ticket groups")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetTicketGroupsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetTicketGroupsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + PassangerSex = filterQuery.PassangerSex? + .Select(s => Sex.FromName(s)).ToHashSet(), + PassangerBirthDateGreaterThanOrEqualTo = + filterQuery.PassangerBirthDateGreaterThanOrEqualTo, + PassangerBirthDateLessThanOrEqualTo = + filterQuery.PassangerBirthDateLessThanOrEqualTo, + PurchaseTimeGreaterThanOrEqualTo = + filterQuery.PurchaseTimeGreaterThanOrEqualTo, + PurchaseTimeLessThanOrEqualTo = + filterQuery.PurchaseTimeLessThanOrEqualTo, + Statuses = filterQuery.Statuses? + .Select(s => TicketStatus.FromName(s)).ToHashSet(), + VehicleTypes = filterQuery.VehicleTypes? + .Select(vt => VehicleType.FromName(vt)).ToHashSet(), + TravelTimeGreaterThanOrEqualTo = + filterQuery.TravelTimeGreaterThanOrEqualTo, + TravelTimeLessThanOrEqualTo = + filterQuery.TravelTimeLessThanOrEqualTo, + AccountGuid = filterQuery.AccountUuid + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a ticket group by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(TicketGroupDto))] + [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 Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetTicketGroupQuery() { Guid = uuid }, + cancellationToken); + } +} diff --git a/src/HttpApi/Controllers/TrainsController.cs b/src/HttpApi/Controllers/TrainsController.cs new file mode 100644 index 0000000..b1bb992 --- /dev/null +++ b/src/HttpApi/Controllers/TrainsController.cs @@ -0,0 +1,194 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Trains; +using cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain; +using cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage; +using cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain; +using cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain; +using cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain; +using cuqmbr.TravelGuide.Application.Trains.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("trains")] +public class TrainsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a train")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddTrainViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddTrainCommand() + { + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all trains")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetTrainsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetTrainsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CapacityGreaterOrEqualThan = + filterQuery.CapacityGreaterThanOrEqualTo, + CapacityLessOrEqualThan = + filterQuery.CapacityLessThanOrEqualTo + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a train by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetTrainQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a train")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(TrainDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateTrainViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateTrainCommand() + { + Guid = uuid, + Number = viewModel.Number, + Model = viewModel.Model, + Capacity = viewModel.Capacity + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a train")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteTrainCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs b/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs new file mode 100644 index 0000000..5ed284a --- /dev/null +++ b/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs @@ -0,0 +1,119 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; +using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch + .Queries.SearchShortest; +using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch + .Queries.SearchAll; +using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("vehicleEnrollmentSearch")] +public class VehicleEnrollmentSearchController : ControllerBase +{ + [HttpGet("shortest")] + [SwaggerOperation("Search shortest vehicle enrollments with transfers")] + [SwaggerResponse( + StatusCodes.Status200OK, "Search successful", + typeof(VehicleEnrollmentSearchDto))] + [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, "No enrollments found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> SearchShortest( + [FromQuery] SearchShortestViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new SearchShortestQuery() + { + DepartureAddressGuid = viewModel.DepartureAddressUuid, + ArrivalAddressGuid = viewModel.ArrivalAddressUuid, + DepartureDate = viewModel.DepartureDate, + VehicleTypes = viewModel.VehicleTypes + .Select(e => VehicleType.FromName(e)).ToHashSet(), + ShortestByCost = viewModel.ShortestByCost, + ShortestByTime = viewModel.ShortestByTime + }, + cancellationToken)); + } + + [HttpGet("all")] + [SwaggerOperation("Search all vehicle enrollments with transfers")] + [SwaggerResponse( + StatusCodes.Status200OK, "Search successful", + typeof(IEnumerable))] + [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, "No enrollments found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task>> + SearchAll( + [FromQuery] SearchAllViewModel viewModel, + [FromQuery] SortQuery sortQuery, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new SearchAllQuery() + { + DepartureAddressGuid = viewModel.DepartureAddressUuid, + ArrivalAddressGuid = viewModel.ArrivalAddressUuid, + DepartureDate = viewModel.DepartureDate, + VehicleTypes = viewModel.VehicleTypes + .Select(e => VehicleType.FromName(e)).ToHashSet(), + Sort = sortQuery.Sort, + TravelTimeGreaterThanOrEqualTo = + viewModel.TravelTimeGreaterThanOrEqualTo, + TravelTimeLessThanOrEqualTo = + viewModel.TravelTimeLessThanOrEqualTo, + CostGreaterThanOrEqualTo = + viewModel.CostGreaterThanOrEqualTo, + CostLessThanOrEqualTo = + viewModel.CostLessThanOrEqualTo, + NumberOfTransfersGreaterThanOrEqualTo = + viewModel.NumberOfTransfersGreaterThanOrEqualTo, + NumberOfTransfersLessThanOrEqualTo = + viewModel.NumberOfTransfersLessThanOrEqualTo, + DepartureTimeGreaterThanOrEqualTo = + viewModel.DepartureTimeGreaterThanOrEqualTo, + DepartureTimeLessThanOrEqualTo = + viewModel.DepartureTimeLessThanOrEqualTo, + ArrivalTimeGreaterThanOrEqualTo = + viewModel.ArrivalTimeGreaterThanOrEqualTo, + ArrivalTimeLessThanOrEqualTo = + viewModel.ArrivalTimeLessThanOrEqualTo + }, + cancellationToken)); + } +} diff --git a/src/HttpApi/Controllers/VehicleEnrollmentsController.cs b/src/HttpApi/Controllers/VehicleEnrollmentsController.cs new file mode 100644 index 0000000..c79fe23 --- /dev/null +++ b/src/HttpApi/Controllers/VehicleEnrollmentsController.cs @@ -0,0 +1,263 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.VehicleEnrollments; +using cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.AddVehicleEnrollment; +using cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollmentsPage; +using cuqmbr.TravelGuide.Application.VehicleEnrollments + .Queries.GetVehicleEnrollment; +using cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.UpdateVehicleEnrollment; +using cuqmbr.TravelGuide.Application.VehicleEnrollments + .Commands.DeleteVehicleEnrollment; +using cuqmbr.TravelGuide.Application.VehicleEnrollments.Models; +using cuqmbr.TravelGuide.Application.VehicleEnrollments.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("vehicleEnrollments")] +public class VehicleEnrollmentsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add a vehicle enrollment")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(VehicleEnrollmentDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, + "Enrollment travel time overlapping with " + + "other enrollment time of the vehicle", + 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, "Given route not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Given vehicle not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "At least one route address not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddVehicleEnrollmentViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddVehicleEnrollmentCommand() + { + DepartureTime = viewModel.DepartureTime, + Currency = Currency.FromName(viewModel.Currency), + VehicleGuid = viewModel.VehicleUuid, + RouteGuid = viewModel.RouteUuid, + RouteAddressDetails = viewModel.RouteAddressDetails.Select( + rad => new RouteAddressDetailModel() + { + TimeToNextAddress = rad.TimeToNextAddress, + CostToNextAddress = rad.CostToNextAddress, + CurrentAddressStopTime = rad.CurrentAddressStopTime, + RouteAddressGuid = rad.RouteAddressUuid + }) + .ToArray(), + EmployeeGuids = viewModel.EmployeeUuids + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all vehicle enrollments")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetVehicleEnrollmentsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetVehicleEnrollmentsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + // Search = searchQuery.Search, + Sort = sortQuery.Sort, + RouteGuid = filterQuery.RouteGuid, + VehicleGuid = filterQuery.VehicleGuid, + NumberOfAddressesGreaterThanOrEqual = + filterQuery.NumberOfAddressesGreaterThanOrEqual, + NumberOfAddressesLessThanOrEqual = + filterQuery.NumberOfAddressesLessThanOrEqual, + DepartureTimeGreaterThanOrEqual = + filterQuery.DepartureTimeGreaterThanOrEqual, + DepartureTimeLessThanOrEqual = + filterQuery.DepartureTimeLessThanOrEqual, + ArrivalTimeGreaterThanOrEqual = + filterQuery.ArrivalTimeGreaterThanOrEqual, + ArrivalTimeLessThanOrEqual = + filterQuery.ArrivalTimeLessThanOrEqual, + TravelTimeGreaterThanOrEqual = + filterQuery.TravelTimeGreaterThanOrEqual, + TravelTimeLessThanOrEqual = + filterQuery.TravelTimeLessThanOrEqual, + TimeMovingGreaterThanOrEqual = + filterQuery.TimeMovingGreaterThanOrEqual, + TimeMovingLessThanOrEqual = + filterQuery.TimeMovingLessThanOrEqual, + TimeInStopsGreaterThanOrEqual = + filterQuery.TimeInStopsGreaterThanOrEqual, + TimeInStopsLessThanOrEqual = + filterQuery.TimeInStopsLessThanOrEqual, + CostGreaterThanOrEqual = + filterQuery.CostGreaterThanOrEqual, + CostLessThanOrEqual = + filterQuery.CostLessThanOrEqual, + Currency = Currency.FromName(filterQuery.Currency), + EmployeeGuids = filterQuery.EmployeeUuids + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get a vehicle enrollment by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(VehicleEnrollmentDto))] + [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(VehicleEnrollmentDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetVehicleEnrollmentQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update a vehicle enrollment")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(VehicleEnrollmentDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, + "Enrollment travel time overlapping with " + + "other enrollment time of the vehicle", + 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.Status404NotFound, "At least one route address not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateVehicleEnrollmentViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateVehicleEnrollmentCommand() + { + Guid = uuid, + DepartureTime = viewModel.DepartureTime, + Currency = Currency.FromName(viewModel.Currency), + RouteAddressDetails = viewModel.RouteAddressDetails.Select( + rad => new RouteAddressDetailModel() + { + TimeToNextAddress = rad.TimeToNextAddress, + CostToNextAddress = rad.CostToNextAddress, + CurrentAddressStopTime = rad.CurrentAddressStopTime, + RouteAddressGuid = rad.RouteAddressUuid + }) + .ToArray(), + EmployeeGuids = viewModel.EmployeeUuids + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete a vehicle enrollment")] + [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 DeleteVehicleEnrollmentCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs b/src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs new file mode 100644 index 0000000..93de31a --- /dev/null +++ b/src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs @@ -0,0 +1,41 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.TicketGroups + .Commands.RemoveOldReservedTicketGroups; + +namespace cuqmbr.TravelGuide.HttpApi.HostedServices; + +public class ReservedTicketRemoverHostedService : BackgroundService +{ + private Timer _timer = null; + + public ReservedTicketRemoverHostedService( + IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public IServiceProvider ServiceProvider; + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _timer = new Timer(async (state) => + { + using var scope = ServiceProvider.CreateScope(); + var _mediator = scope.ServiceProvider + .GetRequiredService(); + + await _mediator.Send( + new RemoveOldReservedTicketGroupsCommand() + { + ReservedFor = TimeSpan.FromMinutes(10) + }, cancellationToken); + }, + null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Dispose(); + await base.StopAsync(cancellationToken); + } +} diff --git a/src/HttpApi/HttpApi.csproj b/src/HttpApi/HttpApi.csproj index 8afd0fb..5719f8b 100644 --- a/src/HttpApi/HttpApi.csproj +++ b/src/HttpApi/HttpApi.csproj @@ -13,7 +13,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs b/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs index 39c0018..6e303b5 100644 --- a/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs +++ b/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs @@ -1,7 +1,7 @@ using cuqmbr.TravelGuide.Application.Common.Exceptions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; -using System.Reflection; +using System.Diagnostics; namespace cuqmbr.TravelGuide.HttpApi.Middlewares; @@ -11,6 +11,8 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware private readonly ILogger _logger; private readonly IStringLocalizer _localizer; + + public GlobalExceptionHandlerMiddleware( ILogger logger, IStringLocalizer localizer) @@ -75,7 +77,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetails(ex.Errors) + await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetailsWithTraceId(ex.Errors) { Status = StatusCodes.Status400BadRequest, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", @@ -90,7 +92,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status401Unauthorized; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status401Unauthorized, Type = "https://datatracker.ietf.org/doc/html/rfc7235#section-3.1", @@ -105,7 +107,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status400BadRequest, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", @@ -120,7 +122,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status400BadRequest, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", @@ -135,7 +137,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status400BadRequest, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", @@ -150,7 +152,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status403Forbidden; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status403Forbidden, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3", @@ -165,7 +167,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status400BadRequest, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", @@ -180,7 +182,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status404NotFound; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status404NotFound, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4", @@ -193,7 +195,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware { context.Response.StatusCode = StatusCodes.Status500InternalServerError; - await context.Response.WriteAsJsonAsync(new ProblemDetails() + await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId() { Status = StatusCodes.Status500InternalServerError, Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", @@ -202,9 +204,26 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware }); } - // class ProblemDetailsWithTraceId : ProblemDetails - // { - // public string TraceId { get; init; } = Activity.Current?.TraceId.ToString(); - // } -} + class ProblemDetailsWithTraceId : ProblemDetails + { + public ProblemDetailsWithTraceId() + { + Extensions = new Dictionary() + { + ["traceId"] = Activity.Current.Id + }; + } + } + class HttpValidationProblemDetailsWithTraceId : HttpValidationProblemDetails + { + public HttpValidationProblemDetailsWithTraceId( + IDictionary errors) : base(errors) + { + Extensions = new Dictionary() + { + ["traceId"] = Activity.Current.Id + }; + } + } +} diff --git a/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs b/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs index 8b02796..3f7e4fa 100644 --- a/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs +++ b/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs @@ -1,13 +1,13 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using System.Globalization; namespace cuqmbr.TravelGuide.HttpApi.Middlewares; public class ThreadCultureSetterMiddleware : IMiddleware { - private readonly CultureService _cultureService; + private readonly SessionCultureService _cultureService; - public ThreadCultureSetterMiddleware(CultureService cultureService) + public ThreadCultureSetterMiddleware(SessionCultureService cultureService) { _cultureService = cultureService; } diff --git a/src/HttpApi/Program.cs b/src/HttpApi/Program.cs index cb281b3..c708d32 100644 --- a/src/HttpApi/Program.cs +++ b/src/HttpApi/Program.cs @@ -1,14 +1,15 @@ using cuqmbr.TravelGuide.Configuration.Persistence; using cuqmbr.TravelGuide.Configuration.Application; -using cuqmbr.TravelGuide.Configuration.Identity; +using cuqmbr.TravelGuide.Configuration.Infrastructure; using cuqmbr.TravelGuide.Configuration.Configuration; using cuqmbr.TravelGuide.Configuration.Logging; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.HttpApi.Services; +using cuqmbr.TravelGuide.HttpApi.HostedServices; using cuqmbr.TravelGuide.HttpApi.Middlewares; using cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; +using System.Net; using Swashbuckle.AspNetCore.SwaggerUI; -using MicroElements.Swashbuckle.FluentValidation.AspNetCore; using Microsoft.OpenApi.Models; using System.Reflection; @@ -22,14 +23,17 @@ services.ConfigureConfiguration(args); services.ConfigureLogging(); +services.ConfigureInfrastructure(); services.ConfigurePersistence(); -services.ConfigureIdentity(); -// services.AddInfrastructure(); services.ConfigureApplication(); +services.AddHttpContextAccessor(); services.AddScoped(); -services.AddScoped(); -services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); + +services.AddHostedService(); services.AddControllers(); @@ -81,8 +85,17 @@ services.AddSwaggerGen(options => "from IANA tz database (https://www.iana.org/time-zones).", Type = SecuritySchemeType.ApiKey }); + + // Set Accept-Currency header in Authorize window + options.OperationFilter(); + options.AddSecurityDefinition("Accept-Currency", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Name = "Accept-Currency", + Description = "ISO-4217 Currency Code.", + Type = SecuritySchemeType.ApiKey + }); }); -services.AddFluentValidationRulesToSwagger(); services.AddScoped(); @@ -90,6 +103,13 @@ services.AddScoped(); services.AddHealthChecks(); +builder.WebHost.ConfigureKestrel((context, options) => +{ + // TODO: Make possible to configure using file/env/cli + options.Configure() + .Endpoint(new IPEndPoint(new IPAddress(0x00000000), 8080)); +}); + var app = builder.Build(); diff --git a/src/HttpApi/Properties/launchSettings.json b/src/HttpApi/Properties/launchSettings.json deleted file mode 100644 index 97084c0..0000000 --- a/src/HttpApi/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:8080" - } - } -} diff --git a/src/HttpApi/Services/AspNetCultureService.cs b/src/HttpApi/Services/AspNetSessionCultureService.cs similarity index 83% rename from src/HttpApi/Services/AspNetCultureService.cs rename to src/HttpApi/Services/AspNetSessionCultureService.cs index 03388ab..7a7d614 100644 --- a/src/HttpApi/Services/AspNetCultureService.cs +++ b/src/HttpApi/Services/AspNetSessionCultureService.cs @@ -1,14 +1,14 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; using System.Globalization; namespace cuqmbr.TravelGuide.HttpApi.Services; -public sealed class AspNetCultureService : CultureService +public sealed class AspNetSessionCultureService : SessionCultureService { private readonly HttpContext _httpContext; private const string DefaultCultureId = "en-US"; - public AspNetCultureService(IHttpContextAccessor httpContextAccessor) + public AspNetSessionCultureService(IHttpContextAccessor httpContextAccessor) { _httpContext = httpContextAccessor.HttpContext!; } diff --git a/src/HttpApi/Services/AspNetSessionCurrencyService.cs b/src/HttpApi/Services/AspNetSessionCurrencyService.cs new file mode 100644 index 0000000..91e4b68 --- /dev/null +++ b/src/HttpApi/Services/AspNetSessionCurrencyService.cs @@ -0,0 +1,38 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.HttpApi.Services; + +public sealed class AspNetSessionCurrencyService : SessionCurrencyService +{ + private readonly HttpContext _httpContext; + + public AspNetSessionCurrencyService(IHttpContextAccessor httpContextAccessor) + { + _httpContext = httpContextAccessor.HttpContext; + } + + public Currency Currency + { + get + { + string? headerCurrencyCode = + _httpContext.Request.Headers["Accept-Currency"]; + + string currencyCode = + headerCurrencyCode?.ToUpper() ?? + cuqmbr.TravelGuide.Domain.Enums.Currency.Default.Name; + + var resultCurrency = + cuqmbr.TravelGuide.Domain.Enums.Currency.FromName(currencyCode); + + if (resultCurrency == null) + { + resultCurrency = + cuqmbr.TravelGuide.Domain.Enums.Currency.Default; + } + + return resultCurrency; + } + } +} diff --git a/src/HttpApi/Services/AspNetTimeZoneService.cs b/src/HttpApi/Services/AspNetSessionTimeZoneService.cs similarity index 72% rename from src/HttpApi/Services/AspNetTimeZoneService.cs rename to src/HttpApi/Services/AspNetSessionTimeZoneService.cs index eddcfb9..55acb54 100644 --- a/src/HttpApi/Services/AspNetTimeZoneService.cs +++ b/src/HttpApi/Services/AspNetSessionTimeZoneService.cs @@ -1,12 +1,12 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Services; namespace cuqmbr.TravelGuide.HttpApi.Services; -public sealed class AspNetTimeZoneService : TimeZoneService +public sealed class AspNetSessionTimeZoneService : SessionTimeZoneService { private readonly HttpContext _httpContext; - public AspNetTimeZoneService(IHttpContextAccessor httpContextAccessor) + public AspNetSessionTimeZoneService(IHttpContextAccessor httpContextAccessor) { _httpContext = httpContextAccessor.HttpContext!; } diff --git a/src/HttpApi/Services/AspNetSessionUserService.cs b/src/HttpApi/Services/AspNetSessionUserService.cs index eed5afc..2f9c8fd 100644 --- a/src/HttpApi/Services/AspNetSessionUserService.cs +++ b/src/HttpApi/Services/AspNetSessionUserService.cs @@ -1,6 +1,6 @@ using System.IdentityModel.Tokens.Jwt; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.HttpApi.Services; @@ -13,7 +13,7 @@ public sealed class AspNetSessionUserService : SessionUserService _httpContext = httpContextAccessor.HttpContext; } - public int? Id + public Guid? Guid { get { @@ -22,16 +22,16 @@ public sealed class AspNetSessionUserService : SessionUserService .Any(p => p.Value == JwtRegisteredClaimNames.Sub)) ?.Value; - var parsed = int.TryParse(claimValue, out var id); + var parsed = System.Guid.TryParse(claimValue, out var guid); - return parsed ? id : null; + return parsed ? guid : null; } } - public Guid? Uuid => Guid.Parse(_httpContext.User.Claims + public string? Username => _httpContext.User.Claims .FirstOrDefault(c => c.Properties - .Any(p => p.Value.Equals("uuid"))) - ?.Value); + .Any(p => p.Value == JwtRegisteredClaimNames.Nickname)) + ?.Value; public string? Email => _httpContext.User.Claims .FirstOrDefault(c => c.Properties @@ -48,6 +48,7 @@ public sealed class AspNetSessionUserService : SessionUserService _httpContext.Request.Cookies["accessToken"] ?? _httpContext.Request.Headers["Authorization"] .ToString()?.Replace("Bearer ", ""); + public string? RefreshToken => _httpContext.Request.Cookies["refreshToken"]; } diff --git a/src/HttpApi/Swashbuckle/OperationFilters/AcceptCurrencyHeaderOperationFilter.cs b/src/HttpApi/Swashbuckle/OperationFilters/AcceptCurrencyHeaderOperationFilter.cs new file mode 100644 index 0000000..a661bfa --- /dev/null +++ b/src/HttpApi/Swashbuckle/OperationFilters/AcceptCurrencyHeaderOperationFilter.cs @@ -0,0 +1,39 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; + +public class AcceptCurrencyHeaderOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // TODO: Remove security requirements + operation.Security ??= new List(); + + var acceptCurrency = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Accept-Currency" + } + }; + + operation.Security.Add(new OpenApiSecurityRequirement + { + [acceptCurrency] = new List() + }); + + if (operation.Parameters == null) + operation.Parameters = new List(); + + operation.Parameters.Add(new OpenApiParameter + { + Name = "Accept-Currency", + Description = "ISO-4217 Currency Code.", + In = ParameterLocation.Header, + Schema = new OpenApiSchema { Type = "String" }, + Required = false + }); + } +} diff --git a/src/HttpApi/appsettings.Development.json b/src/HttpApi/appsettings.Development.json index 982fe9d..1fd9334 100644 --- a/src/HttpApi/appsettings.Development.json +++ b/src/HttpApi/appsettings.Development.json @@ -1,40 +1,42 @@ { - "Kestrel": { - "EndPoints": { - "Http": { - "Url": "http://localhost:4300" - } + "Logging": { + "Type": "SimpleConsole", + "LogLevel": "Information", + "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK", + "UseUtcTimestamp": true + }, + "Localization": { + "DefaultCultureName": "en-US", + "CacheDuration": "00:30:00" + }, + "JsonWebToken": { + "Issuer": "https://api.travel-guide.cuqmbr.xyz", + "Audience": "https://travel-guide.cuqmbr.xyz", + "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", + "AccessTokenValidity": "24:00:00", + "RefreshTokenValidity": "72:00:00" + }, + "Datastore": { + "Type": "postgresql", + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true" + }, + "PaymentProcessing": { + "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", + "ResultAddressBase": "https://travel-guide.cuqmbr.xyz", + "LiqPay": { + "PublicKey": "sandbox_xxxxxxxxxxxx", + "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } }, - "Application": { - "Logging": { - "Type": "SimpleConsole", - "LogLevel": "Information", - "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK", - "UseUtcTimestamp": true - }, - "Datastore": { - "Type": "postgresql", - "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000", - "PartitionName": "application" - }, - "Localization": { - "DefaultCultureName": "en-US", - "CacheDuration": "00:30:00" - } - }, - "Identity": { - "Datastore": { - "Type": "postgresql", - "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000", - "PartitionName": "identity" - }, - "JsonWebToken": { - "Issuer": "https://api.travel-guide.cuqmbr.xyz", - "Audience": "https://travel-guide.cuqmbr.xyz", - "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", - "AccessTokenValidity": "24:00:00", - "RefreshTokenValidity": "72:00:00" + "Email": { + "Smtp": { + "Host": "mail.travel-guide.cuqmbr.xyz", + "Port": "465", + "UseTls": true, + "Username": "no-reply", + "Password": "super-secret-password", + "SenderAddress": "no-reply@travel-guide.cuqmbr.xyz", + "SenderName": "Travel Guide" } } } diff --git a/src/HttpApi/appsettings.json b/src/HttpApi/appsettings.json new file mode 100644 index 0000000..1fd9334 --- /dev/null +++ b/src/HttpApi/appsettings.json @@ -0,0 +1,42 @@ +{ + "Logging": { + "Type": "SimpleConsole", + "LogLevel": "Information", + "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK", + "UseUtcTimestamp": true + }, + "Localization": { + "DefaultCultureName": "en-US", + "CacheDuration": "00:30:00" + }, + "JsonWebToken": { + "Issuer": "https://api.travel-guide.cuqmbr.xyz", + "Audience": "https://travel-guide.cuqmbr.xyz", + "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", + "AccessTokenValidity": "24:00:00", + "RefreshTokenValidity": "72:00:00" + }, + "Datastore": { + "Type": "postgresql", + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true" + }, + "PaymentProcessing": { + "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", + "ResultAddressBase": "https://travel-guide.cuqmbr.xyz", + "LiqPay": { + "PublicKey": "sandbox_xxxxxxxxxxxx", + "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + }, + "Email": { + "Smtp": { + "Host": "mail.travel-guide.cuqmbr.xyz", + "Port": "465", + "UseTls": true, + "Username": "no-reply", + "Password": "super-secret-password", + "SenderAddress": "no-reply@travel-guide.cuqmbr.xyz", + "SenderName": "Travel Guide" + } + } +} diff --git a/src/HttpApi/packages.lock.json b/src/HttpApi/packages.lock.json index f7cac22..59d1bfe 100644 --- a/src/HttpApi/packages.lock.json +++ b/src/HttpApi/packages.lock.json @@ -13,17 +13,6 @@ "Microsoft.Extensions.Localization": "9.0.0" } }, - "MicroElements.Swashbuckle.FluentValidation": { - "type": "Direct", - "requested": "[6.1.0, )", - "resolved": "6.1.0", - "contentHash": "VzqApLPY8xIqXDvWqRuvoDYEoCHII42c4LgvLO3BikKoIVcECD+ZSG727I7yPZ/J07VEoa8aJddoqUtSm4E4gw==", - "dependencies": { - "FluentValidation": "[10.0.0, 12.0.0)", - "MicroElements.OpenApi.FluentValidation": "6.1.0", - "Swashbuckle.AspNetCore.SwaggerGen": "[6.3.0, 8.0.0)" - } - }, "Microsoft.AspNetCore.OpenApi": { "type": "Direct", "requested": "[9.0.4, )", @@ -117,6 +106,11 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "FluentValidation": { "type": "Transitive", "resolved": "11.11.0", @@ -136,6 +130,15 @@ "resolved": "2.14.1", "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" }, + "MailKit": { + "type": "Transitive", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "MediatR": { "type": "Transitive", "resolved": "12.4.1", @@ -159,17 +162,6 @@ "resolved": "2.0.1", "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" }, - "MicroElements.OpenApi.FluentValidation": { - "type": "Transitive", - "resolved": "6.1.0", - "contentHash": "qJPAI3bL70ND6fIi4bGqQf/lpV9wUod23R7JVhVVYRonoT/ZGmPBjMbO//IPPy3yP2F21iMX05brYjyU4/WqwQ==", - "dependencies": { - "FluentValidation": "[10.0.0, 12.0.0)", - "Microsoft.AspNetCore.Http.Abstractions": "2.2.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0" - } - }, "Microsoft.AspNetCore.Authentication": { "type": "Transitive", "resolved": "2.3.0", @@ -214,8 +206,8 @@ }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } @@ -246,15 +238,15 @@ }, "Microsoft.AspNetCore.Cryptography.Internal": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" + "resolved": "2.3.0", + "contentHash": "/qy5r0CD40OccajzDmX3gBfqqxpAJkcXoqlVz0YR70x3gTRq/VuseDU/lZ5eh8vM+KCdmPFAtyGcRWxTyXxuYg==" }, "Microsoft.AspNetCore.Cryptography.KeyDerivation": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", + "resolved": "2.3.0", + "contentHash": "S7pph0JuBkgNqtyiIdLtQ5icZxmpX502zxxvHuMtM5W7IR3CKl1r/Cup+i6+E6B7IF3BeZYF4O3RbcA108syig==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" + "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0" } }, "Microsoft.AspNetCore.DataProtection": { @@ -348,15 +340,6 @@ "Microsoft.Extensions.Identity.Core": "2.3.0" } }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "9.0.4", - "Microsoft.Extensions.Identity.Stores": "9.0.4" - } - }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", "resolved": "9.0.0", @@ -634,13 +617,23 @@ "resolved": "9.0.4", "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", + "resolved": "9.0.4", + "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -678,24 +671,28 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, - "Microsoft.Extensions.Identity.Core": { + "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "9.0.4", - "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", + "contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Diagnostics": "9.0.4", "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", "Microsoft.Extensions.Options": "9.0.4" } }, - "Microsoft.Extensions.Identity.Stores": { + "Microsoft.Extensions.Identity.Core": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", + "resolved": "2.3.0", + "contentHash": "yR0eFnUbAM2k+q5QsX0NKinfShIe1B/aiHXEywiNT5Cs2MvEhxQIbIn5rWXnEAfmwW+i+t5D8odPSEHz/taIyQ==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.4", - "Microsoft.Extensions.Identity.Core": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", + "Microsoft.Extensions.Logging": "8.0.1", + "Microsoft.Extensions.Options": "8.0.2", + "System.ComponentModel.Annotations": "5.0.0" } }, "Microsoft.Extensions.Localization": { @@ -792,23 +789,23 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.8.0" + "Microsoft.IdentityModel.Tokens": "8.11.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.8.0" + "Microsoft.IdentityModel.Abstractions": "8.11.0" } }, "Microsoft.IdentityModel.Protocols": { @@ -830,11 +827,11 @@ }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.IdentityModel.Logging": "8.8.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" } }, "Microsoft.Net.Http.Headers": { @@ -865,6 +862,15 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, "Mono.TextTemplating": { "type": "Transitive", "resolved": "3.0.0", @@ -873,6 +879,11 @@ "System.CodeDom": "6.0.0" } }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "Npgsql": { "type": "Transitive", "resolved": "9.0.3", @@ -891,6 +902,11 @@ "Npgsql": "9.0.3" } }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.10", @@ -936,6 +952,11 @@ "resolved": "7.0.0", "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" + }, "System.Composition": { "type": "Transitive", "resolved": "7.0.0", @@ -984,6 +1005,11 @@ "System.Composition.Runtime": "7.0.0" } }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -1072,7 +1098,12 @@ "FluentValidation": "[11.11.0, )", "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.5, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.11.0, )", + "Microsoft.IdentityModel.Tokens": "[8.11.0, )", + "Newtonsoft.Json": "[13.0.3, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, @@ -1083,7 +1114,6 @@ "AspNetCore.Localizer.Json": "[1.0.1, )", "Domain": "[1.0.0, )", "FluentValidation.DependencyInjectionExtensions": "[11.11.0, )", - "Identity": "[1.0.0, )", "Infrastructure": "[1.0.0, )", "Microsoft.AspNetCore.Identity": "[2.3.1, )", "Microsoft.EntityFrameworkCore.InMemory": "[9.0.4, )", @@ -1103,23 +1133,13 @@ "domain": { "type": "Project" }, - "identity": { - "type": "Project", - "dependencies": { - "Application": "[1.0.0, )", - "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.4, )", - "Microsoft.AspNetCore.Identity": "[2.3.1, )", - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[9.0.4, )", - "Microsoft.Extensions.Options": "[9.0.4, )", - "Microsoft.IdentityModel.JsonWebTokens": "[8.8.0, )", - "Microsoft.IdentityModel.Tokens": "[8.8.0, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" - } - }, "infrastructure": { "type": "Project", "dependencies": { - "Application": "[1.0.0, )" + "Application": "[1.0.0, )", + "MailKit": "[4.12.1, )", + "Microsoft.Extensions.Http": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )" } }, "persistence": { diff --git a/src/Identity/ConfigurationOptions.cs b/src/Identity/ConfigurationOptions.cs deleted file mode 100644 index bf10582..0000000 --- a/src/Identity/ConfigurationOptions.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace cuqmbr.TravelGuide.Identity; - -public sealed class ConfigurationOptions -{ - public static string SectionName { get; } = "Identity"; - - public Datastore Datastore { get; set; } = new(); - - public JsonWebToken JsonWebToken { get; set; } = new(); -} - -public sealed class Datastore -{ - public string Type { get; set; } = "inmemory"; - - public string ConnectionString { get; set; } = "InMemory"; - - public string PartitionName { get; set; } = "identity"; - - public bool Initialize { get; set; } = true; -} - -public sealed class JsonWebToken -{ - public string Issuer { get; set; } = "localhost"; - - public string Audience { get; set; } = "localhost"; - - public string IssuerSigningKey { get; set; } = "change-me"; - - public TimeSpan AccessTokenValidity { get; set; } = TimeSpan.FromMinutes(15); - - public TimeSpan RefreshTokenValidity { get; set; } = TimeSpan.FromDays(3); -} diff --git a/src/Identity/Exceptions/UnSupportedDatastoreException.cs b/src/Identity/Exceptions/UnSupportedDatastoreException.cs deleted file mode 100644 index 7366911..0000000 --- a/src/Identity/Exceptions/UnSupportedDatastoreException.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace cuqmbr.TravelGuide.Identity.Exceptions; - -public class UnSupportedDatastoreException : Exception -{ - public UnSupportedDatastoreException() - : base() { } - - public UnSupportedDatastoreException(string message) - : base(message) { } -} - diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj deleted file mode 100644 index 53c2cb1..0000000 --- a/src/Identity/Identity.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - net9.0 - enable - enable - - - - - - - - - - - - - - - - - - true - - - diff --git a/src/Identity/IdentitySeeder.cs b/src/Identity/IdentitySeeder.cs deleted file mode 100644 index 15e7647..0000000 --- a/src/Identity/IdentitySeeder.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; -using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; -using Microsoft.Extensions.DependencyInjection; -using IdentityRoleEnum = cuqmbr.TravelGuide.Application.Common.Models - .IdentityRole; - -namespace cuqmbr.TravelGuide.Identity; - -public static class IdentitySeeder -{ - public static void Seed(IServiceProvider serviceProvider) - { - using var userManager = serviceProvider - .GetService>(); - userManager.UserValidators.Clear(); - userManager.PasswordValidators.Clear(); - - using var roleManager = serviceProvider - .GetService>(); - roleManager.RoleValidators.Clear(); - - // Seed Roles - foreach (var role in IdentityRoleEnum.Enumerations) - { - var roleString = role.Value.Name; - - var roleExists = roleManager.RoleExistsAsync(roleString).Result; - - if (roleExists) - { - continue; - } - - roleManager.CreateAsync(new IdentityRole() - { - Name = roleString, - ConcurrencyStamp = Guid.NewGuid().ToString("D") - }).Wait(); - } - - // Seed Identity - var users = new (string Email, string Password, IdentityRoleEnum[] Roles)[] - { - ("admin", "admin", new [] { IdentityRoleEnum.Administrator }), - }; - - foreach (var user in users) - { - - var userExists = userManager - .FindByEmailAsync(user.Email).Result is not null; - - if (userExists) - { - continue; - } - - var newUser = new IdentityUser - { - Email = user.Email, - NormalizedEmail = user.Email.ToUpper(), - EmailConfirmed = true, - SecurityStamp = Guid.NewGuid().ToString("D"), - RefreshTokens = default! - }; - - var hashedPassword - = userManager.PasswordHasher.HashPassword(newUser, user.Password); - newUser.PasswordHash = hashedPassword; - - userManager - .CreateAsync(newUser) - .Wait(); - - var userRoles = user.Roles.Select(x => x.Name); - - userManager - .AddToRolesAsync(newUser, userRoles) - .Wait(); - } - - } -} - diff --git a/src/Identity/Models/IdentityRole.cs b/src/Identity/Models/IdentityRole.cs deleted file mode 100644 index b1d9ee4..0000000 --- a/src/Identity/Models/IdentityRole.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace cuqmbr.TravelGuide.Identity.Models; - -public class IdentityRole : Microsoft.AspNetCore.Identity.IdentityRole -{ - public string Uuid { get; set; } = Guid.NewGuid().ToString(); -} diff --git a/src/Identity/Models/IdentityUser.cs b/src/Identity/Models/IdentityUser.cs deleted file mode 100644 index ce9aad1..0000000 --- a/src/Identity/Models/IdentityUser.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace cuqmbr.TravelGuide.Identity.Models; - -public class IdentityUser : Microsoft.AspNetCore.Identity.IdentityUser -{ - public string Uuid { get; set; } = Guid.NewGuid().ToString(); - - public ICollection RefreshTokens { get; set; } -} diff --git a/src/Identity/Models/RefreshToken.cs b/src/Identity/Models/RefreshToken.cs deleted file mode 100644 index a751096..0000000 --- a/src/Identity/Models/RefreshToken.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace cuqmbr.TravelGuide.Identity.Models; - -public class RefreshToken -{ - public int Id { get; set; } - - public string Value { get; set; } = null!; - - public DateTimeOffset CreationTimestamp { get; set; } - - public DateTimeOffset ExpirationTimestamp { get; set; } - - public DateTimeOffset? RevokationTimestamp { get; set; } - - public bool IsExpired => DateTimeOffset.UtcNow >= ExpirationTimestamp; - - public bool IsActive => RevokationTimestamp is null && !IsExpired; - - public int IdentityUserId { get; set; } - - public IdentityUser IdentityUser { get; set; } -} - diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs deleted file mode 100644 index 91c8229..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityRoleClaimConfiguration : IEntityTypeConfiguration> -{ - public void Configure(EntityTypeBuilder> builder) - { - builder - .ToTable("role_claims"); - - builder - .Property(rc => rc.Id) - .HasColumnName("id"); - - builder - .Property(rc => rc.RoleId) - .HasColumnName("role_id"); - - builder - .Property(rc => rc.ClaimType) - .HasColumnName("claim_type"); - - builder - .Property(rc => rc.ClaimValue) - .HasColumnName("claim_value"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs deleted file mode 100644 index e8ee518..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs +++ /dev/null @@ -1,34 +0,0 @@ -using cuqmbr.TravelGuide.Identity.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityRoleConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("roles"); - - builder - .Property(r => r.Id) - .HasColumnName("id"); - - builder - .Property(r => r.Uuid) - .HasColumnName("uuid"); - - builder - .Property(r => r.Name) - .HasColumnName("name"); - - builder - .Property(r => r.NormalizedName) - .HasColumnName("normalized_name"); - - builder - .Property(r => r.ConcurrencyStamp) - .HasColumnName("concurrency_stamp"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs deleted file mode 100644 index 863092f..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityUserClaimConfiguration : IEntityTypeConfiguration> -{ - public void Configure(EntityTypeBuilder> builder) - { - builder - .ToTable("user_claims"); - - builder - .Property(uc => uc.Id) - .HasColumnName("id"); - - builder - .Property(uc => uc.UserId) - .HasColumnName("user_id"); - - builder - .Property(uc => uc.ClaimType) - .HasColumnName("claim_type"); - - builder - .Property(uc => uc.ClaimValue) - .HasColumnName("claim_value"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs deleted file mode 100644 index e99835c..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs +++ /dev/null @@ -1,128 +0,0 @@ -using cuqmbr.TravelGuide.Identity.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityUserConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("users"); - - builder - .Property(u => u.Id) - .HasColumnName("id"); - - builder - .Property(u => u.Uuid) - .HasColumnName("uuid"); - - builder - .Ignore(u => u.UserName); - - builder - .Ignore(u => u.NormalizedUserName); - - builder - .Property(u => u.Email) - .HasColumnName("email"); - - builder - .Property(u => u.NormalizedEmail) - .HasColumnName("normalized_email"); - - builder - .Property(u => u.EmailConfirmed) - .HasColumnName("email_confirmed"); - - builder - .Property(u => u.PasswordHash) - .HasColumnName("password_hash"); - - builder - .Property(u => u.SecurityStamp) - .HasColumnName("security_stamp"); - - builder - .Property(u => u.ConcurrencyStamp) - .HasColumnName("concurrency_stamp"); - - builder - .Ignore(u => u.PhoneNumber); - - builder - .Ignore(u => u.PhoneNumberConfirmed); - - builder - .Property(u => u.TwoFactorEnabled) - .HasColumnName("two_factor_enabled"); - - builder - .Property(u => u.LockoutEnabled) - .HasColumnName("lockout_enabled"); - - builder - .Property(u => u.LockoutEnd) - .HasColumnName("lockout_end"); - - builder - .Property(u => u.AccessFailedCount) - .HasColumnName("access_failed_count"); - - builder - .OwnsMany(u => u.RefreshTokens, - refreshToken => - { - refreshToken - .ToTable("user_refresh_tokens"); - - refreshToken - .HasKey(rt => rt.Id) - .HasName("id"); - - refreshToken - .WithOwner(rt => rt.IdentityUser) - .HasForeignKey(rt => rt.IdentityUserId) - .HasConstraintName("fk_identityUserRefreshTokens_identityUser_userId"); - - refreshToken - .Property(rt => rt.Id) - .HasColumnName("id") - .HasColumnType("int") - .IsRequired(); - - refreshToken - .Property(rt => rt.IdentityUserId) - .HasColumnName("user_id") - .HasColumnType("int") - .IsRequired(); - - refreshToken - .Property(rt => rt.Value) - .HasColumnName("value") - .HasColumnType("varchar(256)") - .IsRequired(); - - refreshToken - .Property(rt => rt.CreationTimestamp) - .HasColumnName("creation_timestamp_utc") - .HasColumnType("timestamptz") - .IsRequired(); - - refreshToken - .Property(rt => rt.ExpirationTimestamp) - .HasColumnName("expiration_timestamp_utc") - .HasColumnType("timestamptz") - .IsRequired(); - - refreshToken - .Property(rt => rt.RevokationTimestamp) - .HasColumnName("revokation_timestamp_utc") - .HasColumnType("timestamptz") - .IsRequired(false); - } - ); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs deleted file mode 100644 index 3464a1c..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityUserLoginConfiguration : IEntityTypeConfiguration> -{ - public void Configure(EntityTypeBuilder> builder) - { - builder - .ToTable("user_logins"); - - builder - .Property(ul => ul.LoginProvider) - .HasColumnName("login_provider"); - - builder - .Property(ul => ul.ProviderKey) - .HasColumnName("provider_key"); - - builder - .Property(ul => ul.ProviderDisplayName) - .HasColumnName("provider_display_name"); - - builder - .Property(ul => ul.UserId) - .HasColumnName("user_id"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs deleted file mode 100644 index 70fee96..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityUserRoleConfiguration : IEntityTypeConfiguration> -{ - public void Configure(EntityTypeBuilder> builder) - { - builder - .ToTable("user_roles"); - - builder - .Property(ur => ur.UserId) - .HasColumnName("user_id"); - - builder - .Property(ur => ur.RoleId) - .HasColumnName("role_id"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs deleted file mode 100644 index 6b91d44..0000000 --- a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; - -public class IdentityUserTokenConfiguration : IEntityTypeConfiguration> -{ - public void Configure(EntityTypeBuilder> builder) - { - builder - .ToTable("user_tokens"); - - builder - .Property(ut => ut.UserId) - .HasColumnName("user_id"); - - builder - .Property(ut => ut.LoginProvider) - .HasColumnName("login_provider"); - - builder - .Property(ut => ut.Name) - .HasColumnName("name"); - - builder - .Property(ut => ut.Value) - .HasColumnName("value"); - } -} diff --git a/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs b/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs deleted file mode 100644 index 5fd201a..0000000 --- a/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs +++ /dev/null @@ -1,355 +0,0 @@ -// -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.Identity.Persistence.PostgreSql; - -#nullable disable - -namespace Identity.Persistence.PostgreSql.Migrations -{ - [DbContext(typeof(PostgreSqlIdentityDbContext))] - [Migration("20250423194315_Initial_migration")] - partial class Initial_migration - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("identity") - .HasAnnotation("ProductVersion", "9.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text") - .HasColumnName("claim_type"); - - b.Property("ClaimValue") - .HasColumnType("text") - .HasColumnName("claim_value"); - - b.Property("RoleId") - .HasColumnType("integer") - .HasColumnName("role_id"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("role_claims", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text") - .HasColumnName("claim_type"); - - b.Property("ClaimValue") - .HasColumnType("text") - .HasColumnName("claim_value"); - - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("user_claims", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("text") - .HasColumnName("login_provider"); - - b.Property("ProviderKey") - .HasColumnType("text") - .HasColumnName("provider_key"); - - b.Property("ProviderDisplayName") - .HasColumnType("text") - .HasColumnName("provider_display_name"); - - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("user_logins", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("RoleId") - .HasColumnType("integer") - .HasColumnName("role_id"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("user_roles", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("LoginProvider") - .HasColumnType("text") - .HasColumnName("login_provider"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Value") - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("user_tokens", "identity"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text") - .HasColumnName("concurrency_stamp"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("name"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("normalized_name"); - - b.Property("Uuid") - .IsRequired() - .HasColumnType("text") - .HasColumnName("uuid"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("roles", "identity"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessFailedCount") - .HasColumnType("integer") - .HasColumnName("access_failed_count"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text") - .HasColumnName("concurrency_stamp"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("email"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean") - .HasColumnName("email_confirmed"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean") - .HasColumnName("lockout_enabled"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone") - .HasColumnName("lockout_end"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("normalized_email"); - - b.Property("PasswordHash") - .HasColumnType("text") - .HasColumnName("password_hash"); - - b.Property("SecurityStamp") - .HasColumnType("text") - .HasColumnName("security_stamp"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean") - .HasColumnName("two_factor_enabled"); - - b.Property("Uuid") - .IsRequired() - .HasColumnType("text") - .HasColumnName("uuid"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.ToTable("users", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => - { - b.OwnsMany("cuqmbr.TravelGuide.Identity.Models.RefreshToken", "RefreshTokens", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); - - b1.Property("CreationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("creation_timestamp_utc"); - - b1.Property("ExpirationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("expiration_timestamp_utc"); - - b1.Property("IdentityUserId") - .HasColumnType("int") - .HasColumnName("user_id"); - - b1.Property("RevokationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("revokation_timestamp_utc"); - - b1.Property("Value") - .IsRequired() - .HasColumnType("varchar(256)") - .HasColumnName("value"); - - b1.HasKey("Id") - .HasName("id"); - - b1.HasIndex("IdentityUserId"); - - b1.ToTable("user_refresh_tokens", "identity"); - - b1.WithOwner("IdentityUser") - .HasForeignKey("IdentityUserId") - .HasConstraintName("fk_identityUserRefreshTokens_identityUser_userId"); - - b1.Navigation("IdentityUser"); - }); - - b.Navigation("RefreshTokens"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs b/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs deleted file mode 100644 index 035edce..0000000 --- a/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs +++ /dev/null @@ -1,281 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Identity.Persistence.PostgreSql.Migrations -{ - /// - public partial class Initial_migration : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "identity"); - - migrationBuilder.CreateTable( - name: "roles", - schema: "identity", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - uuid = table.Column(type: "text", nullable: false), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - normalized_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - concurrency_stamp = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_roles", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "users", - schema: "identity", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - uuid = table.Column(type: "text", nullable: false), - email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - normalized_email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - email_confirmed = table.Column(type: "boolean", nullable: false), - password_hash = table.Column(type: "text", nullable: true), - security_stamp = table.Column(type: "text", nullable: true), - concurrency_stamp = table.Column(type: "text", nullable: true), - two_factor_enabled = table.Column(type: "boolean", nullable: false), - lockout_end = table.Column(type: "timestamp with time zone", nullable: true), - lockout_enabled = table.Column(type: "boolean", nullable: false), - access_failed_count = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_users", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "role_claims", - schema: "identity", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - role_id = table.Column(type: "integer", nullable: false), - claim_type = table.Column(type: "text", nullable: true), - claim_value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_role_claims", x => x.id); - table.ForeignKey( - name: "FK_role_claims_roles_role_id", - column: x => x.role_id, - principalSchema: "identity", - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_claims", - schema: "identity", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - user_id = table.Column(type: "integer", nullable: false), - claim_type = table.Column(type: "text", nullable: true), - claim_value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_user_claims", x => x.id); - table.ForeignKey( - name: "FK_user_claims_users_user_id", - column: x => x.user_id, - principalSchema: "identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_logins", - schema: "identity", - columns: table => new - { - login_provider = table.Column(type: "text", nullable: false), - provider_key = table.Column(type: "text", nullable: false), - provider_display_name = table.Column(type: "text", nullable: true), - user_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_user_logins", x => new { x.login_provider, x.provider_key }); - table.ForeignKey( - name: "FK_user_logins_users_user_id", - column: x => x.user_id, - principalSchema: "identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_refresh_tokens", - schema: "identity", - columns: table => new - { - id = table.Column(type: "int", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - value = table.Column(type: "varchar(256)", nullable: false), - creation_timestamp_utc = table.Column(type: "timestamptz", nullable: false), - expiration_timestamp_utc = table.Column(type: "timestamptz", nullable: false), - revokation_timestamp_utc = table.Column(type: "timestamptz", nullable: true), - user_id = table.Column(type: "int", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("id", x => x.id); - table.ForeignKey( - name: "fk_identityUserRefreshTokens_identityUser_userId", - column: x => x.user_id, - principalSchema: "identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_roles", - schema: "identity", - columns: table => new - { - user_id = table.Column(type: "integer", nullable: false), - role_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_user_roles", x => new { x.user_id, x.role_id }); - table.ForeignKey( - name: "FK_user_roles_roles_role_id", - column: x => x.role_id, - principalSchema: "identity", - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_user_roles_users_user_id", - column: x => x.user_id, - principalSchema: "identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_tokens", - schema: "identity", - columns: table => new - { - user_id = table.Column(type: "integer", nullable: false), - login_provider = table.Column(type: "text", nullable: false), - name = table.Column(type: "text", nullable: false), - value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_user_tokens", x => new { x.user_id, x.login_provider, x.name }); - table.ForeignKey( - name: "FK_user_tokens_users_user_id", - column: x => x.user_id, - principalSchema: "identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_role_claims_role_id", - schema: "identity", - table: "role_claims", - column: "role_id"); - - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - schema: "identity", - table: "roles", - column: "normalized_name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_user_claims_user_id", - schema: "identity", - table: "user_claims", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "IX_user_logins_user_id", - schema: "identity", - table: "user_logins", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "IX_user_refresh_tokens_user_id", - schema: "identity", - table: "user_refresh_tokens", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "IX_user_roles_role_id", - schema: "identity", - table: "user_roles", - column: "role_id"); - - migrationBuilder.CreateIndex( - name: "EmailIndex", - schema: "identity", - table: "users", - column: "normalized_email"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "role_claims", - schema: "identity"); - - migrationBuilder.DropTable( - name: "user_claims", - schema: "identity"); - - migrationBuilder.DropTable( - name: "user_logins", - schema: "identity"); - - migrationBuilder.DropTable( - name: "user_refresh_tokens", - schema: "identity"); - - migrationBuilder.DropTable( - name: "user_roles", - schema: "identity"); - - migrationBuilder.DropTable( - name: "user_tokens", - schema: "identity"); - - migrationBuilder.DropTable( - name: "roles", - schema: "identity"); - - migrationBuilder.DropTable( - name: "users", - schema: "identity"); - } - } -} diff --git a/src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs b/src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs deleted file mode 100644 index 6a55357..0000000 --- a/src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs +++ /dev/null @@ -1,352 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; - -#nullable disable - -namespace Identity.Persistence.PostgreSql.Migrations -{ - [DbContext(typeof(PostgreSqlIdentityDbContext))] - partial class PostgreSqlIdentityDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("identity") - .HasAnnotation("ProductVersion", "9.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text") - .HasColumnName("claim_type"); - - b.Property("ClaimValue") - .HasColumnType("text") - .HasColumnName("claim_value"); - - b.Property("RoleId") - .HasColumnType("integer") - .HasColumnName("role_id"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("role_claims", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text") - .HasColumnName("claim_type"); - - b.Property("ClaimValue") - .HasColumnType("text") - .HasColumnName("claim_value"); - - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("user_claims", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("text") - .HasColumnName("login_provider"); - - b.Property("ProviderKey") - .HasColumnType("text") - .HasColumnName("provider_key"); - - b.Property("ProviderDisplayName") - .HasColumnType("text") - .HasColumnName("provider_display_name"); - - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("user_logins", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("RoleId") - .HasColumnType("integer") - .HasColumnName("role_id"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("user_roles", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("integer") - .HasColumnName("user_id"); - - b.Property("LoginProvider") - .HasColumnType("text") - .HasColumnName("login_provider"); - - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Value") - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("user_tokens", "identity"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text") - .HasColumnName("concurrency_stamp"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("name"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("normalized_name"); - - b.Property("Uuid") - .IsRequired() - .HasColumnType("text") - .HasColumnName("uuid"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("roles", "identity"); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessFailedCount") - .HasColumnType("integer") - .HasColumnName("access_failed_count"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text") - .HasColumnName("concurrency_stamp"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("email"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean") - .HasColumnName("email_confirmed"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean") - .HasColumnName("lockout_enabled"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone") - .HasColumnName("lockout_end"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("normalized_email"); - - b.Property("PasswordHash") - .HasColumnType("text") - .HasColumnName("password_hash"); - - b.Property("SecurityStamp") - .HasColumnType("text") - .HasColumnName("security_stamp"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean") - .HasColumnName("two_factor_enabled"); - - b.Property("Uuid") - .IsRequired() - .HasColumnType("text") - .HasColumnName("uuid"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.ToTable("users", "identity"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => - { - b.OwnsMany("cuqmbr.TravelGuide.Identity.Models.RefreshToken", "RefreshTokens", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); - - b1.Property("CreationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("creation_timestamp_utc"); - - b1.Property("ExpirationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("expiration_timestamp_utc"); - - b1.Property("IdentityUserId") - .HasColumnType("int") - .HasColumnName("user_id"); - - b1.Property("RevokationTimestamp") - .HasColumnType("timestamptz") - .HasColumnName("revokation_timestamp_utc"); - - b1.Property("Value") - .IsRequired() - .HasColumnType("varchar(256)") - .HasColumnName("value"); - - b1.HasKey("Id") - .HasName("id"); - - b1.HasIndex("IdentityUserId"); - - b1.ToTable("user_refresh_tokens", "identity"); - - b1.WithOwner("IdentityUser") - .HasForeignKey("IdentityUserId") - .HasConstraintName("fk_identityUserRefreshTokens_identityUser_userId"); - - b1.Navigation("IdentityUser"); - }); - - b.Navigation("RefreshTokens"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs b/src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs deleted file mode 100644 index faca79d..0000000 --- a/src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; -using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; -using Microsoft.EntityFrameworkCore; -using System.Reflection; -using Microsoft.Extensions.Options; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; - -public class PostgreSqlIdentityDbContext - : IdentityDbContext -{ - private readonly ConfigurationOptions _configuration; - - public PostgreSqlIdentityDbContext( - DbContextOptions options, - IOptions configurationOptions) - : base(options) - { - _configuration = configurationOptions.Value; - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - builder.HasDefaultSchema(_configuration.Datastore.PartitionName); - - builder.ApplyConfigurationsFromAssembly( - Assembly.GetExecutingAssembly(), - t => t.Namespace == - "cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations" - ); - } -} diff --git a/src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs b/src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs deleted file mode 100644 index cabb6d7..0000000 --- a/src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; - -public static class PostgreSqlInitializer -{ - public static void Initialize(IServiceProvider serviceProvider) - { - using var dbContext = serviceProvider - .GetService(); - - var totalMigrationsCount = - dbContext.Database.GetMigrations().Count(); - var appliedMigrationCount = - dbContext.Database.GetAppliedMigrations().Count(); - - if (totalMigrationsCount - appliedMigrationCount > 0) - { - dbContext.Database.Migrate(); - } - - if (appliedMigrationCount == 0) - { - IdentitySeeder.Seed(serviceProvider); - } - } -} diff --git a/src/Identity/Services/JwtAuthenticationService.cs b/src/Identity/Services/JwtAuthenticationService.cs deleted file mode 100644 index f2a8c5d..0000000 --- a/src/Identity/Services/JwtAuthenticationService.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System.Security.Cryptography; -using cuqmbr.TravelGuide.Application.Authenticaion; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; -using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; -using cuqmbr.TravelGuide.Identity.Models; -using System.Security.Claims; -using System.Text; -using Microsoft.IdentityModel.Tokens; -using cuqmbr.TravelGuide.Application.Common.Models; -using System.IdentityModel.Tokens.Jwt; -using cuqmbr.TravelGuide.Application.Common.Exceptions; - -namespace cuqmbr.TravelGuide.Identity.Services; - -public class JwtAuthenticationService : AuthenticationService -{ - private readonly ConfigurationOptions _configuration; - - private readonly UserManager _userManager; - private readonly RoleManager _roleManager; - - public JwtAuthenticationService( - UserManager userManager, - RoleManager roleManager, - IOptions configuration) - { - _userManager = userManager; - _userManager.UserValidators.Clear(); - _userManager.PasswordValidators.Clear(); - - _roleManager = roleManager; - - _configuration = configuration.Value; - } - - public async Task RegisterAsync( - string email, - string password, - CancellationToken cancellationToken) - { - var userWithSameEmail = await _userManager.FindByEmailAsync(email); - if (userWithSameEmail is not null) - { - throw new RegistrationException("User with given email already registered."); - } - - var newUser = new IdentityUser - { - // Id = Guid.NewGuid().ToString(), - Email = email, - RefreshTokens = default! - }; - - var createUserResult = await _userManager.CreateAsync(newUser, password); - - if (createUserResult.Errors.Any()) - { - var errorMessage = String.Join("\n", - createUserResult.Errors.Select(e => e.Description)); - throw new AuthenticationException(errorMessage); - } - - var addToRoleResult = - await _userManager.AddToRoleAsync(newUser, Application.Common.Models.IdentityRole.User.Name); - - if (addToRoleResult.Errors.Any()) - { - var errorMessage = String.Join("\n", - addToRoleResult.Errors.Select(e => e.Description)); - throw new AuthenticationException(errorMessage); - } - } - - public async Task LoginAsync( - string email, - string password, - CancellationToken cancellationToken) - { - var user = await _userManager.FindByEmailAsync(email); - if (user is null) - { - throw new LoginException("No users registered with given email."); - } - - var isPasswordCorrect = await _userManager.CheckPasswordAsync(user, password); - if (!isPasswordCorrect) - { - throw new LoginException("Given password is incorrect."); - } - - var jwtSecurityToken = await CreateJwtAsync(user, cancellationToken); - var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); - - var refreshToken = user.RefreshTokens.FirstOrDefault(t => t.IsActive); - if (refreshToken is null) - { - refreshToken = CreateRefreshToken(); - refreshToken.IdentityUserId = user.Id; - user.RefreshTokens.Add(refreshToken); - var result = await _userManager.UpdateAsync(user); - } - - return new TokensModel(accessToken, refreshToken.Value); - } - - public async Task RenewAccessTokenAsync( - string refreshToken, - CancellationToken cancellationToken) - { - var user = _userManager.Users.SingleOrDefault(u => - u.RefreshTokens.Any(rt => rt.Value == refreshToken)); - if (user is null) - { - throw new AuthenticationException($"Refresh token was not found."); - } - - var refreshTokenObject = user.RefreshTokens.Single(rt => - rt.Value == refreshToken); - if (!refreshTokenObject.IsActive) - { - throw new AuthenticationException("Refresh token is inactive."); - } - - var jwtSecurityToken = await CreateJwtAsync(user, cancellationToken); - var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); - - return new TokensModel(accessToken, refreshToken); - } - - public async Task RevokeRefreshTokenAsync( - string refreshToken, - CancellationToken cancellationToken) - { - var user = _userManager.Users.SingleOrDefault(u => - u.RefreshTokens.Any(t => t.Value == refreshToken)); - if (user is null) - { - throw new AuthenticationException("Invalid refreshToken"); - } - - var refreshTokenObject = user.RefreshTokens.Single(x => - x.Value == refreshToken); - if (!refreshTokenObject.IsActive) - { - throw new AuthenticationException("RefreshToken already revoked"); - } - - refreshTokenObject.RevokationTimestamp = DateTimeOffset.UtcNow; - await _userManager.UpdateAsync(user); - } - - private async Task CreateJwtAsync( - IdentityUser user, - CancellationToken cancellationToken) - { - var userClaims = await _userManager.GetClaimsAsync(user); - - var roles = await _userManager.GetRolesAsync(user); - var roleClaims = new List(); - foreach (var role in roles) - { - roleClaims.Add(new Claim("roles", role)); - } - - var claims = new List() - { - new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), - new Claim(JwtRegisteredClaimNames.Email, user.Email) - } - .Union(userClaims) - .Union(roleClaims); - - var expirationDateTimeUtc = DateTime.UtcNow.Add( - _configuration.JsonWebToken.AccessTokenValidity); - - var symmetricSecurityKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(_configuration.JsonWebToken.IssuerSigningKey)); - var signingCredentials = new SigningCredentials( - symmetricSecurityKey, SecurityAlgorithms.HmacSha256); - - var jwtSecurityToken = new JwtSecurityToken( - issuer: _configuration.JsonWebToken.Issuer, - audience: _configuration.JsonWebToken.Audience, - claims: claims, - expires: expirationDateTimeUtc, - signingCredentials: signingCredentials); - - return jwtSecurityToken; - } - - private RefreshToken CreateRefreshToken() - { - var randomNumber = new byte[32]; - - using var rng = RandomNumberGenerator.Create(); - rng.GetNonZeroBytes(randomNumber); - - return new RefreshToken - { - // Id = Guid.NewGuid().ToString(), - Value = Convert.ToBase64String(randomNumber), - CreationTimestamp = DateTimeOffset.UtcNow, - ExpirationTimestamp = DateTimeOffset.UtcNow.Add( - _configuration.JsonWebToken.RefreshTokenValidity) - }; - } -} diff --git a/src/Identity/packages.lock.json b/src/Identity/packages.lock.json deleted file mode 100644 index 71a67ab..0000000 --- a/src/Identity/packages.lock.json +++ /dev/null @@ -1,641 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net9.0": { - "Microsoft.AspNetCore.Authentication.JwtBearer": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", - "dependencies": { - "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" - } - }, - "Microsoft.AspNetCore.Identity": { - "type": "Direct", - "requested": "[2.3.1, )", - "resolved": "2.3.1", - "contentHash": "JcQ4pNXg+IISfcR95jeO2ZRt38N67MrUEj28HBmwfqD96BUyw4S54tQhrBmCOyPlf2vgNvSz/tsGAG7EgC0yRg==", - "dependencies": { - "Microsoft.AspNetCore.Authentication.Cookies": "2.3.0", - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", - "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", - "Microsoft.Extensions.Identity.Core": "2.3.0" - } - }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "9.0.4", - "Microsoft.Extensions.Identity.Stores": "9.0.4" - } - }, - "Microsoft.Extensions.Options": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", - "Microsoft.Extensions.Primitives": "9.0.4" - } - }, - "Microsoft.IdentityModel.JsonWebTokens": { - "type": "Direct", - "requested": "[8.8.0, )", - "resolved": "8.8.0", - "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.8.0" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "Direct", - "requested": "[8.8.0, )", - "resolved": "8.8.0", - "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.IdentityModel.Logging": "8.8.0" - } - }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", - "Npgsql": "9.0.3" - } - }, - "AspNetCore.Localizer.Json": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "Qfv5l+8X9hLRWt6aB4+fjTzz2Pt4wwrMwwfcyrY4t85sJ+7CuB8Jl9f+yccWfFsAZSODKBAz7yFXidaYslsjlA==", - "dependencies": { - "Microsoft.AspNetCore.Components": "9.0.0", - "Microsoft.Extensions.Caching.Memory": "9.0.0", - "Microsoft.Extensions.Localization": "9.0.0" - } - }, - "AutoMapper": { - "type": "Transitive", - "resolved": "14.0.0", - "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", - "dependencies": { - "Microsoft.Extensions.Options": "8.0.0" - } - }, - "FluentValidation": { - "type": "Transitive", - "resolved": "11.11.0", - "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" - }, - "MediatR": { - "type": "Transitive", - "resolved": "12.4.1", - "contentHash": "0tLxCgEC5+r1OCuumR3sWyiVa+BMv3AgiU4+pz8xqTc+2q1WbUEXFOr7Orm96oZ9r9FsldgUtWvB2o7b9jDOaw==", - "dependencies": { - "MediatR.Contracts": "[2.0.1, 3.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - } - }, - "MediatR.Behaviors.Authorization": { - "type": "Transitive", - "resolved": "12.2.0", - "contentHash": "/rXuisxwJviu9PIffZlcZ6UY0MafX8dNtRi0bS04KciEVxkln8txJZt+rvKgerW3zKdKHfqt2EwRuiOCN9Aszg==", - "dependencies": { - "MediatR": "12.4.1", - "MediatR.Contracts": "2.0.1" - } - }, - "MediatR.Contracts": { - "type": "Transitive", - "resolved": "2.0.1", - "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" - }, - "Microsoft.AspNetCore.Authentication": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "Tq6bxTOe65Ikh9dWVTEOqpvNqBGIQueO0J+zl2rQba0yP0YV66iYDkSz9MqTdRZftvJ2I5kMeRUm9Z2mjEAbUQ==", - "dependencies": { - "Microsoft.AspNetCore.Authentication.Core": "2.3.0", - "Microsoft.AspNetCore.DataProtection": "2.3.0", - "Microsoft.AspNetCore.Http": "2.3.0", - "Microsoft.AspNetCore.Http.Extensions": "2.3.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2", - "Microsoft.Extensions.WebEncoders": "8.0.11" - } - }, - "Microsoft.AspNetCore.Authentication.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "ve6uvLwKNRkfnO/QeN9M8eUJ49lCnWv/6/9p6iTEuiI6Rtsz+myaBAjdMzLuTViQY032xbTF5AdZF5BJzJJyXQ==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" - } - }, - "Microsoft.AspNetCore.Authentication.Cookies": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "w3JPWHreXJ/Uv9CLkQtGCLwTbxZKY+94QPVi1RxcMuBTyRp+C9SdynznHEjnHWnw6QFNEHnBuHmWW3OYrvbpEQ==", - "dependencies": { - "Microsoft.AspNetCore.Authentication": "2.3.0" - } - }, - "Microsoft.AspNetCore.Authentication.Core": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "gnLnKGawBjqBnU9fEuel3VcYAARkjyONAliaGDfMc8o8HBtfh+HrOPEoR8Xx4b2RnMb7uxdBDOvEAC7sul79ig==", - "dependencies": { - "Microsoft.AspNetCore.Authentication.Abstractions": "2.3.0", - "Microsoft.AspNetCore.Http": "2.3.0", - "Microsoft.AspNetCore.Http.Extensions": "2.3.0" - } - }, - "Microsoft.AspNetCore.Authorization": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "qDJlBC5pUQ/3o6/C6Vuo9CGKtV5TAe5AdKeHvDR2bgmw8vwPxsAy3KG5eU0i1C+iAUNbmq+iDTbiKt16f9pRiA==", - "dependencies": { - "Microsoft.AspNetCore.Metadata": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0" - } - }, - "Microsoft.AspNetCore.Components": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "xKzY0LRqWrwuPVzKIF9k1kC21NrLmIE2qPhhKlInEAdYqNe8qcMoPWZy7fo1uScHkz5g73nTqDDra3+aAV7mTQ==", - "dependencies": { - "Microsoft.AspNetCore.Authorization": "9.0.0", - "Microsoft.AspNetCore.Components.Analyzers": "9.0.0" - } - }, - "Microsoft.AspNetCore.Components.Analyzers": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "maOE1qlJ9hf1Fb7PhFLw9bgP9mWckuDOcn1uKNt9/msdJG2YHl3cPRHojYa6CxliGHIXL8Da4qPgeUc4CaOoeg==" - }, - "Microsoft.AspNetCore.Cryptography.Internal": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" - }, - "Microsoft.AspNetCore.Cryptography.KeyDerivation": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" - } - }, - "Microsoft.AspNetCore.DataProtection": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "C+FhGaA8ekrfes0Ujhtkhk74Bpkt6Zt+NrMaGrCWBqW1LFzqw/pXDbMbpcAyI9hbYgZfC6+t01As4LGXbdxG4A==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0", - "Microsoft.AspNetCore.DataProtection.Abstractions": "2.3.0", - "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2", - "Microsoft.Win32.Registry": "4.5.0", - "System.Security.Cryptography.Xml": "8.0.2", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Microsoft.AspNetCore.DataProtection.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "71GdtUkVDagLsBt+YatfzUItnbT2vIjHxWySNE2MkgIDhqT3g4sNNxOj/0PlPTpc1+mG3ZwfUoZ61jIt1wPw7g==" - }, - "Microsoft.AspNetCore.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "4ivq53W2k6Nj4eez9wc81ytfGj6HR1NaZJCpOrvghJo9zHuQF57PLhPoQH5ItyCpHXnrN/y7yJDUm+TGYzrx0w==", - "dependencies": { - "Microsoft.AspNetCore.Hosting.Server.Abstractions": "2.3.0", - "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", - "Microsoft.Extensions.Hosting.Abstractions": "8.0.1" - } - }, - "Microsoft.AspNetCore.Hosting.Server.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "F5iHx7odAbFKBV1DNPDkFFcVmD5Tk7rk+tYm3LMQxHEFFdjlg5QcYb5XhHAefl5YaaPeG6ad+/ck8kSG3/D6kw==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "2.3.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" - } - }, - "Microsoft.AspNetCore.Http": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "I9azEG2tZ4DDHAFgv+N38e6Yhttvf+QjE2j2UYyCACE7Swm5/0uoihCMWZ87oOZYeqiEFSxbsfpT71OYHe2tpw==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", - "Microsoft.AspNetCore.WebUtilities": "2.3.0", - "Microsoft.Extensions.ObjectPool": "8.0.11", - "Microsoft.Extensions.Options": "8.0.2", - "Microsoft.Net.Http.Headers": "2.3.0" - } - }, - "Microsoft.AspNetCore.Http.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "2.3.0", - "System.Text.Encodings.Web": "8.0.0" - } - }, - "Microsoft.AspNetCore.Http.Extensions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "EY2u/wFF5jsYwGXXswfQWrSsFPmiXsniAlUWo3rv/MGYf99ZFsENDnZcQP6W3c/+xQmQXq0NauzQ7jyy+o1LDQ==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Net.Http.Headers": "2.3.0", - "System.Buffers": "4.6.0" - } - }, - "Microsoft.AspNetCore.Http.Features": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.AspNetCore.Metadata": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "X81C891nMuWgzNHyZ0C3s+blSDxRHzQHDFYQoOKtFvFuxGq3BbkLbc5CfiCqIzA/sWIfz6u8sGBgwntQwBJWBw==" - }, - "Microsoft.AspNetCore.WebUtilities": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "trbXdWzoAEUVd0PE2yTopkz4kjZaAIA7xUWekd5uBw+7xE8Do/YOVTeb9d9koPTlbtZT539aESJjSLSqD8eYrQ==", - "dependencies": { - "Microsoft.Net.Http.Headers": "2.3.0", - "System.Text.Encodings.Web": "8.0.0" - } - }, - "Microsoft.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "+5IAX0aicQYCRfN4pAjad+JPwdEYoVEM3Z1Cl8/EiEv3FVHQHdd8TJQpQIslQDDQS/UsUMb0MsOXwqOh+TJtRw==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "9.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "9.0.4", - "Microsoft.Extensions.Caching.Memory": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "E0pkWzI0liqu2ogqJ1kohk2eGkYRhf5tI75HGF6IQDARsshY/0w+prGyLvNuUeV7B8I7vYQZ4CzAKYKxw7b9gQ==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "cMsm1O7g9X5qbB2wjHf3BVVvGwkG+zeXQ+M91I1Bm6RfylFMImqBPzs0+vmuef7fPxr2yOzPhIfJ2wQJfmtaSw==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "OjJ+xh/wQff5b0wiC3SPvoQqTA2boZeJQf+15+3+OJPtjBKzvxuwr25QRIu1p1t+K8ryQ8pzaoZ7eOpXfNzVGA==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "9.0.4", - "Microsoft.Extensions.Caching.Memory": "9.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.4" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "G5rEq1Qez5VJDTEyRsRUnewAspKjaY57VGsdZ8g8Ja6sXXzoiI3PpTd1t43HjHqNWD5A06MQveb2lscn+2CU+w==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", - "Microsoft.Extensions.Logging.Abstractions": "9.0.4", - "Microsoft.Extensions.Options": "9.0.4", - "Microsoft.Extensions.Primitives": "9.0.4" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "nHwq9aPBdBPYXPti6wYEEfgXddfBrYC+CQLn+qISiwQq5tpfaqDZSKOJNxoe9rfQxGf1c+2wC/qWFe1QYJPYqw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.2" - } - }, - "Microsoft.Extensions.Identity.Core": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4", - "Microsoft.Extensions.Options": "9.0.4" - } - }, - "Microsoft.Extensions.Identity.Stores": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.4", - "Microsoft.Extensions.Identity.Core": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" - } - }, - "Microsoft.Extensions.Localization": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Localization.Abstractions": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0" - } - }, - "Microsoft.Extensions.Localization.Abstractions": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.4", - "Microsoft.Extensions.Logging.Abstractions": "9.0.4", - "Microsoft.Extensions.Options": "9.0.4" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "8.0.11", - "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" - }, - "Microsoft.Extensions.WebEncoders": { - "type": "Transitive", - "resolved": "8.0.11", - "contentHash": "EwF+KaQzTa/MoIm8gciABL6xeeiGKowqyam+lPYWukTppwch1P3QeL8CpgtLs8kIWuEowpAAUrVfP1kyZsZgqg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" - } - }, - "Microsoft.IdentityModel.Abstractions": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.8.0" - } - }, - "Microsoft.IdentityModel.Protocols": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.0.1" - } - }, - "Microsoft.IdentityModel.Protocols.OpenIdConnect": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", - "dependencies": { - "Microsoft.IdentityModel.Protocols": "8.0.1", - "System.IdentityModel.Tokens.Jwt": "8.0.1" - } - }, - "Microsoft.Net.Http.Headers": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "/M0wVg6tJUOHutWD3BMOUVZAioJVXe0tCpFiovzv0T9T12TBf4MnaHP0efO8TCr1a6O9RZgQeZ9Gdark8L9XdA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0", - "System.Buffers": "4.6.0" - } - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "VdLJOCXhZaEMY7Hm2GKiULmn7IEPFE4XC5LPSfBVCUIA8YLZVh846gtfBJalsPQF2PlzdD7ecX7DZEulJ402ZQ==" - }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", - "dependencies": { - "System.Security.AccessControl": "4.5.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "Npgsql": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2" - } - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.6.0", - "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" - }, - "System.IdentityModel.Tokens.Jwt": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", - "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", - "Microsoft.IdentityModel.Tokens": "8.0.1" - } - }, - "System.Linq.Dynamic.Core": { - "type": "Transitive", - "resolved": "1.6.2", - "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" - }, - "System.Security.Cryptography.Xml": { - "type": "Transitive", - "resolved": "8.0.2", - "contentHash": "aDM/wm0ZGEZ6ZYJLzgqjp2FZdHbDHh6/OmpGfb7AdZ105zYmPn/83JRU2xLIbwgoNz9U1SLUTJN0v5th3qmvjA==", - "dependencies": { - "System.Security.Cryptography.Pkcs": "8.0.1" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - }, - "application": { - "type": "Project", - "dependencies": { - "AspNetCore.Localizer.Json": "[1.0.1, )", - "AutoMapper": "[14.0.0, )", - "Domain": "[1.0.0, )", - "FluentValidation": "[11.11.0, )", - "MediatR": "[12.4.1, )", - "MediatR.Behaviors.Authorization": "[12.2.0, )", - "Microsoft.Extensions.Logging": "[9.0.4, )", - "System.Linq.Dynamic.Core": "[1.6.2, )" - } - }, - "domain": { - "type": "Project" - } - }, - "net9.0/linux-x64": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", - "dependencies": { - "System.Security.AccessControl": "4.5.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - } - } - } -} \ No newline at end of file diff --git a/src/Infrastructure/ConfigurationOptions.cs b/src/Infrastructure/ConfigurationOptions.cs index 1d4b8a4..87ac871 100644 --- a/src/Infrastructure/ConfigurationOptions.cs +++ b/src/Infrastructure/ConfigurationOptions.cs @@ -2,5 +2,47 @@ namespace cuqmbr.TravelGuide.Infrastructure; public sealed class ConfigurationOptions { - public static string SectionName { get; } = "Infrastructure"; + public static string SectionName { get; } = ""; + + public PaymentProcessingConfigurationOptions PaymentProcessing { get; set; } = new(); + + public EmailConfigurationOptions Email { get; set; } = new(); +} + +public sealed class PaymentProcessingConfigurationOptions +{ + public string CallbackAddressBase { get; set; } + + public string ResultAddressBase { get; set; } + + public LiqPayConfigurationOptions LiqPay { get; set; } = new(); +} + +public sealed class LiqPayConfigurationOptions +{ + public string PublicKey { get; set; } + + public string PrivateKey { get; set; } +} + +public sealed class EmailConfigurationOptions +{ + public SmtpConfigurationOptions Smtp { get; set; } = new(); +} + +public sealed class SmtpConfigurationOptions +{ + public string Host { get; set; } + + public ushort Port { get; set; } + + public bool UseTls { get; set; } + + public string Username { get; set; } + + public string Password { get; set; } + + public string SenderAddress { get; set; } + + public string SenderName { get; set; } } diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index d704cae..cf7f00a 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -10,6 +10,12 @@ + + + + + + true diff --git a/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs new file mode 100644 index 0000000..7c36d53 --- /dev/null +++ b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs @@ -0,0 +1,103 @@ +using System.Globalization; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using Newtonsoft.Json; + +namespace cuqmbr.TravelGuide.Infrastructure.Services; + +// https://github.com/fawazahmed0/exchange-api + +public sealed class ExchangeApiCurrencyConverterService : + CurrencyConverterService +{ + private readonly + IDictionary< + DateOnly, IDictionary< + Currency, IDictionary< + Currency, decimal>>> _cache; + + private const string urlFormat = "https://cdn.jsdelivr.net/" + + "npm/@fawazahmed0/currency-api@{0}/v1/currencies/{1}.json"; + + private readonly IHttpClientFactory _httpClientFactory; + + public ExchangeApiCurrencyConverterService( + IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + + _cache = + new Dictionary< + DateOnly, IDictionary< + Currency, IDictionary< + Currency, decimal>>>(); + } + + public async Task ConvertAsync(decimal amount, Currency from, + Currency to, CancellationToken cancellationToken) + { + return await ConvertAsync(amount, from, to, + DateTimeOffset.UtcNow, cancellationToken); + } + + public async Task ConvertAsync(decimal amount, Currency from, + Currency to, DateTimeOffset time, CancellationToken cancellationToken) + { + if (from.Equals(to) || to.Equals(Currency.Default)) + { + return amount; + } + + var requestDate = DateOnly.FromDateTime(time.ToUniversalTime().Date); + + // Return cached value if available + if (_cache.Keys.Contains(requestDate) && + _cache[requestDate].Keys.Contains(from) && + _cache[requestDate][from].Keys.Contains(to)) + { + return amount * _cache[requestDate][from][to]; + } + + + // Retreive new value from api + + var httpClient = _httpClientFactory.CreateClient(); + + var requestDateString = + requestDate == DateOnly.FromDateTime(DateTime.UtcNow) ? + "latest" : + requestDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + + var url = String.Format(urlFormat, + requestDateString, from.Name.ToLower()); + + var jsonString = + await httpClient.GetStringAsync(url, cancellationToken); + + var obj = JsonConvert.DeserializeObject(jsonString); + + decimal rate = obj[from.Name.ToLower()][to.Name.ToLower()]; + + + // Cache new value + + if (!_cache.Keys.Contains(requestDate)) + { + _cache.Add(requestDate, + new Dictionary>()); + } + + if (!_cache[requestDate].Keys.Contains(from)) + { + _cache[requestDate].Add(from, new Dictionary()); + } + + if (!_cache[requestDate][from].Keys.Contains(to)) + { + _cache[requestDate][from].Add(to, rate); + } + + + return amount * rate; + } +} diff --git a/src/Infrastructure/Services/LiqPayPaymentService.cs b/src/Infrastructure/Services/LiqPayPaymentService.cs new file mode 100644 index 0000000..dce188b --- /dev/null +++ b/src/Infrastructure/Services/LiqPayPaymentService.cs @@ -0,0 +1,78 @@ +using System.Dynamic; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace cuqmbr.TravelGuide.Infrastructure.Services; + +public sealed class LiqPayPaymentService : + cuqmbr.TravelGuide.Application.Common.Services.LiqPayPaymentService +{ + private readonly LiqPayConfigurationOptions _configuration; + private readonly string _callbackAddressBase; + private readonly string _resultAddressBase; + + private readonly IHttpClientFactory _httpClientFactory; + + public LiqPayPaymentService( + IOptions configurationOptions, + IHttpClientFactory httpClientFactory) + { + _configuration = configurationOptions.Value.PaymentProcessing.LiqPay; + _callbackAddressBase = + configurationOptions.Value.PaymentProcessing.CallbackAddressBase; + _resultAddressBase = + configurationOptions.Value.PaymentProcessing.ResultAddressBase; + } + + public string GetPaymentLink( + decimal amount, Currency currency, + string orderId, TimeSpan validity, string description, + string resultPath, string callbackPath) + { + dynamic request = new ExpandoObject(); + + request.version = 3; + request.public_key = _configuration.PublicKey; + request.action = "pay"; + request.amount = amount; + request.currency = currency.Name.ToUpper(); + request.description = description; + request.order_id = orderId; + request.expired_date = DateTimeOffset.UtcNow.Add(validity) + .ToString("yyyy-MM-dd HH:mm:ss"); + request.result_url = $"{_resultAddressBase}{resultPath}"; + request.server_url = $"{_callbackAddressBase}{callbackPath}"; + + var requestJsonString = (string)JsonConvert.SerializeObject(request); + + + var requestJsonStringBytes = Encoding.UTF8.GetBytes(requestJsonString); + + var data = Convert.ToBase64String(requestJsonStringBytes); + + var signature = Convert.ToBase64String(SHA1.HashData( + Encoding.UTF8.GetBytes( + _configuration.PrivateKey + + data + + _configuration.PrivateKey))); + + + return + "https://www.liqpay.ua/api/3/checkout" + + $"?data={data}&signature={signature}"; + } + + public bool IsValidSignature(string postData, string postSignature) + { + var signature = Convert.ToBase64String(SHA1.HashData( + Encoding.UTF8.GetBytes( + _configuration.PrivateKey + + postData + + _configuration.PrivateKey))); + + return postSignature.Equals(signature); + } +} diff --git a/src/Infrastructure/Services/MailKitEmailSenderService.cs b/src/Infrastructure/Services/MailKitEmailSenderService.cs new file mode 100644 index 0000000..36eb50d --- /dev/null +++ b/src/Infrastructure/Services/MailKitEmailSenderService.cs @@ -0,0 +1,50 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using MailKit.Net.Smtp; +using Microsoft.Extensions.Options; +using MimeKit; + +namespace cuqmbr.TravelGuide.Infrastructure.Services; + +public sealed class MailKitEmailSenderService : EmailSenderService +{ + + private readonly SmtpConfigurationOptions _configuration; + + public MailKitEmailSenderService( + IOptions configuration) + { + _configuration = configuration.Value.Email.Smtp; + } + + public async Task SendAsync(string[] addresses, string subject, + string body, CancellationToken cancellationToken) + { + var message = new MimeMessage(); + + message.From.Add(new MailboxAddress( + _configuration.SenderName, _configuration.SenderAddress)); + foreach (var address in addresses) + { + message.To.Add(new MailboxAddress("", address)); + } + message.Subject = subject; + + message.Body = new TextPart("plain") + { + Text = body + }; + + + using var client = new SmtpClient(); + + await client.ConnectAsync(_configuration.Host, + _configuration.Port, _configuration.UseTls, + cancellationToken); + + await client.AuthenticateAsync(_configuration.Username, + _configuration.Password, cancellationToken); + + await client.SendAsync(message, cancellationToken); + await client.DisconnectAsync(true, cancellationToken); + } +} diff --git a/src/Infrastructure/Services/Pbkdf2PasswordHasherService.cs b/src/Infrastructure/Services/Pbkdf2PasswordHasherService.cs new file mode 100644 index 0000000..2f4e5bf --- /dev/null +++ b/src/Infrastructure/Services/Pbkdf2PasswordHasherService.cs @@ -0,0 +1,32 @@ +using System.Security.Cryptography; +using cuqmbr.TravelGuide.Application.Common.Services; + +namespace cuqmbr.TravelGuide.Infrastructure.Services; + +public sealed class Pbkdf2PasswordHasherService : PasswordHasherService +{ + private const int IterationCount = 210_000; + + private readonly HashAlgorithmName Hash = HashAlgorithmName.SHA3_512; + private const byte HashLengthBytes = 64; + + public Task HashAsync(byte[] password, byte[] salt, + CancellationToken cancellationToken) + { + return Task.FromResult( + Rfc2898DeriveBytes.Pbkdf2( + password, salt, IterationCount, + Hash, HashLengthBytes)); + } + + public Task IsValidHashAsync(byte[] hash, byte[] password, + byte[] salt, CancellationToken cancellationToken) + { + var computedHash = Rfc2898DeriveBytes.Pbkdf2( + password, salt, IterationCount, + Hash, HashLengthBytes); + + return Task.FromResult( + Enumerable.SequenceEqual(computedHash, hash)); + } +} diff --git a/src/Infrastructure/packages.lock.json b/src/Infrastructure/packages.lock.json index 3fc0122..ccfa74f 100644 --- a/src/Infrastructure/packages.lock.json +++ b/src/Infrastructure/packages.lock.json @@ -2,11 +2,35 @@ "version": 1, "dependencies": { "net9.0": { - "Microsoft.NET.ILLink.Tasks": { + "MailKit": { "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" + "requested": "[4.12.1, )", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, + "Microsoft.Extensions.Http": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Diagnostics": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "AspNetCore.Localizer.Json": { "type": "Transitive", @@ -26,6 +50,11 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "FluentValidation": { "type": "Transitive", "resolved": "11.11.0", @@ -54,6 +83,14 @@ "resolved": "2.0.1", "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, "Microsoft.AspNetCore.Authorization": { "type": "Transitive", "resolved": "9.0.0", @@ -103,6 +140,31 @@ "Microsoft.Extensions.Primitives": "9.0.0" } }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "KIVBrMbItnCJDd1RF4KEaE8jZwDJcDUJW5zXpbwQ05HNYTK1GveHxHK0B3SjgDJuR48GRACXAO+BLhL8h34S7g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cdrjcl9RIcwt3ECbnpP0Gt1+pkjdW90mq5yFYy8D9qRj2NqFFcv3yDp141iEamsd9E218sGxK8WHaIOcrqgDJg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "9.0.4", @@ -116,6 +178,25 @@ "resolved": "9.0.4", "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, "Microsoft.Extensions.Localization": { "type": "Transitive", "resolved": "9.0.0", @@ -159,16 +240,108 @@ "Microsoft.Extensions.Primitives": "9.0.4" } }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "aridVhAT3Ep+vsirR1pzjaOw0Jwiob6dc73VFQn2XmDfBA2X98M8YKO1GarvsXRX7gX1Aj+hj2ijMzrMHDOm0A==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.Binder": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "9.0.4", "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.11.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.11.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" + } + }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, "System.Linq.Dynamic.Core": { "type": "Transitive", "resolved": "1.6.2", "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" + }, "application": { "type": "Project", "dependencies": { @@ -178,14 +351,18 @@ "FluentValidation": "[11.11.0, )", "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.5, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.11.0, )", + "Microsoft.IdentityModel.Tokens": "[8.11.0, )", + "Newtonsoft.Json": "[13.0.3, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, "domain": { "type": "Project" } - }, - "net9.0/linux-x64": {} + } } } \ No newline at end of file diff --git a/src/Persistence/ConfigurationOptions.cs b/src/Persistence/ConfigurationOptions.cs index 67f5dd5..a239384 100644 --- a/src/Persistence/ConfigurationOptions.cs +++ b/src/Persistence/ConfigurationOptions.cs @@ -2,7 +2,7 @@ namespace cuqmbr.TravelGuide.Persistence; public sealed class ConfigurationOptions { - public static string SectionName { get; } = "Application:Datastore"; + public static string SectionName { get; } = "Datastore"; public string Type { get; set; } = "inmemory"; diff --git a/src/Persistence/DbSeeder.cs b/src/Persistence/DbSeeder.cs index baefefe..229bc7d 100644 --- a/src/Persistence/DbSeeder.cs +++ b/src/Persistence/DbSeeder.cs @@ -1,12 +1,105 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.Extensions.DependencyInjection; namespace cuqmbr.TravelGuide.Persistence; public static class DbSeeder { - public static void Seed(UnitOfWork unitOfWork) + public static void Seed(IServiceProvider serviceProvider) { - // Do Seeding Here + var unitOfWork = + serviceProvider.GetRequiredService(); + + var passwordHasher = + serviceProvider.GetRequiredService(); + + + // Seed Roles + { + var datastoreRoles = unitOfWork.RoleRepository + .GetPageAsync(1, IdentityRole.Enumerations.Count, + CancellationToken.None) + .Result.Items.Select(r => r.Value); + + var roles = IdentityRole.Enumerations.Select(r => r.Value); + + foreach (var role in roles) + { + + if (datastoreRoles.Contains(role)) + { + continue; + } + + unitOfWork.RoleRepository.AddOneAsync( + new Role() { Value = role }, + CancellationToken.None).Wait(); + } + + unitOfWork.SaveAsync(CancellationToken.None).Wait(); + } + + // Seed Accounts + { + var accounts = + new (string Username, string Email, + string Password, IdentityRole[] Roles)[] + { + ("admin", "admin", "admin", + new [] { IdentityRole.Administrator }), + }; + + var roles = unitOfWork.RoleRepository + .GetPageAsync(1, IdentityRole.Enumerations.Count, + CancellationToken.None) + .Result.Items; + + foreach (var account in accounts) + { + var datastoreAccount = + unitOfWork.AccountRepository.GetOneAsync( + e => e.Email == account.Email, CancellationToken.None) + .Result; + + if (datastoreAccount != null) + { + continue; + } + + var password = Encoding.UTF8.GetBytes(account.Password); + + var salt = RandomNumberGenerator.GetBytes(128 / 8); + var hash = passwordHasher + .HashAsync(password, salt, CancellationToken.None) + .Result; + + var saltBase64 = Convert.ToBase64String(salt); + var hashBase64 = Convert.ToBase64String(hash); + + unitOfWork.AccountRepository.AddOneAsync( + new Account() + { + Username = account.Username, + Email = account.Email, + PasswordHash = hashBase64, + PasswordSalt = saltBase64, + AccountRoles = account.Roles.Select(ar => + new AccountRole() + { + RoleId = roles.Single(dr => dr.Value.Equals(ar)).Id + }) + .ToArray() + }, + CancellationToken.None).Wait(); + } + + unitOfWork.SaveAsync(CancellationToken.None).Wait(); + } unitOfWork.Dispose(); } diff --git a/src/Persistence/InMemory/Configurations/BaseConfiguration.cs b/src/Persistence/InMemory/Configurations/BaseConfiguration.cs deleted file mode 100644 index 4ed923a..0000000 --- a/src/Persistence/InMemory/Configurations/BaseConfiguration.cs +++ /dev/null @@ -1,55 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.InMemory.Configurations; - -public class BaseConfiguration : IEntityTypeConfiguration - where TEntity : EntityBase -{ - public virtual void Configure(EntityTypeBuilder builder) - { - builder - .HasKey(b => b.Id); - // .HasName($"pk_{builder.Metadata.GetTableName()}"); - - builder - .Property(b => b.Id) - .HasColumnName("id"); - // .HasColumnType("bigint") - // .UseSequence( - // $"{builder.Metadata.GetTableName()}_" + - // $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + - // "sequence"); - // - // builder - // .HasIndex(b => b.Id) - // .HasDatabaseName( - // "ix_" + - // $"{builder.Metadata.GetTableName()}_" + - // $"{builder.Property(b => b.Id).Metadata.GetColumnName()}") - // .IsUnique(); - // - // - // builder - // .HasAlternateKey(b => b.Guid) - // .HasName( - // "altk_" + - // $"{builder.Metadata.GetTableName()}_" + - // $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}"); - // - builder - .Property(b => b.Guid) - .HasColumnName("uuid") - // .HasColumnType("uuid") - .IsRequired(true); - // - // builder - // .HasIndex(b => b.Guid) - // .HasDatabaseName( - // "ix_" + - // $"{builder.Metadata.GetTableName()}_" + - // $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}") - // .IsUnique(); - } -} diff --git a/src/Persistence/InMemory/Configurations/CountryConfiguration.cs b/src/Persistence/InMemory/Configurations/CountryConfiguration.cs deleted file mode 100644 index a335dfa..0000000 --- a/src/Persistence/InMemory/Configurations/CountryConfiguration.cs +++ /dev/null @@ -1,31 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.InMemory.Configurations; - -public class CountryConfiguration : BaseConfiguration -{ - public override void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("countries"); - - base.Configure(builder); - - - builder - .Property(c => c.Name) - .HasColumnName("name") - // .HasColumnType("varchar(64)") - .IsRequired(true); - - // TODO: Remove comment - // builder - // .HasAlternateKey(c => c.Name) - // .HasName( - // "altk_" + - // $"{builder.Metadata.GetTableName()}_" + - // $"{builder.Property(c => c.Name).Metadata.GetColumnName()}"); - } -} diff --git a/src/Persistence/InMemory/Configurations/RegionConfiguration.cs b/src/Persistence/InMemory/Configurations/RegionConfiguration.cs deleted file mode 100644 index 87c4f8f..0000000 --- a/src/Persistence/InMemory/Configurations/RegionConfiguration.cs +++ /dev/null @@ -1,40 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.InMemory.Configurations; - -public class RegionConfiguration : BaseConfiguration -{ - public override void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("regions"); - - base.Configure(builder); - - - builder - .Property(r => r.Name) - .HasColumnName("name") - // .HasColumnType("varchar(64)") - .IsRequired(true); - - - builder - .Property(r => r.CountryId) - .HasColumnName("country_id") - // .HasColumnType("bigint") - .IsRequired(true); - - builder - .HasOne(r => r.Country) - .WithMany(c => c.Regions) - .HasForeignKey(r => r.CountryId) - .HasConstraintName( - "fk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(r => r.CountryId).Metadata.GetColumnName()}") - .OnDelete(DeleteBehavior.Cascade); - } -} diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index 6c74ec5..2d41303 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -1,7 +1,7 @@ -using System.Reflection; -// using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; -// using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using cuqmbr.TravelGuide.Persistence.TypeConverters; namespace cuqmbr.TravelGuide.Persistence.InMemory; @@ -14,32 +14,52 @@ public class InMemoryDbContext : DbContext { base.OnModelCreating(builder); - // builder.HasPostgresEnum( - // "vehicle_type", - // VehicleType.Enumerations.Select(e => e.Value.Name).ToArray()); - // builder - .ApplyConfigurationsFromAssembly( - Assembly.GetExecutingAssembly(), - t => t.Namespace == - "cuqmbr.TravelGuide.Persistence.InMemory.Configurations"); + .Entity() + .ToTable("vehicles") + .UseTphMappingStrategy() + .HasDiscriminator(v => v.VehicleType) + .HasValue(VehicleType.Bus) + .HasValue(VehicleType.Aircraft) + .HasValue(VehicleType.Train); } protected override void ConfigureConventions( ModelConfigurationBuilder builder) { - // builder - // .Properties() - // .HaveColumnType("vehicle_type") - // .HaveConversion(); + builder + .Properties() + .HaveColumnType("varchar(16)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(8)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(64)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + + builder + .Properties() + .HaveConversion(); + + + builder + .Properties() + .HaveColumnType("varchar(64)") + .HaveConversion(); } } - -// public class VehicleTypeConverter : ValueConverter -// { -// public VehicleTypeConverter() -// : base( -// v => v.Name, -// v => VehicleType.FromName(v)) -// { } -// } diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index 23af267..ee832c6 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -1,5 +1,5 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Persistence.InMemory.Repositories; namespace cuqmbr.TravelGuide.Persistence.InMemory; @@ -15,11 +15,75 @@ public sealed class InMemoryUnitOfWork : UnitOfWork CountryRepository = new InMemoryCountryRepository(_dbContext); RegionRepository = new InMemoryRegionRepository(_dbContext); + CityRepository = new InMemoryCityRepository(_dbContext); + AddressRepository = new InMemoryAddressRepository(_dbContext); + RouteRepository = new InMemoryRouteRepository(_dbContext); + VehicleRepository = new InMemoryVehicleRepository(_dbContext); + BusRepository = new InMemoryBusRepository(_dbContext); + AircraftRepository = new InMemoryAircraftRepository(_dbContext); + TrainRepository = new InMemoryTrainRepository(_dbContext); + VehicleEnrollmentRepository = + new InMemoryVehicleEnrollmentRepository(_dbContext); + RouteAddressRepository = + new InMemoryRouteAddressRepository(_dbContext); + CompanyRepository = new InMemoryCompanyRepository(_dbContext); + EmployeeRepository = new InMemoryEmployeeRepository(_dbContext); + TicketGroupRepository = new InMemoryTicketGroupRepository(_dbContext); + TicketRepository = new InMemoryTicketRepository(_dbContext); + RouteAddressDetailRepository = + new InMemoryRouteAddressDetailRepository(_dbContext); + VehicleEnrollmentEmployeeRepository = + new InMemoryVehicleEnrollmentEmployeeRepository(_dbContext); + + AccountRepository = new InMemoryAccountRepository(_dbContext); + RoleRepository = new InMemoryRoleRepository(_dbContext); + AccountRoleRepository = new InMemoryAccountRoleRepository(_dbContext); + RefreshTokenRepository = new InMemoryRefreshTokenRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } + public RegionRepository RegionRepository { get; init; } + public CityRepository CityRepository { get; init; } + + public AddressRepository AddressRepository { get; init; } + + public RouteRepository RouteRepository { get; init; } + + public VehicleRepository VehicleRepository { get; init; } + + public BusRepository BusRepository { get; init; } + + public AircraftRepository AircraftRepository { get; init; } + + public TrainRepository TrainRepository { get; init; } + + public VehicleEnrollmentRepository VehicleEnrollmentRepository { get; init; } + + public RouteAddressRepository RouteAddressRepository { get; init; } + + public CompanyRepository CompanyRepository { get; init; } + + public EmployeeRepository EmployeeRepository { get; init; } + + public TicketGroupRepository TicketGroupRepository { get; init; } + + public TicketRepository TicketRepository { get; init; } + + public RouteAddressDetailRepository RouteAddressDetailRepository { get; init; } + + public VehicleEnrollmentEmployeeRepository VehicleEnrollmentEmployeeRepository { get; init; } + + + public AccountRepository AccountRepository { get; init; } + + public RoleRepository RoleRepository { get; init; } + + public AccountRoleRepository AccountRoleRepository { get; init; } + + public RefreshTokenRepository RefreshTokenRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); @@ -27,7 +91,7 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public async Task SaveAsync(CancellationToken cancellationToken) { - return await _dbContext.SaveChangesAsync(); + return await _dbContext.SaveChangesAsync(cancellationToken); } public void Dispose() diff --git a/src/Persistence/InMemory/Repositories/InMemoryAccountRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAccountRepository.cs new file mode 100644 index 0000000..aa24b2e --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryAccountRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryAccountRepository : + InMemoryBaseRepository, AccountRepository +{ + public InMemoryAccountRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryAccountRoleRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAccountRoleRepository.cs new file mode 100644 index 0000000..eed11a7 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryAccountRoleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryAccountRoleRepository : + InMemoryBaseRepository, AccountRoleRepository +{ + public InMemoryAccountRoleRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs new file mode 100644 index 0000000..95982ab --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryAddressRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryAddressRepository : + InMemoryBaseRepository
, AddressRepository +{ + public InMemoryAddressRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs new file mode 100644 index 0000000..cb203b6 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryAircraftRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryAircraftRepository : + InMemoryBaseRepository, AircraftRepository +{ + public InMemoryAircraftRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs index cb32f90..609ab72 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; @@ -28,7 +28,7 @@ public abstract class InMemoryBaseRepository : BaseRepository Expression> predicate, CancellationToken cancellationToken) { - return await _dbSet.SingleOrDefaultAsync(predicate); + return await _dbSet.SingleOrDefaultAsync(predicate, cancellationToken); } public async Task GetOneAsync( @@ -39,19 +39,19 @@ public abstract class InMemoryBaseRepository : BaseRepository return await _dbSet .Include(includeSelector) - .SingleOrDefaultAsync(predicate); + .SingleOrDefaultAsync(predicate, cancellationToken); } public async Task> GetPageAsync( int pageNumber, int pageSize, CancellationToken cancellationToken) { - var count = await _dbSet.CountAsync(); + var count = await _dbSet.CountAsync(cancellationToken); var entities = await _dbSet .Skip((pageNumber - 1) * pageSize).Take(pageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PaginatedList( entities, count, @@ -63,13 +63,13 @@ public abstract class InMemoryBaseRepository : BaseRepository int pageNumber, int pageSize, CancellationToken cancellationToken) { - var count = await _dbSet.CountAsync(); + var count = await _dbSet.CountAsync(cancellationToken); var entities = await _dbSet .Skip((pageNumber - 1) * pageSize).Take(pageSize) .Include(includeSelector) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PaginatedList( entities, count, @@ -81,13 +81,15 @@ public abstract class InMemoryBaseRepository : BaseRepository int pageNumber, int pageSize, CancellationToken cancellationToken) { - var count = await _dbSet.Where(predicate).CountAsync(); + var count = await _dbSet + .Where(predicate) + .CountAsync(cancellationToken); var entities = await _dbSet .Where(predicate) .Skip((pageNumber - 1) * pageSize).Take(pageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PaginatedList( entities, count, @@ -100,14 +102,16 @@ public abstract class InMemoryBaseRepository : BaseRepository int pageNumber, int pageSize, CancellationToken cancellationToken) { - var count = await _dbSet.Where(predicate).CountAsync(); + var count = await _dbSet + .Where(predicate) + .CountAsync(cancellationToken); var entities = await _dbSet .Where(predicate) .Skip((pageNumber - 1) * pageSize).Take(pageSize) .Include(includeSelector) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PaginatedList( entities, count, diff --git a/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs new file mode 100644 index 0000000..6b40e7b --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryBusRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryBusRepository : + InMemoryBaseRepository, BusRepository +{ + public InMemoryBusRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs new file mode 100644 index 0000000..b6aca53 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryCityRepository : + InMemoryBaseRepository, CityRepository +{ + public InMemoryCityRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs new file mode 100644 index 0000000..8d65211 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryCompanyRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.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/InMemory/Repositories/InMemoryCountryRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs index a0e0e1f..bc9812f 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs new file mode 100644 index 0000000..939f74e --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryEmployeeRepository : + InMemoryBaseRepository, EmployeeRepository +{ + public InMemoryEmployeeRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryRefreshTokenRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRefreshTokenRepository.cs new file mode 100644 index 0000000..dff079b --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRefreshTokenRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRefreshTokenRepository : + InMemoryBaseRepository, RefreshTokenRepository +{ + public InMemoryRefreshTokenRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs index 63a668e..5d7e7ee 100644 --- a/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs +++ b/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; diff --git a/src/Persistence/InMemory/Repositories/InMemoryRoleRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRoleRepository.cs new file mode 100644 index 0000000..81bbbc4 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRoleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRoleRepository : + InMemoryBaseRepository, RoleRepository +{ + public InMemoryRoleRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs new file mode 100644 index 0000000..9a7ee0b --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRouteAddressDetailRepository : + InMemoryBaseRepository, RouteAddressDetailRepository +{ + public InMemoryRouteAddressDetailRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs new file mode 100644 index 0000000..70e6141 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRouteAddressRepository : + InMemoryBaseRepository, RouteAddressRepository +{ + public InMemoryRouteAddressRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs new file mode 100644 index 0000000..27bb20f --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRouteRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRouteRepository : + InMemoryBaseRepository, RouteRepository +{ + public InMemoryRouteRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs new file mode 100644 index 0000000..b160761 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryTicketGroupRepository : + InMemoryBaseRepository, TicketGroupRepository +{ + public InMemoryTicketGroupRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs new file mode 100644 index 0000000..398bbab --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryTicketRepository : + InMemoryBaseRepository, TicketRepository +{ + public InMemoryTicketRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs new file mode 100644 index 0000000..971391c --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryTrainRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryTrainRepository : + InMemoryBaseRepository, TrainRepository +{ + public InMemoryTrainRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs new file mode 100644 index 0000000..3705f86 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentEmployeeRepository.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryVehicleEnrollmentEmployeeRepository : + InMemoryBaseRepository, + VehicleEnrollmentEmployeeRepository +{ + public InMemoryVehicleEnrollmentEmployeeRepository( + InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs new file mode 100644 index 0000000..520d174 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryVehicleEnrollmentRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryVehicleEnrollmentRepository : + InMemoryBaseRepository, VehicleEnrollmentRepository +{ + public InMemoryVehicleEnrollmentRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs new file mode 100644 index 0000000..ae36b4b --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryVehicleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryVehicleRepository : + InMemoryBaseRepository, VehicleRepository +{ + public InMemoryVehicleRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/Json/JsonDbContext.cs b/src/Persistence/Json/JsonDbContext.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/Persistence/PostgreSql/Configurations/AccountConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AccountConfiguration.cs new file mode 100644 index 0000000..dc16077 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/AccountConfiguration.cs @@ -0,0 +1,48 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class AccountConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("accounts"); + + base.Configure(builder); + + + builder + .Property(a => a.Username) + .HasColumnName("username") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(a => a.Email) + .HasColumnName("email") + .HasColumnType("varchar(256)") + .IsRequired(true); + + // Base64 encoded PBKDF2 SHA512. + // Base64 represents 3 input bytes 4 output bytes, it adds padding + // if the input bytes count isn't divisible by 3. + // 512 / 8 / 3 = 21.(3) => 22 input characters (with padding) => + // 88 output characters. + builder + .Property(a => a.PasswordHash) + .HasColumnName("password_hash") + .HasColumnType("varchar(88)") + .IsRequired(true); + + // Base64 encoded 128 bits + // 128 / 8 / 3 = 5.(3) => 6 in chars \w padding => 24 out chars + builder + .Property(a => a.PasswordSalt) + .HasColumnName("password_salt") + .HasColumnType("varchar(24)") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/AccountRoleConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AccountRoleConfiguration.cs new file mode 100644 index 0000000..50ae6aa --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/AccountRoleConfiguration.cs @@ -0,0 +1,64 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class AccountRoleConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("account_roles"); + + base.Configure(builder); + + + builder + .Property(ar => ar.AccountId) + .HasColumnName("account_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ar => ar.Account) + .WithMany(a => a.AccountRoles) + .HasForeignKey(ar => ar.AccountId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ar => ar.AccountId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ar => ar.AccountId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ar => ar.AccountId).Metadata.GetColumnName()}"); + + + builder + .Property(ar => ar.RoleId) + .HasColumnName("role_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ar => ar.Role) + .WithMany(r => r.AccountRoles) + .HasForeignKey(ar => ar.RoleId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ar => ar.RoleId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ar => ar.RoleId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ar => ar.RoleId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs index 2efda70..a7c96f2 100644 --- a/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs @@ -1,4 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -9,7 +10,23 @@ public class AddressConfiguration : BaseConfiguration
public override void Configure(EntityTypeBuilder
builder) { builder - .ToTable("addresses"); + .Property(a => a.VehicleType) + .HasColumnName("vehicle_type") + .HasColumnType("varchar(16)") + .IsRequired(true); + + builder + .ToTable( + "addresses", + a => a.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(a => a.VehicleType) + .Metadata.GetColumnName()}", + $"{builder.Property(a => a.VehicleType) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", VehicleType.Enumerations + .Values.Select(v => v.Name))}')")); base.Configure(builder); @@ -20,13 +37,6 @@ public class AddressConfiguration : BaseConfiguration
.HasColumnType("varchar(128)") .IsRequired(true); - // builder - // .Property(a => a.VehicleType) - // .HasColumnName("vehicle_type") - // .HasColumnType($"{PostgreSqlDbContext.DefaultSchema}.vehicle_type") - // .HasConversion() - // .IsRequired(true); - builder .Property(a => a.CityId) @@ -49,7 +59,6 @@ public class AddressConfiguration : BaseConfiguration
.HasDatabaseName( "ix_" + $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(a => a.CityId).Metadata.GetColumnName()}") - .IsUnique(); + $"{builder.Property(a => a.CityId).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs new file mode 100644 index 0000000..186e510 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/AircraftConfiguration.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class AircraftConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasBaseType(); + + + builder + .Property(b => b.Number) + .HasColumnName("number") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(b => b.Model) + .HasColumnName("model") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(b => b.Capacity) + .HasColumnName("capacity") + .HasColumnType("smallint") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs index 5905cc3..f9b0128 100644 --- a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs @@ -9,34 +9,21 @@ public class BaseConfiguration : IEntityTypeConfiguration { public virtual void Configure(EntityTypeBuilder builder) { + var tableName = builder.Metadata.GetTableName(); + builder .HasKey(b => b.Id) - .HasName($"pk_{builder.Metadata.GetTableName()}"); + .HasName($"pk_{tableName}"); builder .Property(b => b.Id) .HasColumnName("id") .HasColumnType("bigint") .UseSequence( - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + + $"{tableName}_" + + $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + "sequence"); - builder - .HasIndex(b => b.Id) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(b => b.Id).Metadata.GetColumnName()}") - .IsUnique(); - - - builder - .HasAlternateKey(b => b.Guid) - .HasName( - "altk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}"); builder .Property(b => b.Guid) @@ -45,11 +32,10 @@ public class BaseConfiguration : IEntityTypeConfiguration .IsRequired(true); builder - .HasIndex(b => b.Guid) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}") - .IsUnique(); + .HasAlternateKey(b => b.Guid) + .HasName( + "altk_" + + $"{tableName}_" + + $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Configurations/BusConfiguration.cs b/src/Persistence/PostgreSql/Configurations/BusConfiguration.cs new file mode 100644 index 0000000..593f72f --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/BusConfiguration.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class BusConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasBaseType(); + + + builder + .Property(b => b.Number) + .HasColumnName("number") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(b => b.Model) + .HasColumnName("model") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(b => b.Capacity) + .HasColumnName("capacity") + .HasColumnType("smallint") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs index dc6114c..ddafa7e 100644 --- a/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs @@ -42,7 +42,6 @@ public class CityConfiguration : BaseConfiguration .HasDatabaseName( "ix_" + $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(c => c.RegionId).Metadata.GetColumnName()}") - .IsUnique(); + $"{builder.Property(c => c.RegionId).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs new file mode 100644 index 0000000..0f0a923 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/CompanyConfiguration.cs @@ -0,0 +1,66 @@ +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); + + + builder + .Property(c => c.AccountId) + .HasColumnName("account_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(c => c.Account) + .WithOne(a => a.Company) + .HasForeignKey(c => c.AccountId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.ClientNoAction); + + builder + .HasIndex(c => c.AccountId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs b/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs new file mode 100644 index 0000000..345af64 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs @@ -0,0 +1,105 @@ +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 EmployeeConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(e => e.Sex) + .HasColumnName("sex") + .IsRequired(true); + + builder + .ToTable( + "employees", + e => e.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.Sex) + .Metadata.GetColumnName()}", + $"{builder.Property(e => e.Sex) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", Sex.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(e => e.FirstName) + .HasColumnName("first_name") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(e => e.LastName) + .HasColumnName("last_name") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(e => e.Patronymic) + .HasColumnName("patronymic") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(e => e.BirthDate) + .HasColumnName("birth_date") + .HasColumnType("date") + .IsRequired(true); + + + builder + .Property(e => e.CompanyId) + .HasColumnName("company_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(e => e.Company) + .WithMany(c => c.Employees) + .HasForeignKey(e => e.CompanyId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.CompanyId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.ClientNoAction); + + builder + .HasIndex(e => e.CompanyId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.CompanyId).Metadata.GetColumnName()}"); + + + builder + .Property(e => e.AccountId) + .HasColumnName("account_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(e => e.Account) + .WithOne(a => a.Employee) + .HasForeignKey(e => e.AccountId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.AccountId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(e => e.AccountId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.AccountId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs b/src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs new file mode 100644 index 0000000..1878cc6 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs @@ -0,0 +1,63 @@ +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 EmployeeDocumentConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(ed => ed.DocumentType) + .HasColumnName("document_type") + .IsRequired(true); + + builder + .ToTable( + "employee_documents", + ed => ed.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ed => ed.DocumentType) + .Metadata.GetColumnName()}", + $"{builder.Property(ed => ed.DocumentType) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", DocumentType.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(ed => ed.Information) + .HasColumnName("information") + .HasColumnType("varchar(256)") + .IsRequired(true); + + + builder + .Property(ed => ed.EmployeeId) + .HasColumnName("employee_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ed => ed.Employee) + .WithMany(ed => ed.Documents) + .HasForeignKey(ed => ed.EmployeeId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ed => ed.EmployeeId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ed => ed.EmployeeId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ed => ed.EmployeeId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/RefreshTokenConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RefreshTokenConfiguration.cs new file mode 100644 index 0000000..5265083 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RefreshTokenConfiguration.cs @@ -0,0 +1,67 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class RefreshTokenConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("refresh_tokens"); + + base.Configure(builder); + + + // Base64 encoded 128 bits + // 128 / 8 / 3 = 5.(3) => 6 in chars \w padding => 24 out chars + builder + .Property(rt => rt.Value) + .HasColumnName("value") + .HasColumnType("varchar(24)") + .IsRequired(true); + + builder + .Property(rt => rt.CreationTime) + .HasColumnName("creation_time") + .HasColumnType("timestamptz") + .IsRequired(true); + + builder + .Property(rt => rt.ExpirationTime) + .HasColumnName("expiration_time") + .HasColumnType("timestamptz") + .IsRequired(true); + + builder + .Property(rt => rt.RevocationTime) + .HasColumnName("revocation_time") + .HasColumnType("timestamptz") + .IsRequired(false); + + + builder + .Property(rt => rt.AccountId) + .HasColumnName("account_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(rt => rt.Account) + .WithMany(rt => rt.RefreshTokens) + .HasForeignKey(rt => rt.AccountId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rt => rt.AccountId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(rt => rt.AccountId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rt => rt.AccountId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs index 1cd8573..b1005a5 100644 --- a/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs @@ -36,5 +36,12 @@ public class RegionConfiguration : BaseConfiguration $"{builder.Metadata.GetTableName()}_" + $"{builder.Property(r => r.CountryId).Metadata.GetColumnName()}") .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(r => r.CountryId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(r => r.CountryId).Metadata.GetColumnName()}"); } } diff --git a/src/Persistence/PostgreSql/Configurations/RoleConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RoleConfiguration.cs new file mode 100644 index 0000000..90a3e21 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RoleConfiguration.cs @@ -0,0 +1,32 @@ +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 RoleConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(r => r.Value) + .HasColumnName("name") + .IsRequired(true); + + builder + .ToTable( + "roles", + r => r.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(r => r.Value) + .Metadata.GetColumnName()}", + $"{builder.Property(r => r.Value) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", IdentityRole.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs index 547d410..9d44a61 100644 --- a/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs @@ -42,8 +42,7 @@ public class RouteAddressConfiguration : BaseConfiguration .HasDatabaseName( "ix_" + $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}") - .IsUnique(); + $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}"); builder @@ -67,8 +66,7 @@ public class RouteAddressConfiguration : BaseConfiguration .HasDatabaseName( "ix_" + $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}") - .IsUnique(); + $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}"); builder diff --git a/src/Persistence/PostgreSql/Configurations/RouteAddressDetailConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteAddressDetailConfiguration.cs new file mode 100644 index 0000000..56c95cb --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RouteAddressDetailConfiguration.cs @@ -0,0 +1,85 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class RouteAddressDetailConfiguration : + BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("route_address_details"); + + base.Configure(builder); + + + builder + .Property(rad => rad.TimeToNextAddress) + .HasColumnName("time_to_next_address") + .HasColumnType("interval") + .IsRequired(true); + + builder + .Property(rad => rad.CostToNextAddress) + .HasColumnName("cost_to_next_address") + .HasColumnType("numeric(24,12)") + .IsRequired(true); + + builder + .Property(rad => rad.CurrentAddressStopTime) + .HasColumnName("current_address_stop_time") + .HasColumnType("interval") + .IsRequired(true); + + + builder + .Property(rad => rad.VehicleEnrollmentId) + .HasColumnName("vehicle_enrollment_id") + .HasColumnType("bigint") + .IsRequired(true); + + + builder + .HasOne(rad => rad.VehicleEnrollment) + .WithMany(ve => ve.RouteAddressDetails) + .HasForeignKey(rad => rad.VehicleEnrollmentId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rad => rad.VehicleEnrollmentId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(rad => rad.VehicleEnrollmentId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rad => rad.VehicleEnrollmentId).Metadata.GetColumnName()}"); + + + builder + .Property(rad => rad.RouteAddressId) + .HasColumnName("route_address_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(rad => rad.RouteAddress) + .WithMany(ra => ra.Details) + .HasForeignKey(rad => rad.RouteAddressId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rad => rad.RouteAddressId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(rad => rad.RouteAddressId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(rad => rad.RouteAddressId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs index 6c81f2e..de9177a 100644 --- a/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs @@ -1,4 +1,5 @@ using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -9,7 +10,23 @@ public class RouteConfiguration : BaseConfiguration public override void Configure(EntityTypeBuilder builder) { builder - .ToTable("routes"); + .Property(r => r.VehicleType) + .HasColumnName("vehicle_type") + .HasColumnType("varchar(16)") + .IsRequired(true); + + builder + .ToTable( + "routes", + r => r.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(a => a.VehicleType) + .Metadata.GetColumnName()}", + $"{builder.Property(a => a.VehicleType) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", VehicleType.Enumerations + .Values.Select(v => v.Name))}')")); base.Configure(builder); @@ -19,12 +36,5 @@ public class RouteConfiguration : BaseConfiguration .HasColumnName("name") .HasColumnType("varchar(64)") .IsRequired(true); - - // builder - // .Property(r => r.VehicleType) - // .HasColumnName("vehicle_type") - // .HasColumnType($"{PostgreSqlDbContext.DefaultSchema}.vehicle_type") - // .HasConversion() - // .IsRequired(true); } } diff --git a/src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs new file mode 100644 index 0000000..2b2771d --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs @@ -0,0 +1,105 @@ +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 TicketConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(t => t.Currency) + .HasColumnName("currency") + .IsRequired(true); + + builder + .ToTable( + "tickets", + ve => ve.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.Currency) + .Metadata.GetColumnName()}", + $"{builder.Property(ve => ve.Currency) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", Currency.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(t => t.DepartureRouteAddressId) + .HasColumnName("departure_route_address_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .Property(t => t.ArrivalRouteAddressId) + .HasColumnName("arrival_route_address_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .Property(t => t.Order) + .HasColumnName("order") + .HasColumnType("smallint") + .IsRequired(true); + + builder + .Property(t => t.Cost) + .HasColumnName("cost") + .HasColumnType("numeric(24,12)") + .IsRequired(true); + + + builder + .Property(t => t.TicketGroupId) + .HasColumnName("ticket_group_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(t => t.TicketGroup) + .WithMany(tg => tg.Tickets) + .HasForeignKey(t => t.TicketGroupId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(t => t.TicketGroupId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(t => t.TicketGroupId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(t => t.TicketGroupId).Metadata.GetColumnName()}"); + + + builder + .Property(t => t.VehicleEnrollmentId) + .HasColumnName("vehicle_enrollment_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(t => t.VehicleEnrollment) + .WithMany(ve => ve.Tickets) + .HasForeignKey(t => t.VehicleEnrollmentId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(t => t.VehicleEnrollmentId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(t => t.VehicleEnrollmentId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(t => t.VehicleEnrollmentId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs new file mode 100644 index 0000000..3c29245 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs @@ -0,0 +1,117 @@ +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 TicketGroupConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(tg => tg.PassangerSex) + .HasColumnName("passanger_sex") + .IsRequired(true); + + builder + .Property(tg => tg.Status) + .HasColumnName("status") + .IsRequired(true); + + builder + .ToTable( + "ticket_groups", + tg => + { + tg.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(tg => tg.PassangerSex) + .Metadata.GetColumnName()}", + $"{builder.Property(g => g.PassangerSex) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", Sex.Enumerations + .Values.Select(v => v.Name))}')"); + + tg.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(tg => tg.Status) + .Metadata.GetColumnName()}", + $"{builder.Property(g => g.Status) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", TicketStatus.Enumerations + .Values.Select(v => v.Name))}')"); + }); + + base.Configure(builder); + + + builder + .Property(tg => tg.PassangerFirstName) + .HasColumnName("passanger_first_name") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(tg => tg.PassangerLastName) + .HasColumnName("passanger_last_name") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(tg => tg.PassangerPatronymic) + .HasColumnName("passanger_patronymic") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(tg => tg.PassangerBirthDate) + .HasColumnName("passanger_birth_date") + .HasColumnType("date") + .IsRequired(true); + + builder + .Property(tg => tg.PassangerEmail) + .HasColumnName("passanger_email") + .HasColumnType("varchar(256)") + .IsRequired(false); + + builder + .Property(tg => tg.PurchaseTime) + .HasColumnName("purchase_time") + .HasColumnType("timestamptz") + .IsRequired(true); + + builder + .Property(tg => tg.TravelTime) + .HasColumnName("travel_time") + .HasColumnType("interval") + .IsRequired(true); + + + builder + .Property(tg => tg.AccountId) + .HasColumnName("account_id") + .HasColumnType("bigint") + .IsRequired(false); + + builder + .HasOne(tg => tg.Account) + .WithMany(a => a.TicketGroups) + .HasForeignKey(tg => tg.AccountId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.SetNull); + + builder + .HasIndex(c => c.AccountId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.AccountId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs new file mode 100644 index 0000000..fccefeb --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/TrainConfiguration.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class TrainConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasBaseType(); + + + builder + .Property(b => b.Number) + .HasColumnName("number") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(b => b.Model) + .HasColumnName("model") + .HasColumnType("varchar(64)") + .IsRequired(true); + + builder + .Property(b => b.Capacity) + .HasColumnName("capacity") + .HasColumnType("smallint") + .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs b/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs new file mode 100644 index 0000000..d2c0396 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/VehicleConfiguration.cs @@ -0,0 +1,65 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class VehicleConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(a => a.VehicleType) + .HasColumnName("vehicle_type") + .HasColumnType("varchar(16)") + .IsRequired(true); + + builder + .ToTable( + "vehicles", + v => v.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(v => v.VehicleType) + .Metadata.GetColumnName()}", + $"{builder.Property(v => v.VehicleType) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", VehicleType.Enumerations + .Values.Select(v => v.Name))}')")); + + builder + .ToTable("vehicles") + .UseTphMappingStrategy() + .HasDiscriminator(v => v.VehicleType) + .HasValue(VehicleType.Bus) + .HasValue(VehicleType.Aircraft) + .HasValue(VehicleType.Train); + + base.Configure(builder); + + + 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/Configurations/VehicleEnrollmentConfiguration.cs b/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentConfiguration.cs new file mode 100644 index 0000000..3c5ce9f --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentConfiguration.cs @@ -0,0 +1,88 @@ +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 VehicleEnrollmentConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(ve => ve.Currency) + .HasColumnName("currency") + .HasColumnType("varchar(8)") + .IsRequired(true); + + builder + .ToTable( + "vehicle_enrollments", + ve => ve.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.Currency) + .Metadata.GetColumnName()}", + $"{builder.Property(ve => ve.Currency) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", Currency.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(ve => ve.DepartureTime) + .HasColumnName("departure_time") + .HasColumnType("timestamptz") + .IsRequired(true); + + + builder + .Property(ve => ve.VehicleId) + .HasColumnName("vehicle_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ve => ve.Vehicle) + .WithMany(v => v.Enrollments) + .HasForeignKey(ve => ve.VehicleId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.VehicleId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ve => ve.VehicleId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.VehicleId).Metadata.GetColumnName()}"); + + + builder + .Property(ve => ve.RouteId) + .HasColumnName("route_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ve => ve.Route) + .WithMany(r => r.VehicleEnrollments) + .HasForeignKey(ve => ve.RouteId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.RouteId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ve => ve.RouteId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ve => ve.RouteId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentEmployeeConfiguration.cs b/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentEmployeeConfiguration.cs new file mode 100644 index 0000000..11b319f --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/VehicleEnrollmentEmployeeConfiguration.cs @@ -0,0 +1,70 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class VehicleEnrollmentEmployeeConfiguration : + BaseConfiguration +{ + public override void Configure( + EntityTypeBuilder builder) + { + builder + .ToTable("vehicle_enrollment_employees"); + + base.Configure(builder); + + + builder + .Property(vee => vee.EmployeeId) + .HasColumnName("employee_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(vee => vee.Employee) + .WithMany(e => e.VehicleEnrollmentEmployees) + .HasForeignKey(vee => vee.EmployeeId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(vee => vee.EmployeeId) + .Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(vee => vee.EmployeeId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(vee => vee.EmployeeId) + .Metadata.GetColumnName()}"); + + + builder + .Property(vee => vee.VehicleEnrollmentId) + .HasColumnName("vehicle_enrollment_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(vee => vee.VehicleEnrollment) + .WithMany(ve => ve.VehicleEnrollmentEmployees) + .HasForeignKey(vee => vee.VehicleEnrollmentId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(vee => vee.VehicleEnrollmentId) + .Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(vee => vee.VehicleEnrollmentId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(vee => vee.VehicleEnrollmentId) + .Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.Designer.cs new file mode 100644 index 0000000..92c0355 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.Designer.cs @@ -0,0 +1,269 @@ +// +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("20250430180231_Add_Countries_Regions_Cities_Addresses")] + partial class Add_Countries_Regions_Cities_Addresses + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.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_Guid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_addresses_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_addresses_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_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_cities_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_cities_id"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_countries_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_countries_id"); + + 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_Guid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_regions_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_regions_id"); + + b.ToTable("regions", "application"); + }); + + 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.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs b/src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.cs similarity index 61% rename from src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs rename to src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.cs index 2e209c1..bc5649f 100644 --- a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs +++ b/src/Persistence/PostgreSql/Migrations/20250430180231_Add_Countries_Regions_Cities_Addresses.cs @@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace Persistence.PostgreSql.Migrations { /// - public partial class Initial_migration : Migration + public partial class Add_Countries_Regions_Cities_Addresses : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -14,9 +14,6 @@ namespace Persistence.PostgreSql.Migrations migrationBuilder.EnsureSchema( name: "application"); - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:Enum:vehicle_type", "bus,train,aircraft"); - migrationBuilder.CreateSequence( name: "addresses_id_sequence", schema: "application"); @@ -33,14 +30,6 @@ namespace Persistence.PostgreSql.Migrations name: "regions_id_sequence", schema: "application"); - migrationBuilder.CreateSequence( - name: "route_addresses_id_sequence", - schema: "application"); - - migrationBuilder.CreateSequence( - name: "routes_id_sequence", - schema: "application"); - migrationBuilder.CreateTable( name: "countries", schema: "application", @@ -56,22 +45,6 @@ namespace Persistence.PostgreSql.Migrations table.UniqueConstraint("altk_countries_Guid", x => x.uuid); }); - migrationBuilder.CreateTable( - name: "routes", - schema: "application", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.routes_id_sequence')"), - name = table.Column(type: "varchar(64)", nullable: false), - vehicle_type = table.Column(type: "application.vehicle_type", nullable: false), - uuid = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_routes", x => x.id); - table.UniqueConstraint("altk_routes_Guid", x => x.uuid); - }); - migrationBuilder.CreateTable( name: "regions", schema: "application", @@ -125,7 +98,9 @@ namespace Persistence.PostgreSql.Migrations { id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.addresses_id_sequence')"), name = table.Column(type: "varchar(128)", nullable: false), - vehicle_type = table.Column(type: "application.vehicle_type", nullable: false), + Longitude = table.Column(type: "double precision", nullable: false), + Latitude = table.Column(type: "double precision", nullable: false), + vehicle_type = table.Column(type: "varchar(16)", nullable: false), city_id = table.Column(type: "bigint", nullable: false), uuid = table.Column(type: "uuid", nullable: false) }, @@ -133,6 +108,7 @@ namespace Persistence.PostgreSql.Migrations { table.PrimaryKey("pk_addresses", x => x.id); table.UniqueConstraint("altk_addresses_Guid", x => x.uuid); + table.CheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); table.ForeignKey( name: "fk_addresses_city_id", column: x => x.city_id, @@ -142,44 +118,11 @@ namespace Persistence.PostgreSql.Migrations onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateTable( - name: "route_addresses", - schema: "application", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.route_addresses_id_sequence')"), - order = table.Column(type: "smallint", nullable: false), - address_id = table.Column(type: "bigint", nullable: false), - route_id = table.Column(type: "bigint", nullable: false), - uuid = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_route_addresses", x => x.id); - table.UniqueConstraint("altk_route_addresses_address_id_route_id_order", x => new { x.address_id, x.route_id, x.order }); - table.UniqueConstraint("altk_route_addresses_Guid", x => x.uuid); - table.ForeignKey( - name: "fk_route_addresses_address_id", - column: x => x.address_id, - principalSchema: "application", - principalTable: "addresses", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_route_addresses_route_id", - column: x => x.route_id, - principalSchema: "application", - principalTable: "routes", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - migrationBuilder.CreateIndex( name: "ix_addresses_city_id", schema: "application", table: "addresses", - column: "city_id", - unique: true); + column: "city_id"); migrationBuilder.CreateIndex( name: "ix_addresses_id", @@ -206,8 +149,7 @@ namespace Persistence.PostgreSql.Migrations name: "ix_cities_region_id", schema: "application", table: "cities", - column: "region_id", - unique: true); + column: "region_id"); migrationBuilder.CreateIndex( name: "ix_cities_uuid", @@ -231,7 +173,7 @@ namespace Persistence.PostgreSql.Migrations unique: true); migrationBuilder.CreateIndex( - name: "IX_regions_country_id", + name: "ix_regions_country_id", schema: "application", table: "regions", column: "country_id"); @@ -249,65 +191,15 @@ namespace Persistence.PostgreSql.Migrations table: "regions", column: "uuid", unique: true); - - migrationBuilder.CreateIndex( - name: "ix_route_addresses_address_id", - schema: "application", - table: "route_addresses", - column: "address_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_route_addresses_id", - schema: "application", - table: "route_addresses", - column: "id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_route_addresses_route_id", - schema: "application", - table: "route_addresses", - column: "route_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_route_addresses_uuid", - schema: "application", - table: "route_addresses", - column: "uuid", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_routes_id", - schema: "application", - table: "routes", - column: "id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_routes_uuid", - schema: "application", - table: "routes", - column: "uuid", - unique: true); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "route_addresses", - schema: "application"); - migrationBuilder.DropTable( name: "addresses", schema: "application"); - migrationBuilder.DropTable( - name: "routes", - schema: "application"); - migrationBuilder.DropTable( name: "cities", schema: "application"); @@ -335,14 +227,6 @@ namespace Persistence.PostgreSql.Migrations migrationBuilder.DropSequence( name: "regions_id_sequence", schema: "application"); - - migrationBuilder.DropSequence( - name: "route_addresses_id_sequence", - schema: "application"); - - migrationBuilder.DropSequence( - name: "routes_id_sequence", - schema: "application"); } } } diff --git a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.Designer.cs similarity index 93% rename from src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs rename to src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.Designer.cs index d68963c..3e5d1c4 100644 --- a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs +++ b/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.Designer.cs @@ -12,8 +12,8 @@ using cuqmbr.TravelGuide.Persistence.PostgreSql; namespace Persistence.PostgreSql.Migrations { [DbContext(typeof(PostgreSqlDbContext))] - [Migration("20250427160059_Initial_migration")] - partial class Initial_migration + [Migration("20250501112816_Add_Route_and_RouteAddresses")] + partial class Add_Route_and_RouteAddresses { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -24,7 +24,6 @@ namespace Persistence.PostgreSql.Migrations .HasAnnotation("ProductVersion", "9.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "vehicle_type", new[] { "bus", "train", "aircraft" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.HasSequence("addresses_id_sequence"); @@ -57,6 +56,12 @@ namespace Persistence.PostgreSql.Migrations .HasColumnType("uuid") .HasColumnName("uuid"); + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + b.Property("Name") .IsRequired() .HasColumnType("varchar(128)") @@ -64,7 +69,7 @@ namespace Persistence.PostgreSql.Migrations b.Property("VehicleType") .IsRequired() - .HasColumnType("application.vehicle_type") + .HasColumnType("varchar(16)") .HasColumnName("vehicle_type"); b.HasKey("Id") @@ -74,7 +79,6 @@ namespace Persistence.PostgreSql.Migrations .HasName("altk_addresses_Guid"); b.HasIndex("CityId") - .IsUnique() .HasDatabaseName("ix_addresses_city_id"); b.HasIndex("Guid") @@ -85,7 +89,10 @@ namespace Persistence.PostgreSql.Migrations .IsUnique() .HasDatabaseName("ix_addresses_id"); - b.ToTable("addresses", "application"); + 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 => @@ -126,7 +133,6 @@ namespace Persistence.PostgreSql.Migrations .HasDatabaseName("ix_cities_id"); b.HasIndex("RegionId") - .IsUnique() .HasDatabaseName("ix_cities_region_id"); b.ToTable("cities", "application"); @@ -197,7 +203,8 @@ namespace Persistence.PostgreSql.Migrations b.HasAlternateKey("Guid") .HasName("altk_regions_Guid"); - b.HasIndex("CountryId"); + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); b.HasIndex("Guid") .IsUnique() @@ -231,7 +238,7 @@ namespace Persistence.PostgreSql.Migrations b.Property("VehicleType") .IsRequired() - .HasColumnType("application.vehicle_type") + .HasColumnType("varchar(16)") .HasColumnName("vehicle_type"); b.HasKey("Id") @@ -248,7 +255,10 @@ namespace Persistence.PostgreSql.Migrations .IsUnique() .HasDatabaseName("ix_routes_id"); - b.ToTable("routes", "application"); + 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 => @@ -287,7 +297,6 @@ namespace Persistence.PostgreSql.Migrations .HasName("altk_route_addresses_address_id_route_id_order"); b.HasIndex("AddressId") - .IsUnique() .HasDatabaseName("ix_route_addresses_address_id"); b.HasIndex("Guid") @@ -299,7 +308,6 @@ namespace Persistence.PostgreSql.Migrations .HasDatabaseName("ix_route_addresses_id"); b.HasIndex("RouteId") - .IsUnique() .HasDatabaseName("ix_route_addresses_route_id"); b.ToTable("route_addresses", "application"); diff --git a/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.cs b/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.cs new file mode 100644 index 0000000..2908cf2 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250501112816_Add_Route_and_RouteAddresses.cs @@ -0,0 +1,132 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Route_and_RouteAddresses : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "route_addresses_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "routes_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "routes", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.routes_id_sequence')"), + name = table.Column(type: "varchar(64)", nullable: false), + vehicle_type = table.Column(type: "varchar(16)", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_routes", x => x.id); + table.UniqueConstraint("altk_routes_Guid", x => x.uuid); + table.CheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + migrationBuilder.CreateTable( + name: "route_addresses", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.route_addresses_id_sequence')"), + order = table.Column(type: "smallint", nullable: false), + address_id = table.Column(type: "bigint", nullable: false), + route_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_route_addresses", x => x.id); + table.UniqueConstraint("altk_route_addresses_address_id_route_id_order", x => new { x.address_id, x.route_id, x.order }); + table.UniqueConstraint("altk_route_addresses_Guid", x => x.uuid); + table.ForeignKey( + name: "fk_route_addresses_address_id", + column: x => x.address_id, + principalSchema: "application", + principalTable: "addresses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_route_addresses_route_id", + column: x => x.route_id, + principalSchema: "application", + principalTable: "routes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_address_id", + schema: "application", + table: "route_addresses", + column: "address_id"); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_id", + schema: "application", + table: "route_addresses", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_route_id", + schema: "application", + table: "route_addresses", + column: "route_id"); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_uuid", + schema: "application", + table: "route_addresses", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_routes_id", + schema: "application", + table: "routes", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_routes_uuid", + schema: "application", + table: "routes", + column: "uuid", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "route_addresses", + schema: "application"); + + migrationBuilder.DropTable( + name: "routes", + schema: "application"); + + migrationBuilder.DropSequence( + name: "route_addresses_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "routes_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.Designer.cs new file mode 100644 index 0000000..755f1db --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.Designer.cs @@ -0,0 +1,352 @@ +// +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("20250502183338_Remove_duplicate_indices_on_pk_and_altk")] + partial class Remove_duplicate_indices_on_pk_and_altk + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.cs b/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.cs new file mode 100644 index 0000000..92f844f --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250502183338_Remove_duplicate_indices_on_pk_and_altk.cs @@ -0,0 +1,294 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Remove_duplicate_indices_on_pk_and_altk : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "altk_routes_Guid", + schema: "application", + table: "routes"); + + migrationBuilder.DropIndex( + name: "ix_routes_id", + schema: "application", + table: "routes"); + + migrationBuilder.DropIndex( + name: "ix_routes_uuid", + schema: "application", + table: "routes"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_route_addresses_Guid", + schema: "application", + table: "route_addresses"); + + migrationBuilder.DropIndex( + name: "ix_route_addresses_id", + schema: "application", + table: "route_addresses"); + + migrationBuilder.DropIndex( + name: "ix_route_addresses_uuid", + schema: "application", + table: "route_addresses"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_regions_Guid", + schema: "application", + table: "regions"); + + migrationBuilder.DropIndex( + name: "ix_regions_id", + schema: "application", + table: "regions"); + + migrationBuilder.DropIndex( + name: "ix_regions_uuid", + schema: "application", + table: "regions"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_countries_Guid", + schema: "application", + table: "countries"); + + migrationBuilder.DropIndex( + name: "ix_countries_id", + schema: "application", + table: "countries"); + + migrationBuilder.DropIndex( + name: "ix_countries_uuid", + schema: "application", + table: "countries"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_cities_Guid", + schema: "application", + table: "cities"); + + migrationBuilder.DropIndex( + name: "ix_cities_id", + schema: "application", + table: "cities"); + + migrationBuilder.DropIndex( + name: "ix_cities_uuid", + schema: "application", + table: "cities"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_addresses_Guid", + schema: "application", + table: "addresses"); + + migrationBuilder.DropIndex( + name: "ix_addresses_id", + schema: "application", + table: "addresses"); + + migrationBuilder.DropIndex( + name: "ix_addresses_uuid", + schema: "application", + table: "addresses"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_routes_uuid", + schema: "application", + table: "routes", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_route_addresses_uuid", + schema: "application", + table: "route_addresses", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_regions_uuid", + schema: "application", + table: "regions", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_countries_uuid", + schema: "application", + table: "countries", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_cities_uuid", + schema: "application", + table: "cities", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_addresses_uuid", + schema: "application", + table: "addresses", + column: "uuid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "altk_routes_uuid", + schema: "application", + table: "routes"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_route_addresses_uuid", + schema: "application", + table: "route_addresses"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_regions_uuid", + schema: "application", + table: "regions"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_countries_uuid", + schema: "application", + table: "countries"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_cities_uuid", + schema: "application", + table: "cities"); + + migrationBuilder.DropUniqueConstraint( + name: "altk_addresses_uuid", + schema: "application", + table: "addresses"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_routes_Guid", + schema: "application", + table: "routes", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_route_addresses_Guid", + schema: "application", + table: "route_addresses", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_regions_Guid", + schema: "application", + table: "regions", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_countries_Guid", + schema: "application", + table: "countries", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_cities_Guid", + schema: "application", + table: "cities", + column: "uuid"); + + migrationBuilder.AddUniqueConstraint( + name: "altk_addresses_Guid", + schema: "application", + table: "addresses", + column: "uuid"); + + migrationBuilder.CreateIndex( + name: "ix_routes_id", + schema: "application", + table: "routes", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_routes_uuid", + schema: "application", + table: "routes", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_id", + schema: "application", + table: "route_addresses", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_uuid", + schema: "application", + table: "route_addresses", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_regions_id", + schema: "application", + table: "regions", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_regions_uuid", + schema: "application", + table: "regions", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_countries_id", + schema: "application", + table: "countries", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_countries_uuid", + schema: "application", + table: "countries", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_cities_id", + schema: "application", + table: "cities", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_cities_uuid", + schema: "application", + table: "cities", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_addresses_id", + schema: "application", + table: "addresses", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_addresses_uuid", + schema: "application", + table: "addresses", + column: "uuid", + unique: true); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs new file mode 100644 index 0000000..d32eb83 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.Designer.cs @@ -0,0 +1,476 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250503053607_Add_Bus_Aircraft_Train_with_basic_properties")] + partial class Add_Bus_Aircraft_Train_with_basic_properties + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs new file mode 100644 index 0000000..b071d55 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250503053607_Add_Bus_Aircraft_Train_with_basic_properties.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Bus_Aircraft_Train_with_basic_properties : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "vehicles_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "vehicles", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.vehicles_id_sequence')"), + vehicle_type = table.Column(type: "varchar(16)", nullable: false), + number = table.Column(type: "varchar(32)", nullable: true), + model = table.Column(type: "varchar(64)", nullable: true), + capacity = table.Column(type: "smallint", nullable: true), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_vehicles", x => x.id); + table.UniqueConstraint("altk_vehicles_uuid", x => x.uuid); + table.CheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "vehicles", + schema: "application"); + + migrationBuilder.DropSequence( + name: "vehicles_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.Designer.cs new file mode 100644 index 0000000..8dcb5cd --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.Designer.cs @@ -0,0 +1,637 @@ +// +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("20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail")] + partial class Add_Vehicle_Enrollment_and_Route_Address_Detail + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_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.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("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.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 ('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.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.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/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.cs b/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.cs new file mode 100644 index 0000000..2ee55fb --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250504143929_Add_Vehicle_Enrollment_and_Route_Address_Detail.cs @@ -0,0 +1,133 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Vehicle_Enrollment_and_Route_Address_Detail : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "route_address_details_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "vehicle_enrollments_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "vehicle_enrollments", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.vehicle_enrollments_id_sequence')"), + departure_time = table.Column(type: "timestamptz", nullable: false), + currency = table.Column(type: "varchar(8)", nullable: false), + vehicle_id = table.Column(type: "bigint", nullable: false), + route_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_vehicle_enrollments", x => x.id); + table.UniqueConstraint("altk_vehicle_enrollments_uuid", x => x.uuid); + table.CheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('USD', 'EUR', 'UAH')"); + table.ForeignKey( + name: "fk_vehicle_enrollments_route_id", + column: x => x.route_id, + principalSchema: "application", + principalTable: "routes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_vehicle_enrollments_vehicle_id", + column: x => x.vehicle_id, + principalSchema: "application", + principalTable: "vehicles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "route_address_details", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.route_address_details_id_sequence')"), + time_to_next_address = table.Column(type: "interval", nullable: false), + cost_to_next_address = table.Column(type: "numeric(24,12)", nullable: false), + current_address_stop_time = table.Column(type: "interval", nullable: false), + vehicle_enrollment_id = table.Column(type: "bigint", nullable: false), + route_address_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_route_address_details", x => x.id); + table.UniqueConstraint("altk_route_address_details_uuid", x => x.uuid); + table.ForeignKey( + name: "fk_route_address_details_route_address_id", + column: x => x.route_address_id, + principalSchema: "application", + principalTable: "route_addresses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_route_address_details_vehicle_enrollment_id", + column: x => x.vehicle_enrollment_id, + principalSchema: "application", + principalTable: "vehicle_enrollments", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_route_address_details_route_address_id", + schema: "application", + table: "route_address_details", + column: "route_address_id"); + + migrationBuilder.CreateIndex( + name: "ix_route_address_details_vehicle_enrollment_id", + schema: "application", + table: "route_address_details", + column: "vehicle_enrollment_id"); + + migrationBuilder.CreateIndex( + name: "ix_vehicle_enrollments_route_id", + schema: "application", + table: "vehicle_enrollments", + column: "route_id"); + + migrationBuilder.CreateIndex( + name: "ix_vehicle_enrollments_vehicle_id", + schema: "application", + table: "vehicle_enrollments", + column: "vehicle_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "route_address_details", + schema: "application"); + + migrationBuilder.DropTable( + name: "vehicle_enrollments", + schema: "application"); + + migrationBuilder.DropSequence( + name: "route_address_details_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "vehicle_enrollments_id_sequence", + schema: "application"); + } + } +} 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/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs new file mode 100644 index 0000000..04dca68 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs @@ -0,0 +1,841 @@ +// +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("20250515164353_Add_Employee_and_EmployeeDocument")] + partial class Add_Employee_and_EmployeeDocument + { + /// + 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("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_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.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + 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.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + 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("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + }); + + 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/20250515164353_Add_Employee_and_EmployeeDocument.cs b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.cs new file mode 100644 index 0000000..1f8fc90 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.cs @@ -0,0 +1,108 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Employee_and_EmployeeDocument : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "employee_documents_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "employees_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "employees", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.employees_id_sequence')"), + first_name = table.Column(type: "varchar(32)", nullable: false), + last_name = table.Column(type: "varchar(32)", nullable: false), + patronymic = table.Column(type: "varchar(32)", nullable: false), + sex = table.Column(type: "varchar(32)", nullable: false), + birth_date = table.Column(type: "date", nullable: false), + company_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_employees", x => x.id); + table.UniqueConstraint("altk_employees_uuid", x => x.uuid); + table.CheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + table.ForeignKey( + name: "fk_employees_company_id", + column: x => x.company_id, + principalSchema: "application", + principalTable: "companies", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "employee_documents", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.employee_documents_id_sequence')"), + document_type = table.Column(type: "varchar(64)", nullable: false), + information = table.Column(type: "varchar(256)", nullable: false), + employee_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_employee_documents", x => x.id); + table.UniqueConstraint("altk_employee_documents_uuid", x => x.uuid); + table.CheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + table.ForeignKey( + name: "fk_employee_documents_employee_id", + column: x => x.employee_id, + principalSchema: "application", + principalTable: "employees", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_employee_documents_employee_id", + schema: "application", + table: "employee_documents", + column: "employee_id"); + + migrationBuilder.CreateIndex( + name: "ix_employees_company_id", + schema: "application", + table: "employees", + column: "company_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "employee_documents", + schema: "application"); + + migrationBuilder.DropTable( + name: "employees", + schema: "application"); + + migrationBuilder.DropSequence( + name: "employee_documents_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "employees_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs new file mode 100644 index 0000000..37b26bc --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs @@ -0,0 +1,1016 @@ +// +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("20250519212941_Add_Ticket_and_TicketGroup")] + partial class Add_Ticket_and_TicketGroup + { + /// + 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("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_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("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_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.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + 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.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Returned") + .HasColumnType("boolean") + .HasColumnName("returned"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + }); + }); + + 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.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + 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.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + 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("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + }); + + 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.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs new file mode 100644 index 0000000..6cbe97e --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs @@ -0,0 +1,140 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Ticket_and_TicketGroup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "ticket_groups_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "tickets_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "ticket_groups", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.ticket_groups_id_sequence')"), + passanger_first_name = table.Column(type: "varchar(32)", nullable: false), + passanger_last_name = table.Column(type: "varchar(32)", nullable: false), + passanger_patronymic = table.Column(type: "varchar(32)", nullable: false), + passanger_sex = table.Column(type: "varchar(32)", nullable: false), + passanger_birth_date = table.Column(type: "date", nullable: false), + purchase_time = table.Column(type: "timestamptz", nullable: false), + returned = table.Column(type: "boolean", nullable: false), + travel_time = table.Column(type: "interval", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_ticket_groups", x => x.id); + table.UniqueConstraint("altk_ticket_groups_uuid", x => x.uuid); + table.CheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + }); + + migrationBuilder.CreateTable( + name: "tickets", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.tickets_id_sequence')"), + departure_route_address_id = table.Column(type: "bigint", nullable: false), + arrival_route_address_id = table.Column(type: "bigint", nullable: false), + order = table.Column(type: "smallint", nullable: false), + currency = table.Column(type: "varchar(8)", nullable: false), + cost = table.Column(type: "numeric(24,12)", nullable: false), + ticket_group_id = table.Column(type: "bigint", nullable: false), + vehicle_enrollment_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tickets", x => x.id); + table.UniqueConstraint("altk_tickets_uuid", x => x.uuid); + table.CheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + table.ForeignKey( + name: "FK_tickets_route_addresses_arrival_route_address_id", + column: x => x.arrival_route_address_id, + principalSchema: "application", + principalTable: "route_addresses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_tickets_route_addresses_departure_route_address_id", + column: x => x.departure_route_address_id, + principalSchema: "application", + principalTable: "route_addresses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_tickets_ticket_group_id", + column: x => x.ticket_group_id, + principalSchema: "application", + principalTable: "ticket_groups", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_tickets_vehicle_enrollment_id", + column: x => x.vehicle_enrollment_id, + principalSchema: "application", + principalTable: "vehicle_enrollments", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_tickets_arrival_route_address_id", + schema: "application", + table: "tickets", + column: "arrival_route_address_id"); + + migrationBuilder.CreateIndex( + name: "IX_tickets_departure_route_address_id", + schema: "application", + table: "tickets", + column: "departure_route_address_id"); + + migrationBuilder.CreateIndex( + name: "ix_tickets_ticket_group_id", + schema: "application", + table: "tickets", + column: "ticket_group_id"); + + migrationBuilder.CreateIndex( + name: "ix_tickets_vehicle_enrollment_id", + schema: "application", + table: "tickets", + column: "vehicle_enrollment_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "tickets", + schema: "application"); + + migrationBuilder.DropTable( + name: "ticket_groups", + schema: "application"); + + migrationBuilder.DropSequence( + name: "ticket_groups_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "tickets_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs new file mode 100644 index 0000000..6338c55 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs @@ -0,0 +1,1019 @@ +// +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("20250524184743_Add_status_to_Ticket_Group")] + partial class Add_status_to_Ticket_Group + { + /// + 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("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_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("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_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.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + 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.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + 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.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + 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.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + 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("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + }); + + 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.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.cs b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.cs new file mode 100644 index 0000000..d3036f4 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_status_to_Ticket_Group : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "returned", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.AddColumn( + name: "status", + schema: "application", + table: "ticket_groups", + type: "varchar(32)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddCheckConstraint( + name: "ck_ticket_groups_status", + schema: "application", + table: "ticket_groups", + sql: "status IN ('reserved', 'returned', 'purchased')"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropCheckConstraint( + name: "ck_ticket_groups_status", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.DropColumn( + name: "status", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.AddColumn( + name: "returned", + schema: "application", + table: "ticket_groups", + type: "boolean", + nullable: false, + defaultValue: false); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.Designer.cs new file mode 100644 index 0000000..c4ef7f4 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.Designer.cs @@ -0,0 +1,1083 @@ +// +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("20250527113429_Add_Vehicle_Enrollment_Employee")] + partial class Add_Vehicle_Enrollment_Employee + { + /// + 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("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_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("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_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.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + 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.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + 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.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + 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.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + 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.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + 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.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + 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("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + 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.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.cs b/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.cs new file mode 100644 index 0000000..768ef38 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250527113429_Add_Vehicle_Enrollment_Employee.cs @@ -0,0 +1,73 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Vehicle_Enrollment_Employee : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "vehicle_enrollment_employees_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "vehicle_enrollment_employees", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.vehicle_enrollment_employees_id_sequence')"), + employee_id = table.Column(type: "bigint", nullable: false), + vehicle_enrollment_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_vehicle_enrollment_employees", x => x.id); + table.UniqueConstraint("altk_vehicle_enrollment_employees_uuid", x => x.uuid); + table.ForeignKey( + name: "fk_vehicle_enrollment_employees_employee_id", + column: x => x.employee_id, + principalSchema: "application", + principalTable: "employees", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_vehicle_enrollment_employees_vehicle_enrollment_id", + column: x => x.vehicle_enrollment_id, + principalSchema: "application", + principalTable: "vehicle_enrollments", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_vehicle_enrollment_employees_employee_id", + schema: "application", + table: "vehicle_enrollment_employees", + column: "employee_id"); + + migrationBuilder.CreateIndex( + name: "ix_vehicle_enrollment_employees_vehicle_enrollment_id", + schema: "application", + table: "vehicle_enrollment_employees", + column: "vehicle_enrollment_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "vehicle_enrollment_employees", + schema: "application"); + + migrationBuilder.DropSequence( + name: "vehicle_enrollment_employees_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.Designer.cs new file mode 100644 index 0000000..44068d3 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.Designer.cs @@ -0,0 +1,1294 @@ +// +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("20250528083243_Add_Account_Role_and_AccountRole")] + partial class Add_Account_Role_and_AccountRole + { + /// + 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("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + + 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.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "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.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + + 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.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + 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.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + 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.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + + 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.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + + 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.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + 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.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("RefreshTokens"); + }); + + 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("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + + 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.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.cs b/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.cs new file mode 100644 index 0000000..0cdd345 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528083243_Add_Account_Role_and_AccountRole.cs @@ -0,0 +1,175 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Account_Role_and_AccountRole : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "account_roles_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "accounts_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "refresh_tokens_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "roles_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "accounts", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.accounts_id_sequence')"), + username = table.Column(type: "varchar(32)", nullable: false), + email = table.Column(type: "varchar(256)", nullable: false), + password_hash = table.Column(type: "varchar(88)", nullable: false), + password_salt = table.Column(type: "varchar(24)", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_accounts", x => x.id); + table.UniqueConstraint("altk_accounts_uuid", x => x.uuid); + }); + + migrationBuilder.CreateTable( + name: "roles", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.roles_id_sequence')"), + name = table.Column(type: "varchar(64)", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_roles", x => x.id); + table.UniqueConstraint("altk_roles_uuid", x => x.uuid); + table.CheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + + migrationBuilder.CreateTable( + name: "refresh_tokens", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.refresh_tokens_id_sequence')"), + value = table.Column(type: "varchar(24)", nullable: false), + creation_time = table.Column(type: "timestamptz", nullable: false), + expiration_time = table.Column(type: "timestamptz", nullable: false), + revocation_time = table.Column(type: "timestamptz", nullable: true), + account_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_refresh_tokens", x => x.id); + table.UniqueConstraint("altk_refresh_tokens_uuid", x => x.uuid); + table.ForeignKey( + name: "fk_refresh_tokens_account_id", + column: x => x.account_id, + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "account_roles", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.account_roles_id_sequence')"), + account_id = table.Column(type: "bigint", nullable: false), + role_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_account_roles", x => x.id); + table.UniqueConstraint("altk_account_roles_uuid", x => x.uuid); + table.ForeignKey( + name: "fk_account_roles_account_id", + column: x => x.account_id, + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_account_roles_role_id", + column: x => x.role_id, + principalSchema: "application", + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_account_roles_account_id", + schema: "application", + table: "account_roles", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_account_roles_role_id", + schema: "application", + table: "account_roles", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "ix_refresh_tokens_account_id", + schema: "application", + table: "refresh_tokens", + column: "account_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "account_roles", + schema: "application"); + + migrationBuilder.DropTable( + name: "refresh_tokens", + schema: "application"); + + migrationBuilder.DropTable( + name: "roles", + schema: "application"); + + migrationBuilder.DropTable( + name: "accounts", + schema: "application"); + + migrationBuilder.DropSequence( + name: "account_roles_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "accounts_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "refresh_tokens_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "roles_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.Designer.cs new file mode 100644 index 0000000..bd4a395 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.Designer.cs @@ -0,0 +1,1313 @@ +// +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("20250528141733_Add_navigation_from_Employee_to_Account")] + partial class Add_navigation_from_Employee_to_Account + { + /// + 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("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + + 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.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_employees_account_id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "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.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + + 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.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + 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.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + 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.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + + 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.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Employee") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Account"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + + 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.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + 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.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + 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("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + + 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.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.cs b/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.cs new file mode 100644 index 0000000..56f22dd --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_navigation_from_Employee_to_Account : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "account_id", + schema: "application", + table: "employees", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.CreateIndex( + name: "ix_employees_account_id", + schema: "application", + table: "employees", + column: "account_id", + unique: true); + + migrationBuilder.AddForeignKey( + name: "fk_employees_account_id", + schema: "application", + table: "employees", + column: "account_id", + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_employees_account_id", + schema: "application", + table: "employees"); + + migrationBuilder.DropIndex( + name: "ix_employees_account_id", + schema: "application", + table: "employees"); + + migrationBuilder.DropColumn( + name: "account_id", + schema: "application", + table: "employees"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.Designer.cs new file mode 100644 index 0000000..8486e6b --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.Designer.cs @@ -0,0 +1,1335 @@ +// +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("20250528182232_Add_navigation_from_Company_to_Account")] + partial class Add_navigation_from_Company_to_Account + { + /// + 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("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + + 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("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + 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.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_companies_account_id"); + + 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.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_employees_account_id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "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.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + + 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.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + 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.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + 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.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + + 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.Company", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Company") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Company", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_companies_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Employee") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Account"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + + 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.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + 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.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("Company"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + 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("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + + 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.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.cs b/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.cs new file mode 100644 index 0000000..80c669c --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250528182232_Add_navigation_from_Company_to_Account.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_navigation_from_Company_to_Account : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "account_id", + schema: "application", + table: "companies", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.CreateIndex( + name: "ix_companies_account_id", + schema: "application", + table: "companies", + column: "account_id", + unique: true); + + migrationBuilder.AddForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies", + column: "account_id", + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies"); + + migrationBuilder.DropIndex( + name: "ix_companies_account_id", + schema: "application", + table: "companies"); + + migrationBuilder.DropColumn( + name: "account_id", + schema: "application", + table: "companies"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.Designer.cs new file mode 100644 index 0000000..9156d66 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.Designer.cs @@ -0,0 +1,1339 @@ +// +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("20250529131846_Add_email_to_Ticket_Group")] + partial class Add_email_to_Ticket_Group + { + /// + 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("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + + 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("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + 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.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_companies_account_id"); + + 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.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_employees_account_id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "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.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + + 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.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerEmail") + .HasColumnType("varchar(256)") + .HasColumnName("passanger_email"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + 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.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + 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.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + + 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.Company", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Company") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Company", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_companies_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Employee") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Account"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + + 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.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + 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.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("Company"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + 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("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + + 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.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.cs b/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.cs new file mode 100644 index 0000000..836bffb --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_email_to_Ticket_Group : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "passanger_email", + schema: "application", + table: "ticket_groups", + type: "varchar(256)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "passanger_email", + schema: "application", + table: "ticket_groups"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.Designer.cs new file mode 100644 index 0000000..b5ee921 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.Designer.cs @@ -0,0 +1,1359 @@ +// +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("20250530101737_Add_navigation_from_Ticket_Group_to_Account")] + partial class Add_navigation_from_Ticket_Group_to_Account + { + /// + 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("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + + 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("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + 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.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_companies_account_id"); + + 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.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_employees_account_id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "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.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + + 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.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerEmail") + .HasColumnType("varchar(256)") + .HasColumnName("passanger_email"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_ticket_groups_account_id"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + 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.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + 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.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + + 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.Company", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Company") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Company", "AccountId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired() + .HasConstraintName("fk_companies_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Employee") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Account"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + + 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.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("TicketGroups") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_ticket_groups_account_id"); + + b.Navigation("Account"); + }); + + 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.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("Company"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + + b.Navigation("TicketGroups"); + }); + + 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("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + + 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.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.cs b/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.cs new file mode 100644 index 0000000..8959a4b --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250530101737_Add_navigation_from_Ticket_Group_to_Account.cs @@ -0,0 +1,114 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_navigation_from_Ticket_Group_to_Account : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies"); + + migrationBuilder.DropForeignKey( + name: "fk_employees_company_id", + schema: "application", + table: "employees"); + + migrationBuilder.AddColumn( + name: "account_id", + schema: "application", + table: "ticket_groups", + type: "bigint", + nullable: true); + + migrationBuilder.CreateIndex( + name: "ix_ticket_groups_account_id", + schema: "application", + table: "ticket_groups", + column: "account_id"); + + migrationBuilder.AddForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies", + column: "account_id", + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_employees_company_id", + schema: "application", + table: "employees", + column: "company_id", + principalSchema: "application", + principalTable: "companies", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_ticket_groups_account_id", + schema: "application", + table: "ticket_groups", + column: "account_id", + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies"); + + migrationBuilder.DropForeignKey( + name: "fk_employees_company_id", + schema: "application", + table: "employees"); + + migrationBuilder.DropForeignKey( + name: "fk_ticket_groups_account_id", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.DropIndex( + name: "ix_ticket_groups_account_id", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.DropColumn( + name: "account_id", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.AddForeignKey( + name: "fk_companies_account_id", + schema: "application", + table: "companies", + column: "account_id", + principalSchema: "application", + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_employees_company_id", + schema: "application", + table: "employees", + column: "company_id", + principalSchema: "application", + principalTable: "companies", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 9e89c0f..9587b06 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -21,21 +21,126 @@ namespace Persistence.PostgreSql.Migrations .HasAnnotation("ProductVersion", "9.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "vehicle_type", new[] { "bus", "train", "aircraft" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.HasSequence("account_roles_id_sequence"); + + modelBuilder.HasSequence("accounts_id_sequence"); + modelBuilder.HasSequence("addresses_id_sequence"); modelBuilder.HasSequence("cities_id_sequence"); + modelBuilder.HasSequence("companies_id_sequence"); + modelBuilder.HasSequence("countries_id_sequence"); + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("refresh_tokens_id_sequence"); + modelBuilder.HasSequence("regions_id_sequence"); + modelBuilder.HasSequence("roles_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + modelBuilder.HasSequence("route_addresses_id_sequence"); modelBuilder.HasSequence("routes_id_sequence"); + modelBuilder.HasSequence("ticket_groups_id_sequence"); + + modelBuilder.HasSequence("tickets_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollment_employees_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.accounts_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "accounts_id_sequence"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("email"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("varchar(88)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("password_salt"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasAlternateKey("Guid") + .HasName("altk_accounts_uuid"); + + b.ToTable("accounts", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.AccountRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.account_roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "account_roles_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_account_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_account_roles_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_roles_account_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("account_roles", "application"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Property("Id") @@ -54,6 +159,12 @@ namespace Persistence.PostgreSql.Migrations .HasColumnType("uuid") .HasColumnName("uuid"); + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + b.Property("Name") .IsRequired() .HasColumnType("varchar(128)") @@ -61,28 +172,22 @@ namespace Persistence.PostgreSql.Migrations b.Property("VehicleType") .IsRequired() - .HasColumnType("application.vehicle_type") + .HasColumnType("varchar(16)") .HasColumnName("vehicle_type"); b.HasKey("Id") .HasName("pk_addresses"); b.HasAlternateKey("Guid") - .HasName("altk_addresses_Guid"); + .HasName("altk_addresses_uuid"); b.HasIndex("CityId") - .IsUnique() .HasDatabaseName("ix_addresses_city_id"); - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_addresses_id"); - - b.ToTable("addresses", "application"); + 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 => @@ -112,23 +217,65 @@ namespace Persistence.PostgreSql.Migrations .HasName("pk_cities"); b.HasAlternateKey("Guid") - .HasName("altk_cities_Guid"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_cities_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_cities_id"); + .HasName("altk_cities_uuid"); b.HasIndex("RegionId") - .IsUnique() .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("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + 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.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_companies_account_id"); + + b.ToTable("companies", "application"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => { b.Property("Id") @@ -152,19 +299,166 @@ namespace Persistence.PostgreSql.Migrations .HasName("pk_countries"); b.HasAlternateKey("Guid") - .HasName("altk_countries_Guid"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_countries_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_countries_id"); + .HasName("altk_countries_uuid"); b.ToTable("countries", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_employees_account_id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.refresh_tokens_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "refresh_tokens_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("CreationTime") + .HasColumnType("timestamptz") + .HasColumnName("creation_time"); + + b.Property("ExpirationTime") + .HasColumnType("timestamptz") + .HasColumnName("expiration_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RevocationTime") + .HasColumnType("timestamptz") + .HasColumnName("revocation_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(24)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasAlternateKey("Guid") + .HasName("altk_refresh_tokens_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("refresh_tokens", "application"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.Property("Id") @@ -192,21 +486,45 @@ namespace Persistence.PostgreSql.Migrations .HasName("pk_regions"); b.HasAlternateKey("Guid") - .HasName("altk_regions_Guid"); + .HasName("altk_regions_uuid"); - b.HasIndex("CountryId"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_regions_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_regions_id"); + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); b.ToTable("regions", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.roles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "roles_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasAlternateKey("Guid") + .HasName("altk_roles_uuid"); + + b.ToTable("roles", "application", t => + { + t.HasCheckConstraint("ck_roles_name", "name IN ('administrator', 'user', 'company_owner', 'company_employee')"); + }); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => { b.Property("Id") @@ -228,24 +546,19 @@ namespace Persistence.PostgreSql.Migrations b.Property("VehicleType") .IsRequired() - .HasColumnType("application.vehicle_type") + .HasColumnType("varchar(16)") .HasColumnName("vehicle_type"); b.HasKey("Id") .HasName("pk_routes"); b.HasAlternateKey("Guid") - .HasName("altk_routes_Guid"); + .HasName("altk_routes_uuid"); - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_routes_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_routes_id"); - - b.ToTable("routes", "application"); + 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 => @@ -278,30 +591,446 @@ namespace Persistence.PostgreSql.Migrations .HasName("pk_route_addresses"); b.HasAlternateKey("Guid") - .HasName("altk_route_addresses_Guid"); + .HasName("altk_route_addresses_uuid"); b.HasAlternateKey("AddressId", "RouteId", "Order") .HasName("altk_route_addresses_address_id_route_id_order"); b.HasIndex("AddressId") - .IsUnique() .HasDatabaseName("ix_route_addresses_address_id"); - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_route_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_route_addresses_id"); - b.HasIndex("RouteId") - .IsUnique() .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.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_tickets"); + + b.HasAlternateKey("Guid") + .HasName("altk_tickets_uuid"); + + b.HasIndex("ArrivalRouteAddressId"); + + b.HasIndex("DepartureRouteAddressId"); + + b.HasIndex("TicketGroupId") + .HasDatabaseName("ix_tickets_ticket_group_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_tickets_vehicle_enrollment_id"); + + b.ToTable("tickets", "application", t => + { + t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("AccountId") + .HasColumnType("bigint") + .HasColumnName("account_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerEmail") + .HasColumnType("varchar(256)") + .HasColumnName("passanger_email"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + b.Property("TravelTime") + .HasColumnType("interval") + .HasColumnName("travel_time"); + + b.HasKey("Id") + .HasName("pk_ticket_groups"); + + b.HasAlternateKey("Guid") + .HasName("altk_ticket_groups_uuid"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_ticket_groups_account_id"); + + b.ToTable("ticket_groups", "application", t => + { + t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + 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.VehicleEnrollmentEmployee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollment_employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollment_employees_id_sequence"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollment_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollment_employees_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_vehicle_enrollment_employees_employee_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.ToTable("vehicle_enrollment_employees", "application"); + }); + + 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.AccountRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("AccountRoles") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Role", "Role") + .WithMany("AccountRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_role_id"); + + b.Navigation("Account"); + + b.Navigation("Role"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") @@ -326,6 +1055,63 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Region"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Company") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Company", "AccountId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired() + .HasConstraintName("fk_companies_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithOne("Employee") + .HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_account_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Account"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RefreshToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_account_id"); + + b.Navigation("Account"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") @@ -359,6 +1145,142 @@ namespace Persistence.PostgreSql.Migrations 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.Ticket", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress") + .WithMany() + .HasForeignKey("ArrivalRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress") + .WithMany() + .HasForeignKey("DepartureRouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_ticket_group_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tickets_vehicle_enrollment_id"); + + b.Navigation("ArrivalRouteAddress"); + + b.Navigation("DepartureRouteAddress"); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account") + .WithMany("TicketGroups") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_ticket_groups_account_id"); + + b.Navigation("Account"); + }); + + 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.VehicleEnrollmentEmployee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_employee_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("VehicleEnrollmentEmployees") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollment_employees_vehicle_enrollment_id"); + + b.Navigation("Employee"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Account", b => + { + b.Navigation("AccountRoles"); + + b.Navigation("Company"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + + b.Navigation("TicketGroups"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Navigation("AddressRoutes"); @@ -369,19 +1291,64 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Addresses"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => { b.Navigation("Regions"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + + b.Navigation("VehicleEnrollmentEmployees"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.Navigation("Cities"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Role", b => + { + b.Navigation("AccountRoles"); + }); + 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.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + + b.Navigation("VehicleEnrollmentEmployees"); }); #pragma warning restore 612, 618 } diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index fff9242..4825aac 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -1,8 +1,8 @@ using System.Reflection; using cuqmbr.TravelGuide.Domain.Enums; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.Options; +using cuqmbr.TravelGuide.Persistence.TypeConverters; namespace cuqmbr.TravelGuide.Persistence.PostgreSql; @@ -24,10 +24,6 @@ public class PostgreSqlDbContext : DbContext builder.HasDefaultSchema(DefaultSchema); - builder.HasPostgresEnum( - "vehicle_type", - VehicleType.Enumerations.Select(e => e.Value.Name).ToArray()); - builder .ApplyConfigurationsFromAssembly( Assembly.GetExecutingAssembly(), @@ -40,16 +36,37 @@ public class PostgreSqlDbContext : DbContext { builder .Properties() - .HaveColumnType("vehicle_type") + .HaveColumnType("varchar(16)") .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(8)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(64)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + + builder + .Properties() + .HaveConversion(); + + + builder + .Properties() + .HaveColumnType("varchar(64)") + .HaveConversion(); } } - -public class VehicleTypeConverter : ValueConverter -{ - public VehicleTypeConverter() - : base( - v => v.Name, - v => VehicleType.FromName(v)) - { } -} diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 320b7f2..5b42993 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -1,5 +1,5 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; namespace cuqmbr.TravelGuide.Persistence.PostgreSql; @@ -15,11 +15,75 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork CountryRepository = new PostgreSqlCountryRepository(_dbContext); RegionRepository = new PostgreSqlRegionRepository(_dbContext); + CityRepository = new PostgreSqlCityRepository(_dbContext); + AddressRepository = new PostgreSqlAddressRepository(_dbContext); + RouteRepository = new PostgreSqlRouteRepository(_dbContext); + VehicleRepository = new PostgreSqlVehicleRepository(_dbContext); + BusRepository = new PostgreSqlBusRepository(_dbContext); + AircraftRepository = new PostgreSqlAircraftRepository(_dbContext); + TrainRepository = new PostgreSqlTrainRepository(_dbContext); + VehicleEnrollmentRepository = + new PostgreSqlVehicleEnrollmentRepository(_dbContext); + RouteAddressRepository = + new PostgreSqlRouteAddressRepository(_dbContext); + CompanyRepository = new PostgreSqlCompanyRepository(_dbContext); + EmployeeRepository = new PostgreSqlEmployeeRepository(_dbContext); + TicketGroupRepository = new PostgreSqlTicketGroupRepository(_dbContext); + TicketRepository = new PostgreSqlTicketRepository(_dbContext); + RouteAddressDetailRepository = + new PostgreSqlRouteAddressDetailRepository(_dbContext); + VehicleEnrollmentEmployeeRepository = + new PostgreSqlVehicleEnrollmentEmployeeRepository(_dbContext); + + AccountRepository = new PostgreSqlAccountRepository(_dbContext); + RoleRepository = new PostgreSqlRoleRepository(_dbContext); + AccountRoleRepository = new PostgreSqlAccountRoleRepository(_dbContext); + RefreshTokenRepository = new PostgreSqlRefreshTokenRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } + public RegionRepository RegionRepository { get; init; } + public CityRepository CityRepository { get; init; } + + public AddressRepository AddressRepository { get; init; } + + public RouteRepository RouteRepository { get; init; } + + public VehicleRepository VehicleRepository { get; init; } + + public BusRepository BusRepository { get; init; } + + public AircraftRepository AircraftRepository { get; init; } + + public TrainRepository TrainRepository { get; init; } + + public VehicleEnrollmentRepository VehicleEnrollmentRepository { get; init; } + + public RouteAddressRepository RouteAddressRepository { get; init; } + + public CompanyRepository CompanyRepository { get; init; } + + public EmployeeRepository EmployeeRepository { get; init; } + + public TicketGroupRepository TicketGroupRepository { get; init; } + + public TicketRepository TicketRepository { get; init; } + + public RouteAddressDetailRepository RouteAddressDetailRepository { get; init; } + + public VehicleEnrollmentEmployeeRepository VehicleEnrollmentEmployeeRepository { get; init; } + + + public AccountRepository AccountRepository { get; init; } + + public RoleRepository RoleRepository { get; init; } + + public AccountRoleRepository AccountRoleRepository { get; init; } + + public RefreshTokenRepository RefreshTokenRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); @@ -27,7 +91,7 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public async Task SaveAsync(CancellationToken cancellationToken) { - return await _dbContext.SaveChangesAsync(); + return await _dbContext.SaveChangesAsync(cancellationToken); } public void Dispose() diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRepository.cs new file mode 100644 index 0000000..e622829 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlAccountRepository : + PostgreSqlBaseRepository, AccountRepository +{ + public PostgreSqlAccountRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRoleRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRoleRepository.cs new file mode 100644 index 0000000..6a683dc --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAccountRoleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlAccountRoleRepository : + PostgreSqlBaseRepository, AccountRoleRepository +{ + public PostgreSqlAccountRoleRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs new file mode 100644 index 0000000..f50216b --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAddressRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlAddressRepository : + PostgreSqlBaseRepository
, AddressRepository +{ + public PostgreSqlAddressRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs new file mode 100644 index 0000000..d6cb3d8 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlAircraftRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlAircraftRepository : + PostgreSqlBaseRepository, AircraftRepository +{ + public PostgreSqlAircraftRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs index a5c1dae..468fb49 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; @@ -28,7 +28,7 @@ public abstract class PostgreSqlBaseRepository : BaseRepository> predicate, CancellationToken cancellationToken) { - return await _dbSet.SingleOrDefaultAsync(predicate); + return await _dbSet.SingleOrDefaultAsync(predicate, cancellationToken); } public async Task GetOneAsync( @@ -36,22 +36,21 @@ public abstract class PostgreSqlBaseRepository : BaseRepository> includeSelector, CancellationToken cancellationToken) { - return - await _dbSet - .Include(includeSelector) - .SingleOrDefaultAsync(predicate); + return await _dbSet + .Include(includeSelector) + .SingleOrDefaultAsync(predicate, cancellationToken); } public async Task> GetPageAsync( int pageNumber, int pageSize, CancellationToken cancellationToken) { - var count = await _dbSet.CountAsync(); + var count = await _dbSet.CountAsync(cancellationToken); var entities = await _dbSet .Skip((pageNumber - 1) * pageSize).Take(pageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PaginatedList( entities, count, @@ -63,13 +62,13 @@ public abstract class PostgreSqlBaseRepository : BaseRepository( entities, count, @@ -81,13 +80,15 @@ public abstract class PostgreSqlBaseRepository : BaseRepository( entities, count, @@ -100,14 +101,16 @@ public abstract class PostgreSqlBaseRepository : BaseRepository( entities, count, @@ -129,5 +132,4 @@ public abstract class PostgreSqlBaseRepository : BaseRepository, BusRepository +{ + public PostgreSqlBusRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs new file mode 100644 index 0000000..0fdb212 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlCityRepository : + PostgreSqlBaseRepository, CityRepository +{ + public PostgreSqlCityRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs new file mode 100644 index 0000000..c0862bc --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCompanyRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.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) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs index 78d315b..b45a481 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs new file mode 100644 index 0000000..36d1176 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlEmployeeRepository : + PostgreSqlBaseRepository, EmployeeRepository +{ + public PostgreSqlEmployeeRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRefreshTokenRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRefreshTokenRepository.cs new file mode 100644 index 0000000..6b436ed --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRefreshTokenRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRefreshTokenRepository : + PostgreSqlBaseRepository, RefreshTokenRepository +{ + public PostgreSqlRefreshTokenRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs index 036018e..8be382d 100644 --- a/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs @@ -1,4 +1,4 @@ -using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; using cuqmbr.TravelGuide.Domain.Entities; namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRoleRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRoleRepository.cs new file mode 100644 index 0000000..e549ba2 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRoleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRoleRepository : + PostgreSqlBaseRepository, RoleRepository +{ + public PostgreSqlRoleRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs new file mode 100644 index 0000000..11b9c48 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRouteAddressDetailRepository : + PostgreSqlBaseRepository, RouteAddressDetailRepository +{ + public PostgreSqlRouteAddressDetailRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs new file mode 100644 index 0000000..9f8d7e7 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRouteAddressRepository : + PostgreSqlBaseRepository, RouteAddressRepository +{ + public PostgreSqlRouteAddressRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs new file mode 100644 index 0000000..c30b052 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRouteRepository : + PostgreSqlBaseRepository, RouteRepository +{ + public PostgreSqlRouteRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs new file mode 100644 index 0000000..42af631 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlTicketGroupRepository : + PostgreSqlBaseRepository, TicketGroupRepository +{ + public PostgreSqlTicketGroupRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs new file mode 100644 index 0000000..96a9bf6 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlTicketRepository : + PostgreSqlBaseRepository, TicketRepository +{ + public PostgreSqlTicketRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs new file mode 100644 index 0000000..0bab7ce --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTrainRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlTrainRepository : + PostgreSqlBaseRepository, TrainRepository +{ + public PostgreSqlTrainRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs new file mode 100644 index 0000000..1a11fba --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentEmployeeRepository.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlVehicleEnrollmentEmployeeRepository : + PostgreSqlBaseRepository, + VehicleEnrollmentEmployeeRepository +{ + public PostgreSqlVehicleEnrollmentEmployeeRepository( + PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs new file mode 100644 index 0000000..6ae4d01 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleEnrollmentRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlVehicleEnrollmentRepository : + PostgreSqlBaseRepository, VehicleEnrollmentRepository +{ + public PostgreSqlVehicleEnrollmentRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs new file mode 100644 index 0000000..536af9c --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlVehicleRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlVehicleRepository : + PostgreSqlBaseRepository, VehicleRepository +{ + public PostgreSqlVehicleRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/TypeConverters/CurrencyConverter.cs b/src/Persistence/TypeConverters/CurrencyConverter.cs new file mode 100644 index 0000000..9c2bb7f --- /dev/null +++ b/src/Persistence/TypeConverters/CurrencyConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class CurrencyConverter : ValueConverter +{ + public CurrencyConverter() + : base( + v => v.Name, + v => Currency.FromName(v)) + { } +} diff --git a/src/Persistence/TypeConverters/DateTimeOffsetConverter.cs b/src/Persistence/TypeConverters/DateTimeOffsetConverter.cs new file mode 100644 index 0000000..841c33c --- /dev/null +++ b/src/Persistence/TypeConverters/DateTimeOffsetConverter.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +// Convert localized time to UTC + +public class DateTimeOffsetConverter : + ValueConverter +{ + public DateTimeOffsetConverter() + : base( + v => v.ToUniversalTime(), + v => v) + { } +} diff --git a/src/Persistence/TypeConverters/DocumentTypeConverter.cs b/src/Persistence/TypeConverters/DocumentTypeConverter.cs new file mode 100644 index 0000000..83e2632 --- /dev/null +++ b/src/Persistence/TypeConverters/DocumentTypeConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class DocumentTypeConverter : ValueConverter +{ + public DocumentTypeConverter() + : base( + v => v.Name, + v => DocumentType.FromName(v)) + { } +} diff --git a/src/Persistence/TypeConverters/RoleConverter.cs b/src/Persistence/TypeConverters/RoleConverter.cs new file mode 100644 index 0000000..d809f43 --- /dev/null +++ b/src/Persistence/TypeConverters/RoleConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class RoleConverter : ValueConverter +{ + public RoleConverter() + : base( + v => v.Name, + v => IdentityRole.FromName(v)) + { } +} diff --git a/src/Persistence/TypeConverters/SexConverter.cs b/src/Persistence/TypeConverters/SexConverter.cs new file mode 100644 index 0000000..f3d2fd5 --- /dev/null +++ b/src/Persistence/TypeConverters/SexConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class SexConverter : ValueConverter +{ + public SexConverter() + : base( + v => v.Name, + v => Sex.FromName(v)) + { } +} diff --git a/src/Persistence/TypeConverters/TicketStatusConverter.cs b/src/Persistence/TypeConverters/TicketStatusConverter.cs new file mode 100644 index 0000000..5220fc8 --- /dev/null +++ b/src/Persistence/TypeConverters/TicketStatusConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class TicketStatusConverter : ValueConverter +{ + public TicketStatusConverter() + : base( + v => v.Name, + v => TicketStatus.FromName(v)) + { } +} diff --git a/src/Persistence/TypeConverters/VehicleTypeConverter.cs b/src/Persistence/TypeConverters/VehicleTypeConverter.cs new file mode 100644 index 0000000..2274d4d --- /dev/null +++ b/src/Persistence/TypeConverters/VehicleTypeConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class VehicleTypeConverter : ValueConverter +{ + public VehicleTypeConverter() + : base( + v => v.Name, + v => VehicleType.FromName(v)) + { } +} diff --git a/src/Persistence/packages.lock.json b/src/Persistence/packages.lock.json index f9da6c3..bce5250 100644 --- a/src/Persistence/packages.lock.json +++ b/src/Persistence/packages.lock.json @@ -51,12 +51,6 @@ "Microsoft.Extensions.Primitives": "9.0.4" } }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" - }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "Direct", "requested": "[9.0.4, )", @@ -114,6 +108,14 @@ "resolved": "2.0.1", "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, "Microsoft.AspNetCore.Authorization": { "type": "Transitive", "resolved": "9.0.0", @@ -272,6 +274,58 @@ "resolved": "9.0.4", "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.11.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.11.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "Npgsql": { "type": "Transitive", "resolved": "9.0.3", @@ -280,6 +334,11 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.10", @@ -310,6 +369,15 @@ "SQLitePCLRaw.core": "2.1.10" } }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, "System.Linq.Dynamic.Core": { "type": "Transitive", "resolved": "1.6.2", @@ -334,20 +402,18 @@ "FluentValidation": "[11.11.0, )", "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.5, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.11.0, )", + "Microsoft.IdentityModel.Tokens": "[8.11.0, )", + "Newtonsoft.Json": "[13.0.3, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, "domain": { "type": "Project" } - }, - "net9.0/linux-x64": { - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.10", - "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" - } } } } \ No newline at end of file diff --git a/tst/Application.IntegrationTests/BaseTest.cs b/tst/Application.IntegrationTests/BaseTest.cs index c919add..26ebcb3 100644 --- a/tst/Application.IntegrationTests/BaseTest.cs +++ b/tst/Application.IntegrationTests/BaseTest.cs @@ -3,12 +3,11 @@ using cuqmbr.TravelGuide.Configuration.Configuration; using cuqmbr.TravelGuide.Configuration.Logging; using cuqmbr.TravelGuide.Configuration.Application; using cuqmbr.TravelGuide.Configuration.Persistence; -using cuqmbr.TravelGuide.Configuration.Identity; using Moq; using System.Globalization; -using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Services; +using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application.IntegrationTests; @@ -61,8 +60,8 @@ public abstract class TestBase : IDisposable var guid = Guid.NewGuid(); mock.Setup(s => s.Email).Returns(guid.ToString()); - mock.Setup(s => s.Id).Returns(guid.GetHashCode()); - mock.Setup(s => s.Uuid).Returns(Guid.NewGuid()); + mock.Setup(s => s.Guid).Returns(guid); + mock.Setup(s => s.Guid).Returns(Guid.NewGuid()); mock.Setup(s => s.IsAuthenticated).Returns(true); mock.Setup(s => s.Roles).Returns(roles); @@ -88,9 +87,9 @@ public abstract class TestBase : IDisposable var cultureInfo = CultureInfo.GetCultureInfo(culture); _serviceCollection - .AddScoped(_ => + .AddScoped(_ => { - var mock = new Mock(); + var mock = new Mock(); mock .Setup(s => s.Culture) @@ -106,9 +105,9 @@ public abstract class TestBase : IDisposable public void SetTimeZone(string timeZone) { _serviceCollection - .AddScoped(_ => + .AddScoped(_ => { - var mock = new Mock(); + var mock = new Mock(); mock .Setup(s => s.TimeZone) diff --git a/tst/Application.IntegrationTests/CitiesTests.cs b/tst/Application.IntegrationTests/CitiesTests.cs new file mode 100644 index 0000000..438e308 --- /dev/null +++ b/tst/Application.IntegrationTests/CitiesTests.cs @@ -0,0 +1,1554 @@ +using MediatR; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; +using cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; +using cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +namespace cuqmbr.TravelGuide.Application.IntegrationTests; + +public class CitiesTests : TestBase +{ + [Fact] + public async Task AddCity_WithAdminRole_CityAdded() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Region Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionGuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + var getCityResult = await mediator.Send( + new GetCityQuery() + { + Guid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult); + + Assert.NotNull(getCityResult.Name); + Assert.Equal(cityName, getCityResult.Name); + + Assert.NotNull(getCityResult.CountryName); + Assert.Equal(countryName, getCityResult.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult.CountryUuid); + + Assert.NotNull(getCityResult.RegionName); + Assert.Equal(regionName, getCityResult.RegionName); + Assert.Equal(addRegionResult.Uuid, getCityResult.RegionUuid); + } + + [Fact] + public async Task + AddDuplicateCity_WithAdminRole_ThrowsDuplicateEntityException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Region Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionGuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = cityName, + RegionGuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + AddSameCitiesToDifferentRegions_WithAdminRole_CitiesAdded() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionGuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionGuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCityResult1 = await mediator.Send( + new GetCityQuery() + { + Guid = addCityResult1.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult1); + + Assert.NotNull(getCityResult1.Name); + Assert.Equal(cityName, getCityResult1.Name); + + Assert.NotNull(getCityResult1.CountryName); + Assert.Equal(countryName, getCityResult1.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult1.CountryUuid); + + Assert.NotNull(getCityResult1.RegionName); + Assert.Equal(regionName1, getCityResult1.RegionName); + Assert.Equal(addRegionResult1.Uuid, getCityResult1.RegionUuid); + + var getCityResult2 = await mediator.Send( + new GetCityQuery() + { + Guid = addCityResult2.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult2); + + Assert.NotNull(getCityResult2.Name); + Assert.Equal(cityName, getCityResult2.Name); + + Assert.NotNull(getCityResult2.CountryName); + Assert.Equal(countryName, getCityResult2.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult2.CountryUuid); + + Assert.NotNull(getCityResult2.RegionName); + Assert.Equal(regionName2, getCityResult2.RegionName); + Assert.Equal(addRegionResult2.Uuid, getCityResult2.RegionUuid); + } + + [Fact] + public async Task + AddCity_WithNonExistentRegionUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = "Name", + RegionGuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + AddCity_WithInvalidName_WithAdminRole_ThrowsValidationException + (string name) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = name + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + AddCity_WithInvalidRegionUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = "Name", + RegionGuid = + Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task AddCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new AddCityCommand(), + TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task AddCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new AddCityCommand(), + TestContext.Current.CancellationToken)); + } + + + [Fact] + public async Task UpdateCity_WithAdminRole_CityUpdated() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Region Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionGuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + string newName = "Different Name"; + + var updateCityResult = await mediator.Send( + new UpdateCityCommand() + { + Guid = addCityResult.Uuid, + Name = newName, + RegionGuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(updateCityResult); + + Assert.NotNull(updateCityResult.Name); + Assert.Equal(newName, updateCityResult.Name); + Assert.Equal(addCityResult.Uuid, updateCityResult.Uuid); + + Assert.NotNull(updateCityResult.CountryName); + Assert.Equal(countryName, updateCityResult.CountryName); + Assert.Equal(addCountryResult.Uuid, updateCityResult.CountryUuid); + + Assert.NotNull(updateCityResult.RegionName); + Assert.Equal(regionName, updateCityResult.RegionName); + Assert.Equal(addRegionResult.Uuid, updateCityResult.RegionUuid); + } + + [Theory] + [InlineData("")] + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + UpdateCity_WithInvalidName_WithAdminRole_ThrowsValidationException + (string name) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Name = name, + RegionGuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + UpdateCity_WithInvalidRegionUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Guid = Guid.NewGuid(), + Name = "Name", + RegionGuid = + Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + UpdateCity_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, + Name = "Name", + RegionGuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + UpdateCity_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Guid = Guid.NewGuid(), + Name = "Different Name", + RegionGuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + UpdateCity_WithNonExistentRegionUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = "Name", + RegionGuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Guid = addCityResult.Uuid, + Name = "Different Name", + RegionGuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task UpdateCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new UpdateCityCommand(), + TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task UpdateCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new UpdateCityCommand(), + TestContext.Current.CancellationToken)); + } + + + [Fact] + public async Task DeleteCity_WithAdminRole_CityDeleted() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = "Name", + RegionGuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + await mediator.Send( + new DeleteCityCommand() + { + Guid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCityQuery() + { + Guid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + DeleteCity_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteCityCommand() + { + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + DeleteCity_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteCityCommand() + { + Guid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task DeleteCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new DeleteCityCommand(), + TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task DeleteCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new DeleteCityCommand(), + TestContext.Current.CancellationToken)); + } + + + [Fact] + public async Task GetCity_WithAdminRole_CityReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionGuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + var getCityResult = await mediator.Send( + new GetCityQuery() + { + Guid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult); + + Assert.NotNull(getCityResult.Name); + Assert.Equal(cityName, getCityResult.Name); + + Assert.NotNull(getCityResult.CountryName); + Assert.Equal(countryName, getCityResult.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult.CountryUuid); + + Assert.NotNull(getCityResult.RegionName); + Assert.Equal(regionName, getCityResult.RegionName); + Assert.Equal(addRegionResult.Uuid, getCityResult.RegionUuid); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + GetCity_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery() + { + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + GetCity_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery() + { + Guid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery(), + TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery(), + TestContext.Current.CancellationToken)); + } + + + [Fact] + public async Task GetCitiesPage_WithAdminRole_CitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name 1"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "RegionName 1"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionGuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionGuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(2, getCitiesResult.TotalPages); + Assert.True(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + + + getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 2, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(2, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(2, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.True(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + + + getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Equal(2, getCitiesResult.Items.Count()); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + + + Assert.NotNull(getCitiesResult.Items.Last()); + + Assert.NotNull(getCitiesResult.Items.Last().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.Last().Name); + + Assert.NotNull(getCitiesResult.Items.Last().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.Last().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.Last().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.Last().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.Last().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.Last().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSearchByCountryName_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "RegionName 1"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionGuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionGuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "cOuNtRy nAme 1" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName1, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSearchByRegionName_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionGuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionGuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "reGioN nAme 2" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSearchByCityName_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionGuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionGuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "cItY nAme 2" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSort_WithAdminRole_SortedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionGuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionGuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Sort = "-countryName" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Equal(2, getCitiesResult.Items.Count()); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + + + Assert.NotNull(getCitiesResult.Items.Last()); + + Assert.NotNull(getCitiesResult.Items.Last().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.Last().Name); + + Assert.NotNull(getCitiesResult.Items.Last().CountryName); + Assert.Equal( + countryName1, getCitiesResult.Items.Last().CountryName); + Assert.Equal( + addCountryResult1.Uuid, getCitiesResult.Items.Last().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.Last().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.Last().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.Last().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithFilterByCountryUuid_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionGuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionGuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName1, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithFilterByRegionUuid_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionGuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionGuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + RegionGuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Theory] + // Length > 64 (65) + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + GetCitiesPage_WithInvalidSearch_WithAdminRole_ThrowsValidationException + (string search) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery() + { + Search = search + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + public async Task + GetCitiesPage_WithInvalidPageNumber_WithAdminRole_ThrowsValidationException + (int pageNumber) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = pageNumber + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + [InlineData(51)] + [InlineData(int.MaxValue)] + public async Task + GetCitiesPage_WithInvalidPageSize_WithAdminRole_ThrowsValidationException + (int pageSize) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery() + { + PageSize = pageSize + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetCitiesPage_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery(), + TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetCitiesPage_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery(), + TestContext.Current.CancellationToken)); + } +} diff --git a/tst/Application.IntegrationTests/CountriesTests.cs b/tst/Application.IntegrationTests/CountriesTests.cs index 20e6233..72d6af3 100644 --- a/tst/Application.IntegrationTests/CountriesTests.cs +++ b/tst/Application.IntegrationTests/CountriesTests.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Application.Common.Exceptions; using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; using cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry; @@ -7,7 +7,6 @@ using cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; using cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry; using cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; - namespace cuqmbr.TravelGuide.Application.IntegrationTests; public class CountriesTests : TestBase @@ -248,8 +247,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task UpdateCountry_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -265,8 +262,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task DeleteCountry_WithAdminRole_CountryDeleted() @@ -341,8 +336,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task DeleteCountry_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -357,8 +350,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task GetCountry_WithAdminRole_CountryReturned() @@ -434,8 +425,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task GetCountry_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -450,8 +439,6 @@ public class CountriesTests : TestBase }, TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task GetCountriesPage_WithAdminRole_CountriesPageReturned() diff --git a/tst/Application.IntegrationTests/RegionsTests.cs b/tst/Application.IntegrationTests/RegionsTests.cs index 247d706..372300e 100644 --- a/tst/Application.IntegrationTests/RegionsTests.cs +++ b/tst/Application.IntegrationTests/RegionsTests.cs @@ -1,5 +1,5 @@ using MediatR; -using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Application.Common.Exceptions; using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; using cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; @@ -8,13 +8,12 @@ using cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; - namespace cuqmbr.TravelGuide.Application.IntegrationTests; public class RegionsTests : TestBase { [Fact] - public async Task AddRegion_WithAdminRole_RegionCreated() + public async Task AddRegion_WithAdminRole_RegionAdded() { SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); @@ -22,31 +21,33 @@ public class RegionsTests : TestBase string countryName = "Country Name"; - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = countryName }, TestContext.Current.CancellationToken); - string regionName = "Regin Name"; + string regionName = "Region Name"; - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var getRegionResult = await mediator.Send( new GetRegionQuery() { - Uuid = createRegionResult.Uuid, + Guid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult); Assert.NotNull(getRegionResult.Name); Assert.Equal(regionName, getRegionResult.Name); - Assert.Equal(createCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.Equal(addCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.NotNull(getRegionResult.CountryName); + Assert.Equal(addCountryResult.Name, getRegionResult.CountryName); } [Fact] @@ -59,32 +60,32 @@ public class RegionsTests : TestBase string countryName = "Country Name"; - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = countryName }, TestContext.Current.CancellationToken); - string regionName = "Regin Name"; + string regionName = "Region Name"; - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken)); } [Fact] public async Task - AddSameRegionsToDifferentCountries_WithAdminRole_RegionsCreated() + AddSameRegionsToDifferentCountries_WithAdminRole_RegionsAdded() { SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); @@ -92,7 +93,7 @@ public class RegionsTests : TestBase string countryName1 = "Country Name 1"; - var createCountryResult1 = await mediator.Send( + var addCountryResult1 = await mediator.Send( new AddCountryCommand() { Name = countryName1 @@ -100,49 +101,51 @@ public class RegionsTests : TestBase string countryName2 = "Country Name2 "; - var createCountryResult2 = await mediator.Send( + var addCountryResult2 = await mediator.Send( new AddCountryCommand() { Name = countryName2 }, TestContext.Current.CancellationToken); - string regionName = "Regin Name"; + string regionName = "Region Name"; - var createRegionResult1 = await mediator.Send( + var addRegionResult1 = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult1.Uuid + CountryGuid = addCountryResult1.Uuid }, TestContext.Current.CancellationToken); - var createRegionResult2 = await mediator.Send( + var addRegionResult2 = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult2.Uuid + CountryGuid = addCountryResult2.Uuid }, TestContext.Current.CancellationToken); var getRegionResult1 = await mediator.Send( new GetRegionQuery() { - Uuid = createRegionResult1.Uuid, + Guid = addRegionResult1.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult1); Assert.NotNull(getRegionResult1.Name); Assert.Equal(regionName, getRegionResult1.Name); - Assert.Equal(createCountryResult1.Uuid, getRegionResult1.CountryUuid); + Assert.Equal(addCountryResult1.Uuid, getRegionResult1.CountryUuid); var getRegionResult2 = await mediator.Send( new GetRegionQuery() { - Uuid = createRegionResult2.Uuid, + Guid = addRegionResult2.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult2); Assert.NotNull(getRegionResult2.Name); Assert.Equal(regionName, getRegionResult2.Name); - Assert.Equal(createCountryResult2.Uuid, getRegionResult2.CountryUuid); + Assert.Equal(addCountryResult2.Uuid, getRegionResult2.CountryUuid); + Assert.NotNull(getRegionResult2.CountryName); + Assert.Equal(addCountryResult2.Name, getRegionResult2.CountryName); } [Fact] @@ -157,7 +160,7 @@ public class RegionsTests : TestBase mediator.Send(new AddRegionCommand() { Name = "Name", - CountryUuid = Guid.NewGuid() + CountryGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -194,7 +197,7 @@ public class RegionsTests : TestBase mediator.Send(new AddRegionCommand() { Name = "Name", - CountryUuid = + CountryGuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -207,15 +210,11 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new AddRegionCommand() - { - Name = "Name", - CountryUuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new AddRegionCommand(), + TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task AddRegion_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -224,15 +223,11 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new AddRegionCommand() - { - Name = "Name", - CountryUuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new AddRegionCommand(), + TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task UpdateRegion_WithAdminRole_RegionUpdated() @@ -243,7 +238,7 @@ public class RegionsTests : TestBase string countryName = "Country Name"; - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = countryName @@ -251,33 +246,35 @@ public class RegionsTests : TestBase string regionName = "Region Name"; - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = regionName, - CountryUuid = createCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); string newName = "Different Name"; - var editRegionResult = await mediator.Send( + var updateRegionResult = await mediator.Send( new UpdateRegionCommand() { - Uuid = createRegionResult.Uuid, + Guid = addRegionResult.Uuid, Name = newName, - CountryUuid = createCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); var getRegionResult = await mediator.Send( new GetRegionQuery() { - Uuid = createRegionResult.Uuid, + Guid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); Assert.NotNull(getRegionResult); Assert.NotNull(getRegionResult.Name); Assert.Equal(newName, getRegionResult.Name); - Assert.Equal(createCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.Equal(addCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.NotNull(getRegionResult.CountryName); + Assert.Equal(addCountryResult.Name, getRegionResult.CountryName); } [Theory] @@ -295,7 +292,7 @@ public class RegionsTests : TestBase mediator.Send(new UpdateRegionCommand() { Name = name, - CountryUuid = Guid.NewGuid() + CountryGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -313,9 +310,9 @@ public class RegionsTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new UpdateRegionCommand() { - Uuid = Guid.NewGuid(), + Guid = Guid.NewGuid(), Name = "Name", - CountryUuid = + CountryGuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -334,9 +331,9 @@ public class RegionsTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new UpdateRegionCommand() { - Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, Name = "Name", - CountryUuid = Guid.NewGuid() + CountryGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -348,7 +345,7 @@ public class RegionsTests : TestBase var mediator = GetService(); - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = "Name" @@ -357,9 +354,9 @@ public class RegionsTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new UpdateRegionCommand() { - Uuid = Guid.NewGuid(), + Guid = Guid.NewGuid(), Name = "Different Name", - CountryUuid = createCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken)); } @@ -371,25 +368,25 @@ public class RegionsTests : TestBase var mediator = GetService(); - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = "Name" }, TestContext.Current.CancellationToken); - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = "Name", - CountryUuid = createCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new UpdateRegionCommand() { - Uuid = createCountryResult.Uuid, + Guid = addCountryResult.Uuid, Name = "Different Name", - CountryUuid = Guid.NewGuid() + CountryGuid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -401,16 +398,11 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new UpdateRegionCommand() - { - Uuid = Guid.NewGuid(), - Name = "Name", - CountryUuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new UpdateRegionCommand(), + TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task UpdateRegion_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -419,16 +411,11 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new UpdateRegionCommand() - { - Uuid = Guid.NewGuid(), - Name = "Name", - CountryUuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new UpdateRegionCommand(), + TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) [Fact] public async Task DeleteRegion_WithAdminRole_RegionDeleted() @@ -437,29 +424,29 @@ public class RegionsTests : TestBase var mediator = GetService(); - var createCountryResult = await mediator.Send( + var addCountryResult = await mediator.Send( new AddCountryCommand() { Name = "Name" }, TestContext.Current.CancellationToken); - var createRegionResult = await mediator.Send( + var addRegionResult = await mediator.Send( new AddRegionCommand() { Name = "Name", - CountryUuid = createCountryResult.Uuid + CountryGuid = addCountryResult.Uuid }, TestContext.Current.CancellationToken); await mediator.Send( new DeleteRegionCommand() { - Uuid = createRegionResult.Uuid, + Guid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => mediator.Send(new GetRegionQuery() { - Uuid = createRegionResult.Uuid, + Guid = addRegionResult.Uuid, }, TestContext.Current.CancellationToken)); } @@ -477,7 +464,7 @@ public class RegionsTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new DeleteRegionCommand() { - Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty }, TestContext.Current.CancellationToken)); } @@ -492,7 +479,7 @@ public class RegionsTests : TestBase await Assert.ThrowsAsync(() => mediator.Send(new DeleteRegionCommand() { - Uuid = Guid.NewGuid() + Guid = Guid.NewGuid() }, TestContext.Current.CancellationToken)); } @@ -504,14 +491,11 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new DeleteRegionCommand() - { - Uuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new DeleteRegionCommand(), + TestContext.Current.CancellationToken)); } - // TODO: Add more tests with user role (copy tests with admin role) - [Fact] public async Task DeleteRegion_UnAuthnticatedUser_ThrowsForbiddenException() { @@ -520,771 +504,636 @@ public class RegionsTests : TestBase var mediator = GetService(); await Assert.ThrowsAsync(() => - mediator.Send(new DeleteRegionCommand() - { - Uuid = Guid.NewGuid() - }, TestContext.Current.CancellationToken)); + mediator.Send( + new DeleteRegionCommand(), + TestContext.Current.CancellationToken)); } - // TODO: Add more tests with unauthenticated user - // (copy tests with admin role) - // TODO: Add test for GetRegion and GetRegionPage + [Fact] + public async Task GetRegion_WithAdminRole_RegionReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // [Theory] - // // Empty - // [InlineData("")] - // // Length > 64 (65) - // [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] - // public async Task - // AddRegion_WithInvalidName_WithAdminRole_ThrowsValidationException - // (string name) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new AddRegionCommand() - // { - // Name = name - // })); - // } - // - // [Fact] - // public async Task AddRegion_WithUserRole_ThrowsForbiddenException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new AddRegionCommand() - // { - // Name = "Name" - // })); - // } - // - // [Fact] - // public async Task - // AddRegion_WithUnAuthenticatedUser_ThrowsUnAuthorizedException() - // { - // SetUnAuthenticatedUser(); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new AddRegionCommand() - // { - // Name = "Name" - // })); - // } - // - // [Fact] - // public async Task UpdateRegion_WithAdminRole_RegionUpdated() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // var createRegionResult = await mediator.Send( - // new AddRegionCommand() - // { - // Name = "Name" - // }); - // - // string newName = "Different Name"; - // - // var editRegionResult = await mediator.Send( - // new UpdateRegionCommand() - // { - // Guid = createRegionResult.Uuid, - // Name = newName - // }); - // - // var getRegionResult = await mediator.Send( - // new GetRegionQuery() - // { - // Guid = createRegionResult.Uuid, - // }); - // - // Assert.NotNull(getRegionResult); - // Assert.Equal(newName, getRegionResult.Name); - // Assert.Equal(createRegionResult.Uuid, getRegionResult.Uuid); - // } - // - // [Fact] - // public async Task - // UpdateDuplicateRegion_WithAdminRole_DuplicateEntityExceptionThrown() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // var createRegionResult1 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = "Name 1" - // }); - // - // var createRegionResult2 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = "Name 2" - // }); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = createRegionResult2.Uuid, - // Name = createRegionResult1.Name - // })); - // } - // - // [Theory] - // // Empty - // [InlineData("")] - // // Length > 64 (65) - // [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] - // public async Task - // UpdateRegion_WithInvalidName_WithAdminRole_ThrowsValidationException - // (string name) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // var createRegionResult = await mediator.Send( - // new AddRegionCommand() - // { - // Name = "Name 1" - // }); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = createRegionResult.Uuid, - // Name = name - // })); - // } - // - // [Theory] - // [InlineData("")] - // [InlineData("not an uuid")] - // public async Task - // UpdateRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException - // (string uuid) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, - // Name = "Name" - // })); - // } - // - // [Fact] - // public async Task - // UpdateRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = Guid.NewGuid(), - // Name = "Name" - // })); - // } - // - // [Fact] - // public async Task UpdateRegion_WithUserRole_ThrowsForbiddenException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = Guid.NewGuid(), - // Name = "Name" - // })); - // } - // - // // TODO: Add more tests with user role (copy tests with admin role) - // - // [Fact] - // public async Task UpdateRegion_UnAuthnticatedUser_ThrowsForbiddenException() - // { - // SetUnAuthenticatedUser(); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new UpdateRegionCommand() - // { - // Guid = Guid.NewGuid(), - // Name = "Name" - // })); - // } - // - // // TODO: Add more tests with unauthenticated user - // // (copy tests with admin role) - // - // [Fact] - // public async Task DeleteRegion_WithAdminRole_RegionDeleted() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // var createRegionResult = await mediator.Send( - // new AddRegionCommand() - // { - // Name = "Name" - // }); - // - // await mediator.Send( - // new DeleteRegionCommand() - // { - // Guid = createRegionResult.Uuid, - // }); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionQuery() - // { - // Guid = createRegionResult.Uuid, - // })); - // } - // - // [Theory] - // [InlineData("")] - // [InlineData("not an uuid")] - // public async Task - // DeleteRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException - // (string uuid) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new DeleteRegionCommand() - // { - // Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty - // })); - // } - // - // [Fact] - // public async Task - // DeleteRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new DeleteRegionCommand() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // [Fact] - // public async Task DeleteRegion_WithUserRole_ThrowsForbiddenException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new DeleteRegionCommand() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // // TODO: Add more tests with user role (copy tests with admin role) - // - // [Fact] - // public async Task DeleteRegion_UnAuthnticatedUser_ThrowsForbiddenException() - // { - // SetUnAuthenticatedUser(); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new DeleteRegionCommand() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // // TODO: Add more tests with unauthenticated user - // // (copy tests with admin role) - // - // [Fact] - // public async Task GetRegion_WithAdminRole_RegionReturned() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // string name = "Name"; - // - // var createRegionResult = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name - // }); - // - // var getRegionResult = await mediator.Send( - // new GetRegionQuery() - // { - // Guid = createRegionResult.Uuid, - // }); - // - // Assert.NotNull(getRegionResult); - // Assert.NotNull(getRegionResult.Name); - // Assert.Equal(name, getRegionResult.Name); - // Assert.NotNull(getRegionResult.Uuid); - // Assert.Equal(createRegionResult.Uuid, getRegionResult.Uuid); - // } - // - // [Theory] - // [InlineData("")] - // [InlineData("not an uuid")] - // public async Task - // GetRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException - // (string uuid) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionQuery() - // { - // Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty - // })); - // } - // - // [Fact] - // public async Task - // GetRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionQuery() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // [Fact] - // public async Task GetRegion_WithUserRole_ThrowsForbiddenException() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionQuery() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // // TODO: Add more tests with user role (copy tests with admin role) - // - // [Fact] - // public async Task GetRegion_UnAuthnticatedUser_ThrowsForbiddenException() - // { - // SetUnAuthenticatedUser(); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send(new GetRegionQuery() - // { - // Guid = Guid.NewGuid() - // })); - // } - // - // // TODO: Add more tests with unauthenticated user - // // (copy tests with admin role) - // - // [Fact] - // public async Task GetRegionsPage_WithAdminRole_RegionsPageReturned() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // string name1 = "Name 1"; - // string name2 = "Name 2"; - // - // var createRegionResult1 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name1 - // }); - // - // var createRegionResult2 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name2 - // }); - // - // var getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 1 - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(2, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(true, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(1, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult1.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); - // - // getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 2, - // PageSize = 1 - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(2, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(2, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(true, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(1, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult2.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); - // - // getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 10 - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(1, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(2, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult1.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); - // Assert.NotNull(getRegionsResult.Items.Last().Name); - // Assert.Equal( - // createRegionResult2.Name, getRegionsResult.Items.Last().Name); - // Assert.NotNull(getRegionsResult.Items.Last().Uuid); - // Assert.Equal( - // createRegionResult2.Uuid, getRegionsResult.Items.Last().Uuid); - // } - // - // [Fact] - // public async Task - // GetRegionsPage_WithSearch_WithAdminRole_SearchedRegionsPageReturned() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // string name1 = "Name 1"; - // string name2 = "Some 3 String"; - // string name3 = "3 Name Some"; - // - // var createRegionResult1 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name1 - // }); - // - // var createRegionResult2 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name2 - // }); - // - // var createRegionResult3 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name3 - // }); - // - // var getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 10, - // Search = "name" - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(1, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(2, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult1.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); - // Assert.NotNull(getRegionsResult.Items.Last().Name); - // Assert.Equal( - // createRegionResult3.Name, getRegionsResult.Items.Last().Name); - // Assert.NotNull(getRegionsResult.Items.Last().Uuid); - // Assert.Equal( - // createRegionResult3.Uuid, getRegionsResult.Items.Last().Uuid); - // - // getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 10, - // Search = "3" - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(1, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(2, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult2.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); - // Assert.NotNull(getRegionsResult.Items.Last().Name); - // Assert.Equal( - // createRegionResult3.Name, getRegionsResult.Items.Last().Name); - // Assert.NotNull(getRegionsResult.Items.Last().Uuid); - // Assert.Equal( - // createRegionResult3.Uuid, getRegionsResult.Items.Last().Uuid); - // } - // - // [Fact] - // public async Task - // GetRegionsPage_WithSort_WithAdminRole_SortedRegionsPageReturned() - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // string name1 = "Name 1"; - // string name2 = "Some 2"; - // - // var createRegionResult1 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name1 - // }); - // - // var createRegionResult2 = await mediator.Send( - // new AddRegionCommand() - // { - // Name = name2 - // }); - // - // var getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 10, - // Sort = "-name" - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(1, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(2, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult2.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); - // Assert.NotNull(getRegionsResult.Items.Last().Name); - // Assert.Equal( - // createRegionResult1.Name, getRegionsResult.Items.Last().Name); - // Assert.NotNull(getRegionsResult.Items.Last().Uuid); - // Assert.Equal( - // createRegionResult1.Uuid, getRegionsResult.Items.Last().Uuid); - // - // getRegionsResult = await mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = 1, - // PageSize = 10, - // Sort = "+name" - // }); - // - // Assert.NotNull(getRegionsResult); - // Assert.NotNull(getRegionsResult.PageNumber); - // Assert.Equal(1, getRegionsResult.PageNumber); - // Assert.NotNull(getRegionsResult.TotalCount); - // Assert.Equal(2, getRegionsResult.TotalCount); - // Assert.NotNull(getRegionsResult.TotalPages); - // Assert.Equal(1, getRegionsResult.TotalPages); - // Assert.NotNull(getRegionsResult.HasNextPage); - // Assert.Equal(false, getRegionsResult.HasNextPage); - // Assert.NotNull(getRegionsResult.HasPreviousPage); - // Assert.Equal(false, getRegionsResult.HasPreviousPage); - // Assert.NotNull(getRegionsResult.Items); - // Assert.Equal(2, getRegionsResult.Items.Count()); - // Assert.NotNull(getRegionsResult.Items.First().Name); - // Assert.Equal( - // createRegionResult1.Name, getRegionsResult.Items.First().Name); - // Assert.NotNull(getRegionsResult.Items.First().Uuid); - // Assert.Equal( - // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); - // Assert.NotNull(getRegionsResult.Items.Last().Name); - // Assert.Equal( - // createRegionResult2.Name, getRegionsResult.Items.Last().Name); - // Assert.NotNull(getRegionsResult.Items.Last().Uuid); - // Assert.Equal( - // createRegionResult2.Uuid, getRegionsResult.Items.Last().Uuid); - // } - // - // [Theory] - // // Length > 64 (65) - // [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] - // public async Task - // GetRegionsPage_WithInvalidSearch_WithAdminRole_ThrowsValidationException - // (string search) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionsPageQuery() - // { - // Search = search - // })); - // } - // - // [Theory] - // [InlineData(int.MinValue)] - // [InlineData(0)] - // public async Task - // GetRegionsPage_WithInvalidPageNumber_WithAdminRole_ThrowsValidationException - // (int pageNumber) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionsPageQuery() - // { - // PageNumber = pageNumber - // })); - // } - // - // [Theory] - // [InlineData(int.MinValue)] - // [InlineData(0)] - // [InlineData(51)] - // [InlineData(int.MaxValue)] - // public async Task - // GetRegionsPage_WithInvalidPageSize_WithAdminRole_ThrowsValidationException - // (int pageSize) - // { - // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); - // - // var mediator = GetService(); - // - // await Assert.ThrowsAsync(() => - // mediator.Send( - // new GetRegionsPageQuery() - // { - // PageSize = pageSize - // })); - // } + var mediator = GetService(); + + string countryName = "Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryGuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionResult = await mediator.Send( + new GetRegionQuery() + { + Guid = addRegionResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionResult); + Assert.NotNull(getRegionResult.Name); + Assert.Equal(regionName, getRegionResult.Name); + Assert.Equal(addRegionResult.Uuid, getRegionResult.Uuid); + Assert.Equal(addCountryResult.Uuid, getRegionResult.CountryUuid); + Assert.NotNull(getRegionResult.CountryName); + Assert.Equal(addCountryResult.Name, getRegionResult.CountryName); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + GetRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionQuery() + { + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + GetRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionQuery() + { + Guid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetRegion_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionQuery(), + TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetRegion_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionQuery(), + TestContext.Current.CancellationToken)); + } + + + [Fact] + public async Task GetRegionsPage_WithAdminRole_RegionsPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "CountryName 1"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(2, getRegionsResult.TotalPages); + Assert.True(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Single(getRegionsResult.Items); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult1.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + addCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + + getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 2, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(2, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(2, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.True(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Single(getRegionsResult.Items); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult2.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + addCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.First().CountryName); + + getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.Equal(2, getRegionsResult.Items.Count()); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult1.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + addCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Name, getRegionsResult.Items.First().CountryName); + Assert.NotNull(getRegionsResult.Items.Last().Name); + Assert.Equal( + addRegionResult2.Name, getRegionsResult.Items.Last().Name); + Assert.Equal( + addRegionResult2.Uuid, getRegionsResult.Items.Last().Uuid); + Assert.Equal( + addCountryResult2.Uuid, getRegionsResult.Items.Last().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.Last().CountryName); + } + + [Fact] + public async Task + GetRegionsPage_WithSearchByCountryName_WithAdminRole_SearchedRegionsPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "CountryName 1"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "cOUNTRYn" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(1, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Single(getRegionsResult.Items); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult2.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + addCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.First().CountryName); + } + + [Fact] + public async Task + GetRegionsPage_WithSearchByRegionName_WithAdminRole_SearchedRegionsPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "CountryName 1"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "region name 1" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(1, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Single(getRegionsResult.Items); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult1.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + addCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Name, getRegionsResult.Items.First().CountryName); + } + + [Fact] + public async Task + GetRegionsPage_WithSort_WithAdminRole_SortedRegionsPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + Sort = "-countryName" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Equal(2, getRegionsResult.Items.Count()); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult2.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + addCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.First().CountryName); + } + + [Fact] + public async Task + GetRegionsPage_WithFilterByCountryUuid_WithAdminRole_SearchedRegionsPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + string regionName3 = "Region Name 3"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult3 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName3, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + CountryGuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(1, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Single(getRegionsResult.Items); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult1.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + addCountryResult1.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Name, getRegionsResult.Items.First().CountryName); + + getRegionsResult = await mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = 1, + PageSize = 10, + CountryGuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionsResult); + Assert.Equal(1, getRegionsResult.PageNumber); + Assert.Equal(2, getRegionsResult.TotalCount); + Assert.Equal(1, getRegionsResult.TotalPages); + Assert.False(getRegionsResult.HasNextPage); + Assert.False(getRegionsResult.HasPreviousPage); + Assert.NotNull(getRegionsResult.Items); + Assert.Equal(2, getRegionsResult.Items.Count()); + Assert.NotNull(getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult2.Name, getRegionsResult.Items.First().Name); + Assert.Equal( + addRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + Assert.Equal( + addCountryResult2.Uuid, getRegionsResult.Items.First().CountryUuid); + Assert.NotNull(getRegionsResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.First().CountryName); + Assert.NotNull(getRegionsResult.Items.Last().Name); + Assert.Equal( + addRegionResult3.Name, getRegionsResult.Items.Last().Name); + Assert.Equal( + addRegionResult3.Uuid, getRegionsResult.Items.Last().Uuid); + Assert.Equal( + addCountryResult2.Uuid, getRegionsResult.Items.Last().CountryUuid); + Assert.NotNull(getRegionsResult.Items.Last().CountryName); + Assert.Equal( + addCountryResult2.Name, getRegionsResult.Items.First().CountryName); + } + + [Theory] + // Length > 64 (65) + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + GetRegionsPage_WithInvalidSearch_WithAdminRole_ThrowsValidationException + (string search) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionsPageQuery() + { + Search = search + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + public async Task + GetRegionsPage_WithInvalidPageNumber_WithAdminRole_ThrowsValidationException + (int pageNumber) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = pageNumber + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + [InlineData(51)] + [InlineData(int.MaxValue)] + public async Task + GetRegionsPage_WithInvalidPageSize_WithAdminRole_ThrowsValidationException + (int pageSize) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionsPageQuery() + { + PageSize = pageSize + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetRegionsPage_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionsPageQuery(), + TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetRegionsPage_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetRegionQuery(), + TestContext.Current.CancellationToken)); + } } diff --git a/tst/Application.IntegrationTests/packages.lock.json b/tst/Application.IntegrationTests/packages.lock.json index aac2a27..34e1b41 100644 --- a/tst/Application.IntegrationTests/packages.lock.json +++ b/tst/Application.IntegrationTests/packages.lock.json @@ -11,12 +11,6 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" } }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "1rqGTfubVg0qj2PsK6esyq3PIxtYJYrN3LsYUV9RrvH3anmt3fT3ozYdAZZH4U8JU/pt5pPIUk8NBSu26wtekA==" - }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[17.13.0, )", @@ -71,6 +65,11 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", @@ -93,6 +92,15 @@ "Microsoft.Extensions.Dependencyinjection.Abstractions": "2.1.0" } }, + "MailKit": { + "type": "Transitive", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "MediatR": { "type": "Transitive", "resolved": "12.4.1", @@ -160,8 +168,8 @@ }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", + "resolved": "9.0.5", + "contentHash": "8J04KPX5NCo6j5AjY/rgeLTceMBJ8Sq4k+YNxN/7hCrbCH1iwHVw7VGGvlCscj615ewMX3jYDmxxLdutbSPOcA==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } @@ -192,15 +200,15 @@ }, "Microsoft.AspNetCore.Cryptography.Internal": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" + "resolved": "2.3.0", + "contentHash": "/qy5r0CD40OccajzDmX3gBfqqxpAJkcXoqlVz0YR70x3gTRq/VuseDU/lZ5eh8vM+KCdmPFAtyGcRWxTyXxuYg==" }, "Microsoft.AspNetCore.Cryptography.KeyDerivation": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", + "resolved": "2.3.0", + "contentHash": "S7pph0JuBkgNqtyiIdLtQ5icZxmpX502zxxvHuMtM5W7IR3CKl1r/Cup+i6+E6B7IF3BeZYF4O3RbcA108syig==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" + "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0" } }, "Microsoft.AspNetCore.DataProtection": { @@ -294,15 +302,6 @@ "Microsoft.Extensions.Identity.Core": "2.3.0" } }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "9.0.4", - "Microsoft.Extensions.Identity.Stores": "9.0.4" - } - }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", "resolved": "9.0.0", @@ -503,13 +502,23 @@ "resolved": "9.0.4", "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", + "resolved": "9.0.4", + "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Microsoft.Extensions.Options": "8.0.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -547,24 +556,28 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, - "Microsoft.Extensions.Identity.Core": { + "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "9.0.4", - "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", + "contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==", "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Diagnostics": "9.0.4", "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", "Microsoft.Extensions.Options": "9.0.4" } }, - "Microsoft.Extensions.Identity.Stores": { + "Microsoft.Extensions.Identity.Core": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", + "resolved": "2.3.0", + "contentHash": "yR0eFnUbAM2k+q5QsX0NKinfShIe1B/aiHXEywiNT5Cs2MvEhxQIbIn5rWXnEAfmwW+i+t5D8odPSEHz/taIyQ==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.4", - "Microsoft.Extensions.Identity.Core": "9.0.4", - "Microsoft.Extensions.Logging": "9.0.4" + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", + "Microsoft.Extensions.Logging": "8.0.1", + "Microsoft.Extensions.Options": "8.0.2", + "System.ComponentModel.Annotations": "5.0.0" } }, "Microsoft.Extensions.Localization": { @@ -670,23 +683,23 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" + "resolved": "8.11.0", + "contentHash": "X92UuBmvHYtsVrD+R+senFn6wOtSVtliSZNTZI8oHD+WqhYLmLNlHH6avYcbXqEznozxshSYzD/DVAuz54jjtg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", + "resolved": "8.11.0", + "contentHash": "rLvApg2vqs/Kz5kVHwHUMAe3owInYrsPX8QP8CQktubX9R63P+J47nR/IOS4n6ddJCvGInUGRBKqcBGJtuA4Rw==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.8.0" + "Microsoft.IdentityModel.Tokens": "8.11.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", + "resolved": "8.11.0", + "contentHash": "/JNOMdYOQ4Tgbdwu9GbEcRJEpzakizuECCE8dCgY5lKXyqZUdAKXyeq4zITgS81eZYThqjhQZUYaJxOPofbmrg==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.8.0" + "Microsoft.IdentityModel.Abstractions": "8.11.0" } }, "Microsoft.IdentityModel.Protocols": { @@ -708,11 +721,11 @@ }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "8.8.0", - "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", + "resolved": "8.11.0", + "contentHash": "E0iKSD9vv9X+tbHGriMTLkSNK/OOjxOPuf1dt9q32d25Ig+OZaidUqDoUTSS3mWTvPw+x5oXrCTHtDatbzRzTQ==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.IdentityModel.Logging": "8.8.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.11.0" } }, "Microsoft.Net.Http.Headers": { @@ -776,10 +789,19 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, "Newtonsoft.Json": { "type": "Transitive", - "resolved": "13.0.1", - "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Npgsql": { "type": "Transitive", @@ -799,6 +821,11 @@ "Npgsql": "9.0.3" } }, + "QuikGraph": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.10", @@ -834,11 +861,21 @@ "resolved": "4.6.0", "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" + }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -964,7 +1001,12 @@ "FluentValidation": "[11.11.0, )", "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.5, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.11.0, )", + "Microsoft.IdentityModel.Tokens": "[8.11.0, )", + "Newtonsoft.Json": "[13.0.3, )", + "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } }, @@ -975,7 +1017,6 @@ "AspNetCore.Localizer.Json": "[1.0.1, )", "Domain": "[1.0.0, )", "FluentValidation.DependencyInjectionExtensions": "[11.11.0, )", - "Identity": "[1.0.0, )", "Infrastructure": "[1.0.0, )", "Microsoft.AspNetCore.Identity": "[2.3.1, )", "Microsoft.EntityFrameworkCore.InMemory": "[9.0.4, )", @@ -995,23 +1036,13 @@ "domain": { "type": "Project" }, - "identity": { - "type": "Project", - "dependencies": { - "Application": "[1.0.0, )", - "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.4, )", - "Microsoft.AspNetCore.Identity": "[2.3.1, )", - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[9.0.4, )", - "Microsoft.Extensions.Options": "[9.0.4, )", - "Microsoft.IdentityModel.JsonWebTokens": "[8.8.0, )", - "Microsoft.IdentityModel.Tokens": "[8.8.0, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" - } - }, "infrastructure": { "type": "Project", "dependencies": { - "Application": "[1.0.0, )" + "Application": "[1.0.0, )", + "MailKit": "[4.12.1, )", + "Microsoft.Extensions.Http": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )" } }, "persistence": { @@ -1025,51 +1056,6 @@ "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" } } - }, - "net9.0/linux-x64": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", - "dependencies": { - "System.Security.AccessControl": "4.5.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.10", - "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0", - "System.Security.Principal.Windows": "4.5.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - } } } } \ No newline at end of file