DD.WellWorkover.Cloud/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs

533 lines
24 KiB
C#
Raw Normal View History

using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Repository;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services.DrillingProgram
{
public class DrillingProgramService : IDrillingProgramService
{
2022-03-02 16:21:07 +05:00
private static readonly Dictionary<string, DrillingProgramCreateError> drillingProgramCreateErrors = new Dictionary<string, DrillingProgramCreateError>();
2022-02-28 14:44:15 +05:00
private readonly IAsbCloudDbContext context;
private readonly IFileService fileService;
private readonly IUserService userService;
private readonly IWellService wellService;
private readonly IFileRepository fileRepository;
2022-02-28 14:44:15 +05:00
private readonly IConfiguration configuration;
private readonly IBackgroundWorkerService backgroundWorker;
2022-04-25 15:38:44 +05:00
private readonly IEmailService emailService;
private readonly string connectionString;
private const int idFileCategoryDrillingProgram = 1000;
private const int idFileCategoryDrillingProgramPartsStart = 1001;
private const int idFileCategoryDrillingProgramPartsEnd = 1100;
private const int idPartStateNoFile = 0;
private const int idPartStateApproving = 1;
private const int idPartStateApproved = 2;
private const int idMarkTypeReject = 0;
private const int idMarkTypeApprove = 1;
private const int idUserRolePublisher = 1;
private const int idUserRoleApprover = 2;
private const int idStateNotInitialized = 0;
private const int idStateApproving = 1;
private const int idStateCreating = 2;
private const int idStateReady = 3;
2022-02-28 14:44:15 +05:00
private const int idStateError = 4;
public DrillingProgramService(
2022-04-11 18:00:34 +05:00
IAsbCloudDbContext context,
IFileService fileService,
IUserService userService,
IWellService wellService,
IFileRepository fileRepository,
IConfiguration configuration,
2022-04-25 15:38:44 +05:00
IBackgroundWorkerService backgroundWorker,
IEmailService emailService)
2022-06-15 14:57:37 +05:00
{
this.context = context;
this.fileService = fileService;
this.userService = userService;
this.wellService = wellService;
this.fileRepository = fileRepository;
2022-02-28 14:44:15 +05:00
this.configuration = configuration;
this.backgroundWorker = backgroundWorker;
this.connectionString = configuration.GetConnectionString("DefaultConnection");
2022-04-25 15:38:44 +05:00
this.emailService = emailService;
}
public async Task<IEnumerable<UserDto>> GetAvailableUsers(int idWell, CancellationToken token = default)
{
var users = await context.RelationCompaniesWells
.Include(r => r.Company)
.ThenInclude(c => c.Users)
.Where(r => r.IdWell == idWell)
.SelectMany(r => r.Company.Users)
.Where(u => u != null && !string.IsNullOrEmpty(u.Email))
.ToListAsync(token);
var usersDto = users.Adapt<IEnumerable<UserDto>>();
return usersDto;
}
public async Task<IEnumerable<FileCategoryDto>> GetCategoriesAsync(CancellationToken token = default)
{
var result = await context.FileCategories
2022-04-11 18:00:34 +05:00
.Where(c => c.Id > idFileCategoryDrillingProgramPartsStart && c.Id < idFileCategoryDrillingProgramPartsEnd)
.ToListAsync(token);
return result.Select(c => c.Adapt<FileCategoryDto>());
}
public async Task<DrillingProgramStateDto> GetStateAsync(int idWell, int idUser, CancellationToken token = default)
{
var fileCategories = await context.FileCategories
.Where(c => c.Id >= idFileCategoryDrillingProgramPartsStart &&
c.Id < idFileCategoryDrillingProgramPartsEnd)
.ToListAsync(token);
var files = await context.Files
.Include(f => f.FileMarks)
.ThenInclude(m => m.User)
.Include(f => f.Author)
.Include(f => f.FileCategory)
2022-04-11 18:00:34 +05:00
.Where(f => f.IdWell == idWell &&
f.IdCategory >= idFileCategoryDrillingProgram &&
f.IdCategory < idFileCategoryDrillingProgramPartsEnd &&
f.IsDeleted == false)
.OrderBy(f => f.UploadDate)
.ToListAsync(token);
var partEntities = await context.DrillingProgramParts
.Include(p => p.RelatedUsers)
.ThenInclude(r => r.User)
.ThenInclude(u => u.Company)
.Where(p => p.IdWell == idWell)
.ToListAsync(token);
var parts = new List<DrillingProgramPartDto>(partEntities.Count);
2022-03-02 17:41:59 +05:00
var timezoneOffset = wellService.GetTimezone(idWell)?.Hours ?? 5;
foreach (var partEntity in partEntities)
{
2022-03-02 17:41:59 +05:00
var part = ConvertPart(idUser, fileCategories, files, partEntity, timezoneOffset);
parts.Add(part);
}
var state = new DrillingProgramStateDto
{
Parts = parts,
Program = files.FirstOrDefault(f => f.IdCategory == idFileCategoryDrillingProgram)
.Adapt<FileInfoDto>(),
2022-04-11 18:00:34 +05:00
PermissionToEdit = userService.HasPermission(idUser, "DrillingProgram.edit"),
};
if (parts.Any())
{
2022-04-11 18:00:34 +05:00
if (parts.All(p => p.IdState == idPartStateApproved))
{
if (state.Program is not null)
2022-02-28 14:44:15 +05:00
{
state.IdState = idStateReady;
2022-02-28 14:44:15 +05:00
}
else
{
var workId = MakeWorkId(idWell);
if (drillingProgramCreateErrors.ContainsKey(workId))
{
state.IdState = idStateError;
state.Error = drillingProgramCreateErrors[workId];
}
else
state.IdState = idStateCreating;
}
}
else
state.IdState = idStateApproving;
}
else
state.IdState = idStateNotInitialized;
await TryEnqueueMakeProgramAsync(idWell, state, token);
return state;
}
public async Task<int> AddFile(int idWell, int idFileCategory, int idUser, string fileFullName, System.IO.Stream fileStream, CancellationToken token = default)
{
var part = await context.DrillingProgramParts
.Include(p => p.RelatedUsers)
.ThenInclude(r => r.User)
.FirstOrDefaultAsync(p => p.IdWell == idWell && p.IdFileCategory == idFileCategory, token);
2022-04-11 18:00:34 +05:00
if (part == null)
throw new ArgumentInvalidException($"DrillingProgramPart id == {idFileCategory} does not exist", nameof(idFileCategory));
2022-04-11 18:00:34 +05:00
if (!part.RelatedUsers.Any(r => r.IdUser == idUser && r.IdUserRole == idUserRolePublisher))
throw new ForbidException($"User {idUser} is not in the publisher list.");
var result = await fileService.SaveAsync(
2022-04-11 18:00:34 +05:00
part.IdWell,
idUser,
part.IdFileCategory,
fileFullName,
fileStream,
token);
await RemoveDrillingProgramAsync(part.IdWell, token);
2022-04-25 15:38:44 +05:00
2022-06-15 14:57:37 +05:00
await NotifyApproversAsync(part, result.Id, fileFullName, token);
return result.Id;
}
2022-04-11 18:00:34 +05:00
public async Task<int> AddPartsAsync(int idWell, IEnumerable<int> idFileCategories, CancellationToken token = default)
{
if (!idFileCategories.Any())
return 0;
var existingCategories = await context.DrillingProgramParts
.Where(p => p.IdWell == idWell)
.Select(p => p.IdFileCategory)
.ToListAsync(token);
var newParts = idFileCategories
.Where(c => !existingCategories.Any(e => e == c))
.Select(c => new DrillingProgramPart
2022-04-11 18:00:34 +05:00
{
IdWell = idWell,
IdFileCategory = c,
});
context.DrillingProgramParts.AddRange(newParts);
var affected = await context.SaveChangesAsync(token);
await RemoveDrillingProgramAsync(idWell, token);
return affected;
}
public async Task<int> RemovePartsAsync(int idWell, IEnumerable<int> idFileCategories, CancellationToken token = default)
{
var whereQuery = context.DrillingProgramParts
.Where(p => p.IdWell == idWell && idFileCategories.Contains(p.IdFileCategory));
context.DrillingProgramParts.RemoveRange(whereQuery);
await RemoveDrillingProgramAsync(idWell, token);
return await context.SaveChangesAsync(token);
}
public async Task<int> AddUserAsync(int idWell, int idFileCategory, int idUser, int idUserRole, CancellationToken token = default)
{
var user = await userService.GetOrDefaultAsync(idUser, token);
if (user is null)
throw new ArgumentInvalidException($"User id == {idUser} does not exist", nameof(idUser));
var part = await context.DrillingProgramParts
2022-06-15 14:57:37 +05:00
.Include(p => p.FileCategory)
.FirstOrDefaultAsync(p => p.IdWell == idWell && p.IdFileCategory == idFileCategory, token);
if (part is null)
throw new ArgumentInvalidException($"DrillingProgramPart idFileCategory == {idFileCategory} does not exist", nameof(idFileCategory));
if (idUserRole != idUserRoleApprover && idUserRole != idUserRolePublisher)
throw new ArgumentInvalidException($"idUserRole ({idUserRole}), should be approver ({idUserRoleApprover}) or publisher ({idUserRolePublisher})", nameof(idUserRole));
var oldRelation = await context.RelationDrillingProgramPartUsers
.FirstOrDefaultAsync(r => r.IdUser == idUser && r.IdDrillingProgramPart == part.Id, token);
2022-04-11 18:00:34 +05:00
if (oldRelation is not null)
context.RelationDrillingProgramPartUsers.Remove(oldRelation);
var newRelation = new RelationUserDrillingProgramPart
{
IdUser = idUser,
IdDrillingProgramPart = part.Id,
IdUserRole = idUserRole,
};
context.RelationDrillingProgramPartUsers.Add(newRelation);
2022-04-11 18:00:34 +05:00
if (idUserRole == idUserRoleApprover)
await RemoveDrillingProgramAsync(part.IdWell, token);
2022-05-18 11:07:39 +05:00
if (idUserRole == idUserRolePublisher)
await NotifyNewPublisherAsync(idWell, user, part.FileCategory.Name, token);
return await context.SaveChangesAsync(token);
}
public async Task<int> RemoveUserAsync(int idWell, int idFileCategory, int idUser, int idUserRole, CancellationToken token = default)
{
var whereQuery = context.RelationDrillingProgramPartUsers
.Include(r => r.DrillingProgramPart)
.Where(r => r.IdUser == idUser &&
r.IdUserRole == idUserRole &&
r.DrillingProgramPart.IdWell == idWell &&
r.DrillingProgramPart.IdFileCategory == idFileCategory);
context.RelationDrillingProgramPartUsers.RemoveRange(whereQuery);
2022-04-11 18:00:34 +05:00
return await context.SaveChangesAsync(token);
}
public async Task<int> AddOrReplaceFileMarkAsync(FileMarkDto fileMarkDto, int idUser, CancellationToken token)
{
2022-04-11 18:00:34 +05:00
if (fileMarkDto.IdMarkType != idMarkTypeApprove &&
fileMarkDto.IdMarkType != idMarkTypeReject)
throw new ArgumentInvalidException($"В этом методе допустимы только отметки о принятии или отклонении.", nameof(fileMarkDto));
var fileInfo = await fileService.GetInfoAsync(fileMarkDto.IdFile, token)
.ConfigureAwait(false);
if (fileInfo is null)
throw new ArgumentInvalidException($"Файла для такой отметки не существует.", nameof(fileMarkDto));
if (fileInfo.IdCategory < idFileCategoryDrillingProgramPartsStart ||
fileInfo.IdCategory > idFileCategoryDrillingProgramPartsEnd)
throw new ArgumentInvalidException($"Этот метод допустим только для файлов-частей программы бурения.", nameof(fileMarkDto));
var part = await context.DrillingProgramParts
.Include(p => p.RelatedUsers)
2022-05-18 11:07:39 +05:00
.ThenInclude(r => r.User)
.AsNoTracking()
.FirstOrDefaultAsync(p => p.IdWell == fileInfo.IdWell && p.IdFileCategory == fileInfo.IdCategory, token);
2022-05-18 11:07:39 +05:00
var user = part.RelatedUsers.FirstOrDefault(r => r.IdUser == idUser && r.IdUserRole == idUserRoleApprover)?.User;
if (user is null)
throw new ForbidException($"User {idUser} is not in the approvers list.");
2022-05-18 11:07:39 +05:00
fileMarkDto.User = user.Adapt<UserDto>();
var oldMarksIds = fileInfo.FileMarks
?.Where(m => m.User.Id == idUser)
.Select(m => m.Id);
2022-04-11 18:00:34 +05:00
if (oldMarksIds?.Any() == true)
await fileRepository.MarkFileMarkAsDeletedAsync(oldMarksIds, token);
var result = await fileRepository.CreateFileMarkAsync(fileMarkDto, idUser, token)
.ConfigureAwait(false);
2022-04-11 18:00:34 +05:00
if (fileMarkDto.IdMarkType == idMarkTypeReject)
2022-04-25 15:38:44 +05:00
{
await RemoveDrillingProgramAsync(fileInfo.IdWell, token);
await NotifyPublisherOnRejectAsync(fileMarkDto, token);
2022-04-25 15:38:44 +05:00
}
2022-05-18 11:07:39 +05:00
else
2022-04-25 15:38:44 +05:00
{
2022-05-18 11:07:39 +05:00
// если все согласованты согласовали - оповещаем публикатора
var approvers = part.RelatedUsers
.Where(u => u.IdUserRole == idUserRoleApprover);
if (approvers
.All(user => fileInfo.FileMarks
.Any(mark => (mark.IdMarkType == idMarkTypeApprove && mark.User.Id == user.IdUser && !mark.IsDeleted)) ||
(fileMarkDto.IdMarkType == idMarkTypeApprove && user.IdUser == idUser)))
{
await NotifyPublisherOnFullAccepAsync(fileMarkDto, token);
}
2022-04-25 15:38:44 +05:00
}
2022-05-18 11:07:39 +05:00
return result;
}
public async Task<int> MarkAsDeletedFileMarkAsync(int idMark,
CancellationToken token)
{
var fileInfo = await fileRepository.GetByMarkId(idMark, token)
.ConfigureAwait(false);
if (fileInfo.IdCategory < idFileCategoryDrillingProgramPartsStart ||
fileInfo.IdCategory > idFileCategoryDrillingProgramPartsEnd)
throw new ArgumentInvalidException($"Этот метод допустим только для файлов-частей программы бурения.", nameof(idMark));
var result = await fileService.MarkFileMarkAsDeletedAsync(idMark, token)
.ConfigureAwait(false);
await RemoveDrillingProgramAsync(fileInfo.IdWell, token);
return result;
}
private async Task NotifyPublisherOnFullAccepAsync(FileMarkDto fileMark, CancellationToken token)
{
var file = await fileService.GetInfoAsync(fileMark.IdFile, token);
var well = await wellService.GetOrDefaultAsync(file.IdWell, token);
var user = file.Author;
var factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, "Загруженный вами документ полностью согласован");
var body = factory.MakeMailBodyForPublisherOnFullAccept(well, user.Name, file.Id, file.Name);
emailService.EnqueueSend(user.Email, subject, body);
}
private async Task NotifyPublisherOnRejectAsync(FileMarkDto fileMark, CancellationToken token)
{
2022-06-15 14:57:37 +05:00
var file = await fileService.GetInfoAsync(fileMark.IdFile, token);
var well = await wellService.GetOrDefaultAsync(file.IdWell, token);
var user = file.Author;
var factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, "Загруженный вами документ отклонен");
var body = factory.MakeMailBodyForPublisherOnReject(well, user.Name, file.Id, file.Name, fileMark);
emailService.EnqueueSend(user.Email, subject, body);
}
private async Task NotifyApproversAsync(DrillingProgramPart part, int idFile, string fileName, CancellationToken token)
{
var well = await wellService.GetOrDefaultAsync(part.IdWell, token);
var factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, "Загружен новый документ для согласования.");
var users = part.RelatedUsers
.Where(r => r.IdUserRole == idUserRoleApprover)
.Select(r => r.User);
foreach (var user in users)
{
var body = factory.MakeMailBodyForApproverNewFile(well, user.Name, idFile, fileName);
emailService.EnqueueSend(user.Email, subject, body);
}
}
private async Task NotifyNewPublisherAsync(int idWell, UserDto user, string documentCategory, CancellationToken token)
{
var well = await wellService.GetOrDefaultAsync(idWell, token);
var factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, $"От вас ожидается загрузка на портал документа «{documentCategory}»");
var body = factory.MakeMailBodyForNewPublisher(well, user.Name, documentCategory);
emailService.EnqueueSend(user.Email, subject, body);
}
2022-03-02 17:41:59 +05:00
private DrillingProgramPartDto ConvertPart(int idUser, List<FileCategory> fileCategories, List<AsbCloudDb.Model.FileInfo> files, DrillingProgramPart partEntity, double timezoneOffset)
{
var part = new DrillingProgramPartDto
{
IdFileCategory = partEntity.IdFileCategory,
Name = fileCategories.FirstOrDefault(c => c.Id == partEntity.IdFileCategory).Name,
Approvers = partEntity.RelatedUsers
.Where(r => r.IdUserRole == idUserRoleApprover)
.Select(r => r.User.Adapt<UserDto>()),
Publishers = partEntity.RelatedUsers
.Where(r => r.IdUserRole == idUserRolePublisher)
.Select(r => r.User.Adapt<UserDto>()),
PermissionToApprove = partEntity.RelatedUsers
.Any(r => r.IdUserRole == idUserRoleApprover && r.IdUser == idUser),
PermissionToUpload = partEntity.RelatedUsers
.Any(r => r.IdUserRole == idUserRolePublisher && r.IdUser == idUser),
};
var fileEntity = files.LastOrDefault(f => f.IdCategory == partEntity.IdFileCategory);
if (fileEntity is not null)
{
part.IdState = idPartStateApproving;
2022-03-02 17:41:59 +05:00
part.File = new FileInfoDto
{
2022-03-02 17:41:59 +05:00
Id = fileEntity.Id,
Author = fileEntity.Author.Adapt<UserDto>(),
IdAuthor = fileEntity.Author.Id,
IdCategory = fileEntity.IdCategory,
IdWell = fileEntity.IdWell,
Name = fileEntity.Name,
Size = fileEntity.Size,
UploadDate = fileEntity.UploadDate.ToRemoteDateTime(timezoneOffset),
};
var marks = fileEntity.FileMarks?.Where(m => !m.IsDeleted);
if (marks?.Any() == true)
2022-04-11 18:00:34 +05:00
{
part.File.FileMarks = marks.Select(m =>
{
2022-03-02 17:41:59 +05:00
var mark = m.Adapt<FileMarkDto>();
mark.DateCreated = m.DateCreated.ToRemoteDateTime(timezoneOffset);
return mark;
});
var hasReject = marks.Any(m => m.IdMarkType == idMarkTypeReject);
if (!hasReject)
{
var allAproved = part.Approvers.All(a => marks.Any(m => m.IdUser == a.Id && m.IdMarkType == idMarkTypeApprove));
if (allAproved)
part.IdState = idPartStateApproved;
}
}
}
else
part.IdState = idPartStateNoFile;
return part;
}
private async Task TryEnqueueMakeProgramAsync(int idWell, DrillingProgramStateDto state, CancellationToken token)
{
if (state.IdState == idStateCreating)
{
var workId = MakeWorkId(idWell);
if (!backgroundWorker.Contains(workId))
{
var well = await wellService.GetOrDefaultAsync(idWell, token);
var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.xlsx";
var tempResultFilePath = Path.Combine(Path.GetTempPath(), "drillingProgram", resultFileName);
2022-02-28 14:44:15 +05:00
var mailService = new EmailService(backgroundWorker, configuration);
async Task funcProgramMake(string id, CancellationToken token)
{
var contextOptions = new DbContextOptionsBuilder<AsbCloudDbContext>()
.UseNpgsql(connectionString)
.Options;
using var context = new AsbCloudDbContext(contextOptions);
var fileService = new FileService(fileRepository);
var files = state.Parts.Select(p => fileService.GetUrl(p.File));
DrillingProgramMaker.UniteExcelFiles(files, tempResultFilePath, state.Parts, well);
await fileService.MoveAsync(idWell, null, idFileCategoryDrillingProgram, resultFileName, tempResultFilePath, token);
}
2022-04-11 18:00:34 +05:00
Task funcOnErrorProgramMake(string workId, Exception exception, CancellationToken token)
{
2022-02-28 14:44:15 +05:00
var message = $"Не удалось сформировать программу бурения по скважине {well?.Caption}";
2022-04-11 18:00:34 +05:00
drillingProgramCreateErrors[workId] = new()
{
2022-02-28 14:44:15 +05:00
Message = message,
Exception = exception.Message,
};
return Task.CompletedTask;
}
backgroundWorker.Enqueue(workId, funcProgramMake, funcOnErrorProgramMake);
}
}
}
2022-02-28 14:44:15 +05:00
public void ClearError(int idWell)
{
var workId = MakeWorkId(idWell);
drillingProgramCreateErrors.Remove(workId);
}
private async Task<int> RemoveDrillingProgramAsync(int idWell, CancellationToken token)
{
var workId = MakeWorkId(idWell);
backgroundWorker.TryRemove(workId);
var filesIds = await context.Files
.Where(f => f.IdWell == idWell &&
f.IdCategory == idFileCategoryDrillingProgram)
.Select(f => f.Id)
.ToListAsync(token);
if (filesIds.Any())
return await fileService.DeleteAsync(filesIds, token);
else
return 0;
}
2022-04-11 18:00:34 +05:00
private static string MakeWorkId(int idWell)
=> $"Make drilling program for wellId {idWell}";
}
}