diff --git a/AsbCloudApp/Repositories/IChangeLogRepository.cs b/AsbCloudApp/Repositories/IChangeLogRepository.cs index 9a5ab8c9..44a6faed 100644 --- a/AsbCloudApp/Repositories/IChangeLogRepository.cs +++ b/AsbCloudApp/Repositories/IChangeLogRepository.cs @@ -1,15 +1,18 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using AsbCloudApp.Data; +using AsbCloudApp.Requests; namespace AsbCloudApp.Repositories; /// /// Репозиторий для записей с историей /// -public interface IChangeLogRepository - where T : ChangeLogAbstract +public interface IChangeLogRepository + where TDto : ChangeLogAbstract + where TRequest : ChangeLogBaseRequest { /// /// Добавление записей @@ -18,7 +21,7 @@ public interface IChangeLogRepository /// /// /// - Task InsertRange(int idUser, IEnumerable dtos, CancellationToken token); + Task InsertRange(int idUser, IEnumerable dtos, CancellationToken token); /// /// Редактирование записей @@ -27,15 +30,69 @@ public interface IChangeLogRepository /// /// /// - Task UpdateRange(int idUser, IEnumerable dtos, CancellationToken token); + Task UpdateRange(int idUser, IEnumerable dtos, CancellationToken token); + + /// + /// Добавляет Dto у которых id == 0, изменяет dto у которых id != 0 + /// + /// + /// + /// + /// + Task UpdateOrInsertRange(int idUser, IEnumerable dtos, CancellationToken token); + + /// + /// Добавление записей с удалением старых (для импорта) + /// + /// + /// + /// + /// + Task Clear(int idUser, TRequest request, CancellationToken token); + + /// + /// Очистить и добавить новые + /// + /// + /// + /// + /// + /// + Task ClearAndInsertRange(int idUser, TRequest request, IEnumerable dtos, CancellationToken token); + + /// + /// Удаление записей + /// + /// + /// + /// + /// + Task DeleteRange(int idUser, IEnumerable ids, CancellationToken token); + + /// + /// Получение дат изменений записей + /// + /// + /// + /// + Task> GetDatesChange(TRequest request, CancellationToken token); + + /// + /// Получение журнала изменений + /// + /// + /// Фильтр по дате. Если null - вернет все + /// + /// + Task> GetChangeLog(TRequest request, DateOnly? date, CancellationToken token); + + /// + /// Получение записей по параметрам + /// + /// + /// + /// + Task> Get(TRequest request, CancellationToken token); - /// - /// Удаление записей - /// - /// - /// - /// - /// - Task DeleteRange(int idUser, IEnumerable ids, CancellationToken token); } diff --git a/AsbCloudApp/Repositories/IProcessMapPlanBaseRepository.cs b/AsbCloudApp/Repositories/IProcessMapPlanBaseRepository.cs deleted file mode 100644 index 7524002a..00000000 --- a/AsbCloudApp/Repositories/IProcessMapPlanBaseRepository.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using AsbCloudApp.Data.ProcessMapPlan; -using AsbCloudApp.Requests; - -namespace AsbCloudApp.Repositories; - -/// -/// Общий интерфейс для РТК план с учетом истории изменений -/// -/// -public interface IProcessMapPlanBaseRepository: IChangeLogRepository - where T: ProcessMapPlanBaseDto -{ - /// - /// Добавление записей с удалением старых (для импорта) - /// - /// - /// - /// - /// - /// - Task ClearAndInsertRange(int idUser, int idWell, IEnumerable dtos, CancellationToken token); - - /// - /// Получение дат изменений записей - /// - /// - /// - /// - Task> GetDatesChange(int idWell, CancellationToken token); - - /// - /// Получение журнала изменений - /// - /// - /// - /// - /// - Task> GetChangeLog(int idWell, DateOnly? date, CancellationToken token); - - /// - /// Получение записей по параметрам - /// - /// - /// - /// - /// - Task> Get(int idWell, ProcessMapPlanBaseRequest request, CancellationToken token); -} \ No newline at end of file diff --git a/AsbCloudApp/Repositories/IProcessMapPlanRepository.cs b/AsbCloudApp/Repositories/IProcessMapPlanRepository.cs index 1828b3ed..7e41cb58 100644 --- a/AsbCloudApp/Repositories/IProcessMapPlanRepository.cs +++ b/AsbCloudApp/Repositories/IProcessMapPlanRepository.cs @@ -4,12 +4,14 @@ using System.Threading; using AsbCloudApp.Data.ProcessMaps; using AsbCloudApp.Requests; using AsbCloudApp.Services; +using System; namespace AsbCloudApp.Repositories; /// /// РТК план /// +[Obsolete] public interface IProcessMapPlanRepository : IRepositoryWellRelated where TDto : ProcessMapPlanBaseDto { 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/AsbCloudApp/Requests/ProcessMapPlanBaseRequest.cs b/AsbCloudApp/Requests/ProcessMapPlanBaseRequest.cs index 74fc0ec4..006c26e5 100644 --- a/AsbCloudApp/Requests/ProcessMapPlanBaseRequest.cs +++ b/AsbCloudApp/Requests/ProcessMapPlanBaseRequest.cs @@ -18,4 +18,37 @@ public class ProcessMapPlanBaseRequest: ChangeLogBaseRequest /// Вернуть данные, которые поменялись с указанной даты /// public DateTimeOffset? UpdateFrom { get; set; } +} + +/// +/// Запрос для получения РТК план по скважине +/// +public class ProcessMapPlanBaseRequestWithWell: ProcessMapPlanBaseRequest +{ + /// + /// Запрос для получения РТК план по скважине + /// + /// + public ProcessMapPlanBaseRequestWithWell(int idWell) + { + IdWell = idWell; + } + + /// + /// Запрос для получения РТК план по скважине + /// + /// + /// + public ProcessMapPlanBaseRequestWithWell(ProcessMapPlanBaseRequest request, int idWell) + { + IdWell=idWell; + IdWellSectionType=request.IdWellSectionType; + UpdateFrom = request.UpdateFrom; + Moment = request.Moment; + } + + /// + /// Id скважины + /// + public int IdWell { get; set; } } \ No newline at end of file diff --git a/AsbCloudDb/Model/ChangeLogAbstract.cs b/AsbCloudDb/Model/ChangeLogAbstract.cs index f4065ea6..d9bc74e0 100644 --- a/AsbCloudDb/Model/ChangeLogAbstract.cs +++ b/AsbCloudDb/Model/ChangeLogAbstract.cs @@ -28,7 +28,7 @@ public abstract class ChangeLogAbstract /// /// Очищено при импорте /// - public const int IdClearedOnImport = 3; + public const int IdCleared = 3; /// /// Ид записи diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index eec917a7..ce33cda3 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -195,10 +195,10 @@ namespace AsbCloudInfrastructure services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddScoped(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddScoped(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -222,7 +222,9 @@ namespace AsbCloudInfrastructure services.AddTransient(); services.AddTransient(); - services.AddTransient, ProcessMapPlanBaseRepository>(); + services.AddTransient< + IChangeLogRepository, + ProcessMapPlanBaseRepository>(); services.AddTransient(); diff --git a/AsbCloudInfrastructure/Repository/ChangeLogRepositoryAbstract.cs b/AsbCloudInfrastructure/Repository/ChangeLogRepositoryAbstract.cs new file mode 100644 index 00000000..84506a21 --- /dev/null +++ b/AsbCloudInfrastructure/Repository/ChangeLogRepositoryAbstract.cs @@ -0,0 +1,279 @@ +using AsbCloudApp.Exceptions; +using AsbCloudApp.Repositories; +using AsbCloudApp.Requests; +using AsbCloudDb.Model; +using Mapster; +using Microsoft.EntityFrameworkCore; +using Npgsql; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.Repository; + +public abstract class ChangeLogRepositoryAbstract : IChangeLogRepository + where TDto : AsbCloudApp.Data.ChangeLogAbstract + where TEntity : ChangeLogAbstract + where TRequest : ChangeLogBaseRequest +{ + protected readonly IAsbCloudDbContext context; + + public ChangeLogRepositoryAbstract(IAsbCloudDbContext context) + { + this.context = context; + } + + public async Task InsertRange(int idUser, IEnumerable dtos, CancellationToken token) + { + var result = 0; + if (dtos.Any()) + { + var entities = dtos.Select(Convert); + var creation = DateTimeOffset.UtcNow; + var dbSet = context.Set(); + foreach (var entity in entities) + { + entity.Id = default; + entity.IdAuthor = idUser; + entity.Creation = creation; + entity.IdState = ChangeLogAbstract.IdStateActual; + entity.IdEditor = null; + entity.IdPrevious = null; + entity.Obsolete = null; + dbSet.Add(entity); + } + + result += await SaveChangesWithExceptionHandling(token); + } + return result; + } + + public async Task UpdateRange(int idUser, IEnumerable dtos, CancellationToken token) + { + if (dtos.Any(d => d.Id == 0)) + throw new ArgumentInvalidException(nameof(dtos), "Отредактированные значения должны иметь id не 0"); + + if (!dtos.Any()) + return 0; + + var updateTime = DateTimeOffset.UtcNow; + + var ids = dtos.Select(d => d.Id); + + using var transaction = context.Database.BeginTransaction(); + var result = 0; + var dbSet = context + .Set(); + + var entitiesToDelete = await dbSet + .Where(e => e.Obsolete == null) + .Where(e => ids.Contains(e.Id)) + .ToArrayAsync(token); + + var entitiesNotFound = dtos.Where(d => !entitiesToDelete.Any(e => e.Id == d.Id)); + if (entitiesNotFound.Any()) + { + var notFoundIds = entitiesNotFound.Select(e => e.Id); + var stringnotFoundIds = string.Join(", ", notFoundIds); + throw new ArgumentInvalidException(nameof(dtos), $"записи с id:[{stringnotFoundIds}] не найдены, или не актуальны."); + } + + foreach (var entity in entitiesToDelete) + { + entity.IdState = ChangeLogAbstract.IdStateReplaced; + entity.Obsolete = updateTime; + entity.IdEditor = idUser; + } + result += await context.SaveChangesAsync(token); + + var entitiesNew = dtos.Select(Convert); + foreach (var entity in entitiesNew) + { + entity.IdPrevious = entity.Id; + entity.Id = default; + entity.Creation = updateTime; + entity.IdAuthor = idUser; + entity.Obsolete = null; + entity.IdEditor = null; + entity.IdState = ChangeLogAbstract.IdStateActual; + dbSet.Add(entity); + } + + result += await SaveChangesWithExceptionHandling(token); + await transaction.CommitAsync(token); + return result; + } + + public async Task UpdateOrInsertRange(int idUser, IEnumerable dtos, CancellationToken token) + { + var itemsToInsert = dtos.Where(e => e.Id == 0); + var itemsToUpdate = dtos.Where(e => e.Id != 0); + + var result = 0; + if (itemsToInsert.Any()) + result += await InsertRange(idUser, itemsToInsert, token); + + if (itemsToUpdate.Any()) + result += await UpdateRange(idUser, itemsToUpdate, token); + + return result; + } + + public async Task Clear(int idUser, TRequest request, CancellationToken token) + { + var updateTime = DateTimeOffset.UtcNow; + var query = BuildQuery(request); + query = query.Where(e => e.Obsolete == null); + + var entitiesToDelete = await query.ToArrayAsync(token); + + foreach (var entity in entitiesToDelete) + { + entity.IdState = ChangeLogAbstract.IdCleared; + entity.Obsolete = updateTime; + entity.IdEditor = idUser; + } + + var result = await SaveChangesWithExceptionHandling(token); + return result; + } + + public async Task ClearAndInsertRange(int idUser, TRequest request, IEnumerable dtos, CancellationToken token) + { + var result = 0; + var transaction = await context.Database.BeginTransactionAsync(token); + result += await Clear(idUser, request, token); + result += await InsertRange(idUser, dtos, token); + await transaction.CommitAsync(token); + return result; + } + + public async Task DeleteRange(int idUser, IEnumerable ids, CancellationToken token) + { + var updateTime = DateTimeOffset.UtcNow; + var query = context.Set() + .Where(e => ids.Contains(e.Id)) + .Where(e => e.Obsolete == null); + + var entitiesToDelete = await query.ToArrayAsync(token); + + foreach (var entity in entitiesToDelete) + { + entity.IdState = ChangeLogAbstract.IdStateDeleted; + entity.Obsolete = updateTime; + entity.IdEditor = idUser; + } + + var result = await SaveChangesWithExceptionHandling(token); + return result; + } + + public async Task> GetDatesChange(TRequest request, CancellationToken token) + { + var query = BuildQuery(request); + + var datesCreateQuery = query + .Select(e => e.Creation) + .Distinct(); + + var datesCreate = await datesCreateQuery.ToArrayAsync(token); + + var datesUpdateQuery = query + .Where(e => e.Obsolete != null) + .Select(e => e.Obsolete!.Value) + .Distinct(); + + var datesUpdate = await datesUpdateQuery.ToArrayAsync(token); + + TimeSpan offset = GetTimezoneOffset(request); + + var dates = Enumerable.Concat(datesCreate, datesUpdate); + dates = dates.Select(date => date.ToOffset(offset)); + var datesOnly = dates + .Select(d => new DateOnly(d.Year, d.Month, d.Day)) + .Distinct(); + + return datesOnly; + } + + public async Task> GetChangeLog(TRequest request, DateOnly? date, CancellationToken token) + { + var query = BuildQuery(request); + TimeSpan offset = GetTimezoneOffset(request); + + if (date.HasValue) + { + var min = new DateTimeOffset(date.Value.Year, date.Value.Month, date.Value.Day, 0, 0, 0, offset).ToUniversalTime(); + var max = min.AddDays(1); + + var createdQuery = query.Where(e => e.Creation >= min && e.Creation <= max); + var editedQuery = query.Where(e => e.Obsolete != null && e.Obsolete >= min && e.Obsolete <= max); + + query = createdQuery.Union(editedQuery); + } + + var entities = await query.ToListAsync(token); + var dtos = entities.Select(e => Convert(e, offset)); + + return dtos; + } + + public async Task> Get(TRequest request, CancellationToken token) + { + var query = BuildQuery(request); + var entities = await query.ToArrayAsync(token); + + TimeSpan offset = GetTimezoneOffset(request); + var dtos = entities.Select(e => Convert(e, offset)); + return dtos; + } + + protected abstract TimeSpan GetTimezoneOffset(TRequest request); + + protected abstract IQueryable BuildQuery(TRequest request); + + protected virtual TEntity Convert(TDto dto) + { + var entity = dto.Adapt(); + entity.Creation = entity.Creation.ToUniversalTime(); + + if(entity.Obsolete.HasValue) + entity.Obsolete = entity.Obsolete.Value.ToUniversalTime(); + + return entity; + } + + protected virtual TDto Convert(TEntity entity, TimeSpan offset) + { + var dto = entity.Adapt(); + dto.Creation = entity.Creation.ToOffset(offset); + + if (entity.Obsolete.HasValue) + dto.Obsolete = entity.Obsolete.Value.ToOffset(offset); + + return dto; + } + + private async Task SaveChangesWithExceptionHandling(CancellationToken token) + { + try + { + var result = await context.SaveChangesAsync(token); + return result; + } + catch (DbUpdateException ex) + { + if (ex.InnerException is PostgresException pgException) + TryConvertPostgresExceptionToValidateException(pgException); + throw; + } + } + + private static void TryConvertPostgresExceptionToValidateException(PostgresException pgException) + { + if (pgException.SqlState == PostgresErrorCodes.ForeignKeyViolation) + throw new ArgumentInvalidException("dtos", pgException.Message + "\r\n" + pgException.Detail); + } +} diff --git a/AsbCloudInfrastructure/Repository/ProcessMapPlanBaseRepository.cs b/AsbCloudInfrastructure/Repository/ProcessMapPlanBaseRepository.cs index 11b4d0fd..0133b527 100644 --- a/AsbCloudInfrastructure/Repository/ProcessMapPlanBaseRepository.cs +++ b/AsbCloudInfrastructure/Repository/ProcessMapPlanBaseRepository.cs @@ -1,115 +1,36 @@ using AsbCloudApp.Data.ProcessMapPlan; -using AsbCloudApp.Exceptions; -using AsbCloudApp.Repositories; using AsbCloudApp.Requests; using AsbCloudApp.Services; using AsbCloudDb.Model; using AsbCloudDb.Model.ProcessMapPlan; -using AsbCloudDb.Model.WellSections; -using Mapster; using Microsoft.EntityFrameworkCore; -using Npgsql; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace AsbCloudInfrastructure.Repository; -public class ProcessMapPlanBaseRepository : IProcessMapPlanBaseRepository +public class ProcessMapPlanBaseRepository : ChangeLogRepositoryAbstract where TDto : ProcessMapPlanBaseDto where TEntity : ProcessMapPlanBase { - private readonly IAsbCloudDbContext context; private readonly IWellService wellService; - public ProcessMapPlanBaseRepository(IAsbCloudDbContext context, IWellService wellService) + public ProcessMapPlanBaseRepository(IAsbCloudDbContext context, IWellService wellService) + : base(context) { - this.context = context; this.wellService = wellService; } - public async Task InsertRange(int idUser, IEnumerable dtos, CancellationToken token) + protected override IQueryable BuildQuery(ProcessMapPlanBaseRequestWithWell request) { - var result = 0; - if (dtos.Any()) - { - var entities = dtos.Select(Convert); - var creation = DateTimeOffset.UtcNow; - var dbSet = context.Set(); - foreach (var entity in entities) { - entity.Id = default; - entity.IdAuthor = idUser; - entity.Creation = creation; - entity.IdState = ChangeLogAbstract.IdStateActual; - entity.IdEditor = null; - entity.Editor = null; - entity.IdPrevious = null; - entity.Obsolete = null; - dbSet.Add(entity); - } - - result += await SaveChangesWithExceptionHandling(token); - } - return result; - } - - public async Task ClearAndInsertRange(int idUser, int idWell, IEnumerable dtos, CancellationToken token) - { - if (dtos.Any(d => d.IdWell != idWell)) - throw new ArgumentInvalidException(nameof(dtos), $"Все записи должны относиться к скважине idWell = {idWell}"); - - using var transaction = context.Database.BeginTransaction(); - var result = 0; - - var dbSet = context.Set(); - var entitiesToMarkDeleted = dbSet - .Where(e => e.IdWell == idWell) - .Where(e => e.Obsolete == null); - var obsolete = DateTimeOffset.UtcNow; - foreach (var entity in entitiesToMarkDeleted) - { - entity.IdState = ChangeLogAbstract.IdClearedOnImport; - entity.Obsolete = obsolete; - entity.IdEditor = idUser; - } - result += await SaveChangesWithExceptionHandling(token); - result += await InsertRange(idUser, dtos, token); - await transaction.CommitAsync(token); - - return result; - } - - public async Task DeleteRange(int idUser, IEnumerable ids, CancellationToken token) - { - var dbSet = context.Set(); - var entitiesToMarkDeleted = dbSet - .Where(e => ids.Contains(e.Id)) - .Where(e => e.Obsolete == null); - var obsolete = DateTimeOffset.UtcNow; - foreach (var entity in entitiesToMarkDeleted) - { - entity.IdState = ChangeLogAbstract.IdStateDeleted; - entity.Obsolete = obsolete; - entity.IdEditor = idUser; - } - var result = await SaveChangesWithExceptionHandling(token); - return result; - } - - public async Task> Get(int idWell, ProcessMapPlanBaseRequest request, CancellationToken token) - { - var timezone = wellService.GetTimezone(idWell); - var offset = TimeSpan.FromHours(timezone.Hours); - var query = context .Set() .Include(e => e.Author) .Include(e => e.Editor) - .Where(e => e.IdWell == idWell); + .Include(e => e.Well) + .Where(e => e.IdWell == request.IdWell); - if(request.IdWellSectionType.HasValue) + if (request.IdWellSectionType.HasValue) query = query.Where(e => e.IdWellSectionType == request.IdWellSectionType); if (request.UpdateFrom.HasValue) @@ -126,156 +47,13 @@ public class ProcessMapPlanBaseRepository : IProcessMapPlanBaseRe .Where(e => e.Obsolete == null || e.Obsolete >= moment); } - var entities = await query.ToArrayAsync(token); - var dtos = entities.Select(e => Convert(e, offset)); - return dtos; + return query; } - public async Task> GetChangeLog(int idWell, DateOnly? date, CancellationToken token) + protected override TimeSpan GetTimezoneOffset(ProcessMapPlanBaseRequestWithWell request) { - var query = context - .Set() - .Include(e => e.Author) - .Include(e => e.Editor) - .Where(e => e.IdWell == idWell); - - var timezone = wellService.GetTimezone(idWell); + var timezone = wellService.GetTimezone(request.IdWell); var offset = TimeSpan.FromHours(timezone.Hours); - - if (date.HasValue) - { - var min = new DateTimeOffset(date.Value.Year, date.Value.Month, date.Value.Day, 0, 0, 0, offset).ToUniversalTime(); - var max = min.AddDays(1); - - var createdQuery = query.Where(e => e.Creation >= min && e.Creation <= max); - var editedQuery = query.Where(e => e.Obsolete != null && e.Obsolete >= min && e.Obsolete <= max); - - query = createdQuery.Union(editedQuery); - } - - var entities = await query.ToListAsync(token); - var dtos = entities.Select(e => Convert(e, offset)); - - return dtos; - } - - public async Task> GetDatesChange(int idWell, CancellationToken token) - { - var wellEntitiesQuery = context - .Set() - .Where(e => e.IdWell == idWell); - - var datesCreateQuery = wellEntitiesQuery - .Select(e => e.Creation) - .Distinct(); - - var datesCreate = await datesCreateQuery.ToArrayAsync(token); - - var datesUpdateQuery = wellEntitiesQuery - .Where(e => e.Obsolete != null) - .Select(e => e.Obsolete!.Value) - .Distinct(); - - var datesUpdate = await datesUpdateQuery.ToArrayAsync(token); - - var timezone = wellService.GetTimezone(idWell); - var offset = TimeSpan.FromHours(timezone.Hours); - - var dates = Enumerable.Concat( datesCreate, datesUpdate); - dates = dates.Select(date => date.ToOffset(offset)); - var datesOnly = dates - .Select(d => new DateOnly(d.Year, d.Month, d.Day)) - .Distinct(); - - return datesOnly; - } - - public async Task UpdateRange(int idUser, IEnumerable dtos, CancellationToken token) - { - if (dtos.Any(d => d.Id == 0)) - throw new ArgumentInvalidException(nameof(dtos), "Отредактированные значения должны иметь id больше 0"); - - if (!dtos.Any()) - return 0; - - using var transaction = context.Database.BeginTransaction(); - var result = 0; - - var ids = dtos.Select(d => d.Id); - var dbSet = context.Set(); - - var entitiesToDelete = dbSet - .Where(e => ids.Contains(e.Id)); - - var updateTime = DateTimeOffset.UtcNow; - foreach (var entity in entitiesToDelete) - { - if(entity.Obsolete is not null) - throw new ArgumentInvalidException(nameof(dtos), "Недопустимо редактировать устаревшие записи"); - entity.IdState = ChangeLogAbstract.IdStateReplaced; - entity.Obsolete = updateTime; - entity.IdEditor = idUser; - } - result += await context.SaveChangesAsync(token); - - var entitiesNew = dtos.Select(Convert); - foreach (var entity in entitiesNew) - { - entity.IdPrevious = entity.Id; - entity.Id = default; - entity.Creation = updateTime; - entity.IdAuthor = idUser; - entity.Obsolete = null; - entity.IdEditor = null; - entity.IdState = ChangeLogAbstract.IdStateActual; - dbSet.Add(entity); - } - - result += await SaveChangesWithExceptionHandling(token); - await transaction.CommitAsync(token); - return result; - } - - protected virtual TEntity Convert(TDto dto) - { - var entity = dto.Adapt(); - entity.Creation = entity.Creation.ToUniversalTime(); - - if(entity.Obsolete.HasValue) - entity.Obsolete = entity.Obsolete.Value.ToUniversalTime(); - - return entity; - } - - protected virtual TDto Convert(TEntity entity, TimeSpan offset) - { - var dto = entity.Adapt(); - dto.Creation = entity.Creation.ToOffset(offset); - - if (entity.Obsolete.HasValue) - dto.Obsolete = entity.Obsolete.Value.ToOffset(offset); - - return dto; - } - - private async Task SaveChangesWithExceptionHandling(CancellationToken token) - { - try - { - var result = await context.SaveChangesAsync(token); - return result; - } - catch (DbUpdateException ex) - { - if (ex.InnerException is PostgresException pgException) - TryConvertPostgresExceptionToValidateException(pgException); - throw; - } - } - - private static void TryConvertPostgresExceptionToValidateException(PostgresException pgException) - { - if (pgException.SqlState == PostgresErrorCodes.ForeignKeyViolation) - throw new ArgumentInvalidException("dtos", pgException.Message + "\r\n" + pgException.Detail); + return offset; } } 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/IProcessMapPlanDrillingClient.cs b/AsbCloudWebApi.IntegrationTests/Clients/IProcessMapPlanDrillingClient.cs index 45dfd59a..7c8c65e3 100644 --- a/AsbCloudWebApi.IntegrationTests/Clients/IProcessMapPlanDrillingClient.cs +++ b/AsbCloudWebApi.IntegrationTests/Clients/IProcessMapPlanDrillingClient.cs @@ -18,6 +18,9 @@ public interface IProcessMapPlanDrillingClient [Delete(BaseRoute)] Task> DeleteRange(int idWell, [Body] IEnumerable ids); + [Delete($"{BaseRoute}/clear")] + Task> Clear(int idWell); + [Get(BaseRoute)] Task>> Get(int idWell, ProcessMapPlanBaseRequest request); @@ -28,5 +31,5 @@ public interface IProcessMapPlanDrillingClient Task>> GetDatesChange(int idWell); [Put(BaseRoute)] - Task> UpdateRangeAsync(int idWell, IEnumerable dtos); + Task> UpdateOrInsertRange(int idWell, IEnumerable dtos); } 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/ProcessMapPlanDrillingControllerTest.cs b/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlanDrillingControllerTest.cs index 7bdf3135..c0ba7093 100644 --- a/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlanDrillingControllerTest.cs +++ b/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlanDrillingControllerTest.cs @@ -179,7 +179,7 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest Assert.Equal(2, count); var oldEntity = dbset.First(p => p.Id == entry.Entity.Id); - Assert.Equal(ProcessMapPlanBase.IdClearedOnImport, oldEntity.IdState); + Assert.Equal(ProcessMapPlanBase.IdCleared, oldEntity.IdState); Assert.Equal(1, oldEntity.IdEditor); Assert.NotNull(oldEntity.Obsolete); Assert.InRange(oldEntity.Obsolete.Value, startTime, doneTime); @@ -194,44 +194,61 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest } [Fact] - public async Task UpdateRange_returns_success() + public async Task UpdateOrInsertRange_returns_success() { // arrange var startTime = DateTimeOffset.UtcNow; - + var dbset = dbContext.Set(); - + var entry = dbset.Add(entity); dbContext.SaveChanges(); entry.State = EntityState.Detached; - var dtoCopy = dto.Adapt(); - dtoCopy.Id = entry.Entity.Id; - dtoCopy.Comment = "nebuchadnezzar"; - dtoCopy.DeltaPressureLimitMax ++; - dtoCopy.DeltaPressurePlan ++; - dtoCopy.FlowPlan ++; - dtoCopy.FlowLimitMax ++; - dtoCopy.RopPlan ++; - dtoCopy.AxialLoadPlan ++; - dtoCopy.AxialLoadLimitMax ++; - dtoCopy.DepthStart ++; - dtoCopy.DepthEnd ++; - dtoCopy.TopDriveSpeedPlan ++; - dtoCopy.TopDriveSpeedLimitMax ++; - dtoCopy.TopDriveTorquePlan ++; - dtoCopy.TopDriveTorqueLimitMax ++; + var dtoUpdate = dto.Adapt(); + dtoUpdate.Id = entry.Entity.Id; + dtoUpdate.Comment = "nebuchadnezzar"; + dtoUpdate.DeltaPressureLimitMax++; + dtoUpdate.DeltaPressurePlan++; + dtoUpdate.FlowPlan++; + dtoUpdate.FlowLimitMax++; + dtoUpdate.RopPlan++; + dtoUpdate.AxialLoadPlan++; + dtoUpdate.AxialLoadLimitMax++; + dtoUpdate.DepthStart++; + dtoUpdate.DepthEnd++; + dtoUpdate.TopDriveSpeedPlan++; + dtoUpdate.TopDriveSpeedLimitMax++; + dtoUpdate.TopDriveTorquePlan++; + dtoUpdate.TopDriveTorqueLimitMax++; + + var dtoInsert = dtoUpdate.Adapt(); + dtoInsert.Id = 0; + dtoInsert.Comment = "nebuchad"; + dtoInsert.DeltaPressureLimitMax++; + dtoInsert.DeltaPressurePlan++; + dtoInsert.FlowPlan++; + dtoInsert.FlowLimitMax++; + dtoInsert.RopPlan++; + dtoInsert.AxialLoadPlan++; + dtoInsert.AxialLoadLimitMax++; + dtoInsert.DepthStart++; + dtoInsert.DepthEnd++; + dtoInsert.TopDriveSpeedPlan++; + dtoInsert.TopDriveSpeedLimitMax++; + dtoInsert.TopDriveTorquePlan++; + dtoInsert.TopDriveTorqueLimitMax++; // act - var result = await client.UpdateRangeAsync(entity.IdWell, new ProcessMapPlanDrillingDto[] { dtoCopy }); + var result = await client.UpdateOrInsertRange(entity.IdWell, new ProcessMapPlanDrillingDto[] { dtoUpdate, dtoInsert }); // assert var doneTime = DateTimeOffset.UtcNow; Assert.Equal(HttpStatusCode.OK, result.StatusCode); - Assert.Equal(2, result.Content); + Assert.Equal(3, result.Content); var count = dbset.Count(); - Assert.Equal(2, count); + Assert.Equal(3, count); var oldEntity = dbset.First(p => p.Id == entry.Entity.Id); Assert.Equal(ProcessMapPlanBase.IdStateReplaced, oldEntity.IdState); @@ -239,7 +256,7 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest Assert.NotNull(oldEntity.Obsolete); Assert.InRange(oldEntity.Obsolete.Value, startTime, doneTime); - var newEntity = dbset.First(p => p.Id != entry.Entity.Id); + var newEntity = dbset.First(p => p.Comment == dtoUpdate.Comment); Assert.Equal(ProcessMapPlanBase.IdStateActual, newEntity.IdState); Assert.Equal(1, newEntity.IdAuthor); Assert.Null(newEntity.IdEditor); @@ -247,7 +264,7 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest Assert.Equal(oldEntity.Id, newEntity.IdPrevious); Assert.InRange(newEntity.Creation, startTime, doneTime); - var expected = dtoCopy.Adapt(); + var expected = dtoUpdate.Adapt(); var excludeProps = new[] { nameof(ProcessMapPlanDrilling.Id), nameof(ProcessMapPlanDrilling.Author), @@ -289,6 +306,39 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest Assert.InRange(actual.Obsolete.Value, startTime, doneTime); } + + [Fact] + public async Task Clear_returns_success() + { + //arrange + var dbset = dbContext.Set(); + + var entry = dbset.Add(entity); + dbContext.SaveChanges(); + entry.State = EntityState.Detached; + + var startTime = DateTimeOffset.UtcNow; + + //act + var response = await client.Clear(dto.IdWell); + + //assert + var doneTime = DateTimeOffset.UtcNow; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(1, response.Content); + + var actual = dbContext + .Set() + .FirstOrDefault(p => p.Id == entry.Entity.Id); + + Assert.NotNull(actual); + Assert.Equal(ProcessMapPlanBase.IdCleared, actual.IdState); + Assert.Equal(1, actual.IdEditor); + Assert.NotNull(actual.Obsolete); + Assert.InRange(actual.Obsolete.Value, startTime, doneTime); + } + + [Fact] public async Task GetDatesChange_returns_success() { 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/ProcessMapPlan/ProcessMapPlanBaseController.cs b/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanBaseController.cs index 1237e628..c3fd0c49 100644 --- a/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanBaseController.cs +++ b/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanBaseController.cs @@ -23,10 +23,10 @@ namespace AsbCloudWebApi.Controllers.ProcessMapPlan; public abstract class ProcessMapPlanBaseController : ControllerBase where TDto : ProcessMapPlanBaseDto { - private readonly IProcessMapPlanBaseRepository repository; + private readonly IChangeLogRepository repository; private readonly IWellService wellService; - public ProcessMapPlanBaseController(IProcessMapPlanBaseRepository repository, IWellService wellService) + public ProcessMapPlanBaseController(IChangeLogRepository repository, IWellService wellService) { this.repository = repository; this.wellService = wellService; @@ -68,7 +68,9 @@ public abstract class ProcessMapPlanBaseController : ControllerBase return this.ValidationBadRequest(nameof(dtos), "all dtos should contain same idWell"); var idUser = await AssertUserHasAccessToWell(idWell, token); - var result = await repository.ClearAndInsertRange(idUser, idWell, dtos, token); + + var request = new ProcessMapPlanBaseRequestWithWell(idWell); + var result = await repository.ClearAndInsertRange(idUser, request, dtos, token); return Ok(result); } @@ -85,10 +87,29 @@ public abstract class ProcessMapPlanBaseController : ControllerBase public async Task DeleteRange([FromRoute]int idWell, IEnumerable ids, CancellationToken token) { var idUser = await AssertUserHasAccessToWell(idWell, token); + var result = await repository.DeleteRange(idUser, ids, token); return Ok(result); } + /// + /// Очистка + /// + /// + /// + /// + [HttpDelete("clear")] + [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + public async Task Clear([FromRoute] int idWell, CancellationToken token) + { + var idUser = await AssertUserHasAccessToWell(idWell, token); + + var request = new ProcessMapPlanBaseRequestWithWell(idWell); + var result = await repository.Clear(idUser, request, token); + return Ok(result); + } + /// /// Получение /// @@ -102,7 +123,9 @@ public abstract class ProcessMapPlanBaseController : ControllerBase public async Task>> Get([FromRoute] int idWell, [FromQuery]ProcessMapPlanBaseRequest request, CancellationToken token) { await AssertUserHasAccessToWell(idWell, token); - var result = await repository.Get(idWell, request, token); + + var serviceRequest = new ProcessMapPlanBaseRequestWithWell(request, idWell); + var result = await repository.Get(serviceRequest, token); return Ok(result); } @@ -119,7 +142,9 @@ public abstract class ProcessMapPlanBaseController : ControllerBase public async Task>> GetChangeLog([FromRoute] int idWell, [FromQuery] DateOnly? date, CancellationToken token) { await AssertUserHasAccessToWell(idWell, token); - var result = await repository.GetChangeLog(idWell, date, token); + + var serviceRequest = new ProcessMapPlanBaseRequestWithWell(idWell); + var result = await repository.GetChangeLog(serviceRequest, date, token); return Ok(result); } @@ -135,24 +160,26 @@ public abstract class ProcessMapPlanBaseController : ControllerBase public async Task>> GetDatesChange([FromRoute] int idWell, CancellationToken token) { await AssertUserHasAccessToWell(idWell, token); - var result = await repository.GetDatesChange(idWell, token); + + var serviceRequest = new ProcessMapPlanBaseRequestWithWell(idWell); + var result = await repository.GetDatesChange(serviceRequest, token); return Ok(result); } /// - /// Редактирование + /// Редактирование или добавление [для пакетного редактирования] /// /// /// /// /// - [HttpPut] + [HttpPut()] [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] - public async Task UpdateRange([FromRoute] int idWell, IEnumerable dtos, CancellationToken token) + public async Task UpdateOrInsertRange([FromRoute] int idWell, IEnumerable dtos, CancellationToken token) { var first = dtos.FirstOrDefault(); - if(first is null) + if (first is null) return NoContent(); if (idWell == 0 || dtos.Any(d => d.IdWell != idWell)) @@ -160,7 +187,7 @@ public abstract class ProcessMapPlanBaseController : ControllerBase var idUser = await AssertUserHasAccessToWell(idWell, token); - var result = await repository.UpdateRange(idUser, dtos, token); + var result = await repository.UpdateOrInsertRange(idUser, dtos, token); return Ok(result); } diff --git a/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanDrillingController.cs b/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanDrillingController.cs index 907f4ec5..b0a20f1e 100644 --- a/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanDrillingController.cs +++ b/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanDrillingController.cs @@ -1,12 +1,15 @@ using AsbCloudApp.Data.ProcessMapPlan; using AsbCloudApp.Repositories; +using AsbCloudApp.Requests; using AsbCloudApp.Services; namespace AsbCloudWebApi.Controllers.ProcessMapPlan; public class ProcessMapPlanDrillingController : ProcessMapPlanBaseController { - public ProcessMapPlanDrillingController(IProcessMapPlanBaseRepository repository, IWellService wellService) + public ProcessMapPlanDrillingController( + IChangeLogRepository repository, + IWellService wellService) : base(repository, wellService) { } 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 4e643a74..650fed2e 100644 --- a/AsbCloudWebApi/Extentions.cs +++ b/AsbCloudWebApi/Extentions.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; +using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Security.Claims; using AsbCloudApp.Data; using AsbCloudApp.Requests.Import; @@ -58,6 +60,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)));