DD.WellWorkover.Cloud/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs
2024-08-19 10:01:07 +05:00

573 lines
25 KiB
C#
Raw Permalink 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.Data.User;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Background;
using AsbCloudInfrastructure.Services.DrillingProgram.Convert;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Requests;
using AsbCloudApp.Services.Notifications;
using AsbCloudInfrastructure.Services.Email;
namespace AsbCloudInfrastructure.Services.DrillingProgram;
# nullable enable
public class DrillingProgramService : IDrillingProgramService
{
private static readonly Dictionary<string, DrillingProgramCreateError> drillingProgramCreateErrors = new Dictionary<string, DrillingProgramCreateError>();
private readonly IAsbCloudDbContext context;
private readonly FileService fileService;
private readonly IUserRepository userRepository;
private readonly IWellService wellService;
private readonly IConfiguration configuration;
private readonly BackgroundWorker backgroundWorker;
private readonly NotificationService notificationService;
private const int idTransportType = 1;
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;
private static readonly string[] validFileExtensions = ConvertToPdf.filesExtensions;
public DrillingProgramService(
IAsbCloudDbContext context,
FileService fileService,
IUserRepository userRepository,
IWellService wellService,
IConfiguration configuration,
BackgroundWorker backgroundWorker,
NotificationService notificationService)
{
this.context = context;
this.fileService = fileService;
this.userRepository = userRepository;
this.wellService = wellService;
this.configuration = configuration;
this.backgroundWorker = backgroundWorker;
this.notificationService = notificationService;
}
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)
.ThenInclude(u => u.Company)
.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 = userRepository.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 EnqueueMakeProgramWorkAsync(idWell, state, token);
return state;
}
private static bool IsFileExtensionValid(string file)
{
var fileExt = Path.GetExtension(file).ToLower();
return validFileExtensions.Contains(fileExt);
}
public async Task<int> AddFile(int idWell, int idFileCategory, int idUser, string fileFullName, Stream fileStream, CancellationToken token = default)
{
if (!IsFileExtensionValid(fileFullName))
throw new FileFormatException($"Файл {fileFullName} - не поддерживаемого формата. Файл не может быть загружен.");
var part = await context.DrillingProgramParts
.Include(p => p.RelatedUsers)
.ThenInclude(r => r.User)
.FirstOrDefaultAsync(p => p.IdWell == idWell && p.IdFileCategory == idFileCategory, token)
?? throw new ArgumentInvalidException(nameof(idFileCategory), $"DrillingProgramPart id == {idFileCategory} does not exist");
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 userRepository.GetOrDefaultAsync(idUser, token)
?? throw new ArgumentInvalidException(nameof(idUser), $"User id: {idUser} does not exist");
var part = await context.DrillingProgramParts
.Include(p => p.FileCategory)
.FirstOrDefaultAsync(p => p.IdWell == idWell && p.IdFileCategory == idFileCategory, token)
?? throw new ArgumentInvalidException(nameof(idFileCategory), $"DrillingProgramPart idFileCategory: {idFileCategory} does not exist");
if (idUserRole != idUserRoleApprover && idUserRole != idUserRolePublisher)
throw new ArgumentInvalidException(nameof(idUserRole), $"idUserRole ({idUserRole}), should be approver ({idUserRoleApprover}) or publisher ({idUserRolePublisher})");
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.GetOrDefaultAsync(fileMarkDto.IdFile, token)
?? 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
?? 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)) == true ||
(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.GetOrDefaultAsync(fileMark.IdFile, token);
var well = await wellService.GetOrDefaultAsync(file!.IdWell, token)
?? throw new ArgumentInvalidException(nameof(file.IdWell), "idWell doesn`t exist");
var user = file.Author!;
var factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, "Загруженный вами документ полностью согласован");
var body = factory.MakeMailBodyForPublisherOnFullAccept(well, user.Name ?? string.Empty, file.Id, file.Name);
await notificationService.NotifyAsync(new NotifyRequest
{
IdUser = user.Id,
IdNotificationCategory = NotificationCategory.IdSystemNotificationCategory,
Title = subject,
Message = body,
IdTransportType = idTransportType
}, token);
}
private async Task NotifyPublisherOnRejectAsync(FileMarkDto fileMark, CancellationToken token)
{
var file = await fileService.GetOrDefaultAsync(fileMark.IdFile, token);
var well = await wellService.GetOrDefaultAsync(file!.IdWell, token)
?? throw new ArgumentInvalidException(nameof(file.IdWell), "idWell doesn`t exist");
var user = file.Author!;
var factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, "Загруженный вами документ отклонен");
var body = factory.MakeMailBodyForPublisherOnReject(well, user.Name ?? string.Empty, file.Id, file.Name, fileMark);
await notificationService.NotifyAsync(new NotifyRequest
{
IdUser = user.Id,
IdNotificationCategory = NotificationCategory.IdSystemNotificationCategory,
Title = subject,
Message = body,
IdTransportType = idTransportType
}, token);
}
private async Task NotifyApproversAsync(DrillingProgramPart part, int idFile, string fileName, CancellationToken token)
{
var well = await wellService.GetOrDefaultAsync(part.IdWell, token)
?? throw new ArgumentInvalidException(nameof(part.IdWell), "idWell doesn`t exist");
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 ?? string.Empty, idFile, fileName);
await notificationService.NotifyAsync(new NotifyRequest
{
IdUser = user.Id,
IdNotificationCategory = NotificationCategory.IdSystemNotificationCategory,
Title = subject,
Message = body,
IdTransportType = idTransportType
}, token);
}
}
private async Task NotifyNewPublisherAsync(int idWell, UserDto user, string documentCategory, CancellationToken token)
{
var well = await wellService.GetOrDefaultAsync(idWell, token)
?? throw new ArgumentInvalidException(nameof(idWell), "idWell doesn`t exist");
var factory = new DrillingMailBodyFactory(configuration);
var subject = factory.MakeSubject(well, $"От вас ожидается загрузка на портал документа «{documentCategory}»");
var body = factory.MakeMailBodyForNewPublisher(well, user.Name ?? string.Empty, documentCategory);
await notificationService.NotifyAsync(new NotifyRequest
{
IdUser = user.Id,
IdNotificationCategory = NotificationCategory.IdSystemNotificationCategory,
Title = subject,
Message = body,
IdTransportType = idTransportType
}, token);
}
private static 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,
Size = fileEntity.Size,
UploadDate = fileEntity.UploadDate.ToOffset(TimeSpan.FromHours(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.ToOffset(TimeSpan.FromHours(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 EnqueueMakeProgramWorkAsync(int idWell, DrillingProgramStateDto state, CancellationToken token)
{
if (state.IdState == idStateCreating)
{
var workId = MakeWorkId(idWell);
if (!backgroundWorker.Works.Any(w => w.Id == workId))
{
var well = (await wellService.GetOrDefaultAsync(idWell, token))!;
var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.pdf";
var convertedFilesDir = Path.Combine(Path.GetTempPath(), "drillingProgram", $"{well.Cluster}_{well.Caption}");
var tempResultFilePath = Path.Combine(convertedFilesDir, resultFileName);
var workAction = async (string workId, IServiceProvider serviceProvider, Action<string, double?> onProgress, CancellationToken token) =>
{
var context = serviceProvider.GetRequiredService<IAsbCloudDbContext>();
var fileService = serviceProvider.GetRequiredService<FileService>();
var files = state.Parts.Select(p => fileService.GetUrl(p.File!));
onProgress($"Start converting {files.Count()} files to PDF.", null);
await ConvertToPdf.GetConverteAndMergedFileAsync(files, tempResultFilePath, convertedFilesDir, token);
await fileService.MoveAsync(idWell, null, idFileCategoryDrillingProgram, resultFileName, tempResultFilePath, token);
};
var onErrorAction = (string workId, Exception exception, CancellationToken token) =>
{
var message = $"Не удалось сформировать программу бурения по скважине {well?.Caption}";
drillingProgramCreateErrors[workId] = new()
{
Message = message,
Exception = exception.Message,
};
return Task.CompletedTask;
};
var work = Work.CreateByDelegate(workId, workAction);
work.OnErrorAsync = onErrorAction;
backgroundWorker.Enqueue(work);
}
}
}
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.TryRemoveFromQueue(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}";
}