⟨/⟩ Ntr/70_Infrastructure/AisOiaPeo/AisDataImport.cs

816 строк · в начало

using System;
using Rugrid.Sdaid.Notation.Documents;

namespace Rugrid.Sdaid.Epas.Ntr.Infrastructure.AisOiaPeo
{
    /// <summary>
    /// Контракт интеграции с АИС ОИА ПЭО. Перенесено дословно из rugrid-ntr/docs/ais_data_import_spec.md.
    /// Каждый заголовок источника — отдельная секция.
    /// </summary>
    public class DOC_AisDataImport : IInfrastructureDocument, IHasStructuralLinks
    {
        public SdaidLink[] Links => new[]
        {
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_EnergyEfficiencyClasses>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_GlazingKind>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_GatesKind>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_InsulationMaterial>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_Material>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_ClimateRegion>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_BuildingPurpose>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_RoofType>(),
        };

        public static string _refName = "Импорт данных из АИС";

        public string Name => "Импорт данных из АИС";

        public string Description => @"";

        public string Version => "1.0";
        public string Status => "accepted";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisNaznachenie : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(object);
        public int Order => 10;
        public string Number => "";
        public string Name => "Назначение";

        public string Body => """"
Два отдельных сервиса выполняют импорт данных из внешней системы АИС (read-only БД `AisReadDbContext`) в соответствующие целевые БД:

| Сервис | Целевая БД | Точка входа | Результат |
|---|---|---|---|
| `AisToNtrImportService` | `NtrDbContext` | `ImportAsync` | `AisToNtrImportResult` |
| `AisToMverImportService` | `MverDbContext` | `ImportAsync` | `AisToMverImportResult` |

---
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisObschieArhitekturnyeResheniya : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(object);
        public int Order => 20;
        public string Number => "";
        public string Name => "Общие архитектурные решения";

        public string Body => """"

"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisUpravleniePamyatyuEfCoreChangeTracker : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisObschieArhitekturnyeResheniya);
        public int Order => 30;
        public string Number => "";
        public string Name => "Управление памятью EF Core (Change Tracker)";

        public string Body => """"
Главная проблема: при импорте «в лоб» EF съедает ~1 ГБ памяти из-за Change Tracker.

**Решение:** оба сервиса принимают `IServiceScopeFactory` (не `DbContext` напрямую) и создают отдельный DI-scope на каждый логический блок импорта. После завершения scope DbContext и его Change Tracker освобождаются.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisTranzakcii : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisObschieArhitekturnyeResheniya);
        public int Order => 40;
        public string Number => "";
        public string Name => "Транзакции";

        public string Body => """"
Каждый блок/шаг импорта оборачивается в явную транзакцию (`BeginTransactionAsync` → `CommitAsync` / `RollbackAsync`). При ошибке откатывается только текущий блок; ранее сохранённые данные остаются в БД.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisPeredachaDannyhMezhduBlokami : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisObschieArhitekturnyeResheniya);
        public int Order => 50;
        public string Number => "";
        public string Name => "Передача данных между блоками";

        public string Body => """"
Между блоками данные передаются через словари маппинга `Dictionary<Guid, Guid>` (AisId → NtrId/MverId). Это необходимо, т.к. каждый блок работает в своём scope и не имеет доступа к tracked-сущностям предыдущего блока.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisIdempotentnostUpsertInsertOnly : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisObschieArhitekturnyeResheniya);
        public int Order => 60;
        public string Number => "";
        public string Name => "Идемпотентность (upsert / insert-only)";

        public string Body => """"
Большинство методов импорта реализуют паттерн **upsert**:
1. Загрузить исходные данные из АИС
2. Загрузить существующие записи из целевой БД по `AisId`
3. Если запись с таким `AisId` есть — обновить поля через Set-методы
4. Если нет — создать новую сущность и добавить в DbContext

**Исключения (insert-only):** некоторые модели не обновляют существующие записи, а только создают новые при отсутствии. Это осознанные решения — такие сущности являются корневыми или не содержат данных из АИС.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisStrategiyaObrabotkiOshibok : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisObschieArhitekturnyeResheniya);
        public int Order => 70;
        public string Number => "";
        public string Name => "Стратегия обработки ошибок";

        public string Body => """"
Оба сервиса используют стратегию **fail-fast**: если при импорте невозможно разрешить FK-зависимость, выбрасывается `NtrEntityNotFoundException` или `NtrApplicationException`. Текущая транзакция откатывается, ранее сохранённые блоки остаются в БД. Повторный запуск безопасен благодаря upsert.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisKonfiguraciya : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisObschieArhitekturnyeResheniya);
        public int Order => 80;
        public string Number => "";
        public string Name => "Конфигурация";

        public string Body => """"
