< Summary

Information
Class: Services.ApiServices.MikadoApi.MikadoHttpClient
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/MikadoApi/MikadoHttpClient.cs
Line coverage
79%
Covered lines: 54
Uncovered lines: 14
Coverable lines: 68
Total lines: 141
Line coverage: 79.4%
Branch coverage
55%
Covered branches: 11
Total branches: 20
Branch coverage: 55%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
GetMyIpAsync(...)100%210%
CodeSearchTestAsync(...)100%11100%
CodeInfoTestAsync(...)100%210%
CodeBrandStockInfoAsync(...)100%11100%
GetStocksInfoAsync(...)100%210%
GetBasketAsync(...)100%11100%
AddToBasketStrictAsync(...)100%11100%
GetOrderHistoryAsync(...)100%11100%
DeleteBasketItemAsync(...)100%11100%
GetExpressOptionsAsync(...)100%11100%
GetDeliveriesAsync(...)100%11100%
get_ClientId()100%11100%
get_Password()100%11100%
Number(...)100%11100%
SendAsync()68.75%171683.87%
ReadSoapFault(...)0%2040%
FormatSoapAction(...)100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/MikadoApi/MikadoHttpClient.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Globalization;
 3using System.Text;
 4using System.Xml.Linq;
 5using Services.ApiServices.ApiSupplierRequestLog;
 6
 7namespace Services.ApiServices.MikadoApi;
 8
 9/// <summary>SOAP 1.1 транспорт WebService Микадо. Порядок полей и namespace каждой операции взяты из WSDL:
 10/// ASMX разбирает тело строгой последовательностью, поэтому переставлять параметры нельзя.</summary>
 11public sealed class MikadoHttpClient
 12{
 13    private const string Provider = "MikadoMskHod20";
 14    // service.asmx объявлен без завершающего слэша, а express.asmx/deliveries.asmx и basket.asmx — со слэшем.
 15    private const string ServiceNamespace = "http://mikado-parts.ru/service";
 16    private const string ExtendedServiceNamespace = "http://mikado-parts.ru/service/";
 17    private const string BasketNamespace = "http://mikado-parts.ru/ws1/";
 118    private static readonly XNamespace SoapNamespace = "http://schemas.xmlsoap.org/soap/envelope/";
 119    private static readonly string[] ClientIdFields = ["ClientID", "nClientID"];
 20
 21    private readonly IHttpClientFactory _factory;
 22    private readonly MikadoConfig _config;
 23    private readonly ApiSupplierRequestLogStore? _logs;
 24
 2625    public MikadoHttpClient(IHttpClientFactory factory, MikadoConfig config, ApiSupplierRequestLogStore? logs = null)
 2626        => (_factory, _config, _logs) = (factory, config, logs);
 27
 28    public Task<string> GetMyIpAsync(CancellationToken ct = default) =>
 029        SendAsync("service.asmx", "Get_MyIP", ServiceNamespace, [], ct);
 30
 31    public Task<string> CodeSearchTestAsync(CancellationToken ct = default) =>
 232        SendAsync("service.asmx", "Code_Search", ServiceNamespace,
 233        [new("Search_Code", "12345"), ClientId, Password, new("FromStockOnly", "FromStockAndByOrder")], ct);
 34
 35    public Task<string> CodeInfoTestAsync(string code, CancellationToken ct = default) =>
 036        SendAsync("service.asmx", "Code_Info", ServiceNamespace,
 037        [new("ZakazCode", code), ClientId, Password], ct);
 38
 39    public Task<string> CodeBrandStockInfoAsync(string code, string brand, CancellationToken ct = default) =>
 340        SendAsync("service.asmx", "CodeBrandStockInfo", ServiceNamespace,
 341        [new("Code", code), new("Brand", brand), ClientId, Password], ct);
 42
 43    public Task<string> GetStocksInfoAsync(CancellationToken ct = default) =>
 044        SendAsync("service.asmx", "Stocks_Ifo", ServiceNamespace, [ClientId, Password], ct);
 45
 46    public Task<string> GetBasketAsync(CancellationToken ct = default) =>
 547        SendAsync("basket.asmx", "Basket_List", BasketNamespace, [ClientId, Password], ct);
 48
 49    public Task<string> AddToBasketStrictAsync(string orderCode, int quantity, int deliveryType, string notes,
 50        int expressId, int stockId, CancellationToken ct = default) =>
 851        SendAsync("basket.asmx", "Basket_Add_StrictQTY", BasketNamespace,
 852        [new("ZakazCode", orderCode), new("QTY", Number(quantity)), new("DeliveryType", Number(deliveryType)),
 853         new("Notes", notes), ClientId, Password,
 854         new("ExpressID", Number(expressId)), new("StockID", Number(stockId))], ct);
 55
 56    /// <summary>Судьба позиции корзины: осталась в буфере, ушла в заказ или удалена.</summary>
 57    public Task<string> GetOrderHistoryAsync(long basketItemId, CancellationToken ct = default) =>
 358        SendAsync("basket.asmx", "Zakaz_History", BasketNamespace,
 359        [new("nOrderID", basketItemId.ToString(CultureInfo.InvariantCulture))], ct);
 60
 61    public Task<string> DeleteBasketItemAsync(long itemId, CancellationToken ct = default) =>
 262        SendAsync("basket.asmx", "Basket_Delete", BasketNamespace,
 263        [new("ItemID", itemId.ToString(CultureInfo.InvariantCulture)), ClientId, Password], ct);
 64
 65    /// <summary>Доступные для конкретной позиции варианты экспресс-доставки.</summary>
 66    public Task<string> GetExpressOptionsAsync(string orderCode, int stockId, CancellationToken ct = default) =>
 267        SendAsync("express.asmx", "ДоступныеЭкспрессЗаказы", ExtendedServiceNamespace,
 268        [ClientId, new("ZakazCode", orderCode), new("StockID", Number(stockId))], ct);
 69
 70    public Task<string> GetDeliveriesAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct = default) =>
 271        SendAsync("deliveries.asmx", "Delivery_List", ExtendedServiceNamespace,
 272        [new("Date_From", dateFrom.ToString("O", CultureInfo.InvariantCulture)),
 273         new("Date_To", dateTo.ToString("O", CultureInfo.InvariantCulture)),
 274         new("nClientID", _config.ClientId), Password], ct);
 75
 2276    private KeyValuePair<string, string> ClientId => new("ClientID", _config.ClientId);
 2277    private KeyValuePair<string, string> Password => new("Password", _config.Password);
 3478    private static string Number(int value) => value.ToString(CultureInfo.InvariantCulture);
 79
 80    private async Task<string> SendAsync(string endpoint, string operation, string operationNamespace,
 81        IReadOnlyCollection<KeyValuePair<string, string>> fields, CancellationToken ct)
 82    {
 28183        foreach (var field in fields)
 84        {
 11485            if (!string.IsNullOrWhiteSpace(field.Value)) continue;
 186            if (ClientIdFields.Contains(field.Key) || field.Key == "Password")
 187                throw new MikadoApiException("Не настроены ClientId или Password для Микадо.");
 88        }
 89
 2690        var xml = new XDocument(new XDeclaration("1.0", "utf-8", null),
 2691            new XElement(SoapNamespace + "Envelope",
 2692                new XElement(SoapNamespace + "Body",
 2693                    new XElement(XName.Get(operation, operationNamespace),
 13594                        fields.Select(x => new XElement(XName.Get(x.Key, operationNamespace), x.Value))))));
 2695        var requestXml = xml.ToString(SaveOptions.DisableFormatting);
 2696        var action = FormatSoapAction(operationNamespace, operation);
 2697        var timer = Stopwatch.StartNew();
 98
 99        HttpResponseMessage response;
 100        try
 101        {
 26102            using var request = new HttpRequestMessage(HttpMethod.Post,
 26103                $"{_config.BaseUrl.TrimEnd('/')}/{endpoint}")
 26104            {
 26105                Content = new StringContent(requestXml, Encoding.UTF8, "text/xml")
 26106            };
 26107            request.Headers.Add("SOAPAction", $"\"{action}\"");
 26108            response = await _factory.CreateClient("MikadoMskHod20Api").SendAsync(request, ct);
 26109        }
 0110        catch (Exception ex) when (ex is not MikadoApiException)
 111        {
 0112            _logs?.Add(Provider, HttpMethod.Post.Method, endpoint, null, timer, requestXml, ex.Message);
 0113            throw new MikadoApiException("Микадо недоступен.", innerException: ex);
 114        }
 115
 26116        using (response)
 117        {
 26118            var responseXml = await response.Content.ReadAsStringAsync(ct);
 26119            _logs?.Add(Provider, HttpMethod.Post.Method, endpoint, (int)response.StatusCode, timer, requestXml, response
 26120            if (!response.IsSuccessStatusCode)
 0121                throw new MikadoApiException($"Микадо вернул HTTP {(int)response.StatusCode}.", (int)response.StatusCode
 26122            if (responseXml.Contains("<soap:Fault", StringComparison.OrdinalIgnoreCase))
 0123                throw new MikadoApiException(ReadSoapFault(responseXml));
 26124            return responseXml;
 125        }
 26126    }
 127
 128    private static string ReadSoapFault(string responseXml)
 129    {
 130        try
 131        {
 0132            var document = XDocument.Parse(responseXml);
 0133            return document.Descendants().FirstOrDefault(x => x.Name.LocalName == "faultstring")?.Value
 0134                   ?? "Микадо вернул ошибку SOAP.";
 135        }
 0136        catch { return "Микадо вернул ошибку SOAP."; }
 0137    }
 138
 139    private static string FormatSoapAction(string operationNamespace, string operation) =>
 26140        $"{operationNamespace.TrimEnd('/')}/{Uri.EscapeDataString(operation)}";
 141}