< Summary

Information
Class: Services.BitrixStockServices.OzonPriceInfoDto
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixStockSyncReportModels.cs
Line coverage
86%
Covered lines: 13
Uncovered lines: 2
Coverable lines: 15
Total lines: 624
Line coverage: 86.6%
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_Article()100%11100%
get_OfferId()100%210%
get_OzonClientId()100%210%
get_TruePrice()100%11100%
get_SellingPrice()100%11100%
get_ColorIndex()100%11100%
get_PricePushedOld()100%11100%
get_PricePushedNew()100%11100%
get_PricePushedAtUtc()100%11100%
get_PushedMarkupPct()100%11100%
get_PushedColorIndexBefore()100%11100%
get_PushedColorIndexAfter()100%11100%
get_PushedCompetitorPrice()100%11100%
get_PricePushStatus()100%11100%
get_PricePushError()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixStockSyncReportModels.cs

#LineLine coverage
 1namespace Services.BitrixStockServices;
 2
 3/// <summary>Рекомендуемое действие по строке склада (только отчёт, без отправки в Ozon).</summary>
 4public static class BitrixStockSyncReportActions
 5{
 6    public const string Match = "Match";
 7    public const string Push = "Push";
 8    public const string Decrease = "Decrease";
 9    public const string Clear = "Clear";
 10    public const string Skip = "Skip";
 11    /// <summary>Товар не найден / не привязан к каталогу Ozon выбранного клиента.</summary>
 12    public const string NotFound = "NotFound";
 13    /// <summary>Товар найден в Ozon (есть FBS-остаток), но нет привязки склада в WarehouseMapping.</summary>
 14    public const string FbsNoWarehouse = "FbsNoWarehouse";
 15    /// <summary>Товар есть в каталоге Ozon, но убран из продажи (Не продаётся / архив).</summary>
 16    public const string NotForSale = "NotForSale";
 17    /// <summary>Этап 3: обнулён лишний FBS-склад (вне защищённых строк отчёта).</summary>
 18    public const string CleanupExtra = "CleanupExtra";
 19    /// <summary>Этап 2: обнулён склад со снятой связью (ApplyDisabledWarehouseMappingIds).</summary>
 20    public const string ClearDisabledLink = "ClearDisabledLink";
 21    /// <summary>Этап 5: сирота — остаток на складе Ozon есть, а строки в отчёте нет. Обнуляется в 0.</summary>
 22    public const string OzonZero = "OzonZero";
 23    /// <summary>Этап 5: товар-упаковка [N] — не сирота, базовый артикул найден в WarehouseStockSnapshot. Остаток = floo
 24    public const string BatchVariant = "BatchVariant";
 25    /// <summary>Этап 5: партийный товар на складе со «Снятой связью» (ApplyDisabledWarehouseMappingIds) — обнулён, как 
 26    public const string BatchVariantDisabledLink = "BatchVariantDisabledLink";
 27    /// <summary>Этап 5: партийный товар [N], базовый артикул найден в 1С, но qty &lt; N — недостаточно для одной упаков
 28    public const string BatchVariantZeroStock = "BatchVariantZeroStock";
 29    /// <summary>Этап 5: партийный товар [N] не создан в Ozon FBS — ошибка валидации карточки. Пуш остатка невозможен.</
 30    public const string BatchVariantNotCreated = "BatchVariantNotCreated";
 31    /// <summary>Этап 5: партийный товар [N] есть в каталоге Ozon (is_created=true), но не добавлен в FBS управление ост
 32    public const string BatchVariantNotInFbs = "BatchVariantNotInFbs";
 33
 34    public static readonly HashSet<string> ApplicableToOzon = new(StringComparer.OrdinalIgnoreCase)
 35    {
 36        Push, Decrease, Clear, FbsNoWarehouse
 37    };
 38}
 39
 40public static class BitrixStockSyncReportApplyStatuses
 41{
 42    public const string Pending = "Pending";
 43    public const string Success = "Success";
 44    public const string Failed = "Failed";
 45    public const string Skipped = "Skipped";
 46}
 47
 48public enum ArchivedProductMode
 49{
 50    /// <summary>Обрабатывать архивные товары как обычные (поведение по умолчанию).</summary>
 51    Include = 0,
 52    /// <summary>Пропустить архивные товары при синхронизации.</summary>
 53    Skip = 1,
 54    /// <summary>Разархивировать перед накатом остатков.</summary>
 55    UnarchiveFirst = 2
 56}
 57
 58public enum UnarchiveFilterMode
 59{
 60    /// <summary>Разархивировать все архивные товары (авто и ручные).</summary>
 61    All = 0,
 62    /// <summary>Разархивировать только авто-архивные товары.</summary>
 63    AutoOnly = 1,
 64    /// <summary>Разархивировать только вручную архивированные товары.</summary>
 65    ManualOnly = 2
 66}
 67
 68public class BitrixStockSyncReportApplyStartRequest
 69{
 70    public string? JobId { get; set; }
 71    public string? ReportId { get; set; } = string.Empty;
 72    /// <summary>Push, Decrease, Clear — пусто = все применимые.</summary>
 73    public List<string>? Actions { get; set; }
 74    /// <summary>Фильтр по артикулам (Article); null/empty = все.</summary>
 75    public List<string>? Articles { get; set; }
 76    /// <summary>Максимум строк за запуск; null = все подходящие.</summary>
 77    public int? MaxLines { get; set; }
 78    /// <summary>Только строки без успешной отправки в Ozon.</summary>
 79    public bool OnlyPending { get; set; } = true;
 80    /// <summary>ID связей складов (WarehouseMapping.Id) которые нужно отключить при накате.</summary>
 81    public List<int>? DisabledWarehouseMappingIds { get; set; }
 82    /// <summary>Как обрабатывать архивные товары при накате.</summary>
 83    public ArchivedProductMode ArchivedMode { get; set; } = ArchivedProductMode.Include;
 84    /// <summary>Фильтр типа архива при разархивации (работает только когда ArchivedMode=UnarchiveFirst).</summary>
 85    public UnarchiveFilterMode UnarchiveFilter { get; set; } = UnarchiveFilterMode.All;
 86    /// <summary>Настройки авто-запуска этапа 3 из пресета (передаётся с этапа 1 → 2 → 3).</summary>
 87    public PresetStageSettings? StageSettings { get; set; }
 88    /// <summary>true = вызов из BitrixStockSyncPipelineOrchestrator: этап не управляет stage-store и не ждёт SignalR-за
 89    public bool Chained { get; set; }
 90}
 91
 92public class BitrixStockSyncReportStartRequest
 93{
 94    public string? JobId { get; set; }
 95    /// <summary>Клиенты Ozon в порядке приоритета (первый обрабатывается первым). Заменяет OzonClientId.</summary>
 96    public List<int> OzonClientIds { get; set; } = new();
 97    /// <summary>Устаревшее — для обратной совместимости с API без мультиклиентов.</summary>
 98    public int OzonClientId { get; set; }
 99    public string? Article { get; set; }
 100    public List<string>? SelectedStoreTitles { get; set; }
 101    /// <summary>Показывать только строки, где требуется действие (без Match/Skip).</summary>
 102    public bool OnlyRequiresAction { get; set; } = false;
 103    /// <summary>ID связей складов (WarehouseMapping.Id) которые нужно отключить при накате.</summary>
 104    public List<int>? DisabledWarehouseMappingIds { get; set; }
 105    /// <summary>Настройки авто-запуска этапов 2 и 3 из пресета. Если заданы — сервер сам цепочкой запустит следующие эт
 106    public PresetStageSettings? StageSettings { get; set; }
 107    /// <summary>true = вызов из BitrixStockSyncPipelineOrchestrator: этап не управляет stage-store и не ждёт SignalR-за
 108    public bool Chained { get; set; }
 109
 110    /// <summary>Возвращает итоговый упорядоченный список ID клиентов с учётом обратной совместимости.</summary>
 111    public List<int> GetOrderedClientIds()
 112    {
 113        if (OzonClientIds.Any(id => id > 0))
 114            return OzonClientIds.Where(id => id > 0).ToList();
 115        if (OzonClientId > 0)
 116            return new List<int> { OzonClientId };
 117        return new List<int>();
 118    }
 119}
 120
 121public class BitrixStockSyncReportDto
 122{
 123    public string? ReportId { get; set; } = string.Empty;
 124    public DateTime GeneratedAtUtc { get; set; }
 125    public string? OzonClientName { get; set; } = string.Empty;
 126    public int OzonClientId { get; set; }
 127    /// <summary>Все клиенты в отчёте в порядке обработки (для мультиклиентных отчётов).</summary>
 128    public List<int> MultiClientIds { get; set; } = new();
 129    public Dictionary<int, string> MultiClientNames { get; set; } = new();
 130    public bool DryRun { get; set; } = true;
 131    public string? Warning { get; set; }
 132    public List<string> LogLines { get; set; } = new();
 133    public BitrixStockSyncReportSummaryDto Summary { get; set; } = new();
 134    public int Page { get; set; } = 1;
 135    public int PageSize { get; set; } = 100;
 136    public int TotalRows { get; set; }
 137    public int TotalPages { get; set; }
 138    public string? Status { get; set; } = string.Empty;
 139    public DateTime? CompletedAtUtc { get; set; }
 140    public string? SelectedStoresJson { get; set; }
 141    public string? ArticleFilter { get; set; }
 142    public List<string>? OneCStoresFilter { get; set; }
 143    public string? OzonWarehouseFilter { get; set; }
 144    public string? ActionFilter { get; set; }
 145    public bool OnlyOzonAppliedSuccess { get; set; }
 146    public bool OnlyOzonAppliedFailed { get; set; }
 147    public bool OnlyArchived { get; set; }
 148    public int OzonAppliedSuccessCount { get; set; }
 149    public int ArchivedCount { get; set; }
 150    public int AutoArchivedCount { get; set; }
 151    public int OzonApplyFailedCount { get; set; }
 152    public int OzonApplyPendingCount { get; set; }
 153    public DateTime? OzonApplyLastAtUtc { get; set; }
 154    public int OzonCleanupSuccessCount { get; set; }
 155    public int OzonCleanupFailedCount { get; set; }
 156    public DateTime? OzonCleanupLastAtUtc { get; set; }
 157    public List<BitrixStockSyncReportWarehouseLineDto> Rows { get; set; } = new();
 158}
 159
 160public class BitrixStockSyncReportSummaryDto
 161{
 162    public int ProductsScanned { get; set; }
 163    public int ProductsInReport { get; set; }
 164    public int WarehouseLinesTotal { get; set; }
 165    public int MatchCount { get; set; }
 166    public int PushCount { get; set; }
 167    public int DecreaseCount { get; set; }
 168    public int ClearCount { get; set; }
 169    public int SkipCount { get; set; }
 170    public int NotFoundCount { get; set; }
 171}
 172
 173public class BitrixStockSyncReportAggregatesDto
 174{
 175    /// <summary>Количество строк по действию: FbsNoWarehouse, NotForSale.</summary>
 176    public Dictionary<string, int> ActionCounts { get; set; } = new();
 177    /// <summary>Количество строк по OzonClientId (ключ — строка с ID).</summary>
 178    public Dictionary<string, int> ClientCounts { get; set; } = new();
 179    /// <summary>Имена клиентов по OzonClientId.</summary>
 180    public Dictionary<string, string> ClientNames { get; set; } = new();
 181    /// <summary>Количество строк по категории изменения C0: equal/similar/up/down/none.</summary>
 182    public Dictionary<string, int> DeltaCounts { get; set; } = new();
 183}
 184
 185public class BitrixStockSyncReportQuery
 186{
 187    public int Page { get; set; } = 1;
 188    public int PageSize { get; set; } = 100;
 189    public string? Article { get; set; }
 190    /// <summary>Точное совпадение по складам 1С (из списка).</summary>
 191    public List<string>? OneCStores { get; set; }
 192    public string? OzonWarehouse { get; set; }
 193    /// <summary>Устаревшее — единственное действие. Используйте Actions.</summary>
 194    public string? Action { get; set; }
 195    /// <summary>Фильтр по нескольким действиям (приоритет над Action). Пусто = все.</summary>
 196    public List<string>? Actions { get; set; }
 197    public bool OnlyRequiresAction { get; set; } = false;
 198    /// <summary>Только строки с OzonApplyStatus = Success.</summary>
 199    public bool OnlyOzonAppliedSuccess { get; set; } = false;
 200    /// <summary>Только строки с OzonApplyStatus = Failed.</summary>
 201    public bool OnlyOzonAppliedFailed { get; set; } = false;
 202    /// <summary>Только архивированные товары.</summary>
 203    public bool OnlyArchived { get; set; } = false;
 204    /// <summary>Показывать строки без остатков (1С=0 и Ozon=0). По умолчанию они скрыты как шум.</summary>
 205    public bool ShowZeroRows { get; set; } = false;
 206    /// <summary>Фильтр по клиентам Ozon (пусто = все).</summary>
 207    public List<int>? ClientIds { get; set; }
 208    /// <summary>Только строки-«сироты», обнулённые этапом 5 (IsOzonOrphan = 1).</summary>
 209    public bool OnlyOzonOrphan { get; set; } = false;
 210}
 211
 212public class BitrixStockSyncReportRunsQuery
 213{
 214    public int Page { get; set; } = 1;
 215    public int PageSize { get; set; } = 20;
 216    public int? OzonClientId { get; set; }
 217    public string? Status { get; set; }
 218    /// <summary>Верхняя граница PageSize: 100 для постраничного списка, больше — для грида истории целиком.</summary>
 219    public int MaxPageSize { get; set; } = 100;
 220}
 221
 222public class BitrixStockSyncReportPricePushStartRequest
 223{
 224    public string  JobId       { get; set; } = string.Empty;
 225    public string  ReportId    { get; set; } = string.Empty;
 226    /// <summary>null = все артикулы со снимком цен.</summary>
 227    public int?    MaxArticles { get; set; }
 228    /// <summary>Пропустить артикулы, для которых уже записан успешный накат цены.</summary>
 229    public bool    OnlyPending { get; set; } = true;
 230    public PresetStageSettings? StageSettings { get; set; }
 231    /// <summary>true = вызов из BitrixStockSyncPipelineOrchestrator: этап не управляет stage-store и не ждёт SignalR-за
 232    public bool Chained { get; set; }
 233}
 234
 235public class BitrixStockSyncReportRunDto
 236{
 237    public string? ReportId { get; set; } = string.Empty;
 238    public DateTime GeneratedAtUtc { get; set; }
 239    public DateTime? CompletedAtUtc { get; set; }
 240    public string? Status { get; set; } = string.Empty;
 241    public int OzonClientId { get; set; }
 242    public string? OzonClientName { get; set; } = string.Empty;
 243    /// <summary>Все клиенты отчёта (пустой = одиночный отчёт, имя в OzonClientName).</summary>
 244    public List<string> MultiClientNames { get; set; } = new();
 245    public string? SelectedStores { get; set; }
 246    public int ProductsScanned { get; set; }
 247    public int ProductsInReport { get; set; }
 248    public int WarehouseLinesTotal { get; set; }
 249    public int PushCount { get; set; }
 250    public int DecreaseCount { get; set; }
 251    public int ClearCount { get; set; }
 252    public int MatchCount { get; set; }
 253    public int SkipCount { get; set; }
 254    public int NotFoundCount { get; set; }
 255    public int OzonAppliedSuccessCount { get; set; }
 256    public int OzonApplyFailedCount { get; set; }
 257    public DateTime? OzonApplyLastAtUtc { get; set; }
 258    public int OzonCleanupSuccessCount { get; set; }
 259    public int OzonCleanupFailedCount { get; set; }
 260    public DateTime? OzonCleanupLastAtUtc { get; set; }
 261    /// <summary>Этап 5: сироты найдено (сумма по Ozon-файлам).</summary>
 262    public int OrphansFound { get; set; }
 263    /// <summary>Этап 5: сироты обнулено.</summary>
 264    public int OrphansZeroed { get; set; }
 265    /// <summary>Этап 4: цены успешно накатаны.</summary>
 266    public int PricePushCount { get; set; }
 267    /// <summary>Этап 4: цена без изменений (±1 ₽).</summary>
 268    public int PricePushUnchangedCount { get; set; }
 269    /// <summary>Этап 4: ошибки наката цен.</summary>
 270    public int PricePushErrorCount { get; set; }
 271    public string? Warning { get; set; }
 272    /// <summary>Активный этап (1/2/3), 0 = не выполняется. Заполняется контроллером из BitrixStockActiveStageStore.</su
 273    public int ActiveStage { get; set; }
 274}
 275
 276public class BitrixStockSyncReportRunsPageDto
 277{
 278    public List<BitrixStockSyncReportRunDto> Items { get; set; } = new();
 279    public int Page { get; set; }
 280    public int PageSize { get; set; }
 281    public int TotalCount { get; set; }
 282    public int TotalPages { get; set; }
 283}
 284
 285public class BitrixStockSyncReportWarehouseLineDto
 286{
 287    public long LineId { get; set; }
 288    public int ProductId { get; set; }
 289    public string? Article { get; set; } = string.Empty;
 290    public string? OneCStore { get; set; } = string.Empty;
 291    public string? OzonWarehouseName { get; set; } = string.Empty;
 292    public long OzonWarehouseId { get; set; }
 293    public string? MappingDescription { get; set; } = string.Empty;
 294    public int OneCQty { get; set; }
 295    public int OzonQty { get; set; }
 296    public int Delta { get; set; }
 297    public string? Action { get; set; } = BitrixStockSyncReportActions.Match;
 298    public string? ActionLabel { get; set; } = string.Empty;
 299    public string? ActionIcon { get; set; } = string.Empty;
 300    public string? RowClass { get; set; } = string.Empty;
 301    public string? Note { get; set; }
 302    public long? OzonProductId { get; set; }
 303    public string? OfferId { get; set; }
 304    public string? OzonApplyStatus { get; set; }
 305    public DateTime? OzonAppliedUtc { get; set; }
 306    public string? OzonApplyError { get; set; }
 307    /// <summary>Фактический остаток в Ozon после применения наката. Null до применения.</summary>
 308    public int? OzonQtyAfter { get; set; }
 309    public string? ManufacturerName { get; set; }
 310    /// <summary>Цена C0 из таблицы WarehouseStockSnapshot (1С) на момент построения отчёта.</summary>
 311    public decimal? OneCPrice { get; set; }
 312    /// <summary>Цена C0 из предыдущего завершённого отчёта — для сравнения динамики.</summary>
 313    public decimal? PrevOneCPrice { get; set; }
 314    public bool IsArchived { get; set; }
 315    public bool IsAutoArchived { get; set; }
 316    public int OzonClientId { get; set; }
 317    public string? OzonClientName { get; set; } = string.Empty;
 318    public string? OzonCleanupStatus { get; set; }
 319    public DateTime? OzonCleanupAtUtc { get; set; }
 320    public string? OzonCleanupError { get; set; }
 321    /// <summary>Товар участвует в акции Ozon на момент генерации отчёта.</summary>
 322    public bool InPromo { get; set; }
 323    public string? PromoName { get; set; }
 324    /// <summary>Количество единиц, зарезервированных заказами «Не указан»/«Заказан реализатору» по данному складу. Вычт
 325    public int ReservedQty { get; set; }
 326    /// <summary>Строка добавлена этапом 5 как «сирота»: есть остаток на Ozon, но нет в снэпшоте.</summary>
 327    public bool IsOzonOrphan { get; set; }
 328    public string? OzonOrphanStatus { get; set; }
 329    public DateTime? OzonOrphanAtUtc { get; set; }
 330    /// <summary>Размер партии [N] из OfferId (например 10 для [10]). 1 если не партия.</summary>
 331    public int BatchMultiplier { get; set; } = 1;
 332    /// <summary>Нормализованный базовый артикул партии (без [N]), например «8581837000=hyn». Null для обычных строк.</s
 333    public string? BatchBaseArticle { get; set; }
 334    /// <summary>Цена, накаченная на Ozon этапом 5 (победитель × N). Null — если цена не пушилась или не партийный товар
 335    public decimal? BatchPushedPrice { get; set; }
 336}
 337
 338public class OrderReserveLineDto
 339{
 340    public string? Article { get; set; } = string.Empty;
 341    public string? Store { get; set; } = string.Empty;
 342    public int ReservedQty { get; set; }
 343    public int OrderCount { get; set; }
 344    /// <summary>Номера отправлений / ключи заказов, вошедших в резерв по артикулу и складу.</summary>
 345    public List<string> OrderNumbers { get; set; } = new();
 346    /// <summary>Заказы резерва с текущим статусом и цветом статуса.</summary>
 347    public List<OrderReserveOrderDto> Orders { get; set; } = new();
 348}
 349
 350public class OrderReserveOrderDto
 351{
 352    public string? OrderNumber { get; set; } = string.Empty;
 353    public string? ShipmentNumber { get; set; }
 354    public DateTime? ProcessingDate { get; set; }
 355    public string? StatusName { get; set; } = string.Empty;
 356    public string? StatusColor { get; set; } = "#ffffff";
 357}
 358
 359public class OrderReserveMapDto
 360{
 361    public int TotalOrders { get; set; }
 362    public int TotalArticles { get; set; }
 363    public List<OrderReserveLineDto> Lines { get; set; } = new();
 364}
 365
 366public class ReserveStatusOptionDto
 367{
 368    public int Id { get; set; }
 369    public string? Name { get; set; } = string.Empty;
 370    public string? Color { get; set; } = "#ffffff";
 371    public bool IsDefault { get; set; }
 372}
 373
 374public class BitrixStockSyncCleanupStartRequest
 375{
 376    public string? JobId { get; set; }
 377    public string? ReportId { get; set; } = string.Empty;
 378    public bool OnlyPending { get; set; } = true;
 379    public int? MaxProducts { get; set; }
 380    public List<string>? Articles { get; set; }
 381    /// <summary>Настройки авто-запуска этапа 4 из пресета (передаётся с этапа 2 → 3 → 4).</summary>
 382    public PresetStageSettings? StageSettings { get; set; }
 383    /// <summary>true = вызов из BitrixStockSyncPipelineOrchestrator: этап не управляет stage-store и не ждёт SignalR-за
 384    public bool Chained { get; set; }
 385}
 386
 387// ─── Stage 5: Ozon Orphan Stock Cleanup ──────────────────────────────────────
 388
 389public class BitrixStockOzonOrphanCleanupRequest
 390{
 391    public string? ReportId { get; set; } = string.Empty;
 392    public string? JobId { get; set; }
 393    /// <summary>true = только предпросмотр, без реального обнуления.</summary>
 394    public bool DryRun { get; set; } = false;
 395    public PresetStageSettings? StageSettings { get; set; }
 396    /// <summary>true = вызов из BitrixStockSyncPipelineOrchestrator: этап не управляет stage-store и не ждёт SignalR-за
 397    public bool Chained { get; set; }
 398}
 399
 400/// <summary>Ссылка на скачанный Ozon-файл отчёта по остаткам FBS.</summary>
 401public class OzonReportFileDto
 402{
 403    public long Id { get; set; }
 404    public string? ReportId { get; set; } = string.Empty;
 405    public int OzonClientId { get; set; }
 406    public string? OzonWarehouseId { get; set; } = string.Empty;
 407    public string? OzonWarehouseName { get; set; } = string.Empty;
 408    public string? BitrixWarehouseName { get; set; } = string.Empty;
 409    public string? OzonReportCode { get; set; } = string.Empty;
 410    public string? FileUrl { get; set; }
 411    public int RowsTotal { get; set; }
 412    public int RowsWithStock { get; set; }
 413    public int OrphansFound { get; set; }
 414    public int OrphansZeroed { get; set; }
 415    public DateTime CreatedAtUtc { get; set; }
 416}
 417
 418public class OzonOrphanPreviewLine
 419{
 420    public string? OfferId { get; set; } = string.Empty;
 421    public string? OzonWarehouseName { get; set; } = string.Empty;
 422    public string? BitrixWarehouseName { get; set; } = string.Empty;
 423    public int AvailableQty { get; set; }
 424    public string? OzonOrphanStatus { get; set; }
 425}
 426
 427public class OzonOrphanPreviewDto
 428{
 429    public List<OzonOrphanPreviewLine> Orphans { get; set; } = new();
 430    public List<OzonReportFileDto> ReportFiles { get; set; } = new();
 431    public int TotalOrphans { get; set; }
 432    /// <summary>true — этап 5 уже был запущен для этого отчёта.</summary>
 433    public bool IsAlreadyRun { get; set; }
 434}
 435
 436public class BitrixStockSyncCleanupPreviewDto
 437{
 438    public string? ReportId { get; set; } = string.Empty;
 439    /// <summary>Всего групп артикул+клиент с остатком &gt; 0 в отчёте (без Ozon API).</summary>
 440    public int TotalGroups { get; set; }
 441    /// <summary>Сколько групп проверено через Ozon API суммарно (offset + batch).</summary>
 442    public int ScannedGroups { get; set; }
 443    /// <summary>Смещение для следующей порции превью.</summary>
 444    public int NextSkipGroups { get; set; }
 445    /// <summary>Есть ещё группы для подгрузки.</summary>
 446    public bool HasMore { get; set; }
 447    /// <summary>Превью обрезано лимитом — для полной картины запустите очистку.</summary>
 448    public bool IsTruncated { get; set; }
 449    public int TotalProducts { get; set; }
 450    public int TotalWarehousesToZero { get; set; }
 451    public List<BitrixStockSyncCleanupProductDto> Products { get; set; } = new();
 452}
 453
 454public class BitrixStockSyncCleanupZeroRequest
 455{
 456    public int OzonClientId { get; set; }
 457    public string? Article { get; set; } = string.Empty;
 458    /// <summary>Ozon warehouse id — строка, т.к. в JS число &gt; 2^53-1 теряет точность.</summary>
 459    public string? WarehouseId { get; set; } = string.Empty;
 460    public string? OfferId { get; set; }
 461    public long OzonProductId { get; set; }
 462    public int Present { get; set; }
 463    public string? WarehouseName { get; set; }
 464}
 465
 466public class BitrixStockSyncCleanupZeroResult
 467{
 468    public bool Success { get; set; }
 469    public string? Error { get; set; }
 470    public int QtyBefore { get; set; }
 471    public bool ReportLineWritten { get; set; }
 472}
 473
 474public class BitrixStockSyncCleanupProductDto
 475{
 476    public int OzonClientId { get; set; }
 477    public string? OzonClientName { get; set; } = string.Empty;
 478    public string? Article { get; set; } = string.Empty;
 479    public string? OfferId { get; set; } = string.Empty;
 480    public long OzonProductId { get; set; }
 481    public List<BitrixStockSyncCleanupWarehouseDto> ProtectedWarehouses { get; set; } = new();
 482    public List<BitrixStockSyncCleanupWarehouseDto> WarehousesToZero { get; set; } = new();
 483}
 484
 485public class BitrixStockSyncCleanupWarehouseDto
 486{
 487    public long WarehouseId { get; set; }
 488    public string? WarehouseName { get; set; } = string.Empty;
 489    public int Present { get; set; }
 490    public long? ReportLineId { get; set; }
 491    public string? CleanupStatus { get; set; }
 492    public string? CleanupError { get; set; }
 493}
 494
 495/// <summary>Результат поиска товара в Ozon API с разбивкой остатков по схемам.</summary>
 496public record OzonApiFoundInfo(
 497    bool Found,
 498    int FbsTotal,
 499    int RfbsTotal,
 500    int FboTotal,
 501    string? FoundOfferId,
 502    long OzonProductId,
 503    bool NotForSale = false)
 504{
 505    public static readonly OzonApiFoundInfo NotFound = new(false, 0, 0, 0, null, 0);
 506
 507    /// <summary>Суммарный остаток по схемам FBS+rFBS (управляемый через /v4/product/info/stocks).</summary>
 508    public int FbsFamilyTotal => FbsTotal + RfbsTotal;
 509}
 510
 511public class BitrixStockArchivedPreviewClientDto
 512{
 513    public int ClientId { get; set; }
 514    public string? ClientName { get; set; } = string.Empty;
 515    public int AutoCount { get; set; }
 516    public int ManualCount { get; set; }
 517}
 518
 519public class BitrixStockArchivedPreviewLineDto
 520{
 521    public string? Article { get; set; } = string.Empty;
 522    public bool IsAutoArchived { get; set; }
 523    public string? ClientName { get; set; } = string.Empty;
 524    public string? OneCStore { get; set; } = string.Empty;
 525    public int OneCQty { get; set; }
 526    public string? OzonWarehouse { get; set; } = string.Empty;
 527    public int OzonQty { get; set; }
 528}
 529
 530public class BitrixStockArchivedPreviewDto
 531{
 532    public List<BitrixStockArchivedPreviewClientDto> Clients { get; set; } = new();
 533    public List<BitrixStockArchivedPreviewLineDto> Items { get; set; } = new();
 534    public int TotalArchived { get; set; }
 535    public int TotalAuto { get; set; }
 536    public int TotalManual { get; set; }
 537}
 538
 539public class ReportPromoStatusDto
 540{
 541    public string  Article      { get; set; } = "";
 542    public int     OzonClientId { get; set; }
 543    public bool    InPromo      { get; set; }
 544    public string? PromoName    { get; set; }
 545}
 546
 547public class ReportPriceSettingsDto
 548{
 549    public decimal NoPromoMarkupPercent { get; set; } = 5.0m;
 550    public decimal PromoMarkupPercent   { get; set; } = 10.0m;
 551}
 552
 553/// <summary>Результат этапа 2 (накат остатков). Используется оркестратором для решения о запуске этапа 3.</summary>
 554public class BitrixStockApplyResult
 555{
 556    public int  SuccessCount { get; set; }
 557    public int  FailedCount  { get; set; }
 558    public int  Total        { get; set; }
 559    /// <summary>true = этап не выполнялся: distributed-lock занят другим накатом/очисткой.</summary>
 560    public bool LockBusy     { get; set; }
 561}
 562
 563/// <summary>Итог прогона цепочки этапов оркестратором.</summary>
 564public class BitrixStockPipelineResult
 565{
 566    public bool    Success   { get; set; }
 567    /// <summary>Номер последнего успешно завершённого этапа.</summary>
 568    public int     LastStage { get; set; }
 569    public string? Error     { get; set; }
 570}
 571
 572public class OzonPriceSnapshotRecord
 573{
 574    public string ReportId     { get; set; } = "";
 575    public string Article      { get; set; } = "";
 576    public string OfferId      { get; set; } = "";
 577    public int    OzonClientId { get; set; }
 578    public string ClientName   { get; set; } = "";
 579    public string PriceDataJson{ get; set; } = "";
 580}
 581
 582/// <summary>Используется для SqlQueryRaw в GetSyncReportOzonPrices.</summary>
 583public class OzonPriceSnapshotRow
 584{
 585    public string   ClientName      { get; set; } = "";
 586    public string   Article         { get; set; } = "";
 587    public string   OfferId         { get; set; } = "";
 588    public int      OzonClientId    { get; set; }
 589    public string   PriceDataJson   { get; set; } = "";
 590    public decimal? PricePushedOld        { get; set; }
 591    public decimal? PricePushedNew        { get; set; }
 592    public DateTime? PricePushedAtUtc     { get; set; }
 593    public decimal? PushedMarkupPct       { get; set; }
 594    public string?  PushedColorIndexBefore { get; set; }
 595    public string?  PushedColorIndexAfter  { get; set; }
 596    public decimal? PushedCompetitorPrice  { get; set; }
 597}
 598
 599/// <summary>Лёгкий DTO для отображения цены в строке отчёта и Excel-экспорта.</summary>
 600public class OzonPriceInfoDto
 601{
 162602    public string   Article          { get; set; } = "";
 603    /// <summary>OfferId из Ozon (может содержать суффикс партии [N], напр. "9046709030=TYT[5]").</summary>
 0604    public string?  OfferId          { get; set; }
 0605    public int      OzonClientId     { get; set; }
 606    /// <summary>Цена с Ozon Картой (customer_price / true_price).</summary>
 242607    public decimal? TruePrice        { get; set; }
 608    /// <summary>Цена продажи до применения Ozon Карты (marketing_seller_price).</summary>
 136609    public decimal? SellingPrice     { get; set; }
 610    /// <summary>Цветовой индекс конкурентоспособности: SUPER/GREEN/YELLOW/RED/WITHOUT_INDEX.</summary>
 133611    public string?  ColorIndex       { get; set; }
 612    /// <summary>Цена до наката (null — не накатывалась).</summary>
 157613    public decimal? PricePushedOld        { get; set; }
 614    /// <summary>Цена после наката.</summary>
 333615    public decimal? PricePushedNew        { get; set; }
 616    /// <summary>Время наката UTC.</summary>
 116617    public DateTime? PricePushedAtUtc     { get; set; }
 108618    public decimal? PushedMarkupPct       { get; set; }
 109619    public string?  PushedColorIndexBefore { get; set; }
 109620    public string?  PushedColorIndexAfter  { get; set; }
 101621    public decimal? PushedCompetitorPrice  { get; set; }
 71622    public string?  PricePushStatus  { get; set; }
 3623    public string?  PricePushError   { get; set; }
 624}