`ImportAisDataConfiguration` содержит `AllowedCompanyNames` — список LIKE-паттернов для фильтрации компаний. Фильтр применяется в обоих сервисах при загрузке орг. структуры, зданий и связанных данных.

---
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisAistontrimportservice : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(object);
        public int Order => 90;
        public string Number => "";
        public string Name => "AisToNtrImportService";

        public string Body => """"
Импортирует данные НТР (нормативное теплопотребление). Работает с `NtrDbContext`.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlokiImporta : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisAistontrimportservice);
        public int Order => 100;
        public string Number => "";
        public string Name => "Блоки импорта";

        public string Body => """"

"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlok1SpravochnikiOdnaTranzakciyaOdinScope : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisBlokiImporta);
        public int Order => 110;
        public string Number => "";
        public string Name => "Блок 1: Справочники (одна транзакция, один scope)";

        public string Body => """"
Записей мало, грузим все разом.

| Метод | Источник АИС | Целевая сущность | Возвращает |
|---|---|---|---|
| `ImportClimateRegionsAsync` | `RefDClimateData` | `RefClimateRegionEntity` | `Dict<AisId, NtrId>` |
| `ImportBasementAtticHeatingTypesAsync` | `RefDBasementAtticHeatingTypes` | `RefBasementAtticHeatingTypeEntity` | `Dict<AisId, NtrId>` |
| `ImportBuildingPurposesAsync` | `RefDBuildingTypes` | `RefBuildingPurposeEntity` | `Dict<AisId, NtrId>` |
| `ImportBuildingShapesAsync` | `RefDBuildingShapes` | `RefBuildingShapeEntity` | `Dict<AisId, NtrId>` |
| `ImportElectricityConsumerTypesAsync` | `RefDConsumerDeviceKinds` | `RefElectricityConsumerTypeEntity` | `Dict<AisId, NtrId>` |
| `ImportGateKindsAsync` | `RefDGateKinds` | `RefGateKindEntity` | `Dict<AisId, NtrId>` |
| `ImportGlazingKindsAsync` | `RefDGlazingKinds` | `RefGlazingKindEntity` | `Dict<AisId, NtrId>` |
| `ImportInsulationMaterialsAsync` | `RefDHeaterMaterials` | `RefInsulationMaterialEntity` | `Dict<AisId, NtrId>` |
| `ImportMaterialsAsync` | `RefDConstructionMaterials` | `RefMaterialEntity` | `Dict<AisId, NtrId>` |
| `ImportRoofTypesAsync` | `RefDRoofTypes` | `RefRoofTypeEntity` | `Dict<AisId, NtrId>` |
| `ImportEnergyEfficiencyClassesAsync` | `RefDEnergyEfficiencyClasses` | `RefEnergyEfficiencyClassEntity` | `int` (count) |
| `ImportSolarRadiationByDirectionsAsync` | `RefDSsrs` | `RefSolarRadiationByDirectionEntity` | `int` (count) |
| `ImportSpecificHeatConsumptionNormsAsync` | — | `RefSpecificHeatConsumptionNormEntity` | `int` (count) |

**Нюанс:** `ImportSolarRadiationByDirectionsAsync` выполняет маппинг сокращённых месяцев (`янв`, `фев`...) в полные названия и номера.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlok2OrgStrukturaOdnaTranzakciyaOdinScope : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisBlokiImporta);
        public int Order => 120;
        public string Number => "";
        public string Name => "Блок 2: Орг. структура (одна транзакция, один scope)";

        public string Body => """"
