< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.EmailRoutingResultsController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/EmailRoutingResultsController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 219
Coverable lines: 219
Total lines: 414
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 46
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
Index()0%620%
UploadedFiles()0%620%
GridRows(...)100%210%
GridFacets(...)100%210%
UploadedGridRows(...)100%210%
UploadedGridFacets(...)100%210%
UploadedQuery()100%210%
ResultRowsAsync()0%4260%
ResultFacetsAsync()0%4260%
GetImapStatus()100%210%
TestImapSettings()0%620%
SaveImapSettings()0%620%
GetDropboxFolders()0%620%
Details()0%620%
RunNow()100%210%
DryRun()100%210%
Reprocess()0%620%
GetImapConnectionStatusAsync()0%620%
MapImapConnection(...)100%210%
MapSettingsEdit(...)100%210%
MapSettingsInput(...)100%210%
TryValidateImapSettings(...)0%342180%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/EmailRoutingResultsController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.EntityFrameworkCore;
 4using Microsoft.Extensions.Options;
 5using OzonDomains.Models;
 6using OzonDomains.Models.Authorization.Enums;
 7using OzonOrdersWeb.Areas.Studio2.ViewModels.EmailRouting;
 8using OzonOrdersWeb.Authorization;
 9using OzonOrdersWeb.WebServices.Grid;
 10using OzonOrdersWeb.WebServices.Journals;
 11using OzonRepositories.Context;
 12using Services.ApiServices.DropBoxApi;
 13using Services.DataServices;
 14using Services.EmailRoutingServices;
 15
 16namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 17
 18[Area("Studio2")]
 19[Authorize(Roles = "User,Admin")]
 20public class EmailRoutingResultsController : Controller
 21{
 22    private readonly EmailAttachmentProcessingResultDataServices _resultDataServices;
 23    private readonly EmailRoutingRuleDataServices _rulesDataServices;
 24    private readonly IEmailAttachmentRoutingService _routingService;
 25    private readonly IEmailInboxClient _emailInboxClient;
 26    private readonly IImapSettingsStore _imapSettingsStore;
 27    private readonly DropboxApiClient _dropboxClient;
 28    private readonly IOptionsMonitor<ImapSettings> _imapSettings;
 29    private readonly ApplicationDbContext _context;
 30
 031    public EmailRoutingResultsController(
 032        EmailAttachmentProcessingResultDataServices resultDataServices,
 033        EmailRoutingRuleDataServices rulesDataServices,
 034        IEmailAttachmentRoutingService routingService,
 035        IEmailInboxClient emailInboxClient,
 036        IImapSettingsStore imapSettingsStore,
 037        DropboxApiClient dropboxClient,
 038        IOptionsMonitor<ImapSettings> imapOptions,
 039        ApplicationDbContext context)
 40    {
 041        _resultDataServices = resultDataServices;
 042        _rulesDataServices = rulesDataServices;
 043        _routingService = routingService;
 044        _emailInboxClient = emailInboxClient;
 045        _imapSettingsStore = imapSettingsStore;
 046        _dropboxClient = dropboxClient;
 047        _imapSettings = imapOptions;
 048        _context = context;
 049    }
 50
 51    private const string IndexPageCode = "Studio2.EmailRoutingResults.Index";
 52    private const string UploadedPageCode = "Studio2.EmailRoutingResults.UploadedFiles";
 53
 54    /// <summary>
 55    /// Каркас страницы: IMAP-карточка, счётчики по статусам и последние письма. Результаты обработки вложений грид
 56    /// подгружает блоками через <see cref="GridRows"/>.
 57    /// </summary>
 58    [HttpGet]
 59    public async Task<IActionResult> Index(CancellationToken cancellationToken)
 60    {
 061        var messagesLimit = Math.Max(1, _imapSettings.CurrentValue.FetchLimit);
 62        // Проекция без вложений: FileBytes писем в список не нужны.
 063        var messages = await _context.EmailMessageInboxes.AsNoTracking()
 064            .OrderByDescending(m => m.ReceivedAtUtc ?? m.CreatedAt).ThenByDescending(m => m.Id)
 065            .Take(messagesLimit)
 066            .Select(m => new EmailInboxMessageRow(m.Id, m.ReceivedAtUtc, m.CreatedAt, m.SenderEmail, m.Subject,
 067                m.Attachments.Count, m.ProcessingStatus.ToString()))
 068            .ToListAsync(cancellationToken);
 069        var statusCounts = await _resultDataServices.GetStatusCounts();
 70
 71        // Живая проверка IMAP (до 8с на подключение к почте) сюда не идёт — она тормозила открытие страницы
 72        // на каждый заход. Карточка рендерится пустой и сразу подтягивается через GetImapStatus.
 073        var vm = new EmailRoutingResultsViewModel
 074        {
 075            MessageRows = messages,
 076            CountUploaded = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.Uploaded),
 077            CountOrdersImported = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.OrdersImported),
 078            CountRejected = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.RejectedByRule),
 079            CountFailed = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.Failed),
 080            CountDuplicate = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.Duplicate),
 081            LastMessagesLimit = messagesLimit,
 082            ImapSettingsEdit = User.IsInRole("Admin") ? MapSettingsEdit(_imapSettingsStore.GetCurrent()) : null
 083        };
 084        return View(vm);
 085    }
 86
 87    /// <summary>Загруженные в Dropbox вложения (грид — <see cref="UploadedGridRows"/>) и просмотр папок Dropbox.</summa
 88    public async Task<IActionResult> UploadedFiles(string? dropboxFolder = null)
 89    {
 090        var rules = await _rulesDataServices.GetRules();
 91
 092        Dictionary<string, Dictionary<string, string>> dropboxFolders = new();
 093        if (!string.IsNullOrWhiteSpace(dropboxFolder))
 94        {
 95            try
 96            {
 097                dropboxFolders = await _dropboxClient.GetFolderContentsAsync(dropboxFolder);
 098            }
 099            catch
 100            {
 101                // если Dropbox недоступен — продолжаем без папок
 0102            }
 103        }
 104
 0105        var vm = new EmailRoutingResultsViewModel
 0106        {
 0107            TotalCount = await _context.EmailAttachmentProcessingResults
 0108                .CountAsync(r => r.Status == EmailAttachmentProcessingStatus.Uploaded),
 0109            AvailableRules = rules,
 0110            DropboxFolders = dropboxFolders,
 0111            DropboxBaseFolder = dropboxFolder,
 0112        };
 0113        return View(vm);
 0114    }
 115
 116    [HttpPost("Studio2/EmailRoutingResults/grid")]
 117    [ValidateAntiForgeryToken]
 118    [PageAuthorize(IndexPageCode, AccessType.View)]
 119    public Task<IActionResult> GridRows([FromBody] ServerGridRequest? request, CancellationToken cancellationToken) =>
 0120        ResultRowsAsync(_context.EmailAttachmentProcessingResults.AsNoTracking(), request, cancellationToken);
 121
 122    [HttpPost("Studio2/EmailRoutingResults/grid/facets")]
 123    [ValidateAntiForgeryToken]
 124    [PageAuthorize(IndexPageCode, AccessType.View)]
 125    public Task<IActionResult> GridFacets([FromBody] ServerGridRequest? request, CancellationToken cancellationToken) =>
 0126        ResultFacetsAsync(_context.EmailAttachmentProcessingResults.AsNoTracking(), request, cancellationToken);
 127
 128    [HttpPost("Studio2/EmailRoutingResults/uploaded/grid")]
 129    [ValidateAntiForgeryToken]
 130    [PageAuthorize(UploadedPageCode, AccessType.View)]
 131    public Task<IActionResult> UploadedGridRows([FromBody] ServerGridRequest? request, CancellationToken cancellationTok
 0132        ResultRowsAsync(UploadedQuery(), request, cancellationToken);
 133
 134    [HttpPost("Studio2/EmailRoutingResults/uploaded/grid/facets")]
 135    [ValidateAntiForgeryToken]
 136    [PageAuthorize(UploadedPageCode, AccessType.View)]
 137    public Task<IActionResult> UploadedGridFacets([FromBody] ServerGridRequest? request, CancellationToken cancellationT
 0138        ResultFacetsAsync(UploadedQuery(), request, cancellationToken);
 139
 140    private IQueryable<EmailAttachmentProcessingResult> UploadedQuery() =>
 0141        _context.EmailAttachmentProcessingResults.AsNoTracking().Where(r => r.Status == EmailAttachmentProcessingStatus.
 142
 143    private async Task<IActionResult> ResultRowsAsync(IQueryable<EmailAttachmentProcessingResult> query, ServerGridReque
 144        CancellationToken cancellationToken)
 145    {
 0146        var error = ServerGridQuery.Validate(request);
 0147        if (error != null) return BadRequest(new { success = false, error });
 148
 0149        var def = JournalGridDefinitions.EmailResults;
 0150        var filtered = ServerGridQuery.ApplyFilters(query, def, request!);
 0151        var total = await filtered.CountAsync(cancellationToken);
 0152        var start = Math.Max(0, request!.StartRow);
 0153        var take = Math.Clamp(request.EndRow - start, 1, ServerGridQuery.MaxBlockSize);
 0154        var rows = start >= total
 0155            ? []
 0156            : await ServerGridQuery.ApplySort(filtered, def, request.SortModel).Skip(start).Take(take)
 0157                .Select(r => new
 0158                {
 0159                    r.Id,
 0160                    Date = r.EmailMessage != null && r.EmailMessage.ReceivedAtUtc != null ? r.EmailMessage.ReceivedAtUtc
 0161                    DateIsReceived = r.EmailMessage != null && r.EmailMessage.ReceivedAtUtc != null,
 0162                    r.CreatedAt,
 0163                    Status = r.Status.ToString(),
 0164                    RuleName = r.Rule != null ? r.Rule.Name : null,
 0165                    Sender = r.EmailMessage != null ? r.EmailMessage.SenderEmail : null,
 0166                    Subject = r.EmailMessage != null ? r.EmailMessage.Subject : null,
 0167                    FileName = r.Attachment != null ? r.Attachment.FileName : r.DropboxFileName,
 0168                    r.DropboxFolderPath, r.DropboxFilePath, r.DropboxDownloadLink,
 0169                    Reason = r.Error ?? r.Reason,
 0170                    IsError = r.Error != null
 0171                })
 0172                .ToListAsync(cancellationToken);
 0173        var lastRow = start + rows.Count >= total ? total : -1;
 0174        return Ok(new { success = true, rows, totalCount = total, lastRow });
 0175    }
 176
 177    private async Task<IActionResult> ResultFacetsAsync(IQueryable<EmailAttachmentProcessingResult> query, ServerGridReq
 178        CancellationToken cancellationToken)
 179    {
 0180        if (request is null || string.IsNullOrWhiteSpace(request.ColId)) return BadRequest(new { success = false, error 
 0181        request.StartRow = 0;
 0182        request.EndRow = 1;
 0183        var error = ServerGridQuery.Validate(request);
 0184        if (error != null) return BadRequest(new { success = false, error });
 0185        var values = await ServerGridQuery.FacetsAsync(query, JournalGridDefinitions.EmailResults, request, cancellation
 0186        return Ok(new { success = true, values });
 0187    }
 188
 189    /// <summary>
 190    /// JSON-эндпоинт для онлайн-обновления статуса IMAP на странице результатов.
 191    /// </summary>
 192    [HttpGet]
 193    public async Task<IActionResult> GetImapStatus(CancellationToken cancellationToken)
 194    {
 0195        var status = await GetImapConnectionStatusAsync(cancellationToken);
 0196        return Json(status);
 0197    }
 198
 199    /// <summary>
 200    /// Проверка IMAP-подключения с указанными настройками (без сохранения).
 201    /// </summary>
 202    [HttpPost]
 203    [Authorize(Roles = "Admin")]
 204    [ValidateAntiForgeryToken]
 205    public async Task<IActionResult> TestImapSettings(
 206        [FromBody] EmailImapSettingsEditViewModel model,
 207        CancellationToken cancellationToken)
 208    {
 0209        if (!TryValidateImapSettings(model, out var error))
 0210            return BadRequest(new { error });
 211
 0212        using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 0213        cts.CancelAfter(TimeSpan.FromSeconds(12));
 214
 0215        var merged = _imapSettingsStore.MergeWithCurrent(MapSettingsInput(model));
 0216        var status = await ImapConnectionTester.CheckAsync(merged, cancellationToken: cts.Token);
 0217        return Json(MapImapConnection(status));
 0218    }
 219
 220    /// <summary>
 221    /// Сохранение IMAP-настроек в appsettings.json.
 222    /// </summary>
 223    [HttpPost]
 224    [Authorize(Roles = "Admin")]
 225    [ValidateAntiForgeryToken]
 226    public async Task<IActionResult> SaveImapSettings(
 227        [FromBody] EmailImapSettingsEditViewModel model,
 228        CancellationToken cancellationToken)
 229    {
 0230        if (!TryValidateImapSettings(model, out var error))
 0231            return BadRequest(new { error });
 232
 233        try
 234        {
 0235            await _imapSettingsStore.SaveAsync(MapSettingsInput(model), cancellationToken);
 0236        }
 0237        catch (Exception ex)
 238        {
 0239            return BadRequest(new { error = $"Не удалось сохранить настройки: {ex.Message}" });
 240        }
 241
 0242        var status = await GetImapConnectionStatusAsync(cancellationToken);
 0243        return Json(new
 0244        {
 0245            success = true,
 0246            message = "Настройки почты сохранены.",
 0247            status
 0248        });
 0249    }
 250
 251    /// <summary>
 252    /// JSON-эндпоинт для динамической загрузки содержимого папки Dropbox
 253    /// </summary>
 254    [HttpGet]
 255    public async Task<IActionResult> GetDropboxFolders(string path)
 256    {
 0257        if (string.IsNullOrWhiteSpace(path))
 0258            return Json(new { });
 259
 260        try
 261        {
 0262            var contents = await _dropboxClient.GetFolderContentsAsync(path);
 0263            return Json(contents);
 264        }
 0265        catch (Exception ex)
 266        {
 0267            return Json(new { error = ex.Message });
 268        }
 0269    }
 270
 271    public async Task<IActionResult> Details(int id)
 272    {
 0273        var result = await _resultDataServices.GetResult(id);
 0274        if (result == null)
 0275            return NotFound();
 276
 0277        return View(result);
 0278    }
 279
 280    [HttpPost]
 281    [Authorize(Roles = "Admin")]
 282    [ValidateAntiForgeryToken]
 283    public async Task<IActionResult> RunNow()
 284    {
 0285        var result = await _routingService.ProcessInboxAsync(false);
 0286        TempData["TransactionResult"] = $"Обработка завершена. {result.ToSummaryString()}";
 0287        return RedirectToAction(nameof(Index));
 0288    }
 289
 290    [HttpPost]
 291    [Authorize(Roles = "Admin")]
 292    [ValidateAntiForgeryToken]
 293    public async Task<IActionResult> DryRun()
 294    {
 0295        var result = await _routingService.ProcessInboxAsync(true);
 0296        TempData["TransactionResult"] = $"Тестовый прогон завершён. {result.ToSummaryString()} Файлы в Dropbox не загруж
 0297        return RedirectToAction(nameof(Index));
 0298    }
 299
 300    [HttpPost]
 301    [Authorize(Roles = "Admin")]
 302    [ValidateAntiForgeryToken]
 303    public async Task<IActionResult> Reprocess(long messageId)
 304    {
 0305        var processed = await _routingService.ReprocessMessageAsync(messageId);
 0306        TempData["TransactionResult"] = processed > 0
 0307            ? "Письмо успешно переобработано."
 0308            : "Письмо не найдено или уже обработано.";
 0309        return RedirectToAction(nameof(Index));
 0310    }
 311
 312    private async Task<EmailImapConnectionViewModel> GetImapConnectionStatusAsync(CancellationToken cancellationToken = 
 313    {
 0314        using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 0315        cts.CancelAfter(TimeSpan.FromSeconds(8));
 316        try
 317        {
 0318            return MapImapConnection(await _emailInboxClient.GetConnectionStatusAsync(cts.Token));
 319        }
 0320        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
 321        {
 0322            var settings = _imapSettings.CurrentValue;
 0323            return new EmailImapConnectionViewModel
 0324            {
 0325                IsConfigured = !string.IsNullOrWhiteSpace(settings.Host) &&
 0326                               !string.IsNullOrWhiteSpace(settings.UserName),
 0327                UserName = settings.UserName,
 0328                Host = settings.Host,
 0329                Port = settings.Port,
 0330                Folder = settings.Folder,
 0331                FetchLimit = settings.FetchLimit,
 0332                IsConnected = false,
 0333                ErrorMessage = "Превышено время ожидания проверки IMAP (8 сек).",
 0334                CheckedAtUtc = DateTime.UtcNow
 0335            };
 336        }
 0337    }
 338
 339    private static EmailImapConnectionViewModel MapImapConnection(ImapConnectionStatus status) =>
 0340        new()
 0341        {
 0342            IsConfigured = status.IsConfigured,
 0343            IsConnected = status.IsConnected,
 0344            UserName = status.UserName,
 0345            Host = status.Host,
 0346            Port = status.Port,
 0347            Folder = status.Folder,
 0348            FetchLimit = status.FetchLimit,
 0349            UnseenCount = status.UnseenCount,
 0350            UnseenReadyCount = status.UnseenReadyCount,
 0351            ErrorMessage = status.ErrorMessage,
 0352            CheckedAtUtc = status.CheckedAtUtc
 0353        };
 354
 355    private static EmailImapSettingsEditViewModel MapSettingsEdit(ImapSettings settings) =>
 0356        new()
 0357        {
 0358            Host = settings.Host,
 0359            Port = settings.Port,
 0360            UserName = settings.UserName,
 0361            UseSsl = settings.UseSsl,
 0362            Folder = settings.Folder,
 0363            FetchLimit = settings.FetchLimit,
 0364            HasSavedPassword = !string.IsNullOrWhiteSpace(settings.Password)
 0365        };
 366
 367    private static EmailImapSettingsInput MapSettingsInput(EmailImapSettingsEditViewModel model) =>
 0368        new()
 0369        {
 0370            Host = model.Host,
 0371            Port = model.Port,
 0372            UserName = model.UserName,
 0373            Password = model.Password,
 0374            UseSsl = model.UseSsl,
 0375            Folder = model.Folder,
 0376            FetchLimit = model.FetchLimit
 0377        };
 378
 379    private static bool TryValidateImapSettings(EmailImapSettingsEditViewModel model, out string error)
 380    {
 0381        error = string.Empty;
 0382        if (model == null)
 383        {
 0384            error = "Настройки не переданы.";
 0385            return false;
 386        }
 387
 0388        if (string.IsNullOrWhiteSpace(model.Host))
 389        {
 0390            error = "Укажите IMAP-сервер.";
 0391            return false;
 392        }
 393
 0394        if (string.IsNullOrWhiteSpace(model.UserName))
 395        {
 0396            error = "Укажите email.";
 0397            return false;
 398        }
 399
 0400        if (model.Port is < 1 or > 65535)
 401        {
 0402            error = "Порт должен быть от 1 до 65535.";
 0403            return false;
 404        }
 405
 0406        if (model.FetchLimit is < 1 or > 500)
 407        {
 0408            error = "Лимит писем должен быть от 1 до 500.";
 0409            return false;
 410        }
 411
 0412        return true;
 413    }
 414}