diff --git a/AsbCloudApp/Repositories/IWellOperationRepository.cs b/AsbCloudApp/Repositories/IWellOperationRepository.cs
index d1db155e..8cb313ac 100644
--- a/AsbCloudApp/Repositories/IWellOperationRepository.cs
+++ b/AsbCloudApp/Repositories/IWellOperationRepository.cs
@@ -2,6 +2,7 @@
using AsbCloudApp.Requests;
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
@@ -114,5 +115,20 @@ namespace AsbCloudApp.Repositories
///
///
Task GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken);
- }
+
+ ///
+ /// Валидация данных
+ ///
+ ///
+ ///
+ IEnumerable Validate(IEnumerable wellOperations);
+
+ ///
+ /// Валидация данных (проверка с базой)
+ ///
+ ///
+ ///
+ ///
+ Task> ValidateWithDbAsync(IEnumerable wellOperations, CancellationToken cancellationToken);
+ }
}
\ No newline at end of file
diff --git a/AsbCloudInfrastructure/Repository/WellOperationRepository.cs b/AsbCloudInfrastructure/Repository/WellOperationRepository.cs
index fb0a8c12..208e94ac 100644
--- a/AsbCloudInfrastructure/Repository/WellOperationRepository.cs
+++ b/AsbCloudInfrastructure/Repository/WellOperationRepository.cs
@@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -22,6 +23,8 @@ namespace AsbCloudInfrastructure.Repository;
public class WellOperationRepository : IWellOperationRepository
{
private const string KeyCacheSections = "OperationsBySectionSummarties";
+ private const int Gap = 90;
+
private readonly IAsbCloudDbContext db;
private readonly IMemoryCache memoryCache;
private readonly IWellService wellService;
@@ -50,7 +53,7 @@ public class WellOperationRepository : IWellOperationRepository
}
var result = categories
- .OrderBy(o => o.Name)
+ .OrderBy(o => o.Name)
.Adapt>();
return result;
@@ -89,14 +92,14 @@ public class WellOperationRepository : IWellOperationRepository
}
private async Task GetDateLastAssosiatedPlanOperationAsync(
- int idWell,
- DateTime? lessThenDate,
- double timeZoneHours,
+ int idWell,
+ DateTime? lessThenDate,
+ double timeZoneHours,
CancellationToken token)
{
- if (lessThenDate is null)
+ if (lessThenDate is null)
return null;
-
+
var currentDateOffset = lessThenDate.Value.ToUtcDateTimeOffset(timeZoneHours);
var timeZoneOffset = TimeSpan.FromHours(timeZoneHours);
@@ -187,7 +190,7 @@ public class WellOperationRepository : IWellOperationRepository
public async Task GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken)
{
var timezone = wellService.GetTimezone(idWell);
-
+
var query = db.WellOperations.Where(o => o.IdWell == idWell && o.IdType == idType);
if (!await query.AnyAsync(cancellationToken))
@@ -195,7 +198,7 @@ public class WellOperationRepository : IWellOperationRepository
var minDate = await query.MinAsync(o => o.DateStart, cancellationToken);
var maxDate = await query.MaxAsync(o => o.DateStart, cancellationToken);
-
+
return new DatesRangeDto
{
From = minDate.ToRemoteDateTime(timezone.Hours),
@@ -306,12 +309,13 @@ public class WellOperationRepository : IWellOperationRepository
DeltaDepth = g.Sum(o => o.DurationDepth),
IdParent = parentRelationDictionary[g.Key].IdParent
});
-
+
while (dtos.All(x => x.IdParent != null))
{
dtos = dtos
.GroupBy(o => o.IdParent!)
- .Select(g => {
+ .Select(g =>
+ {
var idCategory = g.Key ?? int.MinValue;
var category = parentRelationDictionary.GetValueOrDefault(idCategory);
var newDto = new WellGroupOpertionDto
@@ -330,6 +334,66 @@ public class WellOperationRepository : IWellOperationRepository
return dtos;
}
+ public async Task> ValidateWithDbAsync(IEnumerable wellOperationDtos, CancellationToken token)
+ {
+ var firstOperation = wellOperationDtos
+ .FirstOrDefault();
+
+ if (firstOperation is null)
+ return Enumerable.Empty();
+
+ var request = new WellOperationRequest()
+ {
+ IdWell = firstOperation.IdWell,
+ OperationType = firstOperation.IdType,
+ };
+
+ var entities = await BuildQuery(request)
+ .AsNoTracking()
+ .ToArrayAsync(token);
+
+ var wellOperationsUnion = entities.Union(wellOperationDtos).OrderBy(o => o.DateStart);
+
+ var results = Validate(wellOperationsUnion);
+ return results;
+ }
+
+ public IEnumerable Validate(IEnumerable wellOperationDtos)
+ {
+ var enumerator = wellOperationDtos.OrderBy(o => o.DateStart)
+ .GetEnumerator();
+
+ if (!enumerator.MoveNext())
+ yield break;
+
+ var previous = enumerator.Current;
+
+ while(enumerator.MoveNext())
+ {
+ var current = enumerator.Current;
+ var previousDateStart = previous.DateStart.ToUniversalTime();
+ var currentDateStart = current.DateStart.ToUniversalTime();
+
+ var previousDateEnd = previous.DateStart.AddHours(previous.DurationHours).ToUniversalTime();
+
+ if (previousDateStart.AddDays(Gap) < currentDateStart)
+ {
+ yield return new ValidationResult(
+ "Разница дат между операциями не должна превышать 90 дней",
+ new[] { nameof(wellOperationDtos) });
+ }
+
+ if (previousDateEnd > currentDateStart)
+ {
+ yield return new ValidationResult(
+ "Предыдущая операция не завершена",
+ new[] { nameof(wellOperationDtos) });
+ }
+
+ previous = current;
+ }
+ }
+
///
public async Task InsertRangeAsync(
IEnumerable wellOperationDtos,
diff --git a/AsbCloudWebApi.IntegrationTests/BaseIntegrationTest.cs b/AsbCloudWebApi.IntegrationTests/BaseIntegrationTest.cs
index d18c54bf..add2e07b 100644
--- a/AsbCloudWebApi.IntegrationTests/BaseIntegrationTest.cs
+++ b/AsbCloudWebApi.IntegrationTests/BaseIntegrationTest.cs
@@ -6,7 +6,7 @@ namespace AsbCloudWebApi.IntegrationTests;
public abstract class BaseIntegrationTest : IClassFixture
{
- private readonly IServiceScope scope;
+ protected readonly IServiceScope scope;
protected readonly IAsbCloudDbContext dbContext;
diff --git a/AsbCloudWebApi.IntegrationTests/Clients/IWellOperationClient.cs b/AsbCloudWebApi.IntegrationTests/Clients/IWellOperationClient.cs
new file mode 100644
index 00000000..2db55d36
--- /dev/null
+++ b/AsbCloudWebApi.IntegrationTests/Clients/IWellOperationClient.cs
@@ -0,0 +1,17 @@
+using AsbCloudApp.Data;
+using Microsoft.AspNetCore.Mvc;
+using Refit;
+
+namespace AsbCloudWebApi.IntegrationTests.Clients;
+
+public interface IWellOperationClient
+{
+ private const string BaseRoute = "/api/well/{idWell}/wellOperations";
+
+ [Post(BaseRoute + "/{idType}/{deleteBeforeInsert}")]
+ Task> InsertRangeAsync(int idWell, int idType, bool deleteBeforeInsert, [Body] IEnumerable dtos);
+
+ [Put(BaseRoute + "/{idOperation}")]
+ Task> UpdateAsync(int idWell, int idOperation, [FromBody] WellOperationDto value, CancellationToken token);
+
+}
\ No newline at end of file
diff --git a/AsbCloudWebApi.IntegrationTests/Controllers/WellOperationControllerTest.cs b/AsbCloudWebApi.IntegrationTests/Controllers/WellOperationControllerTest.cs
new file mode 100644
index 00000000..3ac32e40
--- /dev/null
+++ b/AsbCloudWebApi.IntegrationTests/Controllers/WellOperationControllerTest.cs
@@ -0,0 +1,186 @@
+using AsbCloudApp.Data;
+using AsbCloudDb.Model;
+using AsbCloudWebApi.IntegrationTests.Clients;
+using System.Net;
+using Xunit;
+
+namespace AsbCloudWebApi.IntegrationTests.Controllers;
+
+
+public class WellOperationControllerTest : BaseIntegrationTest
+{
+ private static int idWell = 1;
+
+ private readonly WellOperationDto[] dtos = new WellOperationDto[]
+ {
+ new WellOperationDto()
+ {
+ Id = 2,
+ IdWell = idWell,
+ IdType = 1,
+ DateStart = DateTimeOffset.Now,
+ CategoryInfo = "1",
+ CategoryName = "1",
+ Comment = "1",
+ Day = 1,
+ DepthEnd = 20,
+ DepthStart = 10,
+ DurationHours = 1,
+ IdCategory = 5000,
+ IdParentCategory = null,
+ IdPlan = null,
+ IdUser = 1,
+ IdWellSectionType = 1,
+ LastUpdateDate = DateTimeOffset.Now,
+ NptHours = 1,
+ WellSectionTypeName = null,
+ UserName = null
+ }
+ };
+
+ private readonly WellOperationDto[] dtosWithError = new WellOperationDto[]
+ {
+ new WellOperationDto()
+ {
+ Id = 3,
+ IdWell = idWell,
+ IdType = 1,
+ DateStart = DateTimeOffset.Now,
+ CategoryInfo = "1",
+ CategoryName = "1",
+ Comment = "1",
+ Day = 1,
+ DepthEnd = 20,
+ DepthStart = 10,
+ DurationHours = 1,
+ IdCategory = 5000,
+ IdParentCategory = null,
+ IdPlan = null,
+ IdUser = 1,
+ IdWellSectionType = 1,
+ LastUpdateDate = DateTimeOffset.Now,
+ NptHours = 1,
+ WellSectionTypeName = null,
+ UserName = null
+ },
+ new WellOperationDto()
+ {
+ Id = 4,
+ IdWell = idWell,
+ IdType = 1,
+ DateStart = DateTimeOffset.Now.AddDays(1000),
+ CategoryInfo = "1",
+ CategoryName = "1",
+ Comment = "1",
+ Day = 1,
+ DepthEnd = 20,
+ DepthStart = 10,
+ DurationHours = 1,
+ IdCategory = 5000,
+ IdParentCategory = null,
+ IdPlan = null,
+ IdUser = 1,
+ IdWellSectionType = 1,
+ LastUpdateDate = DateTimeOffset.Now,
+ NptHours = 1,
+ WellSectionTypeName = null,
+ UserName = null
+ }
+ };
+
+ private IWellOperationClient wellOperationClient;
+
+ public WellOperationControllerTest(WebAppFactoryFixture factory)
+ : base(factory)
+ {
+ wellOperationClient = factory.GetAuthorizedHttpClient();
+ }
+
+ ///
+ /// Успешное добавление операций (без предварительной очистки данных)
+ ///
+ ///
+ [Fact]
+ public async Task InsertRange_returns_success()
+ {
+ dbContext.CleanupDbSet();
+ //act
+ var response = await wellOperationClient.InsertRangeAsync(idWell, 1, false, dtos);
+
+ //assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+
+ ///
+ /// Неуспешное добавление операций (без предварительной очистки данных)
+ ///
+ ///
+ [Fact]
+ public async Task InsertRange_returns_error()
+ {
+ //act
+ var response = await wellOperationClient.InsertRangeAsync(idWell, 1, false, dtosWithError);
+
+ //assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ ///
+ /// Успешное добавление операций (с предварительной очисткой данных)
+ ///
+ ///
+ [Fact]
+ public async Task InsertRangeWithDeleteBefore_returns_success()
+ {
+ //act
+ var response = await wellOperationClient.InsertRangeAsync(idWell, 1, true, dtos);
+
+ //assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ ///
+ /// Неуспешное добавление операций (с предварительной очисткой данных)
+ ///
+ ///
+ [Fact]
+ public async Task InsertRangeWithDeleteBefore_returns_error()
+ {
+ //act
+ var response = await wellOperationClient.InsertRangeAsync(idWell, 1, true, dtosWithError);
+
+ //assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ ///
+ /// Успешное обновление операции
+ ///
+ ///
+ [Fact]
+ public async Task UpdateAsync_returns_success()
+ {
+ //act
+ var dto = dtos.FirstOrDefault()!;
+ var response = await wellOperationClient.UpdateAsync(idWell, 1, dto, CancellationToken.None);
+
+ //assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ ///
+ /// Неуспешное обновление операции
+ ///
+ ///
+ [Fact]
+ public async Task UpdateAsync_returns_error()
+ {
+ //act
+ var dto = dtosWithError.LastOrDefault()!;
+ var response = await wellOperationClient.UpdateAsync(idWell, 1, dto, CancellationToken.None);
+
+ //assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudWebApi.IntegrationTests/Data/Defaults.cs b/AsbCloudWebApi.IntegrationTests/Data/Defaults.cs
index ddb7dbe7..45219449 100644
--- a/AsbCloudWebApi.IntegrationTests/Data/Defaults.cs
+++ b/AsbCloudWebApi.IntegrationTests/Data/Defaults.cs
@@ -14,7 +14,28 @@ namespace AsbCloudWebApi.IntegrationTests.Data
Surname = "test"
}
};
-
+
+ public static WellOperation[] WellOperations = new WellOperation[]
+ {
+ new()
+ {
+ Id = 1,
+ IdWell = 1,
+ IdType = 1,
+ DateStart = DateTimeOffset.UtcNow.AddDays(-1),
+ CategoryInfo = "1",
+ Comment = "1",
+ DepthEnd = 20,
+ DepthStart = 10,
+ DurationHours = 1,
+ IdCategory = 5000,
+ IdPlan = null,
+ IdUser = 1,
+ IdWellSectionType = 1,
+ LastUpdateDate = DateTimeOffset.UtcNow
+ }
+ };
+
public static Deposit[] Deposits = new Deposit[] {
new()
{
diff --git a/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs b/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs
index a0111303..af7303b6 100644
--- a/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs
+++ b/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs
@@ -60,7 +60,8 @@ public class WebAppFactoryFixture : WebApplicationFactory,
dbContext.AddRange(Data.Defaults.RelationsCompanyWell);
dbContext.AddRange(Data.Defaults.Telemetries);
dbContext.AddRange(Data.Defaults.Drillers);
- await dbContext.SaveChangesAsync();
+ dbContext.AddRange(Data.Defaults.WellOperations);
+ await dbContext.SaveChangesAsync();
}
public new async Task DisposeAsync()
diff --git a/AsbCloudWebApi/Controllers/WellOperationController.cs b/AsbCloudWebApi/Controllers/WellOperationController.cs
index ae0c8b92..74d4e16f 100644
--- a/AsbCloudWebApi/Controllers/WellOperationController.cs
+++ b/AsbCloudWebApi/Controllers/WellOperationController.cs
@@ -1,7 +1,13 @@
using AsbCloudApp.Data;
+using AsbCloudApp.Data.WellOperationImport;
+using AsbCloudApp.Data.WellOperationImport.Options;
+using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
+using AsbCloudApp.Services.WellOperationImport;
+using AsbCloudDb.Model;
+using AsbCloudInfrastructure;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -12,12 +18,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using AsbCloudApp.Data.WellOperationImport;
-using AsbCloudApp.Services.WellOperationImport;
-using AsbCloudApp.Data.WellOperationImport.Options;
-using AsbCloudApp.Exceptions;
-using AsbCloudDb.Model;
-using AsbCloudInfrastructure;
namespace AsbCloudWebApi.Controllers
{
@@ -39,8 +39,8 @@ namespace AsbCloudWebApi.Controllers
private readonly IWellOperationExcelParser wellOperationGazpromKhantosExcelParser;
private readonly IUserRepository userRepository;
- public WellOperationController(IWellOperationRepository operationRepository,
- IWellService wellService,
+ public WellOperationController(IWellOperationRepository operationRepository,
+ IWellService wellService,
IWellOperationImportTemplateService wellOperationImportTemplateService,
IWellOperationExportService wellOperationExportService,
IWellOperationImportService wellOperationImportService,
@@ -231,12 +231,16 @@ namespace AsbCloudWebApi.Controllers
if (!await CanUserEditWellOperationsAsync(idWell, cancellationToken))
return Forbid();
-
+
wellOperation.IdWell = idWell;
wellOperation.LastUpdateDate = DateTimeOffset.UtcNow;
wellOperation.IdUser = User.GetUserId();
wellOperation.IdType = idType;
+ var validationResult = await operationRepository.ValidateWithDbAsync(new[] { wellOperation }, cancellationToken);
+ if (validationResult.Any())
+ return this.ValidationBadRequest(validationResult);
+
var result = await operationRepository.InsertRangeAsync(new[] { wellOperation }, cancellationToken);
return Ok(result);
@@ -278,7 +282,7 @@ namespace AsbCloudWebApi.Controllers
await operationRepository.DeleteAsync(existingOperations.Select(o => o.Id), cancellationToken);
}
-
+
foreach (var wellOperation in wellOperations)
{
wellOperation.IdWell = idWell;
@@ -287,11 +291,32 @@ namespace AsbCloudWebApi.Controllers
wellOperation.IdType = idType;
}
+
+ var validationResult = await Validate(wellOperations, deleteBeforeInsert, cancellationToken);
+ if (validationResult.Any())
+ return this.ValidationBadRequest(validationResult);
+
var result = await operationRepository.InsertRangeAsync(wellOperations, cancellationToken);
return Ok(result);
}
+
+ ///
+ /// Валидация данных перед вставкой / обновлением / импортом
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task> Validate(IEnumerable wellOperations, bool deleteBeforeInsert, CancellationToken cancellationToken)
+ {
+ if (deleteBeforeInsert)
+ return operationRepository.Validate(wellOperations);
+ else
+ return await operationRepository.ValidateWithDbAsync(wellOperations, cancellationToken);
+ }
+
///
/// Обновляет выбранную операцию на скважине
///
@@ -308,7 +333,7 @@ namespace AsbCloudWebApi.Controllers
{
if (!await CanUserAccessToWellAsync(idWell, token))
return Forbid();
-
+
if (!await CanUserEditWellOperationsAsync(idWell, token))
return Forbid();
@@ -317,6 +342,10 @@ namespace AsbCloudWebApi.Controllers
value.LastUpdateDate = DateTimeOffset.UtcNow;
value.IdUser = User.GetUserId();
+ var validationResult = await operationRepository.ValidateWithDbAsync(new[] { value }, token);
+ if (validationResult.Any())
+ return this.ValidationBadRequest(validationResult);
+
var result = await operationRepository.UpdateAsync(value, token)
.ConfigureAwait(false);
return Ok(result);
@@ -336,7 +365,7 @@ namespace AsbCloudWebApi.Controllers
{
if (!await CanUserAccessToWellAsync(idWell, token))
return Forbid();
-
+
if (!await CanUserEditWellOperationsAsync(idWell, token))
return Forbid();
@@ -373,7 +402,7 @@ namespace AsbCloudWebApi.Controllers
deleteBeforeInsert,
cancellationToken);
}
-
+
///
/// Импорт плановых операций из excel (xlsx) файла. Стандартный заполненный шаблон
///
@@ -393,7 +422,7 @@ namespace AsbCloudWebApi.Controllers
{
IdType = WellOperation.IdOperationTypePlan
};
-
+
return ImportExcelFileAsync(idWell, files, options,
(stream, _) => wellOperationDefaultExcelParser.Parse(stream, options),
null,
@@ -522,7 +551,7 @@ namespace AsbCloudWebApi.Controllers
return this.ValidationBadRequest(nameof(files), "Требуется xlsx файл.");
using Stream stream = file.OpenReadStream();
-
+
try
{
var sheet = parseMethod(stream, options);
@@ -541,8 +570,8 @@ namespace AsbCloudWebApi.Controllers
//TODO: очень быстрый костыль
if (deleteBeforeInsert is not null && options.IdType == WellOperation.IdOperationTypeFact)
{
- return await InsertRangeAsync(idWell, options.IdType,
- deleteBeforeInsert.Value,
+ return await InsertRangeAsync(idWell, options.IdType,
+ deleteBeforeInsert.Value,
wellOperations,
cancellationToken);
}
@@ -554,21 +583,21 @@ namespace AsbCloudWebApi.Controllers
return this.ValidationBadRequest(nameof(files), ex.Message);
}
}
-
+
private async Task CanUserAccessToWellAsync(int idWell, CancellationToken token)
{
int? idCompany = User.GetCompanyId();
return idCompany is not null && await wellService.IsCompanyInvolvedInWellAsync((int)idCompany,
idWell, token).ConfigureAwait(false);
}
-
+
private async Task CanUserEditWellOperationsAsync(int idWell, CancellationToken token)
{
var idUser = User.GetUserId();
if (!idUser.HasValue)
return false;
-
+
var well = await wellService.GetOrDefaultAsync(idWell, token);
if (well is null)
diff --git a/AsbCloudWebApi/Extentions.cs b/AsbCloudWebApi/Extentions.cs
index ba87612f..5659292c 100644
--- a/AsbCloudWebApi/Extentions.cs
+++ b/AsbCloudWebApi/Extentions.cs
@@ -3,6 +3,8 @@ using AsbCloudWebApi.Converters;
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
using System.Security.Claims;
namespace Microsoft.AspNetCore.Mvc
@@ -53,6 +55,27 @@ namespace Microsoft.AspNetCore.Mvc
return controller.BadRequest(problem);
}
+ ///
+ ///
+ /// Returns BadRequest with ValidationProblemDetails as body
+ ///
+ ///
+ /// Используйте этот метод только если валидацию нельзя сделать через
+ /// атрибуты валидации или IValidatableObject модели.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static BadRequestObjectResult ValidationBadRequest(this ControllerBase controller, IEnumerable validationResults)
+ {
+ var errors = validationResults
+ .SelectMany(e => e.MemberNames.Select(name=> new { name, e.ErrorMessage }))
+ .ToDictionary(e => e.name, e => new[] { e.ErrorMessage ?? string.Empty });
+ var problem = new ValidationProblemDetails(errors);
+ return controller.BadRequest(problem);
+ }
+
public static MvcOptions UseDateOnlyTimeOnlyStringConverters(this MvcOptions options)
{
TypeDescriptor.AddAttributes(typeof(DateOnly), new TypeConverterAttribute(typeof(DateOnlyTypeConverter)));