diff --git a/AsbCloudApp/Data/WellSections/WellSectionPlanDto.cs b/AsbCloudApp/Data/WellSections/WellSectionPlanDto.cs new file mode 100644 index 00000000..5a17ad44 --- /dev/null +++ b/AsbCloudApp/Data/WellSections/WellSectionPlanDto.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace AsbCloudApp.Data.WellSections; + +/// +/// Секция скважины - план +/// +public class WellSectionPlanDto : ItemInfoDto, + IId, + IWellRelated, + IValidatableObject +{ + /// + public int Id { get; set; } + + /// + public int IdWell { get; set; } + + /// + /// Тип секции + /// + [Required(ErrorMessage = "Поле обязательно для заполнение")] + [Range(1, int.MaxValue)] + public int IdSectionType { get; set; } + + /// + /// Начальная глубина бурения, м + /// + [Required(ErrorMessage = "Поле обязательно для заполнение")] + [Range(0, 10000, ErrorMessage = "Допустимое значение от 1 до 10000")] + public double DepthStart { get; set; } + + /// + /// Конечная глубина бурения, м + /// + [Required(ErrorMessage = "Поле обязательно для заполнение")] + [Range(0, 10000, ErrorMessage = "Допустимое значение от 1 до 10000")] + public double DepthEnd { get; set; } + + /// + /// Внешний диаметр + /// + [Range(0, 10000, ErrorMessage = "Допустимое значение от 1 до 10000")] + public double? OuterDiameter { get; set; } + + /// + /// Внутренний диаметр + /// + [Range(0, 10000, ErrorMessage = "Допустимое значение от 1 до 10000")] + public double? InnerDiameter { get; set; } + + /// + public IEnumerable Validate(ValidationContext validationContext) + { + if (!OuterDiameter.HasValue && !InnerDiameter.HasValue) + yield break; + + if (!OuterDiameter.HasValue) + yield return new ValidationResult("Поле обязательно для заполнение", new[] { nameof(OuterDiameter) }); + + if (!InnerDiameter.HasValue) + yield return new ValidationResult("Поле обязательно для заполнение", new[] { nameof(InnerDiameter) }); + + if (OuterDiameter <= InnerDiameter) + yield return new ValidationResult("Внешний диаметр не должен быть больше или равен внутреннему", + new[] { nameof(OuterDiameter) }); + + if (InnerDiameter >= OuterDiameter) + yield return new ValidationResult("Внутренний диаметр не должен больше или равен внутреннему", + new[] { nameof(InnerDiameter) }); + } +} \ No newline at end of file diff --git a/AsbCloudApp/Services/WellSections/IWellSectionPlanService.cs b/AsbCloudApp/Services/WellSections/IWellSectionPlanService.cs new file mode 100644 index 00000000..599c8509 --- /dev/null +++ b/AsbCloudApp/Services/WellSections/IWellSectionPlanService.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Data.WellSections; + +namespace AsbCloudApp.Services.WellSections; + +/// +/// Секция скважины - план +/// +public interface IWellSectionPlanService +{ + /// + /// Добавить секцию + /// + /// + /// + /// + Task InsertAsync(WellSectionPlanDto wellSectionPlan, CancellationToken cancellationToken); + + /// + /// Обновить секцию + /// + /// + /// + /// + Task UpdateAsync(WellSectionPlanDto wellSectionPlan, CancellationToken cancellationToken); + + /// + /// Получить типы секций + /// + /// + /// + /// + Task> GetWellSectionTypesAsync(int idWell, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/WellSections/WellSectionPlanService.cs b/AsbCloudInfrastructure/Services/WellSections/WellSectionPlanService.cs new file mode 100644 index 00000000..7258f508 --- /dev/null +++ b/AsbCloudInfrastructure/Services/WellSections/WellSectionPlanService.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Data.WellSections; +using AsbCloudApp.Exceptions; +using AsbCloudApp.Services; +using AsbCloudApp.Services.WellSections; + +namespace AsbCloudInfrastructure.Services.WellSections; + +public class WellSectionPlanService : IWellSectionPlanService +{ + private readonly IRepositoryWellRelated wellSectionPlanRepository; + private readonly ICrudRepository wellSectionTypeRepository; + + public WellSectionPlanService(IRepositoryWellRelated wellSectionPlanRepository, + ICrudRepository wellSectionTypeRepository) + { + this.wellSectionPlanRepository = wellSectionPlanRepository; + this.wellSectionTypeRepository = wellSectionTypeRepository; + } + + public async Task InsertAsync(WellSectionPlanDto wellSectionPlan, CancellationToken cancellationToken) + { + await EnsureUniqueSectionTypeInWellAsync(wellSectionPlan, cancellationToken); + + return await wellSectionPlanRepository.InsertAsync(wellSectionPlan, cancellationToken); + } + + public async Task UpdateAsync(WellSectionPlanDto wellSectionPlan, CancellationToken cancellationToken) + { + await EnsureUniqueSectionTypeInWellAsync(wellSectionPlan, cancellationToken); + + wellSectionPlan.LastUpdateDate = DateTime.UtcNow; + + return await wellSectionPlanRepository.UpdateAsync(wellSectionPlan, cancellationToken); + } + + public async Task> GetWellSectionTypesAsync(int idWell, CancellationToken cancellationToken) + { + var wellSectionTypes = (await wellSectionTypeRepository.GetAllAsync(cancellationToken)) + .OrderBy(w => w.Order); + + var planWellSections = await wellSectionPlanRepository.GetByIdWellAsync(idWell, cancellationToken); + + if (!planWellSections.Any()) + return wellSectionTypes; + + return wellSectionTypes.Where(w => planWellSections.Any(s => s.IdSectionType == w.Id)); + } + + private async Task EnsureUniqueSectionTypeInWellAsync(WellSectionPlanDto section, CancellationToken cancellationToken) + { + var existingWellSectionPlan = (await wellSectionPlanRepository.GetByIdWellAsync(section.IdWell, cancellationToken)) + .SingleOrDefault(s => s.IdSectionType == section.IdSectionType); + + if (existingWellSectionPlan is not null && existingWellSectionPlan.Id != section.Id) + { + var sectionType = await wellSectionTypeRepository.GetOrDefaultAsync(section.IdSectionType, cancellationToken); + + throw new ArgumentInvalidException($"Секция '{sectionType?.Caption}' уже добавлена в конструкцию скважины", + nameof(section.IdSectionType)); + } + } +} \ No newline at end of file diff --git a/AsbCloudWebApi.Tests/UnitTests/Services/WellSections/WellSectionPlanServiceTests.cs b/AsbCloudWebApi.Tests/UnitTests/Services/WellSections/WellSectionPlanServiceTests.cs new file mode 100644 index 00000000..f5a2edad --- /dev/null +++ b/AsbCloudWebApi.Tests/UnitTests/Services/WellSections/WellSectionPlanServiceTests.cs @@ -0,0 +1,169 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Data.WellSections; +using AsbCloudApp.Exceptions; +using AsbCloudApp.Services; +using AsbCloudInfrastructure.Services.WellSections; +using NSubstitute; +using Xunit; + +namespace AsbCloudWebApi.Tests.UnitTests.Services.WellSections; + +public class WellSectionPlanServiceTests +{ + private const int idWellSectionPlan = 1; + private const int idWell = 3; + private const int idWellSectionType = 54; + + private readonly IEnumerable fakePlanWellSections = new WellSectionPlanDto[] + { + new() + { + Id = idWellSectionPlan, + IdWell = idWell, + IdSectionType = idWellSectionType + } + }; + + private readonly IEnumerable fakeWellSectionTypes = new WellSectionTypeDto[] + { + new() + { + Id = idWellSectionType + } + }; + + private readonly IRepositoryWellRelated wellSectionPlanRepositoryMock = + Substitute.For>(); + + private readonly ICrudRepository wellSectionTypeRepositoryMock = + Substitute.For>(); + + private readonly WellSectionPlanService wellSectionPlanService; + + public WellSectionPlanServiceTests() + { + wellSectionPlanService = new WellSectionPlanService(wellSectionPlanRepositoryMock, wellSectionTypeRepositoryMock); + + wellSectionTypeRepositoryMock.GetAllAsync(Arg.Any()) + .ReturnsForAnyArgs(fakeWellSectionTypes); + } + + [Fact] + public async Task InsertAsync_InsertNewWellSectionTypeWithUniqueSectionTypeInWell_InvokesWellSectionPlanRepositoryInsertAsync() + { + //arrange + var insertedWellSection = new WellSectionPlanDto(); + + //act + await wellSectionPlanService.InsertAsync(insertedWellSection, CancellationToken.None); + + //assert + await wellSectionPlanRepositoryMock.Received().InsertAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task InsertAsync_InsertNewWellSectionTypeWithNotUniqueSectionTypeInWell_ReturnsDuplicateException() + { + //arrange + var insertedWellSection = new WellSectionPlanDto + { + Id = 2, + IdSectionType = idWellSectionType + }; + + wellSectionPlanRepositoryMock.GetByIdWellAsync(Arg.Any(), Arg.Any()) + .ReturnsForAnyArgs(fakePlanWellSections); + + //act + Task Result() => wellSectionPlanService.InsertAsync(insertedWellSection, CancellationToken.None); + + //assert + await Assert.ThrowsAsync(Result); + } + + [Fact] + public async Task UpdateAsync_UpdateExistingWellSectionTypeWithUniqueSectionTypeInWell_InvokesWellSectionPlanRepositoryUpdateAsync() + { + //arrange + var updatedWellSection = new WellSectionPlanDto + { + Id = idWellSectionPlan, + IdSectionType = idWellSectionType + }; + + //act + await wellSectionPlanService.UpdateAsync(updatedWellSection, CancellationToken.None); + + //assert + await wellSectionPlanRepositoryMock.Received().UpdateAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task UpdateAsync_ReturnsLastUpdateDateNotNull() + { + //arrange + var updatedWellSection = new WellSectionPlanDto + { + Id = idWellSectionPlan, + IdSectionType = idWellSectionType + }; + + //act + await wellSectionPlanService.UpdateAsync(updatedWellSection, CancellationToken.None); + + //assert + Assert.NotNull(updatedWellSection.LastUpdateDate); + } + + [Fact] + public async Task UpdateAsync_UpdateExistingWellSectionTypeWithNotUniqueSectionTypeInWell_ReturnsDuplicateException() + { + //arrange + var updatedWellSection = new WellSectionPlanDto + { + Id = 2, + IdSectionType = idWellSectionType + }; + + wellSectionPlanRepositoryMock.GetByIdWellAsync(Arg.Any(), Arg.Any()) + .ReturnsForAnyArgs(fakePlanWellSections); + + //act + Task Result() => wellSectionPlanService.UpdateAsync(updatedWellSection, CancellationToken.None); + + //assert + await Assert.ThrowsAsync(Result); + } + + [Fact] + public async Task GetWellSectionTypesAsync_EmptyCollectionWellSectionPlan_ReturnsCollectionWellSectionType() + { + //arrange + wellSectionPlanRepositoryMock.GetByIdWellAsync(Arg.Any(), Arg.Any()) + .ReturnsForAnyArgs(Enumerable.Empty()); + + //act + var result = await wellSectionPlanService.GetWellSectionTypesAsync(idWell, CancellationToken.None); + + //assert + Assert.Single(result); + } + + [Fact] + public async Task GetWellSectionTypesAsync_NotEmptyCollectionWellSectionPlan_ReturnsCollectionWellSectionType() + { + //arrange + wellSectionPlanRepositoryMock.GetByIdWellAsync(Arg.Any(), Arg.Any()) + .ReturnsForAnyArgs(fakePlanWellSections); + + //act + var result = await wellSectionPlanService.GetWellSectionTypesAsync(idWell, CancellationToken.None); + + //assert + Assert.Single(result); + } +} \ No newline at end of file