< Summary

Information
Class: Services.ImportProductPricesServices.ImportProductPricesManager
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ImportProductPricesServices/ImportProductPricesManager.cs
Line coverage
47%
Covered lines: 168
Uncovered lines: 185
Coverable lines: 353
Total lines: 602
Line coverage: 47.5%
Branch coverage
55%
Covered branches: 77
Total branches: 138
Branch coverage: 55.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ImportProductPricesServices/ImportProductPricesManager.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Text;
 3using Microsoft.Extensions.Http;
 4using OzonDomains.Models;
 5using Services.ApiServices.OzonApi;
 6using Services.ApiServices.OzonApi.Filters;
 7using SlqStudio.Application.Services.EmailService;
 8
 9namespace Services.ImportProductPricesServices;
 10
 11public class ImportProductPricesManager
 12{
 13    private readonly IHttpClientFactory _httpClientFactory;
 14    private readonly IEmailService? _emailService;
 15    private OzonApiDataManager? _ozonApiDataManager;
 16
 2917    public ImportProductPricesManager(IHttpClientFactory httpClientFactory, IEmailService? emailService = null)
 18    {
 2919        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
 2920        _emailService = emailService;
 2921    }
 22
 23    public async Task<ProductImportPricesResponse> SetProductPrices(string article,
 24                                                                    OzonClient ozonClient,
 25                                                                    double YourPrice, double OldPrice,
 26                                                                    double MinPrice, double CostPrice)
 27    {
 028        var pricesForArticle = await GetProductPrices(article, ozonClient);
 029        if (pricesForArticle?.Items == null || pricesForArticle.Items.Count == 0)
 30        {
 031            return new ProductImportPricesResponse
 032            {
 033                Result = new List<ProductImportPriceResult>
 034                {
 035                    new ProductImportPriceResult
 036                    {
 037                        OfferId = article,
 038                        ProductId = 0,
 039                        Updated = false,
 040                        Errors = [new ProductImportPriceError()
 041                        {
 042                            Message = "Цены не найдены",
 043                            Code = "SERVER_ERROR"
 044                        }]
 045                    }
 046                }
 047            };
 48        }
 49        try
 50        {
 051            var result = await SetUserPrice(pricesForArticle.Items[0], ozonClient,
 052                                                                  YourPrice, OldPrice, MinPrice, CostPrice);
 053            return result;
 54        }
 055        catch (Exception ex)
 56        {
 057            return new ProductImportPricesResponse
 058            {
 059                Result = new List<ProductImportPriceResult>
 060                {
 061                    new ProductImportPriceResult
 062                    {
 063                        OfferId = article,
 064                        ProductId = 0,
 065                        Updated = false,
 066                        Errors = [new ProductImportPriceError()
 067                        {
 068                            Message = $"Ошибка при расчёте цены: {ex.Message}",
 069                            Code = "SERVER_ERROR"
 070                        }]
 071                    }
 072                }
 073            };
 74        }
 075    }
 76
 77    /// <summary>
 78    /// Устанавливает цену напрямую по известным productId/offerId/vat из снимка, без запроса к Ozon.
 79    /// </summary>
 80    public async Task<ProductImportPricesResponse> SetProductPricesDirect(
 81        string offerId, long productId, string vat,
 82        OzonClient ozonClient,
 83        double yourPrice, double oldPrice, double minPrice, double costPrice)
 84    {
 085        EnsureClient(ozonClient);
 086        var request = new List<ProductPriceItem>
 087        {
 088            new ProductPriceItem
 089            {
 090                OfferId  = offerId,
 091                ProductId = productId,
 092                NetPrice  = Math.Round(costPrice,  MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture)
 093                MinPrice  = Math.Round(minPrice,   MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture)
 094                Price     = Math.Round(yourPrice,  MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture)
 095                OldPrice  = Math.Round(oldPrice,   MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture)
 096                Vat       = vat,
 097            }
 098        };
 099        return await _ozonApiDataManager!.ImportProductPrices(request);
 0100    }
 101
 102    public async Task<ProductImportPricesResponse> UpdateProductPrices(string article,
 103                                                                 OzonClient ozonClient,
 104                                                                 double constantIndex,
 105                                                                 double percent)
 106    {
 0107        var pricesForArticle = await GetProductPrices(article, ozonClient);
 0108        if (pricesForArticle?.Items == null || pricesForArticle.Items.Count == 0)
 109        {
 0110            return new ProductImportPricesResponse
 0111            {
 0112                Result = new List<ProductImportPriceResult>
 0113                {
 0114                    new ProductImportPriceResult
 0115                    {
 0116                        OfferId = article,
 0117                        ProductId = 0,
 0118                        Updated = false,
 0119                        Errors = [new ProductImportPriceError()
 0120                        {
 0121                        Message = "Цены не найдены",
 0122                        Code = "SERVER_ERROR"
 0123                    }]
 0124                    }
 0125                }
 0126            };
 127        }
 128        try
 129        {
 0130            var recommendation = await GetPriceDumpingPreview(pricesForArticle.Items[0], ozonClient, constantIndex, perc
 0131            if (recommendation == null)
 0132                throw new InvalidOperationException("Не удалось рассчитать рекомендованную цену для демпинга");
 133
 0134            var result = await SetNewPrice(pricesForArticle.Items[0], recommendation.TargetSellerPrice, ozonClient);
 0135            return result;
 136        }
 0137        catch (Exception ex)
 138        {
 0139            return new ProductImportPricesResponse
 0140            {
 0141                Result = new List<ProductImportPriceResult>
 0142                {
 0143                    new ProductImportPriceResult
 0144                    {
 0145                        OfferId = article,
 0146                        ProductId = 0,
 0147                        Updated = false,
 0148                        Errors = [new ProductImportPriceError()
 0149                        {
 0150                            Message = $"Ошибка при расчёте цены: {ex.Message}",
 0151                            Code = "SERVER_ERROR"
 0152                        }]
 0153                    }
 0154                }
 0155            };
 156        }
 0157    }
 158
 159    private async Task<ProductPricesResponse> GetProductPrices(string article, OzonClient ozonClient)
 160    {
 0161        EnsureClient(ozonClient);
 162
 0163        return await _ozonApiDataManager!.GetProductPricesByArticlesModel(new[] { article });
 0164    }
 165
 166    public async Task<PriceDumpingPreview?> GetPriceDumpingPreview(
 167        ProductPriceItemResponse productPriceItemResponse,
 168        OzonClient ozonClient,
 169        double constantIndex = 0.91,
 170        double percent = 2)
 171    {
 0172        if (productPriceItemResponse == null)
 0173            throw new ArgumentNullException(nameof(productPriceItemResponse));
 174
 0175        EnsureClient(ozonClient);
 0176        var priceDetails = await GetProductPriceDetails(productPriceItemResponse);
 0177        return BuildPriceDumpingPreview(productPriceItemResponse, priceDetails, constantIndex, percent);
 0178    }
 179
 180    internal PriceDumpingPreview BuildPriceDumpingPreview(
 181        ProductPriceItemResponse productPriceItemResponse,
 182        ProductPriceDetailsItemResponse? priceDetails,
 183        double constantIndex = 0.91,
 184        double percent = 2)
 185    {
 21186        if (productPriceItemResponse == null)
 1187            throw new ArgumentNullException(nameof(productPriceItemResponse), "Объект цены не может быть null");
 188
 20189        if (productPriceItemResponse.Price == null)
 1190            throw new ArgumentException("Данные о цене отсутствуют", nameof(productPriceItemResponse));
 191
 19192        if (productPriceItemResponse.PriceIndexes == null ||
 19193            productPriceItemResponse.PriceIndexes.OzonIndexData == null)
 2194            throw new ArgumentException("Отсутствуют данные об индексах цен", nameof(productPriceItemResponse));
 195
 17196        if (constantIndex <= 0)
 2197            throw new ArgumentOutOfRangeException(nameof(constantIndex), "Коэффициент должен быть больше 0");
 198
 15199        if (percent <= 0)
 1200            throw new ArgumentOutOfRangeException(nameof(percent), "Процент должен быть больше 0");
 201
 14202        var sellerPrice = GetBasePrice(productPriceItemResponse);
 14203        if (sellerPrice <= 0)
 1204            throw new InvalidOperationException("У товара отсутствует маркетинговая или текущая цена для расчета");
 205
 206        const double fallbackCustomerFactor = 0.91d;
 207
 13208        var currentCustomerPriceSource = GetCurrentCustomerPriceSource(productPriceItemResponse, priceDetails, fallbackC
 13209        var currentCustomerPrice = currentCustomerPriceSource.Price;
 13210        if (currentCustomerPrice <= 0)
 0211            throw new InvalidOperationException("Не удалось определить текущую цену покупателя для расчета демпинга");
 212
 13213        var customerPriceFactor = currentCustomerPrice / sellerPrice;
 13214        if (customerPriceFactor <= 0)
 0215            customerPriceFactor = Math.Round(fallbackCustomerFactor, 2);
 216
 13217        customerPriceFactor = Math.Clamp(customerPriceFactor, 0.01d, 1d);
 218
 13219        var requestedTargetIndex = Math.Clamp(constantIndex, 0.01d, 2d);
 13220        var targetIndex = GetTargetIndex(requestedTargetIndex, percent);
 13221        var candidates = GetTargetCandidates(productPriceItemResponse, currentCustomerPrice, targetIndex, fallbackCustom
 14222            .Where(x => x.TargetCustomerPrice > 0)
 14223            .Select(x => new PriceDumpingCandidatePreview(
 14224                x.Source,
 14225                x.IndexValue,
 14226                x.SourcePrice,
 14227                x.TargetCustomerPrice,
 14228                x.TargetCustomerPrice / customerPriceFactor))
 13229            .ToList();
 230
 13231        if (candidates.Count == 0)
 0232            throw new InvalidOperationException("Нет индексов цен Ozon, по которым можно рассчитать зеленую цену");
 233
 13234        var bestCandidate = candidates
 14235            .OrderByDescending(x => x.TargetCustomerPrice)
 13236            .First();
 237
 13238        var targetCustomerPrice = bestCandidate.TargetCustomerPrice;
 13239        var targetSellerPrice = targetCustomerPrice / customerPriceFactor;
 240
 13241        if (targetSellerPrice <= 0)
 0242            throw new InvalidOperationException("Рассчитанная цена продавца получилась меньше или равна 0");
 243
 13244        var currentPriceExplanation = BuildCurrentPriceExplanation(
 13245            currentCustomerPriceSource,
 13246            sellerPrice,
 13247            fallbackCustomerFactor);
 248
 13249        var candidatesExplanation = BuildCandidatesExplanation(candidates);
 13250        var targetPriceExplanation = BuildTargetPriceExplanation(
 13251            requestedTargetIndex,
 13252            targetIndex,
 13253            currentCustomerPrice,
 13254            customerPriceFactor,
 13255            percent,
 13256            bestCandidate);
 257
 13258        return new PriceDumpingPreview
 13259        {
 13260            CurrentSellerPrice = Math.Round(sellerPrice, 2),
 13261            CurrentCustomerPrice = Math.Round(currentCustomerPrice, 2),
 13262            CustomerPriceFactor = Math.Round(customerPriceFactor, 4),
 13263            TargetCustomerPrice = Math.Round(targetCustomerPrice, 2),
 13264            TargetSellerPrice = Math.Round(targetSellerPrice, 2),
 13265            TargetIndex = Math.Round(targetIndex, 2),
 13266            Source = bestCandidate.Source,
 13267            SourceIndexValue = Math.Round(bestCandidate.IndexValue, 2),
 13268            SourceMinPrice = Math.Round(bestCandidate.SourcePrice, 2),
 13269            DiscountPercent = Math.Round(priceDetails?.DiscountPercent * 100m ?? 0m, 2),
 13270            CurrentPriceExplanation = currentPriceExplanation,
 13271            TargetPriceExplanation = targetPriceExplanation,
 13272            CandidatesExplanation = candidatesExplanation
 13273        };
 274    }
 275
 276    private static double GetBasePrice(ProductPriceItemResponse productPriceItemResponse)
 277    {
 15278        var price = productPriceItemResponse.Price;
 15279        if (price == null)
 0280            return 0;
 281
 15282        if (price.MarketingSellerPrice > 0)
 13283            return price.MarketingSellerPrice;
 284
 2285        if (price.MarketingPrice > 0)
 1286            return price.MarketingPrice;
 287
 1288        return price.Price > 0 ? price.Price : 0;
 289    }
 290
 291    private static double GetMinIndexPrice(ProductPriceItemResponse productPriceItemResponse)
 292    {
 1293        var priceIndexes = productPriceItemResponse.PriceIndexes;
 1294        if (priceIndexes == null)
 0295            return 0;
 296
 1297        if (priceIndexes.OzonIndexData?.MinPrice > 0)
 0298            return priceIndexes.OzonIndexData.MinPrice;
 299
 1300        if (priceIndexes.ExternalIndexData?.MinPrice > 0)
 0301            return priceIndexes.ExternalIndexData.MinPrice;
 302
 1303        if (priceIndexes.SelfMarketplacesIndexData?.MinPrice > 0)
 0304            return priceIndexes.SelfMarketplacesIndexData.MinPrice;
 305
 1306        return productPriceItemResponse.Price?.MinPrice > 0
 1307            ? productPriceItemResponse.Price.MinPrice
 1308            : 0;
 309    }
 310
 311    private IEnumerable<PriceDumpingCandidate> GetTargetCandidates(
 312        ProductPriceItemResponse productPriceItemResponse,
 313        double currentCustomerPrice,
 314        double targetIndex,
 315        double constantIndex)
 316    {
 13317        var priceIndexes = productPriceItemResponse.PriceIndexes;
 13318        if (priceIndexes == null)
 0319            yield break;
 320
 13321        var hasRealCandidates = false;
 322
 13323        if (priceIndexes.OzonIndexData?.PriceIndexValue > 0)
 324        {
 12325            hasRealCandidates = true;
 12326            yield return new PriceDumpingCandidate(
 12327                "Ozon",
 12328                priceIndexes.OzonIndexData.PriceIndexValue,
 12329                priceIndexes.OzonIndexData.MinPrice,
 12330                currentCustomerPrice * targetIndex / priceIndexes.OzonIndexData.PriceIndexValue);
 331        }
 332
 13333        if (priceIndexes.ExternalIndexData?.PriceIndexValue > 0)
 334        {
 1335            hasRealCandidates = true;
 1336            yield return new PriceDumpingCandidate(
 1337                "Другие площадки",
 1338                priceIndexes.ExternalIndexData.PriceIndexValue,
 1339                priceIndexes.ExternalIndexData.MinPrice,
 1340                currentCustomerPrice * targetIndex / priceIndexes.ExternalIndexData.PriceIndexValue);
 341        }
 342
 13343        if (priceIndexes.SelfMarketplacesIndexData?.PriceIndexValue > 0)
 344        {
 0345            hasRealCandidates = true;
 0346            yield return new PriceDumpingCandidate(
 0347                "Ваши площадки",
 0348                priceIndexes.SelfMarketplacesIndexData.PriceIndexValue,
 0349                priceIndexes.SelfMarketplacesIndexData.MinPrice,
 0350                currentCustomerPrice * targetIndex / priceIndexes.SelfMarketplacesIndexData.PriceIndexValue);
 351        }
 352
 13353        if (!hasRealCandidates)
 354        {
 1355            var fallbackMinPrice = GetMinIndexPrice(productPriceItemResponse);
 1356            var fallbackBasePrice = GetBasePrice(productPriceItemResponse);
 1357            if (fallbackMinPrice > 0 && fallbackBasePrice > 0)
 358            {
 1359                var fallbackTruePrice = GetTruePrice(fallbackBasePrice, constantIndex);
 1360                var fallbackIndex = fallbackTruePrice > 0 ? fallbackTruePrice / fallbackMinPrice : 0;
 1361                if (fallbackIndex > 0)
 362                {
 1363                    yield return new PriceDumpingCandidate(
 1364                        "Расчетный fallback",
 1365                        fallbackIndex,
 1366                        fallbackMinPrice,
 1367                        currentCustomerPrice * targetIndex / fallbackIndex);
 368                }
 369            }
 370        }
 13371    }
 372
 373    private static CurrentCustomerPriceSource GetCurrentCustomerPriceSource(
 374        ProductPriceItemResponse productPriceItemResponse,
 375        ProductPriceDetailsItemResponse? priceDetails,
 376        double constantIndex,
 377        double sellerPrice)
 378    {
 13379        if (priceDetails?.CustomerPrice?.Amount > 0)
 1380            return new CurrentCustomerPriceSource(
 1381                (double)priceDetails.CustomerPrice.Amount,
 1382                "prices/details.customer_price",
 1383                $"Цена с картой взята из prices/details.customer_price = {priceDetails.CustomerPrice.Amount:0.##}");
 384
 12385        if (productPriceItemResponse.Price?.Price > 0)
 10386            return new CurrentCustomerPriceSource(
 10387                productPriceItemResponse.Price.Price,
 10388                "v5.price.price",
 10389                $"Цена с картой взята из v5.price.price = {productPriceItemResponse.Price.Price:0.##}");
 390
 2391        var fallbackPrice = GetTruePriceStatic(sellerPrice, constantIndex);
 2392        return new CurrentCustomerPriceSource(
 2393            fallbackPrice,
 2394            "fallback",
 2395            $"Цена с картой рассчитана fallback: {sellerPrice:0.##} * {Math.Round(constantIndex, 2):0.##} = {fallbackPri
 396    }
 397
 398    private static string BuildCurrentPriceExplanation(CurrentCustomerPriceSource currentCustomerPriceSource, double sel
 399    {
 13400        return $"Базовая цена продавца = {sellerPrice:0.##}\n" +
 13401               $"Источник цены с картой: {currentCustomerPriceSource.Source}\n" +
 13402               currentCustomerPriceSource.Explanation +
 13403               (currentCustomerPriceSource.Source == "fallback"
 13404                   ? string.Empty
 13405                   : $"\nFallback для проверки: {sellerPrice:0.##} * {Math.Round(constantIndex, 2):0.##} = {GetTruePrice
 406    }
 407
 408    private static string BuildCandidatesExplanation(IReadOnlyCollection<PriceDumpingCandidatePreview> candidates)
 409    {
 13410        var builder = new StringBuilder();
 68411        foreach (var candidate in candidates.OrderBy(x => x.TargetCustomerPrice))
 412        {
 14413            if (builder.Length > 0)
 1414                builder.AppendLine();
 415
 14416            builder.Append($"{candidate.Source}: ориентир {candidate.SourcePrice:0.##}, индекс {candidate.IndexValue:0.#
 14417            builder.Append($"цена с картой {candidate.TargetCustomerPrice:0.##}, цена продавца {candidate.TargetSellerPr
 418        }
 419
 13420        return builder.ToString();
 421    }
 422
 423    private static string BuildTargetPriceExplanation(
 424        double requestedTargetIndex,
 425        double targetIndex,
 426        double currentCustomerPrice,
 427        double customerPriceFactor,
 428        double percent,
 429        PriceDumpingCandidatePreview bestCandidate)
 430    {
 13431        var sellerPrice = currentCustomerPrice / customerPriceFactor;
 432
 13433        return $"Запрошенный индекс = {requestedTargetIndex:0.##}\n" +
 13434               $"Нормализованный целевой индекс = clamp({requestedTargetIndex:0.##}, 0.80, 0.90) - {percent:0.##}/1000 =
 13435               $"Коэффициент цены с картой = {currentCustomerPrice:0.##} / {sellerPrice:0.##} = {customerPriceFactor:0.#
 13436               $"Выбран рынок: {bestCandidate.Source}\n" +
 13437               $"Реком. цена с картой = {currentCustomerPrice:0.##} * {targetIndex:0.##} / {bestCandidate.IndexValue:0.#
 13438               $"Реком. цена продавца = {bestCandidate.TargetCustomerPrice:0.##} / {customerPriceFactor:0.####} = {bestC
 439    }
 440
 441    internal static double GetTargetIndex(double requestedTargetIndex, double percent)
 442    {
 18443        var normalizedTargetIndex = Math.Clamp(requestedTargetIndex, 0.8d, 0.9d);
 18444        var safetyAdjustment = Math.Max(percent, 0d) / 1000d;
 18445        return Math.Clamp(normalizedTargetIndex - safetyAdjustment, 0.8d, 0.9d);
 446    }
 447
 448    private async Task<ProductPriceDetailsItemResponse?> GetProductPriceDetails(ProductPriceItemResponse productPriceIte
 449    {
 0450        if (string.IsNullOrWhiteSpace(productPriceItemResponse.OfferId))
 0451            return null;
 452
 0453        var attributes = await _ozonApiDataManager!.GetProductAttributesAsync(
 0454            [],
 0455            [productPriceItemResponse.OfferId],
 0456            null,
 0457            limit: 1,
 0458            sortBy: "offer_id",
 0459            sortDir: "ASC");
 460
 0461        var skuValue = (attributes.Result ?? Enumerable.Empty<ProductAttributesItemResponse>())
 0462            .FirstOrDefault(x => string.Equals(x.OfferId, productPriceItemResponse.OfferId, StringComparison.Ordinal))?.
 463
 0464        if (!long.TryParse(skuValue, out var sku) || sku <= 0)
 0465            return null;
 466
 0467        var priceDetails = await _ozonApiDataManager!.GetProductPricesDetailsAsync([sku]);
 0468        return (priceDetails.Prices ?? Enumerable.Empty<ProductPriceDetailsItemResponse>())
 0469            .FirstOrDefault(x => x.Sku == sku);
 0470    }
 471
 472    private void EnsureClient(OzonClient ozonClient)
 473    {
 0474        var clientId = ozonClient.DecryptClientId ?? string.Empty;
 0475        var apiKey = ozonClient.DecryptApiKey ?? string.Empty;
 0476        if (_ozonApiDataManager == null)
 477        {
 0478            _ozonApiDataManager = new OzonApiDataManager(_httpClientFactory, _emailService, clientId, apiKey);
 479        }
 480        else
 481        {
 0482            _ozonApiDataManager.SetClient(clientId, apiKey);
 483        }
 0484    }
 485
 486    public double GetTruePrice(double MarketingPrice, double ConstantIndex)
 487    {
 5488        return MarketingPrice * Math.Round(ConstantIndex, 2);
 489    }
 490
 491    private static double GetTruePriceStatic(double marketingPrice, double constantIndex)
 492    {
 13493        return marketingPrice * Math.Round(constantIndex, 2);
 494    }
 495
 496    internal double ApplyPercent(double price, double percent)
 497    {
 4498        return price * (1 - percent / 100);
 499    }
 500
 501    /// <summary>
 502    /// Возвращает скорректированную «старую» цену, чтобы разница между старой и новой
 503    /// ценой соответствовала минимальным требованиям Ozon по ценовым уровням.
 504    /// Вынесено как internal для тестирования без обращения к API.
 505    /// </summary>
 506    internal static double ComputeMinOldPrice(double newPrice, double currentOldPrice)
 507    {
 9508        var roundedNewPrice = Math.Round(newPrice, MidpointRounding.AwayFromZero);
 509
 9510        if (currentOldPrice <= 0)
 1511            return currentOldPrice;
 512
 513        double minDifference;
 8514        if (roundedNewPrice < 400)
 2515            minDifference = 20;
 6516        else if (roundedNewPrice <= 10000)
 4517            minDifference = roundedNewPrice * 0.05;
 518        else
 2519            minDifference = 500;
 520
 8521        return currentOldPrice - roundedNewPrice < minDifference
 8522            ? roundedNewPrice + minDifference
 8523            : currentOldPrice;
 524    }
 525
 526    private async Task<ProductImportPricesResponse> SetUserPrice(
 527        ProductPriceItemResponse productPriceItemResponse,
 528        OzonClient ozonClient,
 529        double YourPrice, double OldPrice,
 530        double MinPrice, double CostPrice)
 531    {
 0532        EnsureClient(ozonClient);
 0533        var request = new List<ProductPriceItem>
 0534        {
 0535            new ProductPriceItem
 0536            {
 0537                OfferId = productPriceItemResponse.OfferId,
 0538                ProductId = productPriceItemResponse.ProductId,
 0539                NetPrice = Math.Round(CostPrice, MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture),
 0540                MinPrice = Math.Round(MinPrice, MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture),
 0541                Price = Math.Round(YourPrice, MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture),
 0542                OldPrice = Math.Round(OldPrice, MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture),
 0543                Vat = productPriceItemResponse.Price.Vat.ToString(CultureInfo.InvariantCulture),
 0544            }
 0545        };
 546
 0547        var response = await _ozonApiDataManager!.ImportProductPrices(request);
 0548        return response;
 0549    }
 550
 551    private async Task<ProductImportPricesResponse> SetNewPrice(
 552    ProductPriceItemResponse productPriceItemResponse,
 553    double newPrice,
 554    OzonClient ozonClient)
 555    {
 0556        EnsureClient(ozonClient);
 557
 0558        var roundedNewPrice = Math.Round(newPrice, MidpointRounding.AwayFromZero);
 0559        var oldPrice = ComputeMinOldPrice(roundedNewPrice, productPriceItemResponse.Price.MarketingSellerPrice);
 560
 561
 0562        var request = new List<ProductPriceItem>
 0563        {
 0564            new ProductPriceItem
 0565            {
 0566                OfferId = productPriceItemResponse.OfferId,
 0567                ProductId = productPriceItemResponse.ProductId,
 0568                NetPrice = Math.Round(productPriceItemResponse.Price.NetPrice, MidpointRounding.AwayFromZero)
 0569                    .ToString(CultureInfo.InvariantCulture),
 0570                MinPrice = roundedNewPrice.ToString(CultureInfo.InvariantCulture),
 0571                Price = roundedNewPrice.ToString(CultureInfo.InvariantCulture),
 0572                OldPrice = oldPrice > 0 ? oldPrice.ToString(CultureInfo.InvariantCulture) : null,
 0573                Vat = productPriceItemResponse.Price.Vat.ToString(CultureInfo.InvariantCulture),
 0574            }
 0575        };
 576
 0577        var response = await _ozonApiDataManager!.ImportProductPrices(request);
 0578        return response;
 0579    }
 580
 581}
 582
 583public sealed class PriceDumpingPreview
 584{
 585    public double CurrentSellerPrice { get; set; }
 586    public double CurrentCustomerPrice { get; set; }
 587    public double CustomerPriceFactor { get; set; }
 588    public double TargetCustomerPrice { get; set; }
 589    public double TargetSellerPrice { get; set; }
 590    public double TargetIndex { get; set; }
 591    public string Source { get; set; } = string.Empty;
 592    public double SourceIndexValue { get; set; }
 593    public double SourceMinPrice { get; set; }
 594    public decimal DiscountPercent { get; set; }
 595    public string CurrentPriceExplanation { get; set; } = string.Empty;
 596    public string TargetPriceExplanation { get; set; } = string.Empty;
 597    public string CandidatesExplanation { get; set; } = string.Empty;
 598}
 599
 600internal sealed record PriceDumpingCandidate(string Source, double IndexValue, double SourcePrice, double TargetCustomer
 601internal sealed record PriceDumpingCandidatePreview(string Source, double IndexValue, double SourcePrice, double TargetC
 602internal sealed record CurrentCustomerPriceSource(double Price, string Source, string Explanation);