< Summary

Information
Class: Services.ApiServices._1CApi.OData.OneCReceiptDuplicateFinder
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/1CApi/OData/OneCReceiptDuplicateFinder.cs
Line coverage
100%
Covered lines: 45
Uncovered lines: 0
Coverable lines: 45
Total lines: 123
Line coverage: 100%
Branch coverage
92%
Covered branches: 24
Total branches: 26
Branch coverage: 92.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
FindAsync()87.5%88100%
IsDuplicate(...)94.44%1818100%
get_RefKey()100%11100%
get_Number()100%11100%
get_Posted()100%11100%
get_DeletionMark()100%11100%
get_IncomingDocNumber()100%11100%
get_CounterpartyKey()100%11100%
get_WarehouseKey()100%11100%
get_Lines()100%11100%
get_NomenclatureKey()100%11100%
get_Quantity()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/1CApi/OData/OneCReceiptDuplicateFinder.cs

#LineLine coverage
 1using Microsoft.Extensions.Logging;
 2using Newtonsoft.Json;
 3using Services.ApiServices._1CApi.OData.Models;
 4
 5namespace Services.ApiServices._1CApi.OData;
 6
 7/// <summary>Приходная накладная, уже существующая в 1С и совпавшая с создаваемой.</summary>
 8public sealed record ExistingReceipt(Guid RefKey, string? Number, bool Posted);
 9
 10/// <summary>
 11/// Ищет в 1С приходную накладную, которую собираются создать повторно: повтор проведения транзакции,
 12/// ретрай после таймаута. Ключ — «НомерВходящегоДокумента + контрагент»; склад-получатель и состав строк
 13/// уточняют совпадение, иначе под защиту попадали бы законные документы:
 14/// <list type="bullet">
 15/// <item>одна транзакция режется по клиентам Ozon, и у всех документов один входящий номер и один поставщик;</item>
 16/// <item>одну фактуру поставщика проводят частями — другими позициями или другим количеством.</item>
 17/// </list>
 18/// Отбор в 1С Fresh ограничен: <c>eq</c> по guid отдаёт 500, а guid-условие после
 19/// <c>and</c> молча отбрасывается. Поэтому в запросе только <c>substringof</c> по номеру и булев <c>DeletionMark</c>,
 20/// а контрагента, склад и точное равенство номера проверяет код по ответу.
 21/// </summary>
 22public sealed class OneCReceiptDuplicateFinder
 23{
 24    private const string ReceiptEndpoint = "Document_ПриходнаяНакладная";
 25    private const int ProbeTop = 100;
 26
 27    private const string Select =
 28        "Ref_Key,Number,Date,Posted,DeletionMark,НомерВходящегоДокумента,Контрагент_Key,СтруктурнаяЕдиница_Key,"
 29        + "Запасы/Номенклатура_Key,Запасы/Количество";
 30
 31    private readonly IOneCODataClient _client;
 32    private readonly ILogger _logger;
 33
 434    public OneCReceiptDuplicateFinder(IOneCODataClient client, ILogger logger)
 35    {
 436        _client = client;
 437        _logger = logger;
 438    }
 39
 40    /// <summary>
 41    /// Возвращает уже существующую накладную или null. Сбой поиска дубль не подтверждает: документ создаётся как раньше
 42    /// иначе недоступный запрос отбора остановил бы весь приход, хотя сам POST мог бы пройти.
 43    /// </summary>
 44    public async Task<ExistingReceipt?> FindAsync(
 45        string? incomingNumber,
 46        Guid counterpartyKey,
 47        Guid warehouseKey,
 48        IReadOnlyCollection<ODataReceiptLine> lines)
 49    {
 450        var number = incomingNumber?.Trim();
 451        if (string.IsNullOrEmpty(number) || lines.Count == 0)
 152            return null;
 53
 54        List<ReceiptCandidate> candidates;
 55        try
 56        {
 357            candidates = await _client.GetSinglePageAsync<ReceiptCandidate>(ReceiptEndpoint, ProbeTop, new Dictionary<st
 358            {
 359                ["$filter"] = $"substringof('{number.Replace("'", "''")}', НомерВходящегоДокумента) and DeletionMark eq 
 360                ["$select"] = Select,
 361                ["$orderby"] = "Date desc"
 362            });
 263        }
 164        catch (Exception ex)
 65        {
 166            _logger.LogWarning(ex, "[OData] Не удалось проверить дубль приходной по входящему номеру {Number}; документ 
 167            return null;
 68        }
 69
 470        var duplicate = candidates.FirstOrDefault(c => IsDuplicate(c, number, counterpartyKey, warehouseKey, lines));
 271        return duplicate == null ? null : new ExistingReceipt(duplicate.RefKey, duplicate.Number, duplicate.Posted);
 472    }
 73
 74    /// <summary>
 75    /// Дубль — тот же входящий номер (точно, без регистра), поставщик и склад, и каждая новая строка уже есть в докумен
 76    /// с тем же количеством. Если 1С не вернула строки, совпадение шапки считается дублем.
 77    /// </summary>
 78    public static bool IsDuplicate(
 79        ReceiptCandidate candidate,
 80        string incomingNumber,
 81        Guid counterpartyKey,
 82        Guid warehouseKey,
 83        IReadOnlyCollection<ODataReceiptLine> newLines)
 84    {
 1285        if (candidate.DeletionMark
 1286            || candidate.CounterpartyKey != counterpartyKey
 1287            || candidate.WarehouseKey != warehouseKey
 1288            || !string.Equals(candidate.IncomingDocNumber?.Trim(), incomingNumber.Trim(), StringComparison.OrdinalIgnore
 589            return false;
 90
 791        if (candidate.Lines.Count == 0)
 192            return true;
 93
 694        var unused = candidate.Lines.ToList();
 2795        foreach (var line in newLines)
 96        {
 1797            var index = unused.FindIndex(x => x.NomenclatureKey == line.NomenclatureKey && x.Quantity == line.Quantity);
 998            if (index < 0)
 399                return false;
 6100            unused.RemoveAt(index);
 101        }
 102
 3103        return true;
 3104    }
 105
 106    public sealed class ReceiptCandidate
 107    {
 13108        [JsonProperty("Ref_Key")] public Guid RefKey { get; set; }
 12109        [JsonProperty("Number")] public string? Number { get; set; }
 2110        [JsonProperty("Posted")] public bool Posted { get; set; }
 13111        [JsonProperty("DeletionMark")] public bool DeletionMark { get; set; }
 19112        [JsonProperty("НомерВходящегоДокумента")] public string? IncomingDocNumber { get; set; }
 22113        [JsonProperty("Контрагент_Key")] public Guid CounterpartyKey { get; set; }
 20114        [JsonProperty("СтруктурнаяЕдиница_Key")] public Guid WarehouseKey { get; set; }
 35115        [JsonProperty("Запасы")] public List<ReceiptCandidateLine> Lines { get; set; } = [];
 116    }
 117
 118    public sealed class ReceiptCandidateLine
 119    {
 20120        [JsonProperty("Номенклатура_Key")] public Guid NomenclatureKey { get; set; }
 19121        [JsonProperty("Количество")] public decimal Quantity { get; set; }
 122    }
 123}