| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using Microsoft.Extensions.Http; |
| | | 3 | | using Microsoft.Extensions.Logging; |
| | | 4 | | using Newtonsoft.Json.Linq; |
| | | 5 | | using OzonDomains.Models; |
| | | 6 | | using OzonRepositories.Context; |
| | | 7 | | using Services.ApiServices.OzonApi; |
| | | 8 | | using Services.BitrixStockServices; |
| | | 9 | | using Services.DataServices; |
| | | 10 | | using SlqStudio.Application.Services.EmailService; |
| | | 11 | | |
| | | 12 | | namespace Services.AiAssistant; |
| | | 13 | | |
| | | 14 | | /// <summary> |
| | | 15 | | /// Актуальные остатки и цены товара из Ozon Seller API (клиент из заказа / BitrixStock). |
| | | 16 | | /// </summary> |
| | | 17 | | public sealed class AiOzonLiveProductService |
| | | 18 | | { |
| | | 19 | | private readonly IHttpClientFactory _httpClientFactory; |
| | | 20 | | private readonly IEmailService? _emailService; |
| | | 21 | | private readonly OzonClientServices _ozonClientServices; |
| | | 22 | | private readonly ApplicationDbContext _db; |
| | | 23 | | private readonly AiOrderScopeApplier _scopeApplier; |
| | | 24 | | private readonly IBitrixStockSyncArticleHistoryService _bitrixHistory; |
| | | 25 | | private readonly ILogger<AiOzonLiveProductService> _logger; |
| | | 26 | | |
| | 0 | 27 | | public AiOzonLiveProductService( |
| | 0 | 28 | | IHttpClientFactory httpClientFactory, |
| | 0 | 29 | | IEmailService? emailService, |
| | 0 | 30 | | OzonClientServices ozonClientServices, |
| | 0 | 31 | | ApplicationDbContext db, |
| | 0 | 32 | | AiOrderScopeApplier scopeApplier, |
| | 0 | 33 | | IBitrixStockSyncArticleHistoryService bitrixHistory, |
| | 0 | 34 | | ILogger<AiOzonLiveProductService> logger) |
| | | 35 | | { |
| | 0 | 36 | | _httpClientFactory = httpClientFactory; |
| | 0 | 37 | | _emailService = emailService; |
| | 0 | 38 | | _ozonClientServices = ozonClientServices; |
| | 0 | 39 | | _db = db; |
| | 0 | 40 | | _scopeApplier = scopeApplier; |
| | 0 | 41 | | _bitrixHistory = bitrixHistory; |
| | 0 | 42 | | _logger = logger; |
| | 0 | 43 | | } |
| | | 44 | | |
| | | 45 | | /// <summary> |
| | | 46 | | /// Загружает данные Ozon API и возвращает: |
| | | 47 | | /// — plaintext-контекст для AI (без markdown-таблиц, чтобы AI не ломал форматирование) |
| | | 48 | | /// — структурированный <see cref="AiOzonLiveUiDto"/> для рендеринга HTML-таблицы на клиенте |
| | | 49 | | /// </summary> |
| | | 50 | | public async Task<(string AiContext, AiOzonLiveUiDto? LiveUi)> LoadAsync( |
| | | 51 | | string article, |
| | | 52 | | string? manufacturerPrefix, |
| | | 53 | | int? orderId, |
| | | 54 | | string? userId, |
| | | 55 | | CancellationToken ct = default) |
| | | 56 | | { |
| | 0 | 57 | | article = article.Trim(); |
| | 0 | 58 | | if (string.IsNullOrWhiteSpace(article)) |
| | 0 | 59 | | return ("Артикул не указан для запроса к Ozon API.", null); |
| | | 60 | | |
| | 0 | 61 | | var articleWithKey = BuildArticleWithKey(article, manufacturerPrefix); |
| | 0 | 62 | | var candidates = BuildOfferCandidates(article, articleWithKey, manufacturerPrefix); |
| | 0 | 63 | | await AppendOfferIdsFromBitrixHistoryAsync(article, candidates, ct); |
| | | 64 | | |
| | 0 | 65 | | var clientIds = await ResolveOzonClientIdsAsync(article, orderId, userId, ct); |
| | 0 | 66 | | if (clientIds.Count == 0) |
| | | 67 | | { |
| | 0 | 68 | | var msg = $"Артикул «{article}»: не удалось определить Ozon-клиента. Уточните заказ с этим товаром."; |
| | 0 | 69 | | return (msg, null); |
| | | 70 | | } |
| | | 71 | | |
| | 0 | 72 | | var sb = new System.Text.StringBuilder(); |
| | 0 | 73 | | var liveUi = new AiOzonLiveUiDto |
| | 0 | 74 | | { |
| | 0 | 75 | | Article = article, |
| | 0 | 76 | | CheckedAtMsk = AiMskDateTime.Format(DateTime.UtcNow) |
| | 0 | 77 | | }; |
| | | 78 | | |
| | 0 | 79 | | sb.AppendLine("=== Актуальные данные Ozon Seller API (live) ==="); |
| | 0 | 80 | | sb.AppendLine($"Артикул: {article}{(string.IsNullOrWhiteSpace(manufacturerPrefix) ? "" : $" | product key: {arti |
| | 0 | 81 | | sb.AppendLine($"Offer_id кандидаты: {string.Join(", ", candidates.Select(c => $"«{c}»"))}"); |
| | 0 | 82 | | sb.AppendLine(); |
| | | 83 | | |
| | 0 | 84 | | foreach (var clientId in clientIds.Distinct().Take(3)) |
| | | 85 | | { |
| | 0 | 86 | | var client = await _ozonClientServices.GetOzonClientAsync(clientId); |
| | 0 | 87 | | if (client == null || |
| | 0 | 88 | | string.IsNullOrWhiteSpace(client.DecryptClientId) || |
| | 0 | 89 | | string.IsNullOrWhiteSpace(client.DecryptApiKey)) |
| | | 90 | | continue; |
| | | 91 | | |
| | 0 | 92 | | var (text, clientUi) = await FetchForClientAsync(client, candidates, ct); |
| | 0 | 93 | | if (text != null) |
| | | 94 | | { |
| | 0 | 95 | | sb.AppendLine(text); |
| | 0 | 96 | | sb.AppendLine(); |
| | | 97 | | } |
| | 0 | 98 | | if (clientUi != null) |
| | 0 | 99 | | liveUi.Clients.Add(clientUi); |
| | | 100 | | } |
| | | 101 | | |
| | 0 | 102 | | if (liveUi.Clients.Count == 0) |
| | 0 | 103 | | sb.AppendLine("Товар не найден в Ozon API ни у одного из доступных клиентов."); |
| | | 104 | | |
| | 0 | 105 | | sb.AppendLine( |
| | 0 | 106 | | "ПРИМЕЧАНИЕ: live-остатки — факт на Ozon сейчас. " + |
| | 0 | 107 | | "Остатки в API только целые числа. " + |
| | 0 | 108 | | "Дробные значения в WarehouseStock/1С — другой источник данных."); |
| | | 109 | | |
| | 0 | 110 | | return (sb.ToString().TrimEnd(), liveUi.Clients.Count > 0 ? liveUi : null); |
| | 0 | 111 | | } |
| | | 112 | | |
| | | 113 | | // Для обратной совместимости |
| | | 114 | | public async Task<string> LoadFormattedAsync( |
| | | 115 | | string article, |
| | | 116 | | string? manufacturerPrefix, |
| | | 117 | | int? orderId, |
| | | 118 | | string? userId, |
| | | 119 | | CancellationToken ct = default) |
| | | 120 | | { |
| | 0 | 121 | | var (ctx, _) = await LoadAsync(article, manufacturerPrefix, orderId, userId, ct); |
| | 0 | 122 | | return ctx; |
| | 0 | 123 | | } |
| | | 124 | | |
| | | 125 | | private static string BuildArticleWithKey(string article, string? manufacturerPrefix) => |
| | 0 | 126 | | !string.IsNullOrWhiteSpace(manufacturerPrefix) |
| | 0 | 127 | | ? $"{article}={manufacturerPrefix.Trim()}" |
| | 0 | 128 | | : article; |
| | | 129 | | |
| | | 130 | | private static List<string> BuildOfferCandidates( |
| | | 131 | | string article, |
| | | 132 | | string articleWithKey, |
| | | 133 | | string? manufacturerPrefix) |
| | | 134 | | { |
| | 0 | 135 | | var set = BitrixOzonStockLinkage |
| | 0 | 136 | | .BuildOfferIdCandidates(articleWithKey, article) |
| | 0 | 137 | | .ToList(); |
| | | 138 | | |
| | 0 | 139 | | if (!string.IsNullOrWhiteSpace(manufacturerPrefix)) |
| | | 140 | | { |
| | 0 | 141 | | var mfr = manufacturerPrefix.Trim(); |
| | 0 | 142 | | if (!set.Contains($"{article}={mfr}", StringComparer.OrdinalIgnoreCase)) |
| | 0 | 143 | | set.Add($"{article}={mfr}"); |
| | | 144 | | } |
| | | 145 | | |
| | 0 | 146 | | return set; |
| | | 147 | | } |
| | | 148 | | |
| | | 149 | | private async Task AppendOfferIdsFromBitrixHistoryAsync( |
| | | 150 | | string article, |
| | | 151 | | List<string> candidates, |
| | | 152 | | CancellationToken ct) |
| | | 153 | | { |
| | | 154 | | try |
| | | 155 | | { |
| | 0 | 156 | | var page = await _bitrixHistory.GetAllAsync(article, maxReports: 3, ct); |
| | 0 | 157 | | foreach (var offerId in page.Reports |
| | 0 | 158 | | .SelectMany(r => r.Rows) |
| | 0 | 159 | | .Select(r => r.Line.OfferId) |
| | 0 | 160 | | .Where(id => !string.IsNullOrWhiteSpace(id))) |
| | | 161 | | { |
| | 0 | 162 | | if (!candidates.Contains(offerId!, StringComparer.OrdinalIgnoreCase)) |
| | 0 | 163 | | candidates.Add(offerId!); |
| | | 164 | | } |
| | 0 | 165 | | } |
| | 0 | 166 | | catch (Exception ex) |
| | | 167 | | { |
| | 0 | 168 | | _logger.LogWarning(ex, "AI Ozon live: bitrix offer_id lookup failed for {Article}", article); |
| | 0 | 169 | | } |
| | 0 | 170 | | } |
| | | 171 | | |
| | | 172 | | private async Task<List<int>> ResolveOzonClientIdsAsync( |
| | | 173 | | string article, |
| | | 174 | | int? orderId, |
| | | 175 | | string? userId, |
| | | 176 | | CancellationToken ct) |
| | | 177 | | { |
| | 0 | 178 | | var ids = new List<int>(); |
| | | 179 | | |
| | 0 | 180 | | if (orderId is > 0) |
| | | 181 | | { |
| | 0 | 182 | | var query = _db.Orders.AsNoTracking().Where(o => o.Id == orderId.Value); |
| | 0 | 183 | | query = await _scopeApplier.ApplyAsync(query, userId, ct); |
| | 0 | 184 | | var ozonClientId = await query.Select(o => o.OzonClientId).FirstOrDefaultAsync(ct); |
| | 0 | 185 | | if (ozonClientId is > 0) |
| | 0 | 186 | | ids.Add(ozonClientId.Value); |
| | | 187 | | } |
| | | 188 | | |
| | 0 | 189 | | if (ids.Count == 0) |
| | | 190 | | { |
| | 0 | 191 | | var orderQuery = _db.Orders.AsNoTracking() |
| | 0 | 192 | | .Where(o => o.Article == article && o.OzonClientId != null); |
| | 0 | 193 | | orderQuery = await _scopeApplier.ApplyAsync(orderQuery, userId, ct); |
| | 0 | 194 | | var fromOrder = await orderQuery |
| | 0 | 195 | | .OrderByDescending(o => o.ProcessingDate) |
| | 0 | 196 | | .Select(o => o.OzonClientId!.Value) |
| | 0 | 197 | | .FirstOrDefaultAsync(ct); |
| | 0 | 198 | | if (fromOrder > 0) |
| | 0 | 199 | | ids.Add(fromOrder); |
| | | 200 | | } |
| | | 201 | | |
| | | 202 | | try |
| | | 203 | | { |
| | 0 | 204 | | var page = await _bitrixHistory.GetAllAsync(article, maxReports: 5, ct); |
| | 0 | 205 | | foreach (var cid in page.Reports |
| | 0 | 206 | | .SelectMany(r => r.Rows) |
| | 0 | 207 | | .Select(r => r.Line.OzonClientId) |
| | 0 | 208 | | .Where(id => id > 0)) |
| | | 209 | | { |
| | 0 | 210 | | if (!ids.Contains(cid)) |
| | 0 | 211 | | ids.Add(cid); |
| | | 212 | | } |
| | 0 | 213 | | } |
| | 0 | 214 | | catch (Exception ex) |
| | | 215 | | { |
| | 0 | 216 | | _logger.LogWarning(ex, "AI Ozon live: client resolve from bitrix failed for {Article}", article); |
| | 0 | 217 | | } |
| | | 218 | | |
| | 0 | 219 | | return ids; |
| | 0 | 220 | | } |
| | | 221 | | |
| | | 222 | | private async Task<(string? Text, AiOzonLiveClientUiDto? Ui)> FetchForClientAsync( |
| | | 223 | | OzonClient client, |
| | | 224 | | IReadOnlyList<string> candidates, |
| | | 225 | | CancellationToken ct) |
| | | 226 | | { |
| | 0 | 227 | | var api = new OzonApiDataManager( |
| | 0 | 228 | | _httpClientFactory, |
| | 0 | 229 | | _emailService, |
| | 0 | 230 | | client.DecryptClientId!, |
| | 0 | 231 | | client.DecryptApiKey!); |
| | | 232 | | |
| | 0 | 233 | | var sb = new System.Text.StringBuilder(); |
| | 0 | 234 | | sb.AppendLine($"--- Ozon-клиент «{client.Name}» ---"); |
| | | 235 | | |
| | 0 | 236 | | var ui = new AiOzonLiveClientUiDto { ClientName = client.Name ?? $"id {client.Id}" }; |
| | 0 | 237 | | string? matchedOfferId = null; |
| | 0 | 238 | | JArray? stockItems = null; |
| | | 239 | | |
| | | 240 | | // ── Остатки ────────────────────────────────────────────────────────── |
| | | 241 | | try |
| | | 242 | | { |
| | 0 | 243 | | var stocks = await api.GetProductStocks(candidates.ToArray()); |
| | 0 | 244 | | stockItems = stocks["items"] as JArray; |
| | 0 | 245 | | if (stockItems is { Count: > 0 }) |
| | | 246 | | { |
| | 0 | 247 | | matchedOfferId = stockItems[0]["offer_id"]?.ToString(); |
| | 0 | 248 | | ui.OfferId ??= matchedOfferId; |
| | 0 | 249 | | ui.Found = true; |
| | | 250 | | |
| | | 251 | | // Собираем FBS-SKU для запроса по складам |
| | 0 | 252 | | var fbsSkus = new List<string>(); |
| | 0 | 253 | | foreach (var item in stockItems) |
| | | 254 | | { |
| | 0 | 255 | | if (item["stocks"] is not JArray ss) continue; |
| | 0 | 256 | | foreach (var s in ss) |
| | | 257 | | { |
| | 0 | 258 | | if (string.Equals(s["type"]?.ToString(), "fbs", StringComparison.OrdinalIgnoreCase)) |
| | | 259 | | { |
| | 0 | 260 | | var sku = s["sku"]?.ToString(); |
| | 0 | 261 | | if (!string.IsNullOrEmpty(sku) && sku != "0") fbsSkus.Add(sku); |
| | | 262 | | } |
| | | 263 | | } |
| | | 264 | | } |
| | | 265 | | |
| | | 266 | | // Запрашиваем детализацию по складам (FBS) |
| | 0 | 267 | | Dictionary<string, (string Name, int Present, int Reserved)[]>? whDetail = null; |
| | 0 | 268 | | if (fbsSkus.Count > 0) |
| | | 269 | | { |
| | | 270 | | try |
| | | 271 | | { |
| | 0 | 272 | | var whResp = await api.GetProductWarehouses(fbsSkus.ToArray(), []); |
| | 0 | 273 | | var whItems = whResp["result"] as JArray ?? whResp["products"] as JArray; |
| | 0 | 274 | | if (whItems is { Count: > 0 }) |
| | | 275 | | { |
| | 0 | 276 | | whDetail = whItems |
| | 0 | 277 | | .GroupBy(w => w["sku"]?.ToString() ?? "") |
| | 0 | 278 | | .ToDictionary( |
| | 0 | 279 | | g => g.Key, |
| | 0 | 280 | | g => g.Select(w => ( |
| | 0 | 281 | | Name: w["warehouse_name"]?.ToString() ?? $"id {w["warehouse_id"]}", |
| | 0 | 282 | | Present: w["present"]?.Value<int>() ?? 0, |
| | 0 | 283 | | Reserved: w["reserved"]?.Value<int>() ?? 0 |
| | 0 | 284 | | )).ToArray()); |
| | | 285 | | } |
| | 0 | 286 | | } |
| | 0 | 287 | | catch (Exception ex) |
| | | 288 | | { |
| | 0 | 289 | | _logger.LogWarning(ex, "AI Ozon live: stocks-by-warehouse failed"); |
| | 0 | 290 | | } |
| | | 291 | | } |
| | | 292 | | |
| | 0 | 293 | | sb.AppendLine("Остатки по складам:"); |
| | 0 | 294 | | foreach (var item in stockItems) |
| | | 295 | | { |
| | 0 | 296 | | var oid = item["offer_id"]?.ToString() ?? "—"; |
| | 0 | 297 | | if (item["stocks"] is not JArray whStocks || whStocks.Count == 0) |
| | | 298 | | { |
| | 0 | 299 | | sb.AppendLine($" {oid}: остатков нет"); |
| | 0 | 300 | | continue; |
| | | 301 | | } |
| | 0 | 302 | | foreach (var s in whStocks) |
| | | 303 | | { |
| | 0 | 304 | | var whType = s["type"]?.ToString() ?? "—"; |
| | 0 | 305 | | var present = s["present"]?.Value<int?>() ?? s["stock"]?.Value<int?>() ?? 0; |
| | 0 | 306 | | var reserved = s["reserved"]?.Value<int?>() ?? 0; |
| | 0 | 307 | | var sku = s["sku"]?.ToString(); |
| | | 308 | | |
| | | 309 | | // Для FBS — разворачиваем по складам если есть детализация |
| | 0 | 310 | | if (string.Equals(whType, "fbs", StringComparison.OrdinalIgnoreCase) |
| | 0 | 311 | | && whDetail != null && sku != null && whDetail.TryGetValue(sku, out var whRows) |
| | 0 | 312 | | && whRows.Length > 0) |
| | | 313 | | { |
| | 0 | 314 | | foreach (var wh in whRows) |
| | | 315 | | { |
| | 0 | 316 | | sb.AppendLine($" {oid} | FBS | {wh.Name}: {wh.Present} шт." + |
| | 0 | 317 | | (wh.Reserved > 0 ? $", резерв {wh.Reserved}" : "")); |
| | 0 | 318 | | ui.Stocks.Add(new AiOzonStockRowUiDto |
| | 0 | 319 | | { |
| | 0 | 320 | | WarehouseType = "fbs", |
| | 0 | 321 | | WarehouseName = wh.Name, |
| | 0 | 322 | | Present = wh.Present, |
| | 0 | 323 | | Reserved = wh.Reserved |
| | 0 | 324 | | }); |
| | | 325 | | } |
| | | 326 | | } |
| | | 327 | | else |
| | | 328 | | { |
| | 0 | 329 | | sb.AppendLine($" {oid} | {whType}: {present} шт." + |
| | 0 | 330 | | (reserved > 0 ? $", резерв {reserved}" : "")); |
| | 0 | 331 | | ui.Stocks.Add(new AiOzonStockRowUiDto |
| | 0 | 332 | | { |
| | 0 | 333 | | WarehouseType = whType, |
| | 0 | 334 | | Present = present, |
| | 0 | 335 | | Reserved = reserved |
| | 0 | 336 | | }); |
| | | 337 | | } |
| | | 338 | | } |
| | | 339 | | } |
| | 0 | 340 | | } |
| | | 341 | | else |
| | | 342 | | { |
| | 0 | 343 | | sb.AppendLine("Остатки: не найдено."); |
| | | 344 | | } |
| | 0 | 345 | | } |
| | 0 | 346 | | catch (Exception ex) |
| | | 347 | | { |
| | 0 | 348 | | sb.AppendLine($"Остатки: ошибка — {ex.Message}"); |
| | 0 | 349 | | } |
| | | 350 | | |
| | | 351 | | // ── Карточка товара ─────────────────────────────────────────────────── |
| | | 352 | | try |
| | | 353 | | { |
| | 0 | 354 | | var info = await api.GetProductInfoListAsync(offerIds: candidates.ToArray()); |
| | 0 | 355 | | if (info["items"] is JArray infoItems && infoItems.Count > 0) |
| | | 356 | | { |
| | 0 | 357 | | sb.AppendLine("Карточка товара:"); |
| | 0 | 358 | | foreach (var item in infoItems) |
| | | 359 | | { |
| | 0 | 360 | | var oid = item["offer_id"]?.ToString() ?? "—"; |
| | 0 | 361 | | var name = item["name"]?.ToString(); |
| | 0 | 362 | | var archived = item["is_archived"]?.Value<bool>() == true; |
| | 0 | 363 | | var pid = item["id"]?.Value<long?>(); |
| | | 364 | | |
| | 0 | 365 | | matchedOfferId ??= oid; |
| | 0 | 366 | | ui.OfferId ??= oid; |
| | 0 | 367 | | ui.Found = true; |
| | 0 | 368 | | ui.ProductName ??= name; |
| | 0 | 369 | | ui.ProductId ??= pid; |
| | 0 | 370 | | ui.IsArchived = archived; |
| | | 371 | | |
| | | 372 | | // Атрибуты: описание, категория, фото |
| | 0 | 373 | | var descAttr = item["description_category_id"]?.ToString(); |
| | 0 | 374 | | if (item["description"] is JToken descToken) |
| | 0 | 375 | | ui.Description ??= descToken.ToString().Trim(); |
| | 0 | 376 | | if (item["category_id"] is JToken catToken) |
| | 0 | 377 | | ui.CategoryName ??= catToken.ToString(); |
| | 0 | 378 | | var images = item["images"] as JArray ?? item["primary_image"] as JArray; |
| | 0 | 379 | | if (ui.ImageUrl == null && images is { Count: > 0 }) |
| | 0 | 380 | | ui.ImageUrl = images[0]?.ToString(); |
| | 0 | 381 | | if (ui.ImageUrl == null) |
| | 0 | 382 | | ui.ImageUrl = item["primary_image"]?.ToString(); |
| | | 383 | | _ = descAttr; |
| | | 384 | | |
| | 0 | 385 | | sb.AppendLine($" {oid} | {name ?? "—"} | product_id {pid} | архив: {(archived ? "да" : "нет")}"); |
| | 0 | 386 | | if (!string.IsNullOrWhiteSpace(ui.Description)) |
| | 0 | 387 | | sb.AppendLine($" Описание: {ui.Description[..Math.Min(150, ui.Description.Length)]}…"); |
| | | 388 | | } |
| | | 389 | | } |
| | | 390 | | else |
| | | 391 | | { |
| | 0 | 392 | | sb.AppendLine("Карточка: не найдена."); |
| | | 393 | | } |
| | 0 | 394 | | } |
| | 0 | 395 | | catch (Exception ex) |
| | | 396 | | { |
| | 0 | 397 | | sb.AppendLine($"Карточка: ошибка — {ex.Message}"); |
| | 0 | 398 | | } |
| | | 399 | | |
| | | 400 | | // ── Цены ───────────────────────────────────────────────────────────── |
| | | 401 | | try |
| | | 402 | | { |
| | 0 | 403 | | var priceResp = await api.GetProductPricesByArticlesModel(candidates.ToArray()); |
| | 0 | 404 | | var priceItems = priceResp.Items; |
| | 0 | 405 | | if (priceItems is { Count: > 0 }) |
| | | 406 | | { |
| | 0 | 407 | | sb.AppendLine("Цены:"); |
| | 0 | 408 | | foreach (var pi in priceItems) |
| | | 409 | | { |
| | 0 | 410 | | var oid = pi.OfferId ?? "—"; |
| | 0 | 411 | | var price = pi.Price?.Price ?? 0; |
| | 0 | 412 | | var oldPrice = pi.Price?.OldPrice ?? 0; |
| | 0 | 413 | | var mktPrice = pi.Price?.MarketingPrice ?? 0; |
| | 0 | 414 | | var minPrice = pi.Price?.MinPrice ?? 0; |
| | 0 | 415 | | var idxLabel = BitrixStockSyncReportRowPresenter.FormatPriceIndex(pi.PriceIndexes?.ColorIndex); |
| | 0 | 416 | | var idxColor = pi.PriceIndexes?.ColorIndex; |
| | | 417 | | |
| | 0 | 418 | | matchedOfferId ??= oid; |
| | 0 | 419 | | ui.Found = true; |
| | 0 | 420 | | ui.Price = new AiOzonPriceUiDto |
| | 0 | 421 | | { |
| | 0 | 422 | | Price = (decimal)price, |
| | 0 | 423 | | OldPrice = oldPrice > 0 ? (decimal?)oldPrice : null, |
| | 0 | 424 | | MarketingPrice = mktPrice > 0 ? (decimal?)mktPrice : null, |
| | 0 | 425 | | MinPrice = minPrice > 0 ? (decimal?)minPrice : null, |
| | 0 | 426 | | PriceIndex = string.IsNullOrWhiteSpace(idxLabel) ? null : new AiPriceIndexUiDto |
| | 0 | 427 | | { |
| | 0 | 428 | | Key = idxColor ?? "", |
| | 0 | 429 | | Label = idxLabel, |
| | 0 | 430 | | Bg = PriceIndexBackground(idxColor), |
| | 0 | 431 | | Fg = PriceIndexForeground(idxColor) |
| | 0 | 432 | | } |
| | 0 | 433 | | }; |
| | | 434 | | |
| | 0 | 435 | | sb.AppendLine($" {oid}: {price:N0} ₽" + |
| | 0 | 436 | | (mktPrice > 0 ? $", витрина {mktPrice:N0} ₽" : "") + |
| | 0 | 437 | | (minPrice > 0 ? $", min {minPrice:N0} ₽" : "") + |
| | 0 | 438 | | (oldPrice > 0 ? $", старая {oldPrice:N0} ₽" : "") + |
| | 0 | 439 | | (string.IsNullOrWhiteSpace(idxLabel) ? "" : $", индекс «{idxLabel}»")); |
| | | 440 | | } |
| | | 441 | | } |
| | | 442 | | else |
| | | 443 | | { |
| | 0 | 444 | | sb.AppendLine("Цены: не найдены."); |
| | | 445 | | } |
| | 0 | 446 | | } |
| | 0 | 447 | | catch (Exception ex) |
| | | 448 | | { |
| | 0 | 449 | | sb.AppendLine($"Цены: ошибка — {ex.Message}"); |
| | 0 | 450 | | } |
| | | 451 | | |
| | 0 | 452 | | if (!ui.Found && matchedOfferId == null) |
| | 0 | 453 | | return (null, null); |
| | | 454 | | |
| | 0 | 455 | | if (!string.IsNullOrWhiteSpace(matchedOfferId)) |
| | 0 | 456 | | sb.AppendLine($"Совпавший offer_id: «{matchedOfferId}»."); |
| | | 457 | | |
| | 0 | 458 | | return (sb.ToString().TrimEnd(), ui); |
| | 0 | 459 | | } |
| | | 460 | | |
| | 0 | 461 | | private static string PriceIndexBackground(string? colorIndex) => colorIndex?.ToLowerInvariant() switch |
| | 0 | 462 | | { |
| | 0 | 463 | | "green" => "#D1FAE5", |
| | 0 | 464 | | "yellow" => "#FEF9C3", |
| | 0 | 465 | | "red" => "#FEE2E2", |
| | 0 | 466 | | _ => "#E9ECEF" |
| | 0 | 467 | | }; |
| | | 468 | | |
| | 0 | 469 | | private static string PriceIndexForeground(string? colorIndex) => colorIndex?.ToLowerInvariant() switch |
| | 0 | 470 | | { |
| | 0 | 471 | | "green" => "#065F46", |
| | 0 | 472 | | "yellow" => "#92400E", |
| | 0 | 473 | | "red" => "#991B1B", |
| | 0 | 474 | | _ => "#6C757D" |
| | 0 | 475 | | }; |
| | | 476 | | } |