⟨/⟩ Ntr/35_ChangeRequests/DOC_CR_2026_09_03_ClimateConditions.cs

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

using Rugrid.Sdaid.Notation.Documents;
using Rugrid.Sdaid.Epas.Ntr.Backend.Calc;

namespace Rugrid.Sdaid.Epas.Ntr.ChangeRequests
{
    /// <summary>
    /// Климатические условия модели: явный выбор источника климатических данных
    /// (район здания, район подстанции, данные пользователя) вместо неявного использования
    /// района здания. Раскрывает пункт 4 CR 2026-08-19.
    /// </summary>
    public class DOC_CR_2026_09_03_ClimateConditions : IChangeRequestDocument, IHasStructuralLinks, IFolder<NtrChangeRequestsFolder>
    {
        public SdaidLink[] Links => new[]
        {
            Rel.Uses<DOC_CR_2026_08_19_Etap2Backlog>(),
            Rel.Uses<DOC_TtrCalc>(),
            Rel.Uses<DOC_CalcModel>(),
            Rel.Uses<DOC_TtrCalcInputMapping>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.ConsumptionModel.DOC_HeatingPeriodModel>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.SysReference.DOC_HeatingPeriodConditionsSrc>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_ClimateRegion>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.BuildingModel.DOC_Building>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.OrgObjectModel.DOC_Substation>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_ModelCardHeatingPeriod>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_BuildingInfo>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_SubstationInfo>(),
        };

        public static string _refName = "CR 2026-09-03: обработка источников климатических данных";

        public string Name => "Обработка источников климатических данных";

        public string Description =>
            @"Пользователь выбирает, какие климатические данные использовать для расчёта: 
            климатический район здания, климатический район подстанции или собственные значения. 
            Вкладка «Условия отопительного периода» переименовывается в «Климатические условия» 
            и показывает все доступные наборы данных, включая широту. 
            Широта с другими климатическими данными передаётся в InputModel и проверяется, при отсутствии формируется понятное сообщение в логе.";

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

        public static string Source = """"
            ## Источник (как есть)

            При расчёте модели здания подстанции возникает ошибка:

            > Отсутствуют записи справочника солнечной радиации для стороны света
            """";

