< Summary

Information
Class: Services.ApiServices.InterpartsPriceApi.InterpartsPriceApiDataManager
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/InterpartsPriceApi/InterpartsPriceApiDataManager.cs
Line coverage
6%
Covered lines: 13
Uncovered lines: 181
Coverable lines: 194
Total lines: 294
Line coverage: 6.7%
Branch coverage
0%
Covered branches: 0
Total branches: 116
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%11100%
SearchAsync()0%600240%
BuildRequestUrl(...)0%110100%
ToBoolLower(...)0%620%
RedactRequestUrl(...)0%2040%
ParseOffer(...)0%2970540%
GetValue(...)0%620%
ParseNullableInt(...)0%4260%
ParseNullableBool(...)0%4260%
ParseNullableDouble(...)0%7280%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/InterpartsPriceApi/InterpartsPriceApiDataManager.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Xml.Linq;
 3using Microsoft.Extensions.Caching.Memory;
 4using Microsoft.Extensions.Logging;
 5using Services.ApiServices.InterpartsPriceApi.Models;
 6
 7namespace Services.ApiServices.InterpartsPriceApi;
 8
 9public class InterpartsPriceApiDataManager
 10{
 11    private readonly InterpartsPriceApiConfig _config;
 12    private readonly InterpartsPriceApiClient _client;
 13    private readonly IMemoryCache _cache;
 14    private readonly MemoryCacheEntryOptions _cacheOptions;
 15    private readonly ILogger<InterpartsPriceApiDataManager>? _logger;
 16
 517    public InterpartsPriceApiDataManager(
 518        InterpartsPriceApiConfig config,
 519        InterpartsPriceApiClient client,
 520        IMemoryCache cache,
 521        ILogger<InterpartsPriceApiDataManager>? logger = null)
 22    {
 523        _config = config;
 524        _client = client;
 525        _cache = cache;
 526        _logger = logger;
 527        _cacheOptions = new MemoryCacheEntryOptions()
 528            .SetAbsoluteExpiration(TimeSpan.FromHours(1))
 529            .SetPriority(CacheItemPriority.Normal);
 530    }
 31
 32    public async Task<InterpartsPriceSearchResponse> SearchAsync(InterpartsPriceSearchRequest request)
 33    {
 034        if (request == null)
 35        {
 036            return new InterpartsPriceSearchResponse();
 37        }
 38
 039        if (string.IsNullOrWhiteSpace(request.Code))
 40        {
 041            return new InterpartsPriceSearchResponse();
 42        }
 43
 44        // Кэшируем с учетом параметра useCrosses, т.к. он влияет на итоговую цену.
 045        var normalizedCode = request.Code.Trim();
 046        var normalizedLogin = string.IsNullOrWhiteSpace(request.Login) ? "admin" : request.Login.Trim().ToLowerInvariant
 047        var cacheKey =
 048            $"InterpartsPrice_Search_{normalizedCode}_{request.ProducerId}_login_{normalizedLogin}_useCrosses_{ToBoolLow
 049        if (_cache.TryGetValue(cacheKey, out InterpartsPriceSearchResponse? cached))
 50        {
 051            return cached ?? new InterpartsPriceSearchResponse();
 52        }
 53
 54        // Убедимся, что дальше используем нормализованное значение кода
 055        request.Code = normalizedCode;
 56
 057        var requestUrl = BuildRequestUrl(request);
 058        var httpResult = await _client.GetXmlAsync(requestUrl).ConfigureAwait(false);
 059        var xml = httpResult.Body;
 60
 061        if (!string.IsNullOrWhiteSpace(httpResult.ErrorMessage) || string.IsNullOrWhiteSpace(xml))
 62        {
 063            return new InterpartsPriceSearchResponse
 064            {
 065                ErrorMessage = httpResult.ErrorMessage ?? "Interparts Price API вернул пустой ответ.",
 066                HttpStatusCode = httpResult.StatusCode,
 067                RequestUrl = RedactRequestUrl(requestUrl),
 068                RawResponse = xml
 069            };
 70        }
 71
 72        try
 73        {
 074            var doc = XDocument.Parse(xml);
 075            var response = new InterpartsPriceSearchResponse();
 76
 077            response.SearchProducerId = ParseNullableInt(doc.Descendants("SearchProducerId").FirstOrDefault()?.Value);
 78
 079            foreach (var priceEl in doc.Descendants("price"))
 80            {
 081                (response.Offers ??= []).Add(ParseOffer(priceEl));
 82            }
 83
 084            _cache.Set(cacheKey, response, _cacheOptions);
 085            return response;
 86        }
 087        catch (Exception ex)
 88        {
 089            _logger?.LogError(ex, "Failed to parse InterpartsPriceApi XML");
 090            return new InterpartsPriceSearchResponse
 091            {
 092                ErrorMessage = $"Ошибка разбора XML Interparts Price API: {ex.Message}",
 093                HttpStatusCode = httpResult.StatusCode,
 094                RequestUrl = RedactRequestUrl(requestUrl),
 095                RawResponse = xml
 096            };
 97        }
 098    }
 99
 100    private string BuildRequestUrl(InterpartsPriceSearchRequest request)
 101    {
 102        // price-api.interparts.ru/search.aspx - принимает параметры в querystring
 0103        var baseUrl = (_config.BaseUrl ?? "http://price-api.interparts.ru/search.aspx").TrimEnd();
 0104        var separator = baseUrl.Contains("?") ? "&" : "?";
 105
 0106        var queryPairs = new Dictionary<string, string>
 0107        {
 0108            ["secret123"] = _config.Secret123,
 0109            ["code"] = request.Code ?? "",
 0110            ["regionId"] = request.RegionId.ToString(CultureInfo.InvariantCulture),
 0111            ["producerId"] = request.ProducerId.ToString(CultureInfo.InvariantCulture),
 0112            ["login"] = request.Login ?? "",
 0113            ["coef"] = request.Coef.ToString(CultureInfo.InvariantCulture),
 0114            ["destCurrencyId"] = request.DestCurrencyId.ToString(CultureInfo.InvariantCulture),
 0115            ["useCrosses"] = ToBoolLower(request.UseCrosses),
 0116            ["useSets"] = ToBoolLower(request.UseSets),
 0117            ["calculateManagerPrice"] = ToBoolLower(request.CalculateManagerPrice),
 0118            ["useCrossesSimple"] = ToBoolLower(request.UseCrossesSimple),
 0119        };
 120
 121        // Uri.EscapeDataString нужен, чтобы корректно экранировать символы в code/login
 0122        var query = string.Join("&",
 0123            queryPairs.Select(kvp =>
 0124                $"{kvp.Key}={Uri.EscapeDataString(kvp.Value ?? string.Empty)}"));
 125
 0126        return baseUrl + separator + query;
 127    }
 128
 0129    private static string ToBoolLower(bool value) => value ? "true" : "false";
 130
 131    private static string RedactRequestUrl(string url)
 132    {
 0133        var uri = new Uri(url);
 0134        var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
 0135        query.Remove("secret123");
 0136        return uri.GetLeftPart(UriPartial.Path) + (query.Count == 0 ? string.Empty : "?" + query);
 137    }
 138
 139    private static InterpartsPriceOffer ParseOffer(XElement priceEl)
 140    {
 141        // Все числовые поля приходят в виде строк (часто с ',' разделителем).
 142        // Для чисел с длинной дробной частью используем double?, чтобы парсинг не "падал"
 143        // из-за ограничений decimal.
 0144        return new InterpartsPriceOffer
 0145        {
 0146            AddAdditionalWeightPrice = ParseNullableBool(GetValue(priceEl, "AddAdditionalWeightPrice")),
 0147            Amount = ParseNullableInt(GetValue(priceEl, "Amount")),
 0148            ArtNr = GetValue(priceEl, "ArtNr") ?? string.Empty,
 0149
 0150            CalculatedPartWeight = ParseNullableDouble(GetValue(priceEl, "CalculatedPartWeight")),
 0151            CanBuy = ParseNullableBool(GetValue(priceEl, "CanBuy")),
 0152
 0153            ClientPrice = GetValue(priceEl, "ClientPrice") ?? string.Empty,
 0154            ClientPriceCalculated = ParseNullableBool(GetValue(priceEl, "ClientPriceCalculated")),
 0155            ClientPriceDecimal = ParseNullableDouble(GetValue(priceEl, "ClientPriceDecimal")),
 0156
 0157            DbId = ParseNullableInt(GetValue(priceEl, "DbId")),
 0158            FixedCoef = ParseNullableDouble(GetValue(priceEl, "FixedCoef")),
 0159            HaveImages = ParseNullableBool(GetValue(priceEl, "HaveImages")),
 0160
 0161            InPriceSupplier = ParseNullableDouble(GetValue(priceEl, "InPriceSupplier")),
 0162
 0163            InputPrice = GetValue(priceEl, "InputPrice") ?? string.Empty,
 0164            InputPriceCalculated = ParseNullableBool(GetValue(priceEl, "InputPriceCalculated")),
 0165            InputPriceDecimal = ParseNullableDouble(GetValue(priceEl, "InputPriceDecimal")),
 0166
 0167            IsPriceActual = ParseNullableBool(GetValue(priceEl, "IsPriceActual")),
 0168
 0169            ManagerClientPrice = GetValue(priceEl, "ManagerClientPrice") ?? string.Empty,
 0170            ManagerClientPrice2Decimal = ParseNullableDouble(GetValue(priceEl, "ManagerClientPrice2Decimal")),
 0171            ManagerClientPriceDecimal = ParseNullableDouble(GetValue(priceEl, "ManagerClientPriceDecimal")),
 0172
 0173            ManagerCurrencyId = ParseNullableInt(GetValue(priceEl, "ManagerCurrencyId")),
 0174            Multiplicity = ParseNullableInt(GetValue(priceEl, "Multiplicity")),
 0175
 0176            OurPrice = GetValue(priceEl, "OurPrice") ?? string.Empty,
 0177            OurPrice2 = GetValue(priceEl, "OurPrice2") ?? string.Empty,
 0178            OurPrice2Decimal = ParseNullableDouble(GetValue(priceEl, "OurPrice2Decimal")),
 0179
 0180            OurPriceCalculated = ParseNullableBool(GetValue(priceEl, "OurPriceCalculated")),
 0181            OurPriceCalculated2 = ParseNullableBool(GetValue(priceEl, "OurPriceCalculated2")),
 0182            OurPriceDecimal = ParseNullableDouble(GetValue(priceEl, "OurPriceDecimal")),
 0183
 0184            Pack = ParseNullableInt(GetValue(priceEl, "Pack")),
 0185
 0186            PartCode = GetValue(priceEl, "PartCode") ?? string.Empty,
 0187            PartCodeShort = GetValue(priceEl, "PartCodeShort") ?? string.Empty,
 0188            PartDescription = GetValue(priceEl, "PartDescription") ?? string.Empty,
 0189
 0190            PartRating = ParseNullableInt(GetValue(priceEl, "PartRating")),
 0191            PartWeight = ParseNullableDouble(GetValue(priceEl, "PartWeight")),
 0192
 0193            PriceForWeight = GetValue(priceEl, "PriceForWeight") ?? string.Empty,
 0194            PriceForWeightDecimal = ParseNullableDouble(GetValue(priceEl, "PriceForWeightDecimal")),
 0195            PriceForWeightDecimal2 = ParseNullableDouble(GetValue(priceEl, "PriceForWeightDecimal2")),
 0196
 0197            PriceId = ParseNullableInt(GetValue(priceEl, "PriceId")),
 0198            PriceType = GetValue(priceEl, "PriceType") ?? string.Empty,
 0199
 0200            ProducerId = ParseNullableInt(GetValue(priceEl, "ProducerId")),
 0201            ProducerMarketPrefix = GetValue(priceEl, "ProducerMarketPrefix") ?? string.Empty,
 0202            ProducerName = GetValue(priceEl, "ProducerName") ?? string.Empty,
 0203            ProducerPrefix = GetValue(priceEl, "ProducerPrefix") ?? string.Empty,
 0204            ProducerRating = ParseNullableInt(GetValue(priceEl, "ProducerRating")),
 0205            ProducerTecdocManufacturerId = ParseNullableInt(GetValue(priceEl, "ProducerTecdocManufacturerId")),
 0206            ProducerTecdocSupplierId = ParseNullableInt(GetValue(priceEl, "ProducerTecdocSupplierId")),
 0207            RealProducerId = ParseNullableInt(GetValue(priceEl, "RealProducerId")),
 0208
 0209            SetAmount = ParseNullableInt(GetValue(priceEl, "SetAmount")),
 0210            SetCode = GetValue(priceEl, "SetCode") ?? string.Empty,
 0211            SetDescription = GetValue(priceEl, "SetDescription") ?? string.Empty,
 0212            SetId = ParseNullableInt(GetValue(priceEl, "SetId")),
 0213            SetProducerId = ParseNullableInt(GetValue(priceEl, "SetProducerId")),
 0214            SetProducerName = GetValue(priceEl, "SetProducerName") ?? string.Empty,
 0215
 0216            Source = GetValue(priceEl, "Source") ?? string.Empty,
 0217
 0218            SupplierCurrencyId = ParseNullableInt(GetValue(priceEl, "SupplierCurrencyId")),
 0219            SupplierExternal = ParseNullableBool(GetValue(priceEl, "SupplierExternal")),
 0220            SupplierExtId = GetValue(priceEl, "SupplierExtId") ?? string.Empty,
 0221            SupplierId = ParseNullableInt(GetValue(priceEl, "SupplierId")),
 0222            SupplierImg = GetValue(priceEl, "SupplierImg") ?? string.Empty,
 0223            SupplierName = GetValue(priceEl, "SupplierName") ?? string.Empty,
 0224            SupplierPriceVariation = ParseNullableDouble(GetValue(priceEl, "SupplierPriceVariation")),
 0225            SupplyTypeExtId = GetValue(priceEl, "SupplyTypeExtId") ?? string.Empty,
 0226            SupplyTypeId = ParseNullableInt(GetValue(priceEl, "SupplyTypeId")),
 0227
 0228            SurchargeGroup = GetValue(priceEl, "SurchargeGroup") ?? string.Empty,
 0229            SurchargeGroupPercent = GetValue(priceEl, "SurchargeGroupPercent") ?? string.Empty,
 0230            SurchargeGroupPercentDecimal = ParseNullableDouble(GetValue(priceEl, "SurchargeGroupPercentDecimal")),
 0231
 0232            Term = ParseNullableInt(GetValue(priceEl, "Term")),
 0233            Time = GetValue(priceEl, "Time") ?? string.Empty,
 0234
 0235            UpdatedTime = GetValue(priceEl, "UpdatedTime") ?? string.Empty,
 0236            UpdatedTimeSpan = GetValue(priceEl, "UpdatedTimeSpan") ?? string.Empty,
 0237            WeightVariation = ParseNullableDouble(GetValue(priceEl, "WeightVariation")),
 0238        };
 239    }
 240
 241    private static string? GetValue(XElement parent, string elementName) =>
 0242        parent.Element(elementName)?.Value;
 243
 244    private static int? ParseNullableInt(string? value)
 245    {
 0246        if (string.IsNullOrWhiteSpace(value) || value.Trim() == "-")
 247        {
 0248            return null;
 249        }
 250
 0251        return int.TryParse(value.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var v)
 0252            ? v
 0253            : null;
 254    }
 255
 256    private static bool? ParseNullableBool(string? value)
 257    {
 0258        if (string.IsNullOrWhiteSpace(value))
 259        {
 0260            return null;
 261        }
 262
 0263        var trimmed = value.Trim();
 0264        if (trimmed == "-")
 265        {
 0266            return null;
 267        }
 268
 0269        return bool.TryParse(trimmed, out var v) ? v : null;
 270    }
 271
 272    private static double? ParseNullableDouble(string? value)
 273    {
 0274        if (string.IsNullOrWhiteSpace(value) || value.Trim() == "-")
 275        {
 0276            return null;
 277        }
 278
 0279        var trimmed = value.Trim();
 280
 281        // 1) Пытаемся разобрать по ru-RU (',' как decimal separator)
 0282        var ru = CultureInfo.GetCultureInfo("ru-RU");
 0283        if (double.TryParse(trimmed, NumberStyles.Any, ru, out var ruValue))
 284        {
 0285            return ruValue;
 286        }
 287
 288        // 2) Фоллбек: меняем ',' на '.' и парсим как invariant
 0289        var invariant = trimmed.Replace(',', '.');
 0290        return double.TryParse(invariant, NumberStyles.Any, CultureInfo.InvariantCulture, out var v)
 0291            ? v
 0292            : null;
 293    }
 294}