DD.WellWorkover.Cloud/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs
2022-06-15 14:57:37 +05:00

529 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
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
{
private static readonly Dictionary<string, DrillingProgramCreateError> drillingProgramCreateErrors = new Dictionary<string, DrillingProgramCreateError>();
private readonly IAsbCloudDbContext context;
private readonly IFileService fileService;
private readonly IUserService userService;
private readonly IWellService wellService;
private readonly IConfiguration configuration;
private readonly IBackgroundWorkerService backgroundWorker;
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;
private const int idStateError = 4;
public DrillingProgramService(
IAsbCloudDbContext context,
IFileService fileService,
IUserService userService,
IWellService wellService,
IConfiguration configuration,
IBackgroundWorkerService backgroundWorker,
IEmailService emailService)
{
this.context = context;
this.fileService = fileService;
this.userService = userService;
this.wellService = wellService;
this.configuration = configuration;
this.backgroundWorker = backgroundWorker;
this.connectionString = configuration.GetConnectionString("DefaultConnection");
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
.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)
.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)
.Where(p => p.IdWell == idWell)
.ToListAsync(token);
var parts = new List<DrillingProgramPartDto>(partEntities.Count);
var timezoneOffset = wellService.GetTimezone(idWell)?.Hours ?? 5;
foreach (var partEntity in partEntities)
{
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>(),
PermissionToEdit = userService.HasPermission(idUser, "DrillingProgram.edit"),
};
if (parts.Any())
{
if (parts.All(p => p.IdState == idPartStateApproved))
{
if (state.Program is not null)
{
state.IdState = idStateReady;
}
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);
if (part == null)
throw new ArgumentInvalidException($"DrillingProgramPart id == {idFileCategory} does not exist", nameof(idFileCategory));
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(
part.IdWell,
idUser,
part.IdFileCategory,
fileFullName,
fileStream,
token);
await RemoveDrillingProgramAsync(part.IdWell, token);
await NotifyApproversAsync(part, result.Id, fileFullName, token);
return result.Id;
}
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
{
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.GetAsync(idUser, token);
if (user is null)
throw new ArgumentInvalidException($"User id == {idUser} does not exist", nameof(idUser));
var part = await context.DrillingProgramParts
.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);
if (oldRelation is not null)
context.RelationDrillingProgramPartUsers.Remove(oldRelation);
var newRelation = new RelationUserDrillingProgramPart
{
IdUser = idUser,
IdDrillingProgramPart = part.Id,
IdUserRole = idUserRole,
};
context.RelationDrillingProgramPartUsers.Add(newRelation);
if (idUserRole == idUserRoleApprover)
await RemoveDrillingProgramAsync(part.IdWell, token);
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);
return await context.SaveChangesAsync(token);
}
public async Task<int> AddOrReplaceFileMarkAsync(FileMarkDto fileMarkDto, int idUser, CancellationToken token)
{
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)
.ThenInclude(r => r.User)
.AsNoTracking()
.FirstOrDefaultAsync(p => p.IdWell == fileInfo.IdWell && p.IdFileCategory == fileInfo.IdCategory, token);
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.");
fileMarkDto.User = user.Adapt<UserDto>();
var oldMarksIds = fileInfo.FileMarks
?.Where(m => m.User.Id == idUser)
.Select(m => m.Id);
if (oldMarksIds?.Any() == true)
await fileService.MarkFileMarkAsDeletedAsync(oldMarksIds, token);
var result = await fileService.CreateFileMarkAsync(fileMarkDto, idUser, token)
.ConfigureAwait(false);
if (fileMarkDto.IdMarkType == idMarkTypeReject)
{
await RemoveDrillingProgramAsync(fileInfo.IdWell, token);
await NotifyPublisherOnRejectAsync(fileMarkDto, token);
}
else
{
// если все согласованты согласовали - оповещаем публикатора
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);
}
}
return result;
}
public async Task<int> MarkAsDeletedFileMarkAsync(int idMark,
CancellationToken token)
{
var fileInfo = await fileService.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.GetAsync(file.IdWell, token);
var user = file.Author;
var factory = new MailBodyFactory(configuration);
var subject = MailBodyFactory.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)
{
var file = await fileService.GetInfoAsync(fileMark.IdFile, token);
var well = await wellService.GetAsync(file.IdWell, token);
var user = file.Author;
var factory = new MailBodyFactory(configuration);
var subject = MailBodyFactory.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.GetAsync(part.IdWell, token);
var factory = new MailBodyFactory(configuration);
var subject = MailBodyFactory.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.GetAsync(idWell, token);
var factory = new MailBodyFactory(configuration);
var subject = MailBodyFactory.MakeSubject(well, $"От вас ожидается загрузка на портал документа «{documentCategory}»");
var body = factory.MakeMailBodyForNewPublisher(well, user.Name, documentCategory);
emailService.EnqueueSend(user.Email, subject, body);
}
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;
part.File = new FileInfoDto
{
Id = fileEntity.Id,
Author = fileEntity.Author.Adapt<UserDto>(),
IdAuthor = fileEntity.Author.Id,
IdCategory = fileEntity.IdCategory,
IdWell = fileEntity.IdWell,
Name = fileEntity.Name,
PublishInfo = fileEntity.PublishInfo.Adapt<FilePublishInfoDto>(),
Size = fileEntity.Size,
UploadDate = fileEntity.UploadDate.ToRemoteDateTime(timezoneOffset),
};
var marks = fileEntity.FileMarks?.Where(m => !m.IsDeleted);
if (marks?.Any() == true)
{
part.File.FileMarks = marks.Select(m =>
{
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.GetAsync(idWell, token);
var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.xlsx";
var tempResultFilePath = Path.Combine(Path.GetTempPath(), "drillingProgram", resultFileName);
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(context);
var files = state.Parts.Select(p => fileService.GetUrl(p.File));
DrillingProgramMaker.UniteExcelFiles(files, tempResultFilePath);
await fileService.MoveAsync(idWell, null, idFileCategoryDrillingProgram, resultFileName, tempResultFilePath, token);
}
Task funcOnErrorProgramMake(string workId, Exception exception, CancellationToken token)
{
var message = $"Не удалось сформировать программу бурения по скважине {well?.Caption}";
drillingProgramCreateErrors[workId] = new()
{
Message = message,
Exception = exception.Message,
};
return Task.CompletedTask;
}
backgroundWorker.Enqueue(workId, funcProgramMake, funcOnErrorProgramMake);
}
}
}
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;
}
private static string MakeWorkId(int idWell)
=> $"Make drilling program for wellId {idWell}";
}
}