< Summary

Information
Class: Services.ImportProductPricesServices.CurrentCustomerPriceSource
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ImportProductPricesServices/ImportProductPricesManager.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 602
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Price()100%11100%

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
 17    public ImportProductPricesManager(IHttpClientFactory httpClientFactory, IEmailService? emailService = null)
 18    {
 19        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
 20        _emailService = emailService;
 21    }
 22
 23    public async Task<ProductImportPricesResponse> SetProductPrices(string article,
 24                                                                    OzonClient ozonClient,
 25                                                                    double YourPrice, double OldPrice,
 26                                                                    double MinPrice, double CostPrice)
 27    {
 28        var pricesForArticle = await GetProductPrices(article, ozonClient);
 29        if (pricesForArticle?.Items == null || pricesForArticle.Items.Count == 0)
 30        {
 31            return new ProductImportPricesResponse
 32            {
 33                Result = new List<ProductImportPriceResult>
 34                {
 35                    new ProductImportPriceResult
 36                    {
 37                        OfferId = article,
 38                        ProductId = 0,
 39                        Updated = false,
 40                        Errors = [new ProductImportPriceError()
 41                        {
 42                            Message = "Цены не найдены",
 43                            Code = "SERVER_ERROR"
 44                        }]
 45                    }
 46                }
 47            };
 48        }
 49        try
 50        {
 51            var result = await SetUserPrice(pricesForArticle.Items[0], ozonClient,
 52                                                                  YourPrice, OldPrice, MinPrice, CostPrice);
 53            return result;
 54        }
 55        catch (Exception ex)
 56        {
 57            return new ProductImportPricesResponse
 58            {
 59                Result = new List<ProductImportPriceResult>
 60                {
 61                    new ProductImportPriceResult
 62                    {
 63                        OfferId = article,
 64                        ProductId = 0,
 65                        Updated = false,
 66                        Errors = [new ProductImportPriceError()
 67                        {
 68                            Message = $"Ошибка при расчёте цены: {ex.Message}",
 69                            Code = "SERVER_ERROR"
 70                        }]
 71                    }
 72                }
 73            };
 74        }
 75    }
 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    {
 85        EnsureClient(ozonClient);
 86        var request = new List<ProductPriceItem>
 87        {
 88            new ProductPriceItem
 89            {
 90                OfferId  = offerId,
 91                ProductId = productId,
 92                NetPrice  = Math.Round(costPrice,  MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture)
 93                MinPrice  = Math.Round(minPrice,   MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture)
 94                Price     = Math.Round(yourPrice,  MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture)
 95                OldPrice  = Math.Round(oldPrice,   MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture)
 96                Vat       = vat,
 97            }
 98        };
 99        return await _ozonApiDataManager!.ImportProductPrices(request);
 100    }
 101
 102    public async Task<ProductImportPricesResponse> UpdateProductPrices(string article,
 103                                                                 OzonClient ozonClient,
 104                                                                 double constantIndex,
 105                                                                 double percent)
 106    {
 107        var pricesForArticle = await GetProductPrices(article, ozonClient);
 108        if (pricesForArticle?.Items == null || pricesForArticle.Items.Count == 0)
 109        {
 110            return new ProductImportPricesResponse
 111            {
 112                Result = new List<ProductImportPriceResult>
 113                {
 114                    new ProductImportPriceResult
 115                    {
 116                        OfferId = article,
 117                        ProductId = 0,
 118                        Updated = false,
 119                        Errors = [new ProductImportPriceError()
 120                        {
 121                        Message = "Цены не найдены",
 122                        Code = "SERVER_ERROR"
 123                    }]
 124                    }
 125                }
 126            };
 127        }
 128        try
 129        {
 130            var recommendation = await GetPriceDumpingPreview(pricesForArticle.Items[0], ozonClient, constantIndex, perc
 131            if (recommendation == null)
 132                throw new InvalidOperationException("Не удалось рассчитать рекомендованную цену для демпинга");
 133
 134            var result = await SetNewPrice(pricesForArticle.Items[0], recommendation.TargetSellerPrice, ozonClient);
 135            return result;
 136        }
 137        catch (Exception ex)
 138        {
 139            return new ProductImportPricesResponse
 140            {
 141                Result = new List<ProductImportPriceResult>
 142                {
 143                    new ProductImportPriceResult
 144                    {
 145                        OfferId = article,
 146                        ProductId = 0,
 147                        Updated = false,
 148                        Errors = [new ProductImportPriceError()
 149                        {
 150                            Message = $"Ошибка при расчёте цены: {ex.Message}",
 151                            Code = "SERVER_ERROR"
 152                        }]
 153                    }
 154                }
 155            };
 156        }
 157    }
 158
 159    private async Task<ProductPricesResponse> GetProductPrices(string article, OzonClient ozonClient)
 160    {
 161        EnsureClient(ozonClient);
 162
 163        return await _ozonApiDataManager!.GetProductPricesByArticlesModel(new[] { article });
 164    }
 165
 166    public async Task<PriceDumpingPreview?> GetPriceDumpingPreview(
 167        ProductPriceItemResponse productPriceItemResponse,
 168        OzonClient ozonClient,
 169        double constantIndex = 0.91,
 170        double percent = 2)
 171    {
 172        if (productPriceItemResponse == null)
 173            throw new ArgumentNullException(nameof(productPriceItemResponse));
 174
 175        EnsureClient(ozonClient);
 176        var priceDetails = await GetProductPriceDetails(productPriceItemResponse);
 177        return BuildPriceDumpingPreview(productPriceItemResponse, priceDetails, constantIndex, percent);
 178    }
 179
 180    internal PriceDumpingPreview BuildPriceDumpingPreview(
 181        ProductPriceItemResponse productPriceItemResponse,
 182        ProductPriceDetailsItemResponse? priceDetails,
 183        double constantIndex = 0.91,
 184        double percent = 2)
 185    {
 186        if (productPriceItemResponse == null)
 187            throw new ArgumentNullException(nameof(productPriceItemResponse), "Объект цены не может быть null");
 188
 189        if (productPriceItemResponse.Price == null)
 190            throw new ArgumentException("Данные о цене отсутствуют", nameof(productPriceItemResponse));
 191
 192        if (productPriceItemResponse.PriceIndexes == null ||
 193            productPriceItemResponse.PriceIndexes.OzonIndexData == null)
 194            throw new ArgumentException("Отсутствуют данные об индексах цен", nameof(productPriceItemResponse));
 195
 196        if (constantIndex <= 0)
 197            throw new ArgumentOutOfRangeException(nameof(constantIndex), "Коэффициент должен быть больше 0");
 198
 199        if (percent <= 0)
 200            throw new ArgumentOutOfRangeException(nameof(percent), "Процент должен быть больше 0");
 201
 202        var sellerPrice = GetBasePrice(productPriceItemResponse);
 203        if (sellerPrice <= 0)
 204            throw new InvalidOperationException("У товара отсутствует маркетинговая или текущая цена для расчета");
 205
 206        const double fallbackCustomerFactor = 0.91d;
 207
 208        var currentCustomerPriceSource = GetCurrentCustomerPriceSource(productPriceItemResponse, priceDetails, fallbackC
 209        var currentCustomerPrice = currentCustomerPriceSource.Price;
 210        if (currentCustomerPrice <= 0)
 211            throw new InvalidOperationException("Не удалось определить текущую цену покупателя для расчета демпинга");
 212
 213        var customerPriceFactor = currentCustomerPrice / sellerPrice;
 214        if (customerPriceFactor <= 0)
 215            customerPriceFactor = Math.Round(fallbackCustomerFactor, 2);
 216
 217        customerPriceFactor = Math.Clamp(customerPriceFactor, 0.01d, 1d);
 218
 219        var requestedTargetIndex = Math.Clamp(constantIndex, 0.01d, 2d);
 220        var targetIndex = GetTargetIndex(requestedTargetIndex, percent);
 221        var candidates = GetTargetCandidates(productPriceItemResponse, currentCustomerPrice, targetIndex, fallbackCustom
 222            .Where(x => x.TargetCustomerPrice > 0)
 223            .Select(x => new PriceDumpingCandidatePreview(
 224                x.Source,
 225                x.IndexValue,
 226                x.SourcePrice,
 227                x.TargetCustomerPrice,
 228                x.TargetCustomerPrice / customerPriceFactor))
 229            .ToList();
 230
 231        if (candidates.Count == 0)
 232            throw new InvalidOperationException("Нет индексов цен Ozon, по которым можно рассчитать зеленую цену");
 233
 234        var bestCandidate = candidates
 235            .OrderByDescending(x => x.TargetCustomerPrice)
 236            .First();
 237
 238        var targetCustomerPrice = bestCandidate.TargetCustomerPrice;
 239        var targetSellerPrice = targetCustomerPrice / customerPriceFactor;
 240
 241        if (targetSellerPrice <= 0)
 242            throw new InvalidOperationException("Рассчитанная цена продавца получилась меньше или равна 0");
 243
 244        var currentPriceExplanation = BuildCurrentPriceExplanation(
 245            currentCustomerPriceSource,
 246            sellerPrice,
 247            fallbackCustomerFactor);
 248
 249        var candidatesExplanation = BuildCandidatesExplanation(candidates);
 250        var targetPriceExplanation = BuildTargetPriceExplanation(
 251            requestedTargetIndex,
 252            targetIndex,
 253            currentCustomerPrice,
 254            customerPriceFactor,
 255            percent,
 256            bestCandidate);
 257
 258        return new PriceDumpingPreview
 259        {
 260            CurrentSellerPrice = Math.Round(sellerPrice, 2),
 261            CurrentCustomerPrice = Math.Round(currentCustomerPrice, 2),
 262            CustomerPriceFactor = Math.Round(customerPriceFactor, 4),
 263            TargetCustomerPrice = Math.Round(targetCustomerPrice, 2),
 264            TargetSellerPrice = Math.Round(targetSellerPrice, 2),
 265            TargetIndex = Math.Round(targetIndex, 2),
 266            Source = bestCandidate.Source,
 267            SourceIndexValue = Math.Round(bestCandidate.IndexValue, 2),
 268            SourceMinPrice = Math.Round(bestCandidate.SourcePrice, 2),
 269            DiscountPercent = Math.Round(priceDetails?.DiscountPercent * 100m ?? 0m, 2),
 270            CurrentPriceExplanation = currentPriceExplanation,
 271            TargetPriceExplanation = targetPriceExplanation,
 272            CandidatesExplanation = candidatesExplanation
 273        };
 274    }
 275
 276    private static double GetBasePrice(ProductPriceItemResponse productPriceItemResponse)
 277    {
 278        var price = productPriceItemResponse.Price;
 279        if (price == null)
 280            return 0;
 281
 282        if (price.MarketingSellerPrice > 0)
 283            return price.MarketingSellerPrice;
 284
 285        if (price.MarketingPrice > 0)
 286            return price.MarketingPrice;
 287
 288        return price.Price > 0 ? price.Price : 0;
 289    }
 290
 291    private static double GetMinIndexPrice(ProductPriceItemResponse productPriceItemResponse)
 292    {
 293        var priceIndexes = productPriceItemResponse.PriceIndexes;
 294        if (priceIndexes == null)
 295            return 0;
 296
 297        if (priceIndexes.OzonIndexData?.MinPrice > 0)
 298            return priceIndexes.OzonIndexData.MinPrice;
 299
 300        if (priceIndexes.ExternalIndexData?.MinPrice > 0)
 301            return priceIndexes.ExternalIndexData.MinPrice;
 302
 303        if (priceIndexes.SelfMarketplacesIndexData?.MinPrice > 0)
 304            return priceIndexes.SelfMarketplacesIndexData.MinPrice;
 305
 306        return productPriceItemResponse.Price?.MinPrice > 0
 307            ? productPriceItemResponse.Price.MinPrice
 308            : 0;
 309    }
 310
 311    private IEnumerable<PriceDumpingCandidate> GetTargetCandidates(
 312        ProductPriceItemResponse productPriceItemResponse,
 313        double currentCustomerPrice,
 314        double targetIndex,
 315        double constantIndex)
 316    {
 317        var priceIndexes = productPriceItemResponse.PriceIndexes;
 318        if (priceIndexes == null)
 319            yield break;
 320
 321        var hasRealCandidates = false;
 322
 323        if (priceIndexes.OzonIndexData?.PriceIndexValue > 0)
 324        {
 325            hasRealCandidates = true;
 326            yield return new PriceDumpingCandidate(
 327                "Ozon",
 328                priceIndexes.OzonIndexData.PriceIndexValue,
 329                priceIndexes.OzonIndexData.MinPrice,
 330                currentCustomerPrice * targetIndex / priceIndexes.OzonIndexData.PriceIndexValue);
 331        }
 332
 333        if (priceIndexes.ExternalIndexData?.PriceIndexValue > 0)
 334        {
 335            hasRealCandidates = true;
 336            yield return new PriceDumpingCandidate(
 337                "Другие площадки",
 338                priceIndexes.ExternalIndexData.PriceIndexValue,
 339                priceIndexes.ExternalIndexData.MinPrice,
 340                currentCustomerPrice * targetIndex / priceIndexes.ExternalIndexData.PriceIndexValue);
 341        }
 342
 343        if (priceIndexes.SelfMarketplacesIndexData?.PriceIndexValue > 0)
 344        {
 345            hasRealCandidates = true;
 346            yield return new PriceDumpingCandidate(
 347                "Ваши площадки",
 348                priceIndexes.SelfMarketplacesIndexData.PriceIndexValue,
 349                priceIndexes.SelfMarketplacesIndexData.MinPrice,
 350                currentCustomerPrice * targetIndex / priceIndexes.SelfMarketplacesIndexData.PriceIndexValue);
 351        }
 352
 353        if (!hasRealCandidates)
 354        {
 355            var fallbackMinPrice = GetMinIndexPrice(productPriceItemResponse);
 356            var fallbackBasePrice = GetBasePrice(productPriceItemResponse);
 357            if (fallbackMinPrice > 0 && fallbackBasePrice > 0)
 358            {
 359                var fallbackTruePrice = GetTruePrice(fallbackBasePrice, constantIndex);
 360                var fallbackIndex = fallbackTruePrice > 0 ? fallbackTruePrice / fallbackMinPrice : 0;
 361                if (fallbackIndex > 0)
 362                {
 363                    yield return new PriceDumpingCandidate(
 364                        "Расчетный fallback",
 365                        fallbackIndex,
 366                        fallbackMinPrice,
 367                        currentCustomerPrice * targetIndex / fallbackIndex);
 368                }
 369            }
 370        }
 371    }
 372
 373    private static CurrentCustomerPriceSource GetCurrentCustomerPriceSource(
 374        ProductPriceItemResponse productPriceItemResponse,
 375        ProductPriceDetailsItemResponse? priceDetails,
 376        double constantIndex,
 377        double sellerPrice)
 378    {
 379        if (priceDetails?.CustomerPrice?.Amount > 0)
 380            return new CurrentCustomerPriceSource(
 381                (double)priceDetails.CustomerPrice.Amount,
 382                "prices/details.customer_price",
 383                $"Цена с картой взята из prices/details.customer_price = {priceDetails.CustomerPrice.Amount:0.##}");
 384
 385        if (productPriceItemResponse.Price?.Price > 0)
 386            return new CurrentCustomerPriceSource(
 387                productPriceItemResponse.Price.Price,
 388                "v5.price.price",
 389                $"Цена с картой взята из v5.price.price = {productPriceItemResponse.Price.Price:0.##}");
 390
 391        var fallbackPrice = GetTruePriceStatic(sellerPrice, constantIndex);
 392        return new CurrentCustomerPriceSource(
 393            fallbackPrice,
 394            "fallback",
 395            $"Цена с картой рассчитана fallback: {sellerPrice:0.##} * {Math.Round(constantIndex, 2):0.##} = {fallbackPri
 396    }
 397
 398    private static string BuildCurrentPriceExplanation(CurrentCustomerPriceSource currentCustomerPriceSource, double sel
 399    {
 400        return $"Базовая цена продавца = {sellerPrice:0.##}\n" +
 401               $"Источник цены с картой: {currentCustomerPriceSource.Source}\n" +
 402               currentCustomerPriceSource.Explanation +
 403               (currentCustomerPriceSource.Source == "fallback"
 404                   ? string.Empty
 405                   : $"\nFallback для проверки: {sellerPrice:0.##} * {Math.Round(constantIndex, 2):0.##} = {GetTruePrice
 406    }
 407
 408    private static string BuildCandidatesExplanation(IReadOnlyCollection<PriceDumpingCandidatePreview> candidates)
 409    {
 410        var builder = new StringBuilder();
 411        foreach (var candidate in candidates.OrderBy(x => x.TargetCustomerPrice))
 412        {
 413            if (builder.Length > 0)
 414                builder.AppendLine();
 415
 416            builder.Append($"{candidate.Source}: ориентир {candidate.SourcePrice:0.##}, индекс {candidate.IndexValue:0.#
 417            builder.Append($"цена с картой {candidate.TargetCustomerPrice:0.##}, цена продавца {candidate.TargetSellerPr
 418        }
 419
 420        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    {
 431        var sellerPrice = currentCustomerPrice / customerPriceFactor;
 432
 433        return $"Запрошенный индекс = {requestedTargetIndex:0.##}\n" +
 434               $"Нормализованный целевой индекс = clamp({requestedTargetIndex:0.##}, 0.80, 0.90) - {percent:0.##}/1000 =
 435               $"Коэффициент цены с картой = {currentCustomerPrice:0.##} / {sellerPrice:0.##} = {customerPriceFactor:0.#
 436               $"Выбран рынок: {bestCandidate.Source}\n" +
 437               $"Реком. цена с картой = {currentCustomerPrice:0.##} * {targetIndex:0.##} / {bestCandidate.IndexValue:0.#
 438               $"Реком. цена продавца = {bestCandidate.TargetCustomerPrice:0.##} / {customerPriceFactor:0.####} = {bestC
 439    }
 440
 441    internal static double GetTargetIndex(double requestedTargetIndex, double percent)
 442    {
 443        var normalizedTargetIndex = Math.Clamp(requestedTargetIndex, 0.8d, 0.9d);
 444        var safetyAdjustment = Math.Max(percent, 0d) / 1000d;
 445        return Math.Clamp(normalizedTargetIndex - safetyAdjustment, 0.8d, 0.9d);
 446    }
 447
 448    private async Task<ProductPriceDetailsItemResponse?> GetProductPriceDetails(ProductPriceItemResponse productPriceIte
 449    {
 450        if (string.IsNullOrWhiteSpace(productPriceItemResponse.OfferId))
 451            return null;
 452
 453        var attributes = await _ozonApiDataManager!.GetProductAttributesAsync(
 454            [],
 455            [productPriceItemResponse.OfferId],
 456            null,
 457            limit: 1,
 458            sortBy: "offer_id",
 459            sortDir: "ASC");
 460
 461        var skuValue = (attributes.Result ?? Enumerable.Empty<ProductAttributesItemResponse>())
 462            .FirstOrDefault(x => string.Equals(x.OfferId, productPriceItemResponse.OfferId, StringComparison.Ordinal))?.
 463
 464        if (!long.TryParse(skuValue, out var sku) || sku <= 0)
 465            return null;
 466
 467        var priceDetails = await _ozonApiDataManager!.GetProductPricesDetailsAsync([sku]);
 468        return (priceDetails.Prices ?? Enumerable.Empty<ProductPriceDetailsItemResponse>())
 469            .FirstOrDefault(x => x.Sku == sku);
 470    }
 471
 472    private void EnsureClient(OzonClient ozonClient)
 473    {
 474        var clientId = ozonClient.DecryptClientId ?? string.Empty;
 475        var apiKey = ozonClient.DecryptApiKey ?? string.Empty;
 476        if (_ozonApiDataManager == null)
 477        {
 478            _ozonApiDataManager = new OzonApiDataManager(_httpClientFactory, _emailService, clientId, apiKey);
 479        }
 480        else
 481        {
 482            _ozonApiDataManager.SetClient(clientId, apiKey);
 483        }
 484    }
 485
 486    public double GetTruePrice(double MarketingPrice, double ConstantIndex)
 487    {
 488        return MarketingPrice * Math.Round(ConstantIndex, 2);
 489    }
 490
 491    private static double GetTruePriceStatic(double marketingPrice, double constantIndex)
 492    {
 493        return marketingPrice * Math.Round(constantIndex, 2);
 494    }
 495
 496    internal double ApplyPercent(double price, double percent)
 497    {
 498        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    {
 508        var roundedNewPrice = Math.Round(newPrice, MidpointRounding.AwayFromZero);
 509
 510        if (currentOldPrice <= 0)
 511            return currentOldPrice;
 512
 513        double minDifference;
 514        if (roundedNewPrice < 400)
 515            minDifference = 20;
 516        else if (roundedNewPrice <= 10000)
 517            minDifference = roundedNewPrice * 0.05;
 518        else
 519            minDifference = 500;
 520
 521        return currentOldPrice - roundedNewPrice < minDifference
 522            ? roundedNewPrice + minDifference
 523            : 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    {
 532        EnsureClient(ozonClient);
 533        var request = new List<ProductPriceItem>
 534        {
 535            new ProductPriceItem
 536            {
 537                OfferId = productPriceItemResponse.OfferId,
 538                ProductId = productPriceItemResponse.ProductId,
 539                NetPrice = Math.Round(CostPrice, MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture),
 540                MinPrice = Math.Round(MinPrice, MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture),
 541                Price = Math.Round(YourPrice, MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture),
 542                OldPrice = Math.Round(OldPrice, MidpointRounding.AwayFromZero).ToString(CultureInfo.InvariantCulture),
 543                Vat = productPriceItemResponse.Price.Vat.ToString(CultureInfo.InvariantCulture),
 544            }
 545        };
 546
 547        var response = await _ozonApiDataManager!.ImportProductPrices(request);
 548        return response;
 549    }
 550
 551    private async Task<ProductImportPricesResponse> SetNewPrice(
 552    ProductPriceItemResponse productPriceItemResponse,
 553    double newPrice,
 554    OzonClient ozonClient)
 555    {
 556        EnsureClient(ozonClient);
 557
 558        var roundedNewPrice = Math.Round(newPrice, MidpointRounding.AwayFromZero);
 559        var oldPrice = ComputeMinOldPrice(roundedNewPrice, productPriceItemResponse.Price.MarketingSellerPrice);
 560
 561
 562        var request = new List<ProductPriceItem>
 563        {
 564            new ProductPriceItem
 565            {
 566                OfferId = productPriceItemResponse.OfferId,
 567                ProductId = productPriceItemResponse.ProductId,
 568                NetPrice = Math.Round(productPriceItemResponse.Price.NetPrice, MidpointRounding.AwayFromZero)
 569                    .ToString(CultureInfo.InvariantCulture),
 570                MinPrice = roundedNewPrice.ToString(CultureInfo.InvariantCulture),
 571                Price = roundedNewPrice.ToString(CultureInfo.InvariantCulture),
 572                OldPrice = oldPrice > 0 ? oldPrice.ToString(CultureInfo.InvariantCulture) : null,
 573                Vat = productPriceItemResponse.Price.Vat.ToString(CultureInfo.InvariantCulture),
 574            }
 575        };
 576
 577        var response = await _ozonApiDataManager!.ImportProductPrices(request);
 578        return response;
 579    }
 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
 65602internal sealed record CurrentCustomerPriceSource(double Price, string Source, string Explanation);

Methods/Properties

get_Price()