~1240 записей. Порядок импорта строго иерархический (FK-зависимости):

```
NavCompanies → NavIa (зависит от Companies)
             → NavMes (зависит от Companies)
                 → NavPmes (зависит от Mes)
                     → NavSubstations (зависит от Pmes, ClimateRegions)
                         → Navs (зависит от всех вышеперечисленных)
```

| Метод | Целевая сущность |
|---|---|
| `ImportNavCompaniesAsync` | `NavCompanyEntity` |
| `ImportNavIaAsync` | `NavIaEntity` |
| `ImportMesAsync` | `NavMesEntity` |
| `ImportPmesAsync` | `NavPmesEntity` |
| `ImportSubstationsAsync` | `NavSubstationEntity` |
| `ImportNavsAsync` | `NavEntity` |

**Нюанс `ImportNavsAsync`:** ParentId устанавливается в два прохода — сначала создаются все Nav, затем второй проход устанавливает ParentId через словарь result.

**Отличие от Mver:** `ImportSubstationsAsync` дополнительно принимает `climateRegionsDictByAisId` и устанавливает `ClimateRegionId` подстанции.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlok3ZdaniyaOdnaTranzakciyaOdinScope : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisBlokiImporta);
        public int Order => 130;
        public string Number => "";
        public string Name => "Блок 3: Здания (одна транзакция, один scope)";

        public string Body => """"
~6450 записей.

| Метод | Описание |
|---|---|
| `FetchBuildingSourceDataAsync` | Загружает сырые данные зданий из АИС (включая параметры стен, крыши, подвала). Не создаёт сущности — только DTO. |
| `ImportBuildingsAsync` | Создаёт/обновляет `RegBuildingEntity`. Возвращает `Dict<AisId, BuildingDto>`. |

**Нюанс:** `BuildingSourceDataDto` содержит данные, которые позже распределяются по разным моделям (стены, крыша, контакт с грунтом, форма, условия эксплуатации). Этот DTO живёт в памяти до конца импорта.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlok4ModeliPotrebleniyaKazhdayaOtdelnayaTranzakciyaCher : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisBlokiImporta);
        public int Order => 140;
        public string Number => "";
        public string Name => "Блок 4: Модели потребления (каждая — отдельная транзакция через `ExecuteInSeparateScopeAsync`)";

        public string Body => """"
6450 зданий × 11 типов моделей = ~70950 потенциальных insert/update. Каждый тип создаётся в отдельной транзакции.

| # | Метод | Целевая сущность | Возвращает |
|---|---|---|---|
| 1 | `ImportConsumptionModels` | `CmConsumptionModelEntity` | `Dict<BuildingId, CmId>` (insert-only) |
| 2 | `ImportWindowsModelsAsync` | `CmWindowsModelEntity` | `Dict<CmId, WindowsModelId>` |
| 3 | `ImportGateModelsAsync` | `CmGateModelEntity` | `Dict<CmId, GateModelId>` |
| 4 | `ImportHeatingPeriodModelsAsync` | `CmHeatingPeriodModelEntity` | `Dict<CmId, HeatingPeriodModelId>` (insert-only) |
| 5 | `ImportRoofModelsAsync` | `CmRoofModelEntity` | `int` |
| 6 | `ImportGroundContactsAsync` | `CmGroundContactEntity` | `int` |
| 7 | `ImportVentilationsAsync` | `CmVentilationEntity` | `int` (insert-only) |
| 8 | `ImportLightingModelsAsync` | `CmLightingModelEntity` | `Dict<CmId, LightingModelId>` |
| 9 | `ImportEeConsumerModelAsync` | `CmEeConsumerModelEntity` | `Dict<CmId, EeConsumerModelId>` |
| 10 | `ImportOperatingConditions` | `CmOperatingConditionsEntity` | `int` |
| 11 | `ImportWallsModelsAsync` | `CmWallsModelEntity` | `int` |
| 12 | `ImportShapeAndDimensionsAsync` | `CmShapeAndDimensionsEntity` | `int` |