        public static string Analysis = $$""""
            ## Разбор

            ### От сообщения к причине

            Сообщение верно и бесполезно: оно про следствие. Записей справочника нет потому, что
            они выбираются по широте, а широты нет. Широты может не быть: 
            1. не указана в данных климатического района. 
            2. не указан климатический район здания. 
            3. здание является зданием подстанции, пользователь ожидает, что климатические данные будут определены по климатическому району подстанции.

            В случаях 2 и 3 рядом появляется вторая ошибка: без климатического района нет и данных
            отопительного периода, они тоже приводятся к нулю, и секция
            {{SdaidAnchors.RefTo<TeHeatingDegreeDays>("te.heating_degree_days")}} сообщает о выходе
            $z_{ht}$ за границы диапазона. Две ошибки об одной причине, и ни одна её не называет.

            Пользователь не может обнаружить причину и до расчёта: широта не выводится
            в интерфейсе нигде.

            ### Целевой сценарий

            Пользователю на UI надо показать какие справочные климатические данные есть и дать выбрать какие использовать в расчёте: данные климатического района здания, климатического
            района подстанции, на территории которой здание размещено, или по собственным данным.

            Тем самым закрывается пункт «Поведение модели здания ПС, если Климатический район
            здания не определён» из {{SdaidAnchors.RefTo<DOC_CR_2026_08_19_Etap2Backlog>()}}, где
            предлагалась молчаливая подстановка района подстанции. Подстановки не будет: выбор
            виден на экране и хранится в модели.

            ### Данные

            Всё, что нужно, в БД уже есть, загрузка из АИС ОИА ПЭО правок не требует:
            ссылка на климатический район у {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Domain.BuildingModel.DOC_Building>()}}
            и у {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Domain.OrgObjectModel.DOC_Substation>()}},
            собственная географическая широта подстанции, справочник
            {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_ClimateRegion>()}}.
            Здание связано с подстанцией через организационный объект.

            ### Широта

            Широта — четвёртая величина набора, наравне с продолжительностью, температурой и скоростью
            ветра: она определяет выборку записей справочника солнечной радиации в секции
            {{SdaidAnchors.RefTo<TeSolarRadiationByDirection>("te.solar_radiation_by_direction")}},
            тогда как остальные три входят в
            {{SdaidAnchors.RefTo<TeHeatingDegreeDays>("te.heating_degree_days")}}.

            Сейчас широта источнику не подчиняется: она всегда берётся из климатического района
            здания, независимо от выбранного источника условий ОП. То есть при выборе «Данные
            пользователя» температура и ветер берутся пользовательские, а солнечная радиация —
            по району здания, и пользователь этого не видит, потому что широты на экране нет.
            Широта должна определяться тем же источником, что и остальные три величины, 
            показываться на UI и вносится пользователем, если источник - пользовательские данные.

            Широта должна передаваться в модель входных данных расчёта. Сейчас она в неё не входит:
            {{SdaidAnchors.RefTo<DOC_TtrCalcInputMapping>()}} прямо оговаривает, что широта
            определяется вызывающей стороной и в `InputModel` не передаётся. Пока широта была
            единственно возможной, это было допустимо; как только она становится предметом выбора,
            расчёт не должен добирать её сам, минуя входную модель. Маппинг:

            | Параметр `InputModel` | Характеристика Сервиса | Ед. изм. |
            |---|---|---|
            | `HeatingPeriod.Latitude` (`heatingPeriod.latitudeDeg`) | широта выбранного источника климатических данных | ° |

            Оговорка в разделе `TeInputSection.References` снимается.

            Пользовательская широта — **целое число градусов**, как и широта климатического района:
            записи справочника солнечной радиации ключуются целым градусом, дробное значение
            пришлось бы округлять, и расчёт шёл бы не по введённому пользователем значению.

            ### Географическая широта подстанции — только для информации

            Собственная широта подстанции ведётся строкой вида `55°5'47"N` — градусы, минуты,
            секунды, сторона света. Источником расчётных данных она быть не может: справочник
            солнечной радиации ключуется целым градусом, а перевод строки в градусы — округление,
            которое Сервис сделал бы молча.

            На вкладку климатических условий она не выносится: там показываются данные, участвующие
            в расчёте. Своё место у неё уже есть — карточка подстанции
            ({{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_SubstationInfo>()}}),
            где она стоит рядом с широтой климатического района подстанции, и расхождение между
            ними видно там же.

            ### Источник климатических данных

            Справочник {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.SysReference.DOC_HeatingPeriodConditionsSrc>()}}
            расширяется: прежнее значение «Справочные данные» расщепляется на «Климатический район
            здания» и «Климатический район подстанции». Существующие модели переводятся
            в «Климатический район здания» — их поведение не меняется.

            Значение «Климатический район подстанции» доступно только для зданий, размещённых
            на территории подстанции. Для остальных зданий выбор — район здания или данные
            пользователя.

            **Значение по умолчанию при создании модели:** первое что определено: климатический район здания, если он
            задан; иначе — климатический район подстанции, если здание размещено на территории подстанции. 

            ### Правила валидации

            Проверяется полнота **выбранного** источника, и сообщается именно она: «в климатическом
            районе здания не задана широта», а не «нет записей справочника солнечной радиации».
            Правила вносятся в методику — в секции
            {{SdaidAnchors.RefTo<TeHeatingDegreeDays>("te.heating_degree_days")}} и
            {{SdaidAnchors.RefTo<TeSolarRadiationByDirection>("te.solar_radiation_by_direction")}}, —
            где сейчас источником названы данные климатического района здания.

            Отсутствующее значение не приводится к нулю. Это тот же дефект, что разобран в CR
            по четырёхскатным крышам на длине конька: подстановка правдоподобного числа вместо
            пустого значения теряет причину ошибки, а расчёт возвращает результат по другим данным.

            ### Вкладка «Климатические условия»

            Вкладка {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_ModelCardHeatingPeriod>()}}
            переименовывается и получает третий набор данных — климатический район подстанции,
            доступный только для зданий, размещённых на её территории. Широта добавляется
            в каждый набор.

            Механика выбора остаётся прежней: источник подменяет отображаемый блок данных.
            Виден набор, по которому ведётся расчёт, — этого достаточно, чтобы увидеть незаданную
            широту, а параллельный показ невыбранных наборов загромождал бы вкладку.

            Незаполненное значение внутри показанного блока выводится прочерком, а не скрытой
            строкой: скрытая строка неотличима от отсутствующего поля, а именно из-за этого
            пользователь и не видел, что широта не задана.

            Сущность {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Domain.ConsumptionModel.DOC_HeatingPeriodModel>()}}
            и секции методики не переименовываются: градусо-сутки остаются градусо-сутками
            отопительного периода. Переименование касается вкладки карточки модели.

            ### Что остаётся как есть

            Значение справочника «Фактические данные» (`ActualClimateData`) и три соответствующих
            атрибута модели отопительного периода не трогаются, хотя из справочника значение
            удалено миграцией: их судьба — отдельное решение, в объём этого CR не входит.
            """";

