| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using Services.ApiServices.ArmtekApi.Models.Envelope; |
| | | 3 | | |
| | | 4 | | namespace Services.ApiServices.ArmtekApi; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Не даёт повторно обращаться к Armtek одним логином после ответа о суточной квоте. |
| | | 8 | | /// Ограничение API сбрасывается на стороне поставщика; короткое окно защищает приложение |
| | | 9 | | /// от каскада одинаковых 401, не блокируя интеграцию на весь следующий день. |
| | | 10 | | /// </summary> |
| | | 11 | | public sealed class ArmtekDailyQuotaGuard |
| | | 12 | | { |
| | 1 | 13 | | private static readonly TimeSpan BlockDuration = TimeSpan.FromMinutes(30); |
| | 157 | 14 | | private readonly ConcurrentDictionary<string, DateTimeOffset> _blockedUntil = new(StringComparer.Ordinal); |
| | | 15 | | |
| | | 16 | | public bool IsBlocked(string key) => |
| | 131 | 17 | | _blockedUntil.TryGetValue(key, out var until) && until > DateTimeOffset.UtcNow; |
| | | 18 | | |
| | | 19 | | public void Block(string key) => |
| | 4 | 20 | | _blockedUntil[key] = DateTimeOffset.UtcNow.Add(BlockDuration); |
| | | 21 | | |
| | | 22 | | public static bool IsExceeded(string? text) |
| | | 23 | | { |
| | 91 | 24 | | if (string.IsNullOrWhiteSpace(text)) |
| | 3 | 25 | | return false; |
| | | 26 | | |
| | 88 | 27 | | if (text.Contains("Превышено количество запросов", StringComparison.OrdinalIgnoreCase) || |
| | 88 | 28 | | text.Contains("Превышено суточное количество запросов", StringComparison.OrdinalIgnoreCase)) |
| | 15 | 29 | | return true; |
| | | 30 | | |
| | | 31 | | // Armtek API часто возвращает кириллицу в виде unicode escape-последовательностей (\u041f\u0440\u0435\u0432\u04 |
| | 73 | 32 | | if (text.Contains(@"\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e", StringComparison.OrdinalIgnoreCase) |
| | 73 | 33 | | text.Contains(@"\u043f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e", StringComparison.OrdinalIgnoreCase) |
| | 3 | 34 | | return true; |
| | | 35 | | |
| | 70 | 36 | | if (text.Contains(@"\u", StringComparison.Ordinal)) |
| | | 37 | | { |
| | | 38 | | try |
| | | 39 | | { |
| | 0 | 40 | | var unescaped = System.Text.RegularExpressions.Regex.Unescape(text); |
| | 0 | 41 | | return unescaped.Contains("Превышено количество запросов", StringComparison.OrdinalIgnoreCase) || |
| | 0 | 42 | | unescaped.Contains("Превышено суточное количество запросов", StringComparison.OrdinalIgnoreCase); |
| | | 43 | | } |
| | 0 | 44 | | catch |
| | | 45 | | { |
| | | 46 | | // Некорректный escape в строке |
| | 0 | 47 | | } |
| | | 48 | | } |
| | | 49 | | |
| | 70 | 50 | | return false; |
| | 0 | 51 | | } |
| | | 52 | | |
| | | 53 | | public static bool IsExceeded(IReadOnlyList<ArmtekMessage> messages) => |
| | 6 | 54 | | messages.Any(message => IsExceeded(message.Text)); |
| | | 55 | | } |