| | | 1 | | using OzonDomains.Nomenclature; |
| | | 2 | | using Services.ApiServices._1CApi.OData; |
| | | 3 | | using Services.ApiServices._1CApi.OData.Models; |
| | | 4 | | |
| | | 5 | | namespace Services.ApiServices._1CApi.Nomenclature; |
| | | 6 | | |
| | | 7 | | /// <summary>Значение реквизита «Производитель» в 1С.</summary> |
| | 153 | 8 | | public sealed record ProducerValue(Guid RefKey, string Name); |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Подбирает существующее значение реквизита «Производитель» для группы бренда, чтобы новая карточка получила его, |
| | | 12 | | /// а не отдельное значение с другим написанием (Febi рядом с FEBI BILSTEIN, KYB рядом с Kayaba — так появлялись дубли). |
| | | 13 | | /// Значения читаются из 1С один раз в час. |
| | | 14 | | /// </summary> |
| | | 15 | | public sealed class ProducerValueResolver(IOneCODataClient client, IProducerDirectoryProvider directoryProvider, TimeSpa |
| | | 16 | | { |
| | | 17 | | private readonly TimeSpan _ttl = ttl ?? TimeSpan.FromHours(1); |
| | | 18 | | private readonly SemaphoreSlim _gate = new(1, 1); |
| | | 19 | | private IReadOnlyList<ProducerValue>? _values; |
| | | 20 | | private DateTime _expiresUtc = DateTime.MinValue; |
| | | 21 | | |
| | | 22 | | /// <param name="groups">Группы бренда заказа.</param> |
| | | 23 | | /// <param name="usageByProducerNorm">Сколько карточек у каждого написания (ключ — <c>ArticleKey.Key(имя)</c>): берё |
| | | 24 | | /// <param name="preferredName">Имя из заказа — решает ничью.</param> |
| | | 25 | | /// <returns>null — в 1С нет значения этой группы (нужно создавать новое прежним способом).</returns> |
| | | 26 | | public async Task<ProducerValue?> FindForGroupsAsync( |
| | | 27 | | IReadOnlySet<string> groups, |
| | | 28 | | IReadOnlyDictionary<string, int>? usageByProducerNorm = null, |
| | | 29 | | string? preferredName = null, |
| | | 30 | | CancellationToken cancellationToken = default) |
| | | 31 | | { |
| | | 32 | | if (groups.Count == 0) |
| | | 33 | | return null; |
| | | 34 | | |
| | | 35 | | var directory = await directoryProvider.GetAsync(cancellationToken); |
| | | 36 | | var values = await GetValuesAsync(cancellationToken); |
| | | 37 | | var preferredNorm = ArticleKey.Key(preferredName); |
| | | 38 | | |
| | | 39 | | return values |
| | | 40 | | .Where(v => directory.GroupsOfName(v.Name).Overlaps(groups)) |
| | | 41 | | .OrderByDescending(v => usageByProducerNorm != null && usageByProducerNorm.TryGetValue(ArticleKey.Key(v.Name |
| | | 42 | | .ThenByDescending(v => preferredNorm.Length > 0 && ArticleKey.Key(v.Name) == preferredNorm) |
| | | 43 | | .ThenBy(v => v.Name, StringComparer.OrdinalIgnoreCase) |
| | | 44 | | .FirstOrDefault(); |
| | | 45 | | } |
| | | 46 | | |
| | | 47 | | /// <summary>Значение по имени (для ручной связи брендов): точное совпадение ключа имени.</summary> |
| | | 48 | | public async Task<ProducerValue?> FindByNameAsync(string? name, CancellationToken cancellationToken = default) |
| | | 49 | | { |
| | | 50 | | var norm = ArticleKey.Key(name); |
| | | 51 | | if (norm.Length == 0) |
| | | 52 | | return null; |
| | | 53 | | |
| | | 54 | | var values = await GetValuesAsync(cancellationToken); |
| | | 55 | | return values.FirstOrDefault(v => ArticleKey.Key(v.Name) == norm); |
| | | 56 | | } |
| | | 57 | | |
| | | 58 | | public void Invalidate() => _expiresUtc = DateTime.MinValue; |
| | | 59 | | |
| | | 60 | | private async Task<IReadOnlyList<ProducerValue>> GetValuesAsync(CancellationToken cancellationToken) |
| | | 61 | | { |
| | | 62 | | var current = _values; |
| | | 63 | | if (current != null && DateTime.UtcNow < _expiresUtc) |
| | | 64 | | return current; |
| | | 65 | | |
| | | 66 | | await _gate.WaitAsync(cancellationToken); |
| | | 67 | | try |
| | | 68 | | { |
| | | 69 | | if (_values != null && DateTime.UtcNow < _expiresUtc) |
| | | 70 | | return _values; |
| | | 71 | | |
| | | 72 | | var raw = await client.GetListAsync<ODataPropertyValue>(OneCNomenclatureService.PropertyValuesEndpoint, |
| | | 73 | | new Dictionary<string, string> |
| | | 74 | | { |
| | | 75 | | ["$select"] = "Ref_Key,Description,Owner_Key,DeletionMark", |
| | | 76 | | ["$filter"] = $"Owner_Key eq guid'{OneCNomenclatureService.ProducerPropertyKey}' and DeletionMark eq |
| | | 77 | | }, cancellationToken); |
| | | 78 | | |
| | | 79 | | _values = raw |
| | | 80 | | .Where(v => v.RefKey != Guid.Empty && !string.IsNullOrWhiteSpace(v.Description)) |
| | | 81 | | .Select(v => new ProducerValue(v.RefKey, v.Description!.Trim())) |
| | | 82 | | .ToList(); |
| | | 83 | | _expiresUtc = DateTime.UtcNow + _ttl; |
| | | 84 | | return _values; |
| | | 85 | | } |
| | | 86 | | finally |
| | | 87 | | { |
| | | 88 | | _gate.Release(); |
| | | 89 | | } |
| | | 90 | | } |
| | | 91 | | } |