        public static string BlastRadius = $$""""
            ## Blast radius

            ### Спецификация

            | Документ | Правка |
            |---|---|
            | {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.SysReference.DOC_HeatingPeriodConditionsSrc>()}} | состав справочника: «Справочные данные» расщепляется на «Климатический район здания» и «Климатический район подстанции» |
            | {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Domain.ConsumptionModel.DOC_HeatingPeriodModel>()}} | новый атрибут «Широта (данные пользователя)»; уточнение описания источника данных |
            | {{SdaidAnchors.RefTo<TeHeatingDegreeDays>("te.heating_degree_days")}} | источник $z_{ht}$ и $t_{ext}$ — выбранный источник климатических данных; правило полноты выбранного источника |
            | {{SdaidAnchors.RefTo<TeSolarRadiationByDirection>("te.solar_radiation_by_direction")}} | внесено: широта $φ$ в исходных данных секции — значение выбранного источника климатических данных; правила валидации широты ($φ$ задана, $φ \in [-90; 90]$) и порядок проверки — широта до полноты справочника |
            | {{SdaidAnchors.RefTo<DOC_CalcModel>()}} | новое поле широты в модели входных данных |
            | {{SdaidAnchors.RefTo<DOC_TtrCalcInputMapping>()}} | широта передаётся в `InputModel`; снимается оговорка «определяется вызывающей стороной» |
            | {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_ModelCardHeatingPeriod>()}} | переименование вкладки, набор данных климатического района подстанции, широта в каждом наборе, макет экрана |
            | {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_BuildingInfo>()}} | сверка: широта района здания в описании полей есть, в реализации не выводится |
            | {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_SubstationInfo>()}} | сверка: широта района подстанции и географическая широта |

            ### База данных

            - `CmHeatingPeriodConditionsSrc` — новое значение перечисления и запись справочника;
              описание существующей записи уточняется до «климатического района здания».
            - `CmHeatingPeriodModelEntity` — свойство `CustLatitude` (`int?`) и сеттер по образцу
              соседних; копирование в `Copy`.
            - **Миграция EF**: колонка `cust_latitude` в `ntr.cm_heating_period_models`;
              перевод существующих моделей с «Справочные данные» на «Климатический район здания».
            - Read-модель модели отопительного периода и её конфигурация.

            ### API и контракты

            - `HeatingPeriodModelDetails` и DTO в `ConsumptionModelDtos` — новое поле.
            - `GetConsumptionModelForEditQueryHandler` — проекция поля.
            - `CreateConsumptionModelCommandHandler` — вычисление умолчания источника по данным
              здания и подстанции; `ApplyConsumptionModelEditCommandHandler` — запись значения.
            - `GetBuildingSummaryQuery` — климатические данные района подстанции в ответе,
              чтобы вкладка могла показать этот набор.
            - Swagger-контракт меняется ⇒ перегенерация клиента фронтенда.

            ### Расчёт

            Цепочка: выбранный источник → значения набора (широта, продолжительность, температура,
            ветер) → `InputModel` → секции `te.heating_degree_days` и
            `te.solar_radiation_by_direction`.

            - `CalcInputModelFactory` — чтение климатического района подстанции; `ResolveHeatingPeriod`
              возвращает четвёртую величину — широту — и различает два справочных источника;
              выборка солнечной радиации ведётся по разрешённой широте, а не по району здания.
            - `InputModel` — поле широты; `Ntr.Calc` — приём широты из входной модели.
            - Валидация полноты выбранного источника с указанием причины.

            ### Интерфейс

            - `heating-period.util.ts` — переименование вкладки, третий набор данных (район
              подстанции), широта в наборах, контрол пользовательской широты; механика подмены
              отображаемого блока по выбранному источнику остаётся прежней.
            - `building.util.ts`, `substation.util.ts` — вывод широты; незаполненные значения
              показывать прочерком вместо скрытия строки (`include: … != null`).
            - Прототип: страница вкладки пересобирается по описанию полей.

            ### Тесты

            - Разрешение источника: район здания, район подстанции, данные пользователя.
            - Умолчание при создании модели: район здания приоритетнее района подстанции.
            - Валидация неполного выбранного источника — сообщается причина.
            """";

