| | | 1 | | using System.Globalization; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.SupplierCart; |
| | | 4 | | |
| | | 5 | | namespace OzonOrdersWeb.WebServices.SupplierCart; |
| | | 6 | | |
| | | 7 | | /// <summary>Заказ Studio2, связанный со строкой корзины или заказом поставщика.</summary> |
| | | 8 | | public sealed record SupplierCartStudioOrderCell(int Id, string Number, string? Client); |
| | | 9 | | |
| | | 10 | | /// <summary>Вариант выбора в ячейке (адрес доставки Armtek / ML-Auto).</summary> |
| | | 11 | | public sealed record SupplierCartGridOption(string Value, string Name); |
| | | 12 | | |
| | | 13 | | /// <summary>Плашка над гридом (ошибка API, несвязанные адреса, лимит выборки). Tone — тон темы (danger|warning|info).</ |
| | | 14 | | public sealed record SupplierCartGridNotice(string Tone, string Text, string? LinkText = null, string? LinkAction = null |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Строка грида «Корзина»: общий вид строки для всех API-поставщиков. Id — идентификатор, который ждут POST-действия |
| | | 18 | | /// поставщика (LocalCartItemId, ApiItemId или code_order ZZap), поэтому выбор строк не зависит от DOM. |
| | | 19 | | /// </summary> |
| | | 20 | | public sealed record SupplierCartGridCartRow |
| | | 21 | | { |
| | | 22 | | public string Id { get; init; } = string.Empty; |
| | | 23 | | public bool Selectable { get; init; } |
| | | 24 | | public string? ImageUrl { get; init; } |
| | | 25 | | public string Product { get; init; } = string.Empty; |
| | | 26 | | public string? Article { get; init; } |
| | | 27 | | public string? Brand { get; init; } |
| | | 28 | | public SupplierCartStudioOrderCell? StudioOrder { get; init; } |
| | | 29 | | public string? MissingOrderHint { get; init; } |
| | | 30 | | public DateTime? AddedAt { get; init; } |
| | | 31 | | public string? Warehouse { get; init; } |
| | | 32 | | public string? WarehouseMeta { get; init; } |
| | | 33 | | public int? Period { get; init; } |
| | | 34 | | public decimal? Quantity { get; init; } |
| | | 35 | | public decimal? Price { get; init; } |
| | | 36 | | public decimal? Sum { get; init; } |
| | | 37 | | public string Currency { get; init; } = "RUB"; |
| | | 38 | | public string? Status { get; init; } |
| | | 39 | | public string? StatusName { get; init; } |
| | | 40 | | public string? StatusTone { get; init; } |
| | | 41 | | public string? StatusTitle { get; init; } |
| | | 42 | | public string? Comment { get; init; } |
| | | 43 | | public string? CommentTone { get; init; } |
| | | 44 | | /// <summary>Адрес доставки Armtek («KUNRG|KUNZA») или ML-Auto (Id адреса).</summary> |
| | | 45 | | public string? DeliveryValue { get; init; } |
| | | 46 | | public string? DeliveryName { get; init; } |
| | | 47 | | /// <summary>Склад отгрузки Studio2 связан с адресом поставщика в настройках.</summary> |
| | | 48 | | public bool? DeliveryLinked { get; init; } |
| | | 49 | | public bool CanRequote { get; init; } |
| | | 50 | | public bool ApiOnly { get; init; } |
| | | 51 | | public string? Point { get; init; } |
| | | 52 | | public string? LocationCode { get; init; } |
| | | 53 | | public long? ZZapCode { get; init; } |
| | | 54 | | /// <summary>Есть ли позиция в API-корзине ZZap (null — ZZap не ответил).</summary> |
| | | 55 | | public bool? InZZap { get; init; } |
| | | 56 | | public string? ZZapInfo { get; init; } |
| | | 57 | | } |
| | | 58 | | |
| | | 59 | | /// <summary>Позиция заказа поставщика (раскрытие заказа).</summary> |
| | | 60 | | public sealed record SupplierCartGridOrderItem |
| | | 61 | | { |
| | 60 | 62 | | public string? Article { get; init; } |
| | 60 | 63 | | public string? Brand { get; init; } |
| | 0 | 64 | | public string? Name { get; init; } |
| | 60 | 65 | | public decimal? Quantity { get; init; } |
| | 0 | 66 | | public decimal? Reserved { get; init; } |
| | 0 | 67 | | public decimal? Confirmed { get; init; } |
| | 0 | 68 | | public decimal? Cancelled { get; init; } |
| | 60 | 69 | | public decimal? Price { get; init; } |
| | 60 | 70 | | public decimal? Sum { get; init; } |
| | 60 | 71 | | public string? Status { get; init; } |
| | 60 | 72 | | public string? StatusTone { get; init; } |
| | 60 | 73 | | public string? StatusTitle { get; init; } |
| | 60 | 74 | | public DateTime? DeliveryAt { get; init; } |
| | 0 | 75 | | public string? DeliveryText { get; init; } |
| | 0 | 76 | | public string? Warehouse { get; init; } |
| | 0 | 77 | | public string? Country { get; init; } |
| | 60 | 78 | | public string? Comment { get; init; } |
| | 61 | 79 | | public bool Rejected { get; init; } |
| | | 80 | | } |
| | | 81 | | |
| | | 82 | | /// <summary>Строка грида «Заказы»: заказ (или позиция заказа, если поставщик ведёт статус на позиции).</summary> |
| | | 83 | | public sealed record SupplierCartGridOrderRow |
| | | 84 | | { |
| | | 85 | | public string Id { get; init; } = string.Empty; |
| | | 86 | | public string Number { get; init; } = string.Empty; |
| | | 87 | | public string? NumberMeta { get; init; } |
| | | 88 | | public bool IsTest { get; init; } |
| | | 89 | | public DateTime? CreatedAt { get; init; } |
| | | 90 | | public string? CreatedText { get; init; } |
| | | 91 | | public string? Status { get; init; } |
| | | 92 | | public string? StatusTone { get; init; } |
| | | 93 | | public IReadOnlyList<SupplierCartGridNotice>? StatusNotes { get; init; } |
| | | 94 | | public decimal? Amount { get; init; } |
| | | 95 | | public string Currency { get; init; } = "RUB"; |
| | | 96 | | public int? ItemsCount { get; init; } |
| | | 97 | | public decimal? Quantity { get; init; } |
| | | 98 | | public string? QuantityText { get; init; } |
| | | 99 | | public decimal? Price { get; init; } |
| | | 100 | | public string? Product { get; init; } |
| | | 101 | | public string? Article { get; init; } |
| | | 102 | | public string? Brand { get; init; } |
| | | 103 | | public string? Supplier { get; init; } |
| | | 104 | | public string? SupplierMeta { get; init; } |
| | | 105 | | public string? Delivery { get; init; } |
| | | 106 | | public string? DeliveryMeta { get; init; } |
| | | 107 | | public string? Payment { get; init; } |
| | | 108 | | public string? Buyer { get; init; } |
| | | 109 | | public string? BuyerCode { get; init; } |
| | | 110 | | public string? OrderType { get; init; } |
| | | 111 | | public string? Invoice { get; init; } |
| | | 112 | | public string? Contract { get; init; } |
| | | 113 | | public string? Credit { get; init; } |
| | | 114 | | public string? MinSum { get; init; } |
| | | 115 | | public string? Comment { get; init; } |
| | | 116 | | public string? Source { get; init; } |
| | | 117 | | public string? Point { get; init; } |
| | | 118 | | public DateTime? UpdatedAt { get; init; } |
| | | 119 | | public string? UpdatedText { get; init; } |
| | | 120 | | /// <summary>Группа строк одного заказа (АВД: позиции одного заказа) — итог и детали заказа.</summary> |
| | | 121 | | public decimal? OrderTotal { get; init; } |
| | | 122 | | public string? OrderApiId { get; init; } |
| | | 123 | | public string? User { get; init; } |
| | | 124 | | public string? Brands { get; init; } |
| | | 125 | | public IReadOnlyList<SupplierCartStudioOrderCell> StudioOrders { get; init; } = Array.Empty<SupplierCartStudioOrderC |
| | | 126 | | public bool CanCancel { get; init; } |
| | | 127 | | public IReadOnlyList<SupplierCartGridOrderItem> Items { get; init; } = Array.Empty<SupplierCartGridOrderItem>(); |
| | | 128 | | } |
| | | 129 | | |
| | | 130 | | /// <summary>Данные вкладки поставщика для гридов «Корзина» и «Заказы».</summary> |
| | | 131 | | public sealed record SupplierCartGridPayload |
| | | 132 | | { |
| | | 133 | | public string Provider { get; init; } = string.Empty; |
| | | 134 | | public string ProviderKey { get; init; } = string.Empty; |
| | | 135 | | public string DisplayName { get; init; } = string.Empty; |
| | | 136 | | public string Currency { get; init; } = "RUB"; |
| | | 137 | | public IReadOnlyList<SupplierCartGridCartRow> Cart { get; init; } = Array.Empty<SupplierCartGridCartRow>(); |
| | | 138 | | public IReadOnlyList<SupplierCartGridOrderRow> Orders { get; init; } = Array.Empty<SupplierCartGridOrderRow>(); |
| | | 139 | | /// <summary>Число заказов для бейджа вкладки (у Nikei и Berg строки грида — позиции, а не заказы).</summary> |
| | | 140 | | public int OrderCount { get; init; } |
| | | 141 | | public string? Error { get; init; } |
| | | 142 | | public string? OrdersError { get; init; } |
| | | 143 | | public IReadOnlyList<SupplierCartGridNotice> CartNotices { get; init; } = Array.Empty<SupplierCartGridNotice>(); |
| | | 144 | | public IReadOnlyList<SupplierCartGridNotice> OrderNotices { get; init; } = Array.Empty<SupplierCartGridNotice>(); |
| | | 145 | | public IReadOnlyList<SupplierCartGridOption> DeliveryOptions { get; init; } = Array.Empty<SupplierCartGridOption>(); |
| | | 146 | | } |
| | | 147 | | |
| | | 148 | | /// <summary> |
| | | 149 | | /// Строки гридов страницы «Корзина поставщиков» (/Studio2/SupplierCart) из <see cref="SupplierCartSection"/>. |
| | | 150 | | /// Сюда переехала логика, которая раньше жила в Razor: тоны статусов, имена статусов корзины, суммы АВД, |
| | | 151 | | /// выбор строки, связь заказов с Studio2, фолбэки «Studio2 ?? API». Грид сортирует и фильтрует по этим полям. |
| | | 152 | | /// </summary> |
| | | 153 | | public static class SupplierCartGridRows |
| | | 154 | | { |
| | | 155 | | public const string ToneSuccess = "success"; |
| | | 156 | | public const string ToneInfo = "info"; |
| | | 157 | | public const string ToneWarning = "warning"; |
| | | 158 | | public const string ToneDanger = "danger"; |
| | | 159 | | public const string ToneMuted = "muted"; |
| | | 160 | | |
| | | 161 | | public const string SourceStudio2 = "studio2"; |
| | | 162 | | public const string SourceApi = "api"; |
| | | 163 | | |
| | | 164 | | private const string DefaultMissingHint = "Проверьте комментарий позиции"; |
| | | 165 | | private const string UnlinkedMissingHint = "Заказ Studio2 удалён или не привязан"; |
| | | 166 | | private const string ZZapMissingHint = "Добавлено на сайте ZZap, не из проценки Studio2"; |
| | | 167 | | |
| | | 168 | | /// <summary>Маршрут вкладки (query provider) → ключ поставщика и название. Один источник для вкладок и JSON. |
| | | 169 | | /// Currency — валюта, которую страница всегда показывала для поставщика; null — валюта секции.</summary> |
| | | 170 | | public static readonly IReadOnlyList<(string Route, string ProviderKey, string Name, string? Currency)> Providers = |
| | | 171 | | [ |
| | | 172 | | ("avd", ApiSupplierSettings.AvdProviderKey, "АВД", null), |
| | | 173 | | ("armtek", "Armtek", "Армтек RU", null), |
| | | 174 | | ("armtekby", "ArmtekBy", "Армтек BY", null), |
| | | 175 | | ("mlauto", "MlAuto", "ML-Auto BY", null), |
| | | 176 | | ("mlautoru", "MlAutoRu", "ML-Auto RU", null), |
| | | 177 | | ("forumauto", ApiSupplierSettings.ForumAutoProviderKey, "Форум-Авто (Союз)", null), |
| | | 178 | | ("forumautointerparts", ApiSupplierSettings.ForumAutoInterpartsProviderKey, "Форум-Авто (Интерпартс)", null), |
| | | 179 | | ("forumautopiter", ApiSupplierSettings.ForumAutoPiterProviderKey, "Форум-Авто (Питер)", null), |
| | | 180 | | ("forumautorostov", ApiSupplierSettings.ForumAutoRostovProviderKey, "Форум-Авто (Ростов)", null), |
| | | 181 | | ("forumautoistra", ApiSupplierSettings.ForumAutoIstraProviderKey, "Форум-Авто (Истра)", null), |
| | | 182 | | ("favoritparts", ApiSupplierSettings.FavoritPartsProviderKey, "Фаворит Союз", null), |
| | | 183 | | ("favoritpartsistra", ApiSupplierSettings.FavoritPartsIstraProviderKey, "Фаворит Истра", null), |
| | | 184 | | ("favoritpartsrostov", ApiSupplierSettings.FavoritPartsRostovProviderKey, "Фаворит Ростов", null), |
| | | 185 | | ("moskvorechieistra", ApiSupplierSettings.MoskvorechieIstraProviderKey, "Москворечье (Истра)", "RUB"), |
| | | 186 | | ("motex", ApiSupplierSettings.MotexProviderKey, "МоТехС", "BYN"), |
| | | 187 | | ("shatem", ApiSupplierSettings.ShateMProviderKey, "Шате‑М", "BYN"), |
| | | 188 | | ("profitliga", ApiSupplierSettings.ProfitLigaProviderKey, "Профит-Лига", "RUB"), |
| | | 189 | | ("nikei", ApiSupplierSettings.NikeiProviderKey, "Nikei", "RUB"), |
| | | 190 | | ("mikadomskhod20", ApiSupplierSettings.MikadoMskHod20ProviderKey, "Микадо МскХод20", "RUB"), |
| | | 191 | | ("zzapmoscow", ApiSupplierSettings.ZZapMoscowProviderKey, "ZZap Москва", "RUB"), |
| | | 192 | | ("japarts", ApiSupplierSettings.JapartsProviderKey, "Japarts", "RUB"), |
| | | 193 | | ("berg", ApiSupplierSettings.BergProviderKey, "Берг", "RUB") |
| | | 194 | | ]; |
| | | 195 | | |
| | | 196 | | public static bool IsKnownRoute(string? route) => |
| | | 197 | | route is not null && Providers.Any(p => string.Equals(p.Route, route, StringComparison.OrdinalIgnoreCase)); |
| | | 198 | | |
| | | 199 | | /// <summary>Нормализует маршрут вкладки: неизвестный или пустой — АВД.</summary> |
| | | 200 | | public static string NormalizeRoute(string? route) |
| | | 201 | | { |
| | | 202 | | var value = route?.Trim().ToLowerInvariant(); |
| | | 203 | | return IsKnownRoute(value) ? value! : "avd"; |
| | | 204 | | } |
| | | 205 | | |
| | | 206 | | /// <summary>Тон статуса заказа поставщика по тексту статуса (русские формулировки разных API).</summary> |
| | | 207 | | public static string OrderStatusTone(string? status) |
| | | 208 | | { |
| | | 209 | | var value = (status ?? string.Empty).ToLowerInvariant(); |
| | | 210 | | if (value.Contains("отмен") || value.Contains("отказ") || value.Contains("аннул") || value.Contains("deny")) ret |
| | | 211 | | if (value.Contains("выдан") || value.Contains("готов") || value.Contains("получ") || value.Contains("заверш") || |
| | | 212 | | if (value.Contains("отправ") || value.Contains("отгруж") || value.Contains("в пути") || value.Contains("доставк" |
| | | 213 | | if (value.Contains("ожид") || value.Contains("обработ") || value.Contains("принят") || value.Contains("подтверж" |
| | | 214 | | return ToneMuted; |
| | | 215 | | } |
| | | 216 | | |
| | | 217 | | /// <summary>Имя статуса строки корзины Studio2 (общие коды Pending/Ready/… у всех гибридных корзин).</summary> |
| | | 218 | | public static string CartStatusName(string? status) => status switch |
| | | 219 | | { |
| | | 220 | | "Pending" => "Ожидает проверки", |
| | | 221 | | "Ready" => "Проверено", |
| | | 222 | | "Processing" => "Оформляется", |
| | | 223 | | "Ordered" => "Оформлено", |
| | | 224 | | "Failed" => "Ошибка", |
| | | 225 | | "Removed" => "Удалено", |
| | | 226 | | _ => status ?? string.Empty |
| | | 227 | | }; |
| | | 228 | | |
| | | 229 | | public static string CartStatusTone(string? status) => status switch |
| | | 230 | | { |
| | | 231 | | "Pending" => ToneWarning, |
| | | 232 | | "Ready" or "Ordered" => ToneSuccess, |
| | | 233 | | "Processing" => ToneInfo, |
| | | 234 | | "Failed" => ToneDanger, |
| | | 235 | | _ => ToneMuted |
| | | 236 | | }; |
| | | 237 | | |
| | | 238 | | public static SupplierCartGridPayload Build(string route, SupplierCartSection section, Func<string, string> imageUrl |
| | | 239 | | { |
| | | 240 | | route = NormalizeRoute(route); |
| | | 241 | | var currency = Providers.First(p => p.Route == route).Currency ?? section.CurrencyCode; |
| | | 242 | | var payload = new SupplierCartGridPayload |
| | | 243 | | { |
| | | 244 | | Provider = route, |
| | | 245 | | ProviderKey = section.ProviderKey, |
| | | 246 | | DisplayName = section.DisplayName, |
| | | 247 | | Currency = currency, |
| | | 248 | | Error = section.Error, |
| | | 249 | | OrdersError = section.OrdersError |
| | | 250 | | }; |
| | | 251 | | |
| | | 252 | | var result = route switch |
| | | 253 | | { |
| | | 254 | | "avd" => payload with |
| | | 255 | | { |
| | | 256 | | Cart = section.Rows.Select(r => Cart(r, imageUrl, currency) with |
| | | 257 | | { |
| | | 258 | | Id = r.ApiItemId, |
| | | 259 | | Selectable = long.TryParse(r.ApiItemId, out _), |
| | | 260 | | AddedAt = r.AddedAt, |
| | | 261 | | Warehouse = $"№ {r.ApiItemId} · {r.ApiSupplierName}", |
| | | 262 | | WarehouseMeta = Blank(r.ApiRegion), |
| | | 263 | | Period = r.ApiPeriod |
| | | 264 | | }).ToList(), |
| | | 265 | | Orders = AvdOrders(section), |
| | | 266 | | OrderCount = section.Orders.Count |
| | | 267 | | }, |
| | | 268 | | "armtek" or "armtekby" => Armtek(payload, section, imageUrl, currency), |
| | | 269 | | "mlauto" or "mlautoru" => MlAuto(payload, section, imageUrl, currency, route == "mlauto"), |
| | | 270 | | "forumauto" or "forumautointerparts" or "forumautopiter" or "forumautorostov" or "forumautoistra" => payload |
| | | 271 | | { |
| | | 272 | | Cart = section.Rows.Select(r => StatusCart(r, imageUrl, currency) with |
| | | 273 | | { |
| | | 274 | | Id = Id(r.LocalCartItemId), AddedAt = r.AddedAt, Period = r.ApiPeriod |
| | | 275 | | }).ToList(), |
| | | 276 | | Orders = ForumAutoOrders(section, currency), |
| | | 277 | | OrderCount = section.ForumAutoApiOrders.Count |
| | | 278 | | }, |
| | | 279 | | "favoritparts" or "favoritpartsistra" or "favoritpartsrostov" => payload with |
| | | 280 | | { |
| | | 281 | | Cart = section.Rows.Select(r => StatusCart(r, imageUrl, currency) with |
| | | 282 | | { |
| | | 283 | | Id = r.ApiItemId, AddedAt = r.AddedAt, Period = r.ApiPeriod |
| | | 284 | | }).ToList(), |
| | | 285 | | Orders = FavoritPartsOrders(section, currency), |
| | | 286 | | OrderCount = section.FavoritPartsApiOrders.Count |
| | | 287 | | }, |
| | | 288 | | "moskvorechieistra" => payload with |
| | | 289 | | { |
| | | 290 | | Cart = section.Rows.Select(r => StatusCart(r, imageUrl, currency) with |
| | | 291 | | { |
| | | 292 | | Id = Id(r.LocalCartItemId), AddedAt = r.AddedAt, Warehouse = Blank(r.ApiRegion), Period = r.ApiPerio |
| | | 293 | | }).ToList(), |
| | | 294 | | Orders = section.MoskvorechieIstraOrders.Select(o => new SupplierCartGridOrderRow |
| | | 295 | | { |
| | | 296 | | Id = o.Id.ToString(CultureInfo.InvariantCulture), |
| | | 297 | | Number = o.Number ?? $"API #{o.Id}", |
| | | 298 | | CreatedAt = Local(o.CreatedAt), |
| | | 299 | | Status = o.Status, |
| | | 300 | | StatusTone = ToneSuccess, |
| | | 301 | | ItemsCount = o.ItemsCount, |
| | | 302 | | Amount = o.Amount, |
| | | 303 | | Currency = currency |
| | | 304 | | }).ToList(), |
| | | 305 | | OrderCount = section.MoskvorechieIstraOrders.Count |
| | | 306 | | }, |
| | | 307 | | "motex" => payload with |
| | | 308 | | { |
| | | 309 | | Cart = section.Rows.Select(r => StatusCart(r, imageUrl, currency) with |
| | | 310 | | { |
| | | 311 | | Id = Id(r.LocalCartItemId), Warehouse = Blank(r.ApiRegion) |
| | | 312 | | }).ToList(), |
| | | 313 | | Orders = section.MotexOrders.Select(o => new SupplierCartGridOrderRow |
| | | 314 | | { |
| | | 315 | | Id = o.Id.ToString(CultureInfo.InvariantCulture), |
| | | 316 | | Number = o.ExternalId ?? "—", |
| | | 317 | | NumberMeta = o.ClientOrderNum, |
| | | 318 | | CreatedAt = Local(o.CreatedAt), |
| | | 319 | | Status = o.Status, |
| | | 320 | | StatusTone = OrderStatusTone(o.Status), |
| | | 321 | | ItemsCount = o.ItemsCount, |
| | | 322 | | Amount = o.Amount, |
| | | 323 | | Currency = currency |
| | | 324 | | }).ToList(), |
| | | 325 | | OrderCount = section.MotexOrders.Count |
| | | 326 | | }, |
| | | 327 | | "shatem" => payload with |
| | | 328 | | { |
| | | 329 | | Cart = section.Rows.Select(r => StatusCart(r, imageUrl, currency) with |
| | | 330 | | { |
| | | 331 | | Id = r.ApiItemId, Warehouse = Blank(r.ApiRegion), LocationCode = Blank(r.ApiRegion) |
| | | 332 | | }).ToList(), |
| | | 333 | | Orders = section.ShateMOrders.Select(o => new SupplierCartGridOrderRow |
| | | 334 | | { |
| | | 335 | | Id = o.Id.ToString(CultureInfo.InvariantCulture), |
| | | 336 | | Number = o.ExternalId ?? "—", |
| | | 337 | | CreatedAt = Local(o.CreatedAt), |
| | | 338 | | Status = o.Status, |
| | | 339 | | StatusTone = OrderStatusTone(o.Status), |
| | | 340 | | ItemsCount = o.ItemsCount, |
| | | 341 | | Amount = o.Amount, |
| | | 342 | | Currency = currency |
| | | 343 | | }).ToList(), |
| | | 344 | | OrderCount = section.ShateMOrders.Count |
| | | 345 | | }, |
| | | 346 | | "profitliga" => payload with |
| | | 347 | | { |
| | | 348 | | Cart = section.Rows.Select(r => StatusCart(r, imageUrl, currency) with |
| | | 349 | | { |
| | | 350 | | Id = Id(r.LocalCartItemId), Warehouse = Blank(r.ApiRegion), Point = Blank(r.ProfitLigaPointName), |
| | | 351 | | Comment = Blank(r.ApiComment), CommentTone = ToneDanger |
| | | 352 | | }).ToList(), |
| | | 353 | | Orders = section.ProfitLigaOrders.Select(ProfitLigaOrder(currency)).ToList(), |
| | | 354 | | OrderCount = section.ProfitLigaOrders.Count |
| | | 355 | | }, |
| | | 356 | | "nikei" => Nikei(payload, section, imageUrl, currency), |
| | | 357 | | "mikadomskhod20" => payload with |
| | | 358 | | { |
| | | 359 | | Cart = section.Rows.Select(r => StatusCart(r, imageUrl, currency) with |
| | | 360 | | { |
| | | 361 | | Id = Id(r.LocalCartItemId), Warehouse = Blank(r.ApiRegion), |
| | | 362 | | Comment = Blank(r.ApiComment), CommentTone = ToneDanger |
| | | 363 | | }).ToList() |
| | | 364 | | }, |
| | | 365 | | "zzapmoscow" => ZZap(payload, section, imageUrl, currency), |
| | | 366 | | "japarts" => payload with |
| | | 367 | | { |
| | | 368 | | Cart = section.Rows.Select(r => Cart(r, imageUrl, currency) with |
| | | 369 | | { |
| | | 370 | | Id = r.ApiItemId, |
| | | 371 | | Period = r.ApiPeriod, |
| | | 372 | | Warehouse = Blank(r.ApiRegion), |
| | | 373 | | // Japarts не хранит связь в Studio2: без номера отправления показываем комментарий позиции. |
| | | 374 | | MissingOrderHint = Blank(r.ApiComment) ?? DefaultMissingHint |
| | | 375 | | }).ToList(), |
| | | 376 | | Orders = section.JapartsOrders.Select(o => new SupplierCartGridOrderRow |
| | | 377 | | { |
| | | 378 | | Id = $"{o.GlobalId}|{o.OrderNumber}|{o.Article}", |
| | | 379 | | Number = o.OrderNumber, |
| | | 380 | | NumberMeta = Blank(o.GlobalId), |
| | | 381 | | Product = Blank(o.ProductName), |
| | | 382 | | Article = o.Article, |
| | | 383 | | Brand = o.Brand, |
| | | 384 | | Quantity = o.QuantityOrdered, |
| | | 385 | | QuantityText = $"{Qty(o.QuantityOrdered)} / {Qty(o.QuantityBought)}", |
| | | 386 | | Price = o.Price, |
| | | 387 | | Status = o.Status, |
| | | 388 | | StatusTone = OrderStatusTone(o.Status), |
| | | 389 | | UpdatedAt = o.LastUpdated, |
| | | 390 | | Comment = Blank(o.Comment), |
| | | 391 | | Currency = currency |
| | | 392 | | }).ToList(), |
| | | 393 | | OrderCount = section.JapartsOrders.Count |
| | | 394 | | }, |
| | | 395 | | "berg" => payload with |
| | | 396 | | { |
| | | 397 | | Cart = section.Rows.Select(r => Cart(r, imageUrl, currency) with |
| | | 398 | | { |
| | | 399 | | Id = Id(r.LocalCartItemId), |
| | | 400 | | AddedAt = r.AddedAt?.ToLocalTime(), |
| | | 401 | | Warehouse = Blank(r.ApiSupplierName), |
| | | 402 | | Comment = Blank(r.ApiComment), |
| | | 403 | | CommentTone = ToneDanger |
| | | 404 | | }).ToList(), |
| | | 405 | | Orders = section.BergOrders.SelectMany(o => o.Items.Select(line => new SupplierCartGridOrderRow |
| | | 406 | | { |
| | | 407 | | Id = $"{o.Id}-{line.Sequence}", |
| | | 408 | | Number = o.Id.ToString(CultureInfo.InvariantCulture), |
| | | 409 | | IsTest = o.IsTest, |
| | | 410 | | CreatedAt = o.CreatedAt?.ToLocalTime(), |
| | | 411 | | Product = Blank(line.Name), |
| | | 412 | | Article = line.Article, |
| | | 413 | | Brand = line.Brand, |
| | | 414 | | Quantity = line.Quantity, |
| | | 415 | | Price = line.Price, |
| | | 416 | | Amount = line.Price.HasValue && line.Quantity.HasValue ? line.Price * line.Quantity : null, |
| | | 417 | | Status = line.StateName, |
| | | 418 | | StatusTone = OrderStatusTone(line.StateName), |
| | | 419 | | Supplier = Blank(line.WarehouseName), |
| | | 420 | | StudioOrders = Studio(line.StudioOrderId, line.StudioOrderNumber, null), |
| | | 421 | | Currency = currency |
| | | 422 | | })).ToList(), |
| | | 423 | | OrderCount = section.BergOrders.Count |
| | | 424 | | }, |
| | | 425 | | _ => payload |
| | | 426 | | }; |
| | | 427 | | |
| | | 428 | | // У гибридных корзин ошибка секции — предупреждение о строках поставщика, а не отказ API: корзина Studio2 работ |
| | | 429 | | return route is "profitliga" or "nikei" or "zzapmoscow" && result.Error is not null |
| | | 430 | | ? result with { Error = null, CartNotices = result.CartNotices.Prepend(new SupplierCartGridNotice(ToneWarnin |
| | | 431 | | : result; |
| | | 432 | | } |
| | | 433 | | |
| | | 434 | | // ---------- Корзина ---------- |
| | | 435 | | |
| | | 436 | | /// <summary>Общие ячейки строки корзины (бывший партиал _CartItemCells): картинка, товар, заказ Studio2.</summary> |
| | | 437 | | private static SupplierCartGridCartRow Cart(SupplierCartRow r, Func<string, string> imageUrl, string currency) => ne |
| | | 438 | | { |
| | | 439 | | ImageUrl = string.IsNullOrWhiteSpace(r.StudioProductImageUrl) ? null : imageUrl(r.StudioProductImageUrl), |
| | | 440 | | Product = FirstNonEmpty(r.StudioProductName, r.ApiItemName) ?? string.Empty, |
| | | 441 | | Article = FirstNonEmpty(r.StudioArticle, r.ApiItemNumber), |
| | | 442 | | Brand = FirstNonEmpty(r.StudioManufacturer, r.ApiCatalogName), |
| | | 443 | | StudioOrder = r.StudioOrderId is int id ? new SupplierCartStudioOrderCell(id, r.StudioOrderNumber ?? id.ToString |
| | | 444 | | MissingOrderHint = DefaultMissingHint, |
| | | 445 | | Selectable = true, |
| | | 446 | | Quantity = r.ApiQuantity, |
| | | 447 | | Price = r.ApiPrice, |
| | | 448 | | Sum = r.Sum, |
| | | 449 | | Currency = currency |
| | | 450 | | }; |
| | | 451 | | |
| | | 452 | | /// <summary>Строка гибридной корзины: статус позиции Studio2 хранится в ApiSupplierName.</summary> |
| | | 453 | | private static SupplierCartGridCartRow StatusCart(SupplierCartRow r, Func<string, string> imageUrl, string currency) |
| | | 454 | | Cart(r, imageUrl, currency) with |
| | | 455 | | { |
| | | 456 | | Status = r.ApiSupplierName, |
| | | 457 | | StatusName = CartStatusName(r.ApiSupplierName), |
| | | 458 | | StatusTone = CartStatusTone(r.ApiSupplierName) |
| | | 459 | | }; |
| | | 460 | | |
| | | 461 | | private static SupplierCartGridPayload Armtek(SupplierCartGridPayload payload, SupplierCartSection section, Func<str |
| | | 462 | | { |
| | | 463 | | var options = section.ArmtekDeliveryOptions.Select(o => new SupplierCartGridOption(o.Value, o.DisplayName)).ToLi |
| | | 464 | | var known = options.Select(o => o.Value).ToHashSet(StringComparer.Ordinal); |
| | | 465 | | var extra = new List<SupplierCartGridOption>(); |
| | | 466 | | var cart = section.Rows.Select(r => |
| | | 467 | | { |
| | | 468 | | var value = string.IsNullOrWhiteSpace(r.ArmtekBuyerKunnr) || string.IsNullOrWhiteSpace(r.ArmtekDeliveryAddre |
| | | 469 | | ? null |
| | | 470 | | : $"{r.ArmtekBuyerKunnr}|{r.ArmtekDeliveryAddressKunnr}"; |
| | | 471 | | var name = r.ArmtekDeliveryAddressName ?? r.ArmtekDeliveryAddressKunnr; |
| | | 472 | | // Адрес строки, которого нет в справочнике покупателей, остаётся выбранным — как отдельный вариант списка. |
| | | 473 | | if (value is not null && !known.Contains(value) && extra.All(x => x.Value != value)) |
| | | 474 | | extra.Add(new SupplierCartGridOption(value, $"{name} (нет в справочнике)")); |
| | | 475 | | return StatusCart(r, imageUrl, currency) with |
| | | 476 | | { |
| | | 477 | | Id = Id(r.LocalCartItemId), |
| | | 478 | | AddedAt = r.AddedAt, |
| | | 479 | | Warehouse = Blank(r.ApiRegion), |
| | | 480 | | WarehouseMeta = Blank($"{r.ApiCatalogName} {r.ApiItemNumber}".Trim()), |
| | | 481 | | Period = r.ApiPeriod, |
| | | 482 | | MissingOrderHint = UnlinkedMissingHint, |
| | | 483 | | DeliveryValue = value, |
| | | 484 | | DeliveryName = value is null ? null : $"{r.ArmtekBuyerKunnr} · {name}", |
| | | 485 | | DeliveryLinked = r.ArmtekDeliveryMappingLinked, |
| | | 486 | | CanRequote = string.Equals(r.ApiSupplierName, ArmtekCartItemStatus.Failed, StringComparison.OrdinalIgnor |
| | | 487 | | StatusTitle = Blank(r.ApiComment) |
| | | 488 | | }; |
| | | 489 | | }).ToList(); |
| | | 490 | | |
| | | 491 | | var notices = new List<SupplierCartGridNotice>(); |
| | | 492 | | if (!string.IsNullOrWhiteSpace(section.ArmtekDeliveryOptionsError)) |
| | | 493 | | notices.Add(new SupplierCartGridNotice(ToneWarning, section.ArmtekDeliveryOptionsError)); |
| | | 494 | | if (cart.Any(r => r.DeliveryValue is null)) |
| | | 495 | | notices.Add(new SupplierCartGridNotice(ToneWarning, |
| | | 496 | | "Для части позиций не выбран клиент и адрес доставки Armtek — оформление таких строк заблокировано. Выбе |
| | | 497 | | "настройках API-поставщика", section.ProviderKey == "ArmtekBy" ? "ArmtekBy" : "Armtek")); |
| | | 498 | | |
| | | 499 | | return payload with |
| | | 500 | | { |
| | | 501 | | Cart = cart, |
| | | 502 | | CartNotices = notices, |
| | | 503 | | DeliveryOptions = extra.Concat(options).ToList(), |
| | | 504 | | Orders = section.ArmtekApiOrders.Select(o => new SupplierCartGridOrderRow |
| | | 505 | | { |
| | | 506 | | Id = $"{o.OrderNumber}|{o.BuyerCode}", |
| | | 507 | | Number = o.OrderNumber, |
| | | 508 | | CreatedAt = ParseRu(o.OrderDate), |
| | | 509 | | CreatedText = o.OrderDate, |
| | | 510 | | Buyer = Blank(o.BuyerName), |
| | | 511 | | BuyerCode = Blank(o.BuyerCode), |
| | | 512 | | OrderType = Blank(o.OrderType), |
| | | 513 | | Status = o.Status, |
| | | 514 | | StatusTone = OrderStatusTone(o.Status), |
| | | 515 | | Amount = o.Amount, |
| | | 516 | | Currency = string.IsNullOrWhiteSpace(o.Currency) ? currency : o.Currency, |
| | | 517 | | Delivery = Blank(o.DeliveryMethod), |
| | | 518 | | DeliveryMeta = Blank($"{o.DeliveryFrom} — {o.DeliveryTo}".Trim(' ', '—')), |
| | | 519 | | Invoice = Blank(o.Invoice), |
| | | 520 | | Contract = Blank(o.Contract), |
| | | 521 | | Credit = Blank(o.CreditStatus), |
| | | 522 | | MinSum = Blank(o.MinSumStatus), |
| | | 523 | | Comment = Blank(o.Comment) ?? Blank(o.ExpeditionComment), |
| | | 524 | | StudioOrders = Links(section, o.OrderNumber) |
| | | 525 | | }).ToList(), |
| | | 526 | | OrderCount = section.ArmtekApiOrders.Count |
| | | 527 | | }; |
| | | 528 | | } |
| | | 529 | | |
| | | 530 | | private static SupplierCartGridPayload MlAuto(SupplierCartGridPayload payload, SupplierCartSection section, Func<str |
| | | 531 | | { |
| | | 532 | | var cart = section.Rows.Select(r => StatusCart(r, imageUrl, currency) with |
| | | 533 | | { |
| | | 534 | | Id = Id(r.LocalCartItemId), |
| | | 535 | | Warehouse = Blank(r.ApiRegion), |
| | | 536 | | WarehouseMeta = $"Склад отгрузки: {r.MlAutoShipmentWarehouseName ?? "не указан"}", |
| | | 537 | | Period = r.ApiPeriod, |
| | | 538 | | MissingOrderHint = UnlinkedMissingHint, |
| | | 539 | | DeliveryValue = Blank(r.MlAutoDeliveryAddressId), |
| | | 540 | | DeliveryName = Blank(r.MlAutoDeliveryAddressName), |
| | | 541 | | DeliveryLinked = r.MlAutoDeliveryMappingLinked, |
| | | 542 | | CanRequote = isBy && string.Equals(r.ApiSupplierName, MlAutoCartItemStatus.Failed, StringComparison.OrdinalI |
| | | 543 | | }).ToList(); |
| | | 544 | | |
| | | 545 | | var cartNotices = new List<SupplierCartGridNotice>(); |
| | | 546 | | if (section.Error is not null) cartNotices.Add(new SupplierCartGridNotice(ToneWarning, section.Error)); |
| | | 547 | | if (isBy && section.Rows.Any(x => !x.MlAutoDeliveryMappingLinked && string.IsNullOrWhiteSpace(x.MlAutoDeliveryAd |
| | | 548 | | cartNotices.Add(new SupplierCartGridNotice(ToneWarning, |
| | | 549 | | "Для части позиций склад отгрузки Studio2 не связан с адресом ML-Auto. Выберите адрес вручную в колонке |
| | | 550 | | "настройках ML-Auto", "MlAuto")); |
| | | 551 | | |
| | | 552 | | var orderNotices = new List<SupplierCartGridNotice>(); |
| | | 553 | | if (isBy && section.MlAutoApiOrders.Count >= 60) |
| | | 554 | | orderNotices.Add(new SupplierCartGridNotice(ToneInfo, "Показаны 60 самых свежих заказов периода. Сузьте диап |
| | | 555 | | |
| | | 556 | | var localByNumber = section.MlAutoOrders.ToLookup(x => x.OrderNumber, StringComparer.Ordinal); |
| | | 557 | | var orders = section.MlAutoApiOrders.Select(o => |
| | | 558 | | { |
| | | 559 | | var local = localByNumber[o.Number].FirstOrDefault(); |
| | | 560 | | var notes = o.DetailsError is null ? null : new[] { new SupplierCartGridNotice(ToneDanger, "Состав не загруж |
| | | 561 | | return new SupplierCartGridOrderRow |
| | | 562 | | { |
| | | 563 | | Id = o.Number, |
| | | 564 | | Number = o.Number, |
| | | 565 | | NumberMeta = local is null ? null : $"Studio2: {local.DeliveryAddressName}", |
| | | 566 | | CreatedAt = o.CreatedAt, |
| | | 567 | | Status = o.Status, |
| | | 568 | | StatusTone = OrderStatusTone(o.Status), |
| | | 569 | | StatusNotes = notes, |
| | | 570 | | Payment = o.PaymentType == "—" ? null : o.PaymentType, |
| | | 571 | | ItemsCount = o.ItemsCount, |
| | | 572 | | Quantity = o.Quantity, |
| | | 573 | | Amount = o.Amount, |
| | | 574 | | Currency = currency, |
| | | 575 | | Delivery = o.DeliveryRange == "—" ? null : o.DeliveryRange, |
| | | 576 | | Comment = Blank(o.Comment), |
| | | 577 | | Source = local is null ? SourceApi : SourceStudio2, |
| | | 578 | | Brands = string.Join(", ", o.Items.Select(x => x.Brand).Where(x => !string.IsNullOrWhiteSpace(x)).Distin |
| | | 579 | | StudioOrders = Links(section, o.Number), |
| | | 580 | | Items = o.Items.Select(i => new SupplierCartGridOrderItem |
| | | 581 | | { |
| | | 582 | | Article = i.Pin, |
| | | 583 | | Brand = i.Brand, |
| | | 584 | | Quantity = i.Quantity, |
| | | 585 | | Price = i.Price, |
| | | 586 | | Sum = i.Sum, |
| | | 587 | | Status = i.Status, |
| | | 588 | | StatusTone = OrderStatusTone(i.Status), |
| | | 589 | | StatusTitle = $"Код статуса ML-Auto: {i.StatusCode}", |
| | | 590 | | DeliveryAt = i.EstimatedDelivery, |
| | | 591 | | Comment = Blank(i.Comment), |
| | | 592 | | Rejected = i.Rejected |
| | | 593 | | }).ToList() |
| | | 594 | | }; |
| | | 595 | | }).ToList(); |
| | | 596 | | |
| | | 597 | | return payload with |
| | | 598 | | { |
| | | 599 | | Error = null, |
| | | 600 | | Cart = cart, |
| | | 601 | | CartNotices = cartNotices, |
| | | 602 | | OrderNotices = orderNotices, |
| | | 603 | | DeliveryOptions = section.MlAutoDeliveryOptions.Select(o => new SupplierCartGridOption(o.Id, o.DisplayName)) |
| | | 604 | | Orders = orders, |
| | | 605 | | OrderCount = section.MlAutoApiOrders.Count |
| | | 606 | | }; |
| | | 607 | | } |
| | | 608 | | |
| | | 609 | | private static SupplierCartGridPayload Nikei(SupplierCartGridPayload payload, SupplierCartSection section, Func<stri |
| | | 610 | | { |
| | | 611 | | var index = 0; |
| | | 612 | | var cart = section.Rows.Select(r => |
| | | 613 | | { |
| | | 614 | | index++; |
| | | 615 | | var apiOnly = r.LocalCartItemId is null; |
| | | 616 | | var row = StatusCart(r, imageUrl, currency) with |
| | | 617 | | { |
| | | 618 | | // Строки, добавленные в корзину Nikei вне Studio2, нельзя ни выбрать, ни оформить. |
| | | 619 | | Id = apiOnly ? $"api-{index}" : Id(r.LocalCartItemId), |
| | | 620 | | Selectable = !apiOnly, |
| | | 621 | | ApiOnly = apiOnly, |
| | | 622 | | Warehouse = Blank(r.ApiRegion), |
| | | 623 | | Period = r.ApiPeriod, |
| | | 624 | | Comment = Blank(r.ApiComment), |
| | | 625 | | CommentTone = apiOnly ? ToneMuted : ToneDanger |
| | | 626 | | }; |
| | | 627 | | return apiOnly |
| | | 628 | | ? row with { Status = "ApiOnly", StatusName = "Только Nikei", StatusTone = ToneMuted, MissingOrderHint = |
| | | 629 | | : row; |
| | | 630 | | }).ToList(); |
| | | 631 | | |
| | | 632 | | return payload with |
| | | 633 | | { |
| | | 634 | | Cart = cart, |
| | | 635 | | Orders = section.NikeiOrders.Select((o, i) => new SupplierCartGridOrderRow |
| | | 636 | | { |
| | | 637 | | Id = $"{o.Invoice}-{o.Id?.ToString(CultureInfo.InvariantCulture) ?? i.ToString(CultureInfo.InvariantCult |
| | | 638 | | Number = o.Invoice, |
| | | 639 | | CreatedAt = o.CreatedAt, |
| | | 640 | | Product = Blank(o.ProductName), |
| | | 641 | | Article = o.Article, |
| | | 642 | | Brand = o.Brand, |
| | | 643 | | Quantity = o.Quantity, |
| | | 644 | | Price = o.Price, |
| | | 645 | | Delivery = Blank(o.ShippingName), |
| | | 646 | | DeliveryMeta = Blank(o.ShippingAddress), |
| | | 647 | | Status = o.Status, |
| | | 648 | | StatusTone = OrderStatusTone(o.Status), |
| | | 649 | | StudioOrders = Studio(o.StudioOrderId, o.StudioOrderNumber, null), |
| | | 650 | | Currency = currency |
| | | 651 | | }).ToList(), |
| | | 652 | | OrderCount = section.NikeiOrders.Select(x => x.Invoice).Distinct().Count() |
| | | 653 | | }; |
| | | 654 | | } |
| | | 655 | | |
| | | 656 | | private static SupplierCartGridPayload ZZap(SupplierCartGridPayload payload, SupplierCartSection section, Func<strin |
| | | 657 | | { |
| | | 658 | | var cart = section.Rows.Select(r => |
| | | 659 | | { |
| | | 660 | | var row = StatusCart(r, imageUrl, currency) with |
| | | 661 | | { |
| | | 662 | | Id = r.ZZapCodeOrder?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, |
| | | 663 | | Selectable = r.ZZapCodeOrder.HasValue, |
| | | 664 | | Warehouse = Blank(r.ApiRegion), |
| | | 665 | | ZZapCode = r.ZZapCodeOrder, |
| | | 666 | | InZZap = r.ZZapInApiCart, |
| | | 667 | | ZZapInfo = r.ZZapInApiCart == true ? r.ZZapApiDeliveryText ?? "Позиция есть в корзине ZZap" : null, |
| | | 668 | | Period = r.ApiPeriod, |
| | | 669 | | MissingOrderHint = ZZapMissingHint, |
| | | 670 | | Comment = Blank(r.ApiComment), |
| | | 671 | | CommentTone = ToneDanger |
| | | 672 | | }; |
| | | 673 | | return r.ApiSupplierName == "ApiOnly" |
| | | 674 | | ? row with |
| | | 675 | | { |
| | | 676 | | StatusName = "Только в ZZap", |
| | | 677 | | StatusTone = ToneMuted, |
| | | 678 | | StatusTitle = "Позиция добавлена в корзину на сайте ZZap; проверка и оформление из Studio2 недоступн |
| | | 679 | | ApiOnly = true |
| | | 680 | | } |
| | | 681 | | : row; |
| | | 682 | | }).ToList(); |
| | | 683 | | |
| | | 684 | | var orderNotices = section.OrdersError is null |
| | | 685 | | ? Array.Empty<SupplierCartGridNotice>() |
| | | 686 | | : new[] { new SupplierCartGridNotice(ToneWarning, $"ZZap не ответил, показаны только заказы Studio2: {sectio |
| | | 687 | | |
| | | 688 | | return payload with |
| | | 689 | | { |
| | | 690 | | Cart = cart, |
| | | 691 | | OrdersError = null, |
| | | 692 | | OrderNotices = orderNotices, |
| | | 693 | | Orders = section.ZZapOrders.Select(o => new SupplierCartGridOrderRow |
| | | 694 | | { |
| | | 695 | | Id = o.CodeOrder.ToString(CultureInfo.InvariantCulture), |
| | | 696 | | Number = o.CodeOrder.ToString(CultureInfo.InvariantCulture), |
| | | 697 | | CreatedAt = o.CreatedAt, |
| | | 698 | | Product = Blank(o.ProductName), |
| | | 699 | | Article = o.Article, |
| | | 700 | | Brand = o.Brand, |
| | | 701 | | Supplier = Blank(o.SellerName), |
| | | 702 | | SupplierMeta = Blank(o.SellerPhone), |
| | | 703 | | QuantityText = o.Quantity, |
| | | 704 | | Price = o.Price, |
| | | 705 | | Status = o.Status, |
| | | 706 | | StatusTone = OrderStatusTone(o.Status), |
| | | 707 | | Comment = Blank(o.Message), |
| | | 708 | | UpdatedAt = o.StatusChangedAt, |
| | | 709 | | StudioOrders = Studio(o.StudioOrderId, o.StudioOrderNumber, o.StudioClientName), |
| | | 710 | | CanCancel = o.CanCancel, |
| | | 711 | | Currency = currency |
| | | 712 | | }).ToList(), |
| | | 713 | | OrderCount = section.ZZapOrders.Count |
| | | 714 | | }; |
| | | 715 | | } |
| | | 716 | | |
| | | 717 | | // ---------- Заказы ---------- |
| | | 718 | | |
| | | 719 | | /// <summary>Заказы АВД: строка — позиция заказа; итог, счёт и пользователь заказа — в каждой его строке (группировк |
| | | 720 | | private static IReadOnlyList<SupplierCartGridOrderRow> AvdOrders(SupplierCartSection section) => |
| | | 721 | | section.Orders.SelectMany(order => |
| | | 722 | | { |
| | | 723 | | var user = order.Items.Select(item => item.User).FirstOrDefault(u => !string.IsNullOrWhiteSpace(u)); |
| | | 724 | | return order.Items.Select((item, index) => |
| | | 725 | | { |
| | | 726 | | var price = item.Price ?? item.DealerPrice; |
| | | 727 | | var status = string.IsNullOrWhiteSpace(item.Status) ? "Не указан" : item.Status; |
| | | 728 | | return new SupplierCartGridOrderRow |
| | | 729 | | { |
| | | 730 | | Id = $"{order.OrderId}-{(string.IsNullOrEmpty(item.Id) ? index.ToString(CultureInfo.InvariantCulture |
| | | 731 | | Number = order.OrderNumber, |
| | | 732 | | NumberMeta = Blank(order.OrderInfo), |
| | | 733 | | CreatedAt = ParseRu(order.OrderDate), |
| | | 734 | | CreatedText = order.OrderDate, |
| | | 735 | | Product = Blank(item.ItemName), |
| | | 736 | | Article = Blank(item.ItemNumber), |
| | | 737 | | Brand = Blank(item.CatalogName), |
| | | 738 | | Supplier = Blank(item.SupplierName), |
| | | 739 | | Price = price, |
| | | 740 | | Quantity = item.Quantity, |
| | | 741 | | Amount = price.HasValue && item.Quantity.HasValue ? price * item.Quantity : null, |
| | | 742 | | Status = status, |
| | | 743 | | StatusTone = string.IsNullOrWhiteSpace(item.Status) ? ToneMuted : OrderStatusTone(item.Status), |
| | | 744 | | UpdatedAt = ParseRu(item.StatusDate), |
| | | 745 | | UpdatedText = Blank(item.StatusDate), |
| | | 746 | | OrderTotal = order.Summa, |
| | | 747 | | OrderApiId = order.OrderId, |
| | | 748 | | Invoice = Blank(order.SchetNumber), |
| | | 749 | | User = user, |
| | | 750 | | ItemsCount = order.Items.Count, |
| | | 751 | | Currency = "RUB" |
| | | 752 | | }; |
| | | 753 | | }); |
| | | 754 | | }).ToList(); |
| | | 755 | | |
| | | 756 | | private static IReadOnlyList<SupplierCartGridOrderRow> ForumAutoOrders(SupplierCartSection section, string currency) |
| | | 757 | | section.ForumAutoApiOrders.Select(o => |
| | | 758 | | { |
| | | 759 | | var key = o.Did.ToString(CultureInfo.InvariantCulture); |
| | | 760 | | return new SupplierCartGridOrderRow |
| | | 761 | | { |
| | | 762 | | Id = key, |
| | | 763 | | Number = $"№{o.Did}", |
| | | 764 | | CreatedAt = o.CreatedAt, |
| | | 765 | | Status = o.Status, |
| | | 766 | | StatusTone = OrderStatusTone(o.Status), |
| | | 767 | | StatusNotes = o.DetailsError is null ? null : new[] { new SupplierCartGridNotice(ToneDanger, "Ошибка заг |
| | | 768 | | Delivery = Blank(o.DeliveryAt), |
| | | 769 | | ItemsCount = o.Items.Count, |
| | | 770 | | StudioOrders = Links(section, key), |
| | | 771 | | Brands = string.Join(", ", o.Items.Select(x => x.Brand).Where(x => !string.IsNullOrWhiteSpace(x)).Distin |
| | | 772 | | Currency = currency, |
| | | 773 | | Items = o.Items.Select(i => new SupplierCartGridOrderItem |
| | | 774 | | { |
| | | 775 | | Article = i.Article, |
| | | 776 | | Brand = i.Brand, |
| | | 777 | | Quantity = i.Ordered, |
| | | 778 | | Reserved = i.Reserved, |
| | | 779 | | Confirmed = i.Confirmed, |
| | | 780 | | Price = i.Price, |
| | | 781 | | Country = Blank(i.Country) |
| | | 782 | | }).ToList() |
| | | 783 | | }; |
| | | 784 | | }).ToList(); |
| | | 785 | | |
| | | 786 | | private static IReadOnlyList<SupplierCartGridOrderRow> FavoritPartsOrders(SupplierCartSection section, string curren |
| | | 787 | | section.FavoritPartsApiOrders.Select(o => |
| | | 788 | | { |
| | | 789 | | var notes = new List<SupplierCartGridNotice>(); |
| | | 790 | | if (o.DetailsError is not null) notes.Add(new SupplierCartGridNotice(ToneDanger, "Ошибка загрузки", o.Detail |
| | | 791 | | if (!string.IsNullOrWhiteSpace(o.PostError)) notes.Add(new SupplierCartGridNotice(ToneDanger, "Не проведён у |
| | | 792 | | var cancelled = o.Items.Sum(x => x.CountCancel); |
| | | 793 | | if (cancelled > 0) notes.Add(new SupplierCartGridNotice(ToneWarning, $"Снято: {Qty(cancelled)} шт.")); |
| | | 794 | | return new SupplierCartGridOrderRow |
| | | 795 | | { |
| | | 796 | | Id = o.Id.ToString(), |
| | | 797 | | Number = string.IsNullOrWhiteSpace(o.Number) ? $"№{o.Id}" : $"№{o.Number}", |
| | | 798 | | CreatedAt = o.CreatedAt?.ToLocalTime(), |
| | | 799 | | Status = o.Status, |
| | | 800 | | StatusTone = OrderStatusTone(o.Status), |
| | | 801 | | StatusNotes = notes.Count == 0 ? null : notes, |
| | | 802 | | Delivery = o.ShippingDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture), |
| | | 803 | | ItemsCount = o.Items.Count, |
| | | 804 | | Amount = o.Sum, |
| | | 805 | | Point = Blank(o.TradePointName), |
| | | 806 | | StudioOrders = Links(section, o.Id.ToString()), |
| | | 807 | | Brands = string.Join(", ", o.Items.Select(x => x.Brand).Where(x => !string.IsNullOrWhiteSpace(x)).Distin |
| | | 808 | | Currency = currency, |
| | | 809 | | Items = o.Items.Select(i => new SupplierCartGridOrderItem |
| | | 810 | | { |
| | | 811 | | Article = i.Number, |
| | | 812 | | Brand = i.Brand, |
| | | 813 | | Quantity = i.Count, |
| | | 814 | | Cancelled = i.CountCancel > 0 ? i.CountCancel : null, |
| | | 815 | | Price = i.Price |
| | | 816 | | }).ToList() |
| | | 817 | | }; |
| | | 818 | | }).ToList(); |
| | | 819 | | |
| | | 820 | | private static Func<ProfitLigaApiOrderRow, SupplierCartGridOrderRow> ProfitLigaOrder(string currency) => o => |
| | | 821 | | { |
| | | 822 | | // Профит-Лига возвращает служебный код «Created» вместо русского статуса. |
| | | 823 | | var status = string.Equals(o.Status, "Created", StringComparison.OrdinalIgnoreCase) ? "Создан" : o.Status; |
| | | 824 | | return new SupplierCartGridOrderRow |
| | | 825 | | { |
| | | 826 | | Id = o.Id.ToString(CultureInfo.InvariantCulture), |
| | | 827 | | Number = o.OrderNumber, |
| | | 828 | | CreatedAt = Local(o.CreatedAt), |
| | | 829 | | Point = Blank(o.PointName), |
| | | 830 | | Delivery = Blank(o.DeliveryDate), |
| | | 831 | | Status = status, |
| | | 832 | | StatusTone = OrderStatusTone(status), |
| | | 833 | | ItemsCount = o.ItemsCount, |
| | | 834 | | Amount = o.Amount, |
| | | 835 | | Currency = currency, |
| | | 836 | | Items = o.Items.Select(i => new SupplierCartGridOrderItem |
| | | 837 | | { |
| | | 838 | | Article = i.Article, |
| | | 839 | | Brand = i.Brand, |
| | | 840 | | Name = Blank(i.ProductName), |
| | | 841 | | Quantity = i.Quantity, |
| | | 842 | | Price = i.Price, |
| | | 843 | | Sum = i.Price * i.Quantity, |
| | | 844 | | Status = Blank(i.Status), |
| | | 845 | | StatusTone = OrderStatusTone(i.Status), |
| | | 846 | | Warehouse = Blank(i.WarehouseName), |
| | | 847 | | DeliveryText = Blank(i.ShowDate) |
| | | 848 | | }).ToList() |
| | | 849 | | }; |
| | | 850 | | }; |
| | | 851 | | |
| | | 852 | | // ---------- Утилиты ---------- |
| | | 853 | | |
| | | 854 | | private static IReadOnlyList<SupplierCartStudioOrderCell> Links(SupplierCartSection section, string supplierOrder) = |
| | | 855 | | section.StudioOrderLinksBySupplierOrder.TryGetValue(supplierOrder, out var links) |
| | | 856 | | ? links.Select(l => new SupplierCartStudioOrderCell(l.OrderId, l.ShipmentNumber ?? l.OrderId.ToString(Cultur |
| | | 857 | | : Array.Empty<SupplierCartStudioOrderCell>(); |
| | | 858 | | |
| | | 859 | | private static IReadOnlyList<SupplierCartStudioOrderCell> Studio(int? id, string? number, string? client) => |
| | | 860 | | id is int value |
| | | 861 | | ? new[] { new SupplierCartStudioOrderCell(value, number ?? value.ToString(CultureInfo.InvariantCulture), cli |
| | | 862 | | : Array.Empty<SupplierCartStudioOrderCell>(); |
| | | 863 | | |
| | | 864 | | private static string Id(long? value) => value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty; |
| | | 865 | | |
| | | 866 | | private static string? Blank(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); |
| | | 867 | | |
| | | 868 | | private static string? FirstNonEmpty(params string?[] values) => values.Select(Blank).FirstOrDefault(v => v is not n |
| | | 869 | | |
| | | 870 | | private static string Qty(decimal value) => value.ToString("0.##", CultureInfo.GetCultureInfo("ru-RU")); |
| | | 871 | | |
| | | 872 | | /// <summary>Время API, сохранённое в UTC, — к локальному времени сервера (как раньше показывала страница).</summary |
| | | 873 | | private static DateTime Local(DateTime value) => DateTime.SpecifyKind(value.ToLocalTime(), DateTimeKind.Unspecified) |
| | | 874 | | |
| | | 875 | | /// <summary>Даты АВД и Armtek приходят строкой; для сортировки и фильтра по дате разбираем её, текст остаётся как е |
| | | 876 | | public static DateTime? ParseRu(string? value) |
| | | 877 | | { |
| | | 878 | | if (string.IsNullOrWhiteSpace(value)) return null; |
| | | 879 | | var ru = CultureInfo.GetCultureInfo("ru-RU"); |
| | | 880 | | string[] formats = ["dd.MM.yyyy HH:mm:ss", "dd.MM.yyyy HH:mm", "dd.MM.yyyy", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-ddT |
| | | 881 | | if (DateTime.TryParseExact(value.Trim(), formats, ru, DateTimeStyles.None, out var exact)) return exact; |
| | | 882 | | return DateTime.TryParse(value, ru, DateTimeStyles.None, out var parsed) ? parsed : null; |
| | | 883 | | } |
| | | 884 | | } |