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