diff --git a/SchengenVisaApi/Infrastructure/Database/GeneralExceptions/EntityNotFoundByIdException.cs b/SchengenVisaApi/ApplicationLayer/Services/GeneralExceptions/EntityNotFoundByIdException.cs similarity index 100% rename from SchengenVisaApi/Infrastructure/Database/GeneralExceptions/EntityNotFoundByIdException.cs rename to SchengenVisaApi/ApplicationLayer/Services/GeneralExceptions/EntityNotFoundByIdException.cs diff --git a/SchengenVisaApi/Infrastructure/Database/GeneralExceptions/EntityNotFoundException.cs b/SchengenVisaApi/ApplicationLayer/Services/GeneralExceptions/EntityNotFoundException.cs similarity index 100% rename from SchengenVisaApi/Infrastructure/Database/GeneralExceptions/EntityNotFoundException.cs rename to SchengenVisaApi/ApplicationLayer/Services/GeneralExceptions/EntityNotFoundException.cs diff --git a/SchengenVisaApi/ApplicationLayer/Services/GeneralExceptions/EntityUsedInDatabaseException.cs b/SchengenVisaApi/ApplicationLayer/Services/GeneralExceptions/EntityUsedInDatabaseException.cs new file mode 100644 index 0000000..6aa2958 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/GeneralExceptions/EntityUsedInDatabaseException.cs @@ -0,0 +1,7 @@ +using ApplicationLayer.GeneralExceptions; + +namespace Infrastructure.Database.GeneralExceptions +{ + /// Exception to throw when can't complete some action on entity(delete or something) because it's needed for other entities + public class EntityUsedInDatabaseException(string message) : ApiException(message); +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/Locations/NeededServices/ICitiesRepository.cs b/SchengenVisaApi/ApplicationLayer/Services/Locations/NeededServices/ICitiesRepository.cs index 3efd6dc..ef332d4 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/Locations/NeededServices/ICitiesRepository.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/Locations/NeededServices/ICitiesRepository.cs @@ -3,4 +3,8 @@ using Domains.LocationDomain; namespace ApplicationLayer.Services.Locations.NeededServices; -public interface ICitiesRepository : IGenericRepository; +public interface ICitiesRepository : IGenericRepository +{ + /// Get by name and country identifier + Task GetByNameAsync(Guid requestId, string existingCity, CancellationToken cancellationToken); +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/Locations/NeededServices/ICountriesRepository.cs b/SchengenVisaApi/ApplicationLayer/Services/Locations/NeededServices/ICountriesRepository.cs index a01cbc3..c3bd0ea 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/Locations/NeededServices/ICountriesRepository.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/Locations/NeededServices/ICountriesRepository.cs @@ -6,8 +6,8 @@ namespace ApplicationLayer.Services.Locations.NeededServices; public interface ICountriesRepository : IGenericRepository { /// Gets country by name - /// Name of country to seek - /// Cancellation Token - /// Country or null if not found - Task FindByName(string countryName, CancellationToken cancellationToken); + Task FindByNameAsync(string countryName, CancellationToken cancellationToken); + + /// Gets country by identifier + Task FindByIdAsync(Guid id, CancellationToken cancellationToken); } diff --git a/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/Exceptions/CityCanNotBeDeletedException.cs b/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/Exceptions/CityCanNotBeDeletedException.cs new file mode 100644 index 0000000..ba3b7ed --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/Exceptions/CityCanNotBeDeletedException.cs @@ -0,0 +1,7 @@ +using Infrastructure.Database.GeneralExceptions; + +namespace ApplicationLayer.Services.Locations.RequestHandlers.Exceptions +{ + public class CityCanNotBeDeletedException(string cityName) + : EntityUsedInDatabaseException($"{cityName} can not be deleted because some applicants live or work in it"); +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/Exceptions/CountryNotFoundException.cs b/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/Exceptions/CountryNotFoundException.cs new file mode 100644 index 0000000..c4fa367 --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/Exceptions/CountryNotFoundException.cs @@ -0,0 +1,7 @@ +using Domains.LocationDomain; +using Infrastructure.Database.GeneralExceptions; + +namespace ApplicationLayer.Services.Locations.RequestHandlers.Exceptions +{ + public class CountryNotFoundException(string countryName) : EntityNotFoundException($"Country {countryName} is not supported."); +} diff --git a/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/ILocationRequestsHandler.cs b/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/ILocationRequestsHandler.cs index 710e8c7..eab5625 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/ILocationRequestsHandler.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/ILocationRequestsHandler.cs @@ -10,7 +10,10 @@ namespace ApplicationLayer.Services.Locations.RequestHandlers /// List of available countries Task> HandleGetRequestAsync(CancellationToken cancellationToken); - /// Handles add country requests + /// Handles Task AddCountryAsync(AddCountryRequest request, CancellationToken cancellationToken); + + /// Handles + Task UpdateCountryAsync(UpdateCountryRequest request, CancellationToken cancellationToken); } } diff --git a/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/LocationRequestsHandler.cs b/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/LocationRequestsHandler.cs index e511c4b..be07a39 100644 --- a/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/LocationRequestsHandler.cs +++ b/SchengenVisaApi/ApplicationLayer/Services/Locations/RequestHandlers/LocationRequestsHandler.cs @@ -1,4 +1,5 @@ using ApplicationLayer.InfrastructureServicesInterfaces; +using ApplicationLayer.Services.Applicants.NeededServices; using ApplicationLayer.Services.Locations.NeededServices; using ApplicationLayer.Services.Locations.RequestHandlers.Exceptions; using ApplicationLayer.Services.Locations.Requests; @@ -7,7 +8,11 @@ using Domains.LocationDomain; namespace ApplicationLayer.Services.Locations.RequestHandlers { /// - public class LocationRequestsHandler(ICountriesRepository countries, IUnitOfWork unitOfWork) : ILocationRequestsHandler + public class LocationRequestsHandler( + ICountriesRepository countries, + ICitiesRepository cities, + IApplicantsRepository applicants, + IUnitOfWork unitOfWork) : ILocationRequestsHandler { async Task> ILocationRequestsHandler.HandleGetRequestAsync(CancellationToken cancellationToken) { @@ -16,7 +21,7 @@ namespace ApplicationLayer.Services.Locations.RequestHandlers async Task ILocationRequestsHandler.AddCountryAsync(AddCountryRequest request, CancellationToken cancellationToken) { - if (await countries.FindByName(request.CountryName, cancellationToken) is not null) + if (await countries.FindByNameAsync(request.CountryName, cancellationToken) is not null) { throw new CountryAlreadyExists(request.CountryName); } @@ -38,5 +43,49 @@ namespace ApplicationLayer.Services.Locations.RequestHandlers await unitOfWork.SaveAsync(cancellationToken); } + + async Task ILocationRequestsHandler.UpdateCountryAsync(UpdateCountryRequest request, CancellationToken cancellationToken) + { + if (await countries.FindByNameAsync(request.CountryName, cancellationToken) is not null) + { + throw new CountryAlreadyExists(request.CountryName); + } + + var country = await countries.FindByIdAsync(request.Id, cancellationToken); + if (country is null) + { + throw new CountryNotFoundException(request.CountryName); + } + + var existingCities = country.Cities; + var citiesToAdd = request.Cities.Except(existingCities.Select(c => c.Name)).ToList(); + var citiesToRemove = existingCities.Where(c => !request.Cities.Contains(c.Name)); + var applicantsList = await applicants.GetAllAsync(cancellationToken); + + //todo mapper + foreach (var city in citiesToRemove) + { + if (applicantsList.All(a => a.CityOfBirth.Id != city.Id && a.PlaceOfWork.Address.City.Id != city.Id)) + { + cities.Remove(city); + } + else + { + throw new CityCanNotBeDeletedException(city.Name); + } + } + + foreach (var city in citiesToAdd) + { + await cities.AddAsync(new City { Name = city, Country = country }, cancellationToken); + } + + country.Name = request.CountryName; + country.IsSchengen = request.IsSchengen; + + await countries.UpdateAsync(country, cancellationToken); + + await unitOfWork.SaveAsync(cancellationToken); + } } } diff --git a/SchengenVisaApi/ApplicationLayer/Services/Locations/Requests/UpdateCountryRequest.cs b/SchengenVisaApi/ApplicationLayer/Services/Locations/Requests/UpdateCountryRequest.cs new file mode 100644 index 0000000..8aa5dcc --- /dev/null +++ b/SchengenVisaApi/ApplicationLayer/Services/Locations/Requests/UpdateCountryRequest.cs @@ -0,0 +1,4 @@ +namespace ApplicationLayer.Services.Locations.Requests +{ + public record UpdateCountryRequest(Guid Id, string CountryName, bool IsSchengen, string[] Cities); +} diff --git a/SchengenVisaApi/Infrastructure/Database/Locations/Repositories/Cities/CitiesRepository.cs b/SchengenVisaApi/Infrastructure/Database/Locations/Repositories/Cities/CitiesRepository.cs index c588c05..13104b2 100644 --- a/SchengenVisaApi/Infrastructure/Database/Locations/Repositories/Cities/CitiesRepository.cs +++ b/SchengenVisaApi/Infrastructure/Database/Locations/Repositories/Cities/CitiesRepository.cs @@ -12,4 +12,7 @@ public sealed class CitiesRepository(IGenericReader reader, IGenericWriter write { return base.LoadDomain().Include(c => c.Country); } + + Task ICitiesRepository.GetByNameAsync(Guid countryId, string cityName, CancellationToken cancellationToken) + => LoadDomain().SingleOrDefaultAsync(c => c.Country.Id == countryId && c.Name == cityName, cancellationToken); } diff --git a/SchengenVisaApi/Infrastructure/Database/Locations/Repositories/Countries/CountriesRepository.cs b/SchengenVisaApi/Infrastructure/Database/Locations/Repositories/Countries/CountriesRepository.cs index 6514e6f..2ef1ddb 100644 --- a/SchengenVisaApi/Infrastructure/Database/Locations/Repositories/Countries/CountriesRepository.cs +++ b/SchengenVisaApi/Infrastructure/Database/Locations/Repositories/Countries/CountriesRepository.cs @@ -13,9 +13,15 @@ public sealed class CountriesRepository(IGenericReader reader, IGenericWriter wr return base.LoadDomain().Include(c => c.Cities); } - async Task ICountriesRepository.FindByName(string countryName, CancellationToken cancellationToken) + async Task ICountriesRepository.FindByNameAsync(string countryName, CancellationToken cancellationToken) { var result = await LoadDomain().SingleOrDefaultAsync(c => c.Name == countryName, cancellationToken); return result; } + + async Task ICountriesRepository.FindByIdAsync(Guid id, CancellationToken cancellationToken) + { + var result = await LoadDomain().SingleOrDefaultAsync(c => c.Id == id, cancellationToken); + return result; + } } diff --git a/SchengenVisaApi/SchengenVisaApi/Controllers/LocationsController.cs b/SchengenVisaApi/SchengenVisaApi/Controllers/LocationsController.cs index 35b7cb7..01d499f 100644 --- a/SchengenVisaApi/SchengenVisaApi/Controllers/LocationsController.cs +++ b/SchengenVisaApi/SchengenVisaApi/Controllers/LocationsController.cs @@ -10,7 +10,7 @@ namespace SchengenVisaApi.Controllers { /// Controller for [ApiController] - [Route("countries")] + [Route("locations")] public class LocationsController(ILocationRequestsHandler requestsHandler) : ControllerBase { /// Return countries with cities from DB @@ -35,5 +35,19 @@ namespace SchengenVisaApi.Controllers await requestsHandler.AddCountryAsync(request, cancellationToken); return Ok(); } + + /// Updates country with cities in DB + [HttpPut] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [Route("country")] + [Authorize(policy: PolicyConstants.AdminPolicy)] + public async Task UpdateCountry(UpdateCountryRequest request, CancellationToken cancellationToken) + { + await requestsHandler.UpdateCountryAsync(request, cancellationToken); + return Ok(); + } } } diff --git a/SchengenVisaApi/SchengenVisaApi/ExceptionFilters/GlobalExceptionsFilter.cs b/SchengenVisaApi/SchengenVisaApi/ExceptionFilters/GlobalExceptionsFilter.cs index bb8e20a..781cd2e 100644 --- a/SchengenVisaApi/SchengenVisaApi/ExceptionFilters/GlobalExceptionsFilter.cs +++ b/SchengenVisaApi/SchengenVisaApi/ExceptionFilters/GlobalExceptionsFilter.cs @@ -42,6 +42,11 @@ namespace SchengenVisaApi.ExceptionFilters problemDetails.Title = "Can not add cities with one name to one country"; problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1"; break; + case EntityUsedInDatabaseException: + problemDetails.Status = StatusCodes.Status409Conflict; + problemDetails.Title = "entity is used by someone"; + problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8"; + break; default: problemDetails.Status = StatusCodes.Status400BadRequest; problemDetails.Title = "Bad request";