**Нюансы:**
- `ImportConsumptionModels` создаёт базовую модель через `CmConsumptionModelEntity.CreateBaseModel(buildingId, CmCalcKind.Custom.Id, buildingName)`. Insert-only.
- `ImportVentilationsAsync` создаёт заглушки (isMechVentExist=false, isRecuperatorExist=false) — данных в АИС нет. Insert-only.
- `ImportHeatingPeriodModelsAsync` создаёт модель с `CmHeatingPeriodConditionsSrc.ReferenceClimateData`.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlok5SuschnostiModeleyKazhdayaOtdelnayaTranzakciyaChere : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisBlokiImporta);
        public int Order => 150;
        public string Number => "";
        public string Name => "Блок 5: Сущности моделей (каждая — отдельная транзакция через `ExecuteInSeparateScopeAsync`)";

        public string Body => """"
Дочерние записи моделей из блока 4. Могут быть множественными на одно здание. Используют 3-параметровый `ExecuteInSeparateScopeAsync` (NtrDbContext + AisReadDbContext + CancellationToken).

| Метод | Целевая сущность | Примерное кол-во |
|---|---|---|
| `ImportWindowsEntitiesAsync` | `CmWindowsEntity` | N на здание |
| `ImportGateEntitiesAsync` | `CmGateEntity` | N на здание |
| `ImportLightingEntitiesAsync` | `CmLightingEntity` | N на здание |
| `ImportEeConsumerKindEntitiesAsync` | `CmEeConsumerKindEntity` | ~22641 |
| `ImportHeatingPeriodsAsync` | `CmHeatingPeriodEntity` | N на здание |
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisExecuteinseparatescopeasyncTriPeregruzki : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisAistontrimportservice);
        public int Order => 160;
        public string Number => "";
        public string Name => "ExecuteInSeparateScopeAsync — три перегрузки";

        public string Body => """"
```
// 1. С результатом, без AisReadDbContext (блок 4)
Task<TResult> ExecuteInSeparateScopeAsync<TResult>(Func<NtrDbContext, CancellationToken, Task<TResult>>)

// 2. Без результата, с AisReadDbContext (блок 5)
Task ExecuteInSeparateScopeAsync(Func<NtrDbContext, AisReadDbContext, CancellationToken, Task>)

// 3. Без результата, без AisReadDbContext
Task ExecuteInSeparateScopeAsync(Func<NtrDbContext, CancellationToken, Task>)
```
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisGrafZavisimostey : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisAistontrimportservice);
        public int Order => 170;
        public string Number => "";
        public string Name => "Граф зависимостей";

        public string Body => """"
```
Блок 1 (справочники)
  ├─→ climateRegionsDictByAisId ──────────→ Блок 2 (Substations), Блок 3 (Buildings)
  ├─→ basementAtticHeatingTypesDictByAisId → Блок 4 (RoofModels, GroundContacts)
  ├─→ buildingPurposesDictByAisId ────────→ Блок 4 (OperatingConditions)
  ├─→ buildingShapesDictByAisId ──────────→ Блок 4 (ShapeAndDimensions)
  ├─→ electricityConsumerTypesDictByAisId → Блок 5 (EeConsumerKindEntities)
  ├─→ gateKindsDictByAisId ──────────────→ Блок 5 (GateEntities)
  ├─→ glazingKindsDictByAisId ───────────→ Блок 5 (WindowsEntities)
  ├─→ insulationMaterialsDictByAisId ────→ Блок 4 (RoofModels, WallsModels)
  ├─→ materialsDictByAisId ─────────────→ Блок 4 (RoofModels, WallsModels)
  └─→ roofTypesDictByAisId ─────────────→ Блок 4 (RoofModels)

Блок 2 (орг. структура)
  └─→ navsDictByAisId ──→ Блок 3 (Buildings)

Блок 3 (здания)
  ├─→ buildingSourceDataDtosDictByAisId ──→ Блок 4 (Roof, GroundContact, Walls, Shape, OperatingConditions)
  └─→ buildingsDictByAisId ──────────────→ Блок 4, Блок 5

Блок 4 (модели)
  ├─→ cmDictByBuildingId ───────→ Блок 4 (все модели), Блок 5
  ├─→ windowsModelsDictByCmId ─→ Блок 5 (WindowsEntities)
  ├─→ gateModelsDictByCmId ────→ Блок 5 (GateEntities)
  ├─→ heatingPeriodModelsDictByCmId → Блок 5 (HeatingPeriods)
  ├─→ lightingModelDictByCmId ─→ Блок 5 (LightingEntities)
  └─→ eeConsumerModelDictByCmId → Блок 5 (EeConsumerKindEntities)
```

---
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisAistomverimportservice : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(object);
        public int Order => 180;
        public string Number => "";
        public string Name => "AisToMverImportService";

        public string Body => """"
