< Summary

Information
Class: Services.OneCServices.OneCPostingFailureReporter
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/OneCServices/OneCPostingFailureReporter.cs
Line coverage
88%
Covered lines: 47
Uncovered lines: 6
Coverable lines: 53
Total lines: 128
Line coverage: 88.6%
Branch coverage
71%
Covered branches: 33
Total branches: 46
Branch coverage: 71.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
Classify(...)100%1010100%
OneCError(...)50%22100%
Report(...)75%171687.5%
SendAllAsync()75%44100%
ShouldSend(...)50%291457.14%
ResetCooldownForTests()100%11100%
FirstNonBlank(...)100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/OneCServices/OneCPostingFailureReporter.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using Microsoft.Extensions.Logging;
 3using OzonDomains.Models;
 4using Services.ApiServices._1CApi.Models;
 5using Services.ApiServices.TelegramBotApi;
 6
 7namespace Services.OneCServices;
 8
 9/// <summary>
 10/// Единая точка уведомлений об ошибках проведения в 1С. Сообщает только о сбоях: документ не создан или 1С отказала
 11/// в проведении (с полным ответом 1С). Успешное проведение и намеренные черновики (нехватка остатка по проверке,
 12/// не найдена номенклатура) в Telegram не уходят — пользователь видит их во всплывающем уведомлении интерфейса.
 13/// Вызывается менеджерами документов, поэтому покрывает ручное проведение, сетку транзакций и фоновые задачи.
 14/// </summary>
 15public interface IOneCPostingFailureReporter
 16{
 17    /// <summary>Не ждёт отправки и не бросает исключений.</summary>
 18    void Report(OneCResponse response, string operation, IEnumerable<Order>? orders);
 19}
 20
 821public sealed class OneCPostingFailureReporter(
 822    ITelegramNotifier telegram,
 823    TelegramBotConfig config,
 824    ILogger<OneCPostingFailureReporter>? logger = null) : IOneCPostingFailureReporter
 25{
 26    /// <summary>Одинаковая ошибка по тому же документу не чаще одного раза за окно: ретраи фоновых задач не заспамят ча
 127    internal static readonly TimeSpan Cooldown = TimeSpan.FromMinutes(10);
 28
 129    private static readonly ConcurrentDictionary<string, DateTime> LastSentUtc = new();
 30
 31    public enum FailureKind
 32    {
 33        /// <summary>Не отправляем: проведено или намеренный черновик.</summary>
 34        None,
 35        /// <summary>Документ в 1С не создан (исключение, таймаут, отказ OData).</summary>
 36        NotCreated,
 37        /// <summary>Документ создан, но 1С отказала в проведении.</summary>
 38        PostingRefused
 39    }
 40
 41    public static FailureKind Classify(OneCResponse response)
 42    {
 1643        if (!response.Success)
 244            return FailureKind.NotCreated;
 45
 1446        var intentionalDraft = response.IsDraft || !string.IsNullOrWhiteSpace(response.MissingNomenclature);
 1447        if (!response.IsPosted && !intentionalDraft && !string.IsNullOrWhiteSpace(response.PostingError))
 848            return FailureKind.PostingRefused;
 49
 650        return FailureKind.None;
 51    }
 52
 53    public static string OneCError(OneCResponse response)
 754        => FirstNonBlank(response.ErrorDetail, response.PostingError, response.Message) ?? "(текст ошибки не получен)";
 55
 56    public void Report(OneCResponse response, string operation, IEnumerable<Order>? orders)
 57    {
 58        try
 59        {
 1060            var kind = Classify(response);
 1061            if (kind == FailureKind.None || !telegram.IsConfigured)
 362                return;
 63
 764            var error = OneCError(response);
 765            if (!ShouldSend($"{operation}|{response.DocumentNumber}|{error}", DateTime.UtcNow))
 166                return;
 67
 668            var shipments = (orders ?? [])
 669                .Select(o => o.ShipmentNumber?.Trim())
 670                .Where(n => !string.IsNullOrEmpty(n))
 671                .Select(n => n!)
 672                .Distinct()
 673                .ToList();
 674            var status = kind == FailureKind.NotCreated ? "документ в 1С не создан" : "документ создан, 1С отказала в пр
 675            var hashtag = kind == FailureKind.NotCreated
 676                ? "#1с_документ_не_создан"
 677                : "#1с_документ_не_проведен";
 678            var messages = TelegramMessageFactory.OneCPostingFailure(operation, status, response.DocumentNumber, shipmen
 679                error, FirstNonBlank(response.PostingError, response.Message), config.SiteBaseUrl, hashtag: hashtag);
 80
 681            _ = SendAllAsync(messages);
 682        }
 083        catch (Exception ex)
 84        {
 85            // Уведомление не должно ломать проведение.
 086            logger?.LogWarning(ex, "Не удалось подготовить уведомление об ошибке проведения в 1С");
 087        }
 1088    }
 89
 90    private async Task SendAllAsync(IReadOnlyList<TelegramMessage> messages)
 91    {
 92        try
 93        {
 3194            foreach (var message in messages)
 1095                await telegram.SendAsync(message);
 596        }
 197        catch (Exception ex)
 98        {
 199            logger?.LogWarning(ex, "Не удалось отправить уведомление об ошибке проведения в 1С в Telegram");
 1100        }
 6101    }
 102
 103    internal static bool ShouldSend(string key, DateTime nowUtc)
 104    {
 7105        if (LastSentUtc.Count > 1000)
 106        {
 0107            foreach (var old in LastSentUtc.Where(kv => nowUtc - kv.Value >= Cooldown).Select(kv => kv.Key).ToList())
 0108                LastSentUtc.TryRemove(old, out _);
 109        }
 110
 111        while (true)
 112        {
 7113            if (!LastSentUtc.TryGetValue(key, out var last))
 114            {
 12115                if (LastSentUtc.TryAdd(key, nowUtc)) return true;
 116                continue;
 117            }
 118
 2119            if (nowUtc - last < Cooldown) return false;
 0120            if (LastSentUtc.TryUpdate(key, nowUtc, last)) return true;
 121        }
 122    }
 123
 8124    internal static void ResetCooldownForTests() => LastSentUtc.Clear();
 125
 126    private static string? FirstNonBlank(params string?[] values)
 29127        => values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v));
 128}