        public static string Plan = """"
            ## План

            1. Правки спецификации по таблице blast radius; в изменённых секциях проставить `Status`
               и `Comments` со ссылкой на этот CR.
            2. Справочник источников, домен и БД: значение перечисления, атрибут, миграция
               с переводом существующих моделей, read-модель.
            3. API: контракты, умолчание при создании модели, климатические данные подстанции
               в ответе сводки по зданию; перегенерация клиента.
            4. Расчёт: разрешение источника с широтой, передача широты во входную модель,
               валидация полноты выбранного источника.
            5. UI: вкладка «Климатические условия», вывод широты в карточках здания и подстанции.
            6. Зафиксировать «Итог»; закрыть пункт 4 CR 2026-08-19 ссылкой на этот CR.
            """";

        public static string Acceptance = """"
            ## Acceptance criteria

            - **AC-1.** На вкладке «Климатические условия» выбор источника подменяет отображаемый
              блок данных: показан набор выбранного источника, включая широту.
            - **AC-2.** Незаполненное значение показанного набора выводится прочерком, а не скрытой
              строкой.
            - **AC-3.** Источник «Климатический район подстанции» доступен в списке только
              для зданий, размещённых на территории подстанции.
            - **AC-4.** При создании модели источник по умолчанию: климатический район здания,
              если он задан; иначе климатический район подстанции, если здание размещено
              на её территории.
            - **AC-5.** Расчёт с источником «Климатический район подстанции» использует широту,
              продолжительность, температуру и скорость ветра района подстанции; с источником
              «Данные пользователя» — все четыре введённые пользователем величины, включая широту.
            - **AC-6.** Если в выбранном источнике не заполнена величина, расчёт сообщает именно
              это — с указанием источника и величины, — а не отсутствие записей справочника
              солнечной радиации или выход $z_{ht}$ за границы.
            - **AC-7.** Существующие модели после миграции считаются с прежним результатом:
              источник «Климатический район здания».
            """";

        public static string Result = """"
            ## Итог

            —
            """";
    }
}