Импортирует данные МВЭР (матрица выбора энергоэффективных решений). Работает с `MverDbContext`.
"""";

        public string Version => "";
        public string Status => "to-move";
        public string[] Comments => new[] { "Раздел описывает импорт в МВЭР — кандидат на переезд в отдельный проект спеки Rugrid.Sdaid.Mver." };
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlokiImporta2 : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisAistomverimportservice);
        public int Order => 190;
        public string Number => "";
        public string Name => "Блоки импорта";

        public string Body => """"

"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlok1SpravochnikiMverOdnaTranzakciyaOdinScope : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisBlokiImporta2);
        public int Order => 200;
        public string Number => "";
        public string Name => "Блок 1: Справочники МВЭР (одна транзакция, один scope)";

        public string Body => """"
Записей мало, грузим все разом.

| Метод | Источник АИС | Целевая сущность | Возвращает |
|---|---|---|---|
| `ImportHeatingTypesAsync` | `RefDHeatingTypes` | `RefHeatingTypeEntity` | `Dict<AisId, MverId>` |
| `ImportElectricDeviceTypesAsync` | `RefDConsumerDeviceKinds` | `RefElectricDeviceTypeEntity` | `Dict<AisId, MverId>` |
| `ImportHeatAdjustMethodsAsync` | `RefDHeatAdjustMethods` | `RefHeatAdjustMethodEntity` | `Dict<AisId, MverId>` |
| `ImportPeriodsAsync` | `RefPeriods` | `RefPeriodEntity` | `Dict<AisId, MverId>` |
| `ImportTransformerCoolTypesAsync` | `RefDTransformerCoolingTypes` | `RefTransformerCoolTypeEntity` | `Dict<AisId, MverId>` |
| `ImportTransformerTypesAsync` | `RefDTransformerTypes` | `RefTransformerTypeEntity` | `Dict<AisId, MverId>` |
"""";

        public string Version => "";
        public string Status => "to-move";
        public string[] Comments => new[] { "Раздел описывает импорт в МВЭР — кандидат на переезд в отдельный проект спеки Rugrid.Sdaid.Mver." };
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlok2OrgStrukturaOdnaTranzakciyaOdinScope2 : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisBlokiImporta2);
        public int Order => 210;
        public string Number => "";
        public string Name => "Блок 2: Орг. структура (одна транзакция, один scope)";

        public string Body => """"
Идентична орг. структуре Ntr-импорта по логике, но работает с `MverDbContext`.

```
NavCompanies → NavIa → NavMes → NavPmes → NavSubstations → Navs
```

**Отличие от Ntr:** `ImportSubstationsAsync` не принимает `climateRegionsDictByAisId` — подстанции в МВЭР не привязаны к климатическому региону.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlok3ZdaniyaOdnaTranzakciyaOdinScope2 : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisBlokiImporta2);
        public int Order => 220;
        public string Number => "";
        public string Name => "Блок 3: Здания (одна транзакция, один scope)";

        public string Body => """"
~6450 записей. Создаёт/обновляет `RegBuildingEntity`.

- `FetchBuildingSourceDataAsync` — загружает сырые данные зданий из АИС
- `ImportBuildingsAsync` — upsert `RegBuildingEntity`. Возвращает `Dict<AisId, Guid>`.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisBlok4MverDannyeKazhdayaOtdelnayaTranzakciyaCherezExecut : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisBlokiImporta2);
        public int Order => 230;
        public string Number => "";
        public string Name => "Блок 4: Mver данные (каждая — отдельная транзакция через `ExecuteInSeparateScopeAsync`)";

        public string Body => """"
| Метод | Целевая сущность | Источник АИС | Стратегия |
|---|---|---|---|
| `ImportMverBuildingEeConsumersAsync` | `MverBuildingEeConsumerEntity` | `DocBuildingConsumersEes` | upsert |
| `ImportMverHeatedEquipmentsAsync` | `MverHeatedEquipmentEntity` | `DocHeatedEquipments` | upsert |
| `ImportMverTransformersAsync` | `MverTransformerEntity` | `RefETransformers` | upsert |
| `ImportMverTransformerWorkInfosAsync` | `MverTransformerWorkInfoEntity` | `DocTrWorks` + `DocTrWorksValues` | upsert, батчи по 10К |
| `ImportMverTsnsAsync` | `MverTsnEntity` | `RefETsns` | upsert |

**Нюансы:**
- **Фильтр по типу прибора:** `ImportMverBuildingEeConsumersAsync` — только `RefDConsumerDeviceKind.Code == "conditioner"`.
- **Name** для EeConsumer формируется как `"{DeviceKindName} ({Amount} шт., {Power} кВт)"`.
- **`ImportMverTransformerWorkInfosAsync`** фильтрует по `Year >= 2020`. Обходит данные постранично батчами по 10К с вызовом `SaveChanges + ChangeTracker.Clear()` на каждый батч. Фильтр по типу периода задаётся через константу `MonthPeriodType` в сервисе.
"""";

        public string Version => "";
        public string Status => "to-move";
        public string[] Comments => new[] { "Раздел описывает импорт в МВЭР — кандидат на переезд в отдельный проект спеки Rugrid.Sdaid.Mver." };
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisExecuteinseparatescopeasyncOdnaPeregruzka : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisAistomverimportservice);
        public int Order => 240;
        public string Number => "";
        public string Name => "ExecuteInSeparateScopeAsync — одна перегрузка";

        public string Body => """"
```
// С результатом, с AisReadDbContext
Task<TResult> ExecuteInSeparateScopeAsync<TResult>(Func<MverDbContext, AisReadDbContext, CancellationToken, Task<TResult>>)
```
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisGrafZavisimostey2 : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisAistomverimportservice);
        public int Order => 250;
        public string Number => "";
        public string Name => "Граф зависимостей";

        public string Body => """"
```
Блок 1 (справочники)
  ├─→ heatingTypesDictByAisId ──────────→ Блок 3 (Buildings)
  ├─→ electricDeviceTypesDictByAisId ───→ Блок 4 (MverBuildingEeConsumers)
  ├─→ heatAdjustMethodsDictByAisId ────→ Блок 4 (MverHeatedEquipments)
  ├─→ periodsDictByAisId ──────────────→ Блок 4 (MverTransformerWorkInfos)
  ├─→ transformerCoolTypesDictByAisId ─→ Блок 4 (MverTransformers)
  └─→ transformerTypesDictByAisId ─────→ Блок 4 (MverTransformers)

Блок 2 (орг. структура)
  └─→ navsDictByAisId ──→ Блок 3 (Buildings), Блок 4 (все)

Блок 3 (здания)
  └─→ buildingsDictByAisId ────────────→ Блок 4 (MverBuildingEeConsumers)

Блок 4 (Mver данные)
  └─→ transformersDictByAisId ─────────→ Блок 4 (MverTransformerWorkInfos)
```

---
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisMappingPoleySpravochnikovAisNtr : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(object);
        public int Order => 260;
        public string Number => "";
        public string Name => "Маппинг полей справочников (АИС → НТР)";

        public string Body => """"
Большинство справочников маппят поля 1:1 (Id → AisId, Name → Name, Code → Code). Ниже перечислены только нетривиальные маппинги:
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisRefglazingkindentityVidOstekleniyaTolkoNtr : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisMappingPoleySpravochnikovAisNtr);
        public int Order => 270;
        public string Number => "";
        public string Name => "RefGlazingKindEntity (вид остекления) — только NTR";

        public string Body => """"
| АИС (`RefDGlazingKinds`) | НТР (`RefGlazingKindEntity`) |
|---|---|
| `CoefRf0` | `ThermalResistance` |
| `CoefTf` | `ShadingCoefficient` |
| `CoefKf` | `SolarHeatGainCoefficient` |
| `BreathabilityKgPerM2h` | `AirPermeability` |
| `Emissivity` | `EmissionCoefficient` |
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisRefgatekindentityVidVorotTolkoNtr : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisMappingPoleySpravochnikovAisNtr);
        public int Order => 280;
        public string Number => "";
        public string Name => "RefGateKindEntity (вид ворот) — только NTR";

        public string Body => """"
| АИС (`RefDGateKinds`) | НТР (`RefGateKindEntity`) |
|---|---|
| `CoefRv` | `ThermalResistance` |
| `BreathabilityKgPerM2h` | `AirPermeability` |
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisRefinsulationmaterialentityRefmaterialentityTolkoNtr : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisMappingPoleySpravochnikovAisNtr);
        public int Order => 290;
        public string Number => "";
        public string Name => "RefInsulationMaterialEntity / RefMaterialEntity — только NTR";

        public string Body => """"
| АИС | НТР |
|---|---|
| `LVtMK` | `ThermalConductivity` |
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisRefclimateregionentityKlimaticheskiyRayonTolkoNtr : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisMappingPoleySpravochnikovAisNtr);
        public int Order => 300;
        public string Number => "";
        public string Name => "RefClimateRegionEntity (климатический район) — только NTR";

        public string Body => """"
| АИС (`RefDClimateData`) | НТР (`RefClimateRegionEntity`) |
|---|---|
| `LatitudeSr` | `Latitude` |
| `LongitudeSr` | `Longitude` |
| `DaysAvgTempLe8` | `DaysAvgTempLe8` |
| `AvgTempLe8` | `AvgTempLe8` |
| `MaxAvgWindSpeedLe8` | `MaxAvgWindSpeedLe8` |
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisRefbuildingpurposeentityNaznachenieZdaniyTolkoNtr : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(AisMappingPoleySpravochnikovAisNtr);
        public int Order => 310;
        public string Number => "";
        public string Name => "RefBuildingPurposeEntity (назначение зданий) — только NTR";

        public string Body => """"
| АИС (`RefDBuildingTypes`) | НТР (`RefBuildingPurposeEntity`) |
|---|---|
| `AirM3PerHumanhour` | `SpecificAirConsumptionM3PerHumanHour` |

---
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Раздел спецификации импорта данных из АИС ОИА ПЭО.</summary>
    public class AisOgranicheniyaIOsobennosti : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_AisDataImport);
        public Type Parent => typeof(object);
        public int Order => 320;
        public string Number => "";
        public string Name => "Ограничения и особенности";

        public string Body => """"
1. **Последовательное выполнение** — все блоки внутри каждого сервиса выполняются строго последовательно из-за зависимостей по данным.
2. **Нет частичного повторного запуска** — при падении на блоке N блоки 1..N-1 уже сохранены. Повторный запуск безопасен благодаря upsert.
3. **Фильтрация по компаниям** — весь импорт ограничен компаниями из `AllowedCompanyNames` (LIKE-паттерны).
4. **Оба сервиса независимы** — импортируют в разные схемы БД, могут запускаться независимо.
5. **Орг. структура дублируется** — `NavEntity`, `NavCompanyEntity` и др. импортируются в обе БД отдельно.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }
}