⟨/⟩ Ntr/35_ChangeRequests/DOC_CR_2026_08_31_UpperEnclosuresHipRoof.cs

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

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

namespace Rugrid.Sdaid.Epas.Ntr.ChangeRequests
{
    /// <summary>
    /// Доработка методики второго этапа: расчёт площади верхних ограждающих конструкций
    /// для четырёхскатных крыш (вальмовой и шатровой) при прямоугольной форме здания.
    /// Требует нового атрибута «Длина конька» — сквозная правка от БД до интерфейса.
    /// </summary>
    public class DOC_CR_2026_08_31_UpperEnclosuresHipRoof : IChangeRequestDocument, IHasStructuralLinks, IFolder<NtrChangeRequestsFolder>
    {
        public SdaidLink[] Links => new[]
        {
            Rel.Uses<DOC_TtrCalc>(),
            Rel.Uses<DOC_CalcModel>(),
            Rel.Uses<DOC_TtrCalcInputMapping>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.ConsumptionModel.DOC_RoofModel>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_RoofType>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Infrastructure.AisOiaPeo.DOC_AisDataImport>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_ModelCardRoofModel>(),
        };

        public static string _refName = "CR 2026-08-31: ВОК для четырёхскатной крыши";

        public string Name => "Расчёт площади ВОК для четырёхскатных крыш";

        public string Description =>
            @"Реализовать расчёт площади верхних ограждающих конструкций для типов крыши «Четырёхскатная вальмовая» и «Четырёхскатная шатровая» при прямоугольной форме здания. Формулы зафиксированы в методике; для вальмовой крыши требуется новый исходный параметр «Длина конька», которого нет ни в домене, ни в БД, ни на интерфейсе.";

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

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

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

            Задача разбита на две: **A — односкатная** и **B — четырёхскатные** (вальмовая и шатровая,
            этот CR).
            """";

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

            Формулы для обоих типов крыши уже есть в методике — они были вписаны заранее и перенесены
            в SDAID вместе с остальным текстом. Реализации нет: `MapRoofType` в `CalcInputModelFactory`
            на этих типах кидает исключение, а диспетчеры секций
            {{SdaidAnchors.RefTo<TeAtticWallArea>("te.attic_wall_area")}} и
            {{SdaidAnchors.RefTo<TeRoofCoverArea>("te.roof_cover_area")}} знают только плоскую,
            двускатную и односкатную крышу.

            ### Что считаем

            Площадь стен чердака по методике для обоих типов равна нулю: у вальмовой и шатровой крыши
            скаты сходятся со всех сторон, вертикальных фронтонов нет.

            | Тип | $A_{НП.Черд}$ | $A_{ПК}$ |
            |---|---|---|
            | Четырёхскатная вальмовая (`hipped_rect`) | 0 | $(L_A + L_{конька})·h_{трап} + L_B·h_{треуг}$ |
            | Четырёхскатная шатровая (`pyramidal_rect`) | 0 | $L_A·h^A_{треуг} + L_B·h^B_{треуг}$ |

            где $h_{трап} = \sqrt{(H-z_{ВП})^2 + (L_B/2)^2}$,
            $h_{треуг} = \sqrt{(H-z_{ВП})^2 + ((L_A-L_{конька})/2)^2}$,
            $h^A_{треуг} = \sqrt{(H-z_{ВП})^2 + (L_B/2)^2}$,
            $h^B_{треуг} = \sqrt{(H-z_{ВП})^2 + (L_A/2)^2}$.

            **Шатровая — вырожденный случай вальмовой** при $L_{конька} = 0$: подставив ноль в формулу
            вальмовой, получаем формулу шатровой. Отдельными ветками расчёта они остаются — тип задан
            справочником, — но это объясняет, почему незаполненный конёк у вальмовой крыши опасен:
            расчёт молча даст площадь шатровой — отсюда правило обязательности ниже.

            ### Как различаются вальмовая и шатровая

            В загруженном из АИС ОИА ПЭО справочнике типов крыш для них заведены отдельные записи:
            `hipped` — четырёхскатная вальмовая, `pyramidal` — четырёхскатная шатровая. Тип расчёта
            определяется кодом записи напрямую, как для плоской, двускатной и односкатной крыши.
            Длина конька — исходный параметр формулы вальмовой крыши, а не способ определить тип.

            ### Новый параметр «Длина конька»

            Параметр не ведёся в АИС ОИА ПЭО. Его надо добавить в характеристики верхних ограждающих конструкций,
            в сущность {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Domain.ConsumptionModel.DOC_RoofModel>()}}:

            {{SdaidCode.OfMember<Rugrid.Sdaid.Epas.Ntr.Domain.ConsumptionModel.RoofModel>(nameof(Rugrid.Sdaid.Epas.Ntr.Domain.ConsumptionModel.RoofModel.RidgeLengthM))}}

            Он будет заполняться пользователем в Сервисе, импорт из АИС его не должен затирать.            

            Атрибут добавлен в спецификацию сущности этим CR. Как он ложится на структуру БД,
            read-модель и контракты — в разделе Blast radius.

            ### Правило валидации длины конька

            Правило одно, внесено в методику — раздел «Правила валидации» секции
            {{SdaidAnchors.RefTo<TeRoofCoverArea>("te.roof_cover_area")}}: для вальмовой крыши
            $0 < L_{конька} < L_A$. Место выбрано по использованию: длина конька входит только в эту
            секцию, смежная {{SdaidAnchors.RefTo<TeAtticWallArea>("te.attic_wall_area")}} для
            четырёхскатных крыш даёт ноль.

            Обоснование границ: верхняя — из наклонной высоты торцевого ската
            $h_{треуг}=\sqrt{(H-z_{ВП})^2 + ((L_A-L_{конька})/2)^2}$, при $L_{конька} = L_A$ торцевые
            скаты вырождаются в линию. Сторона сравнения — $L_A$, а не $\max(L_A, L_B)$: конёк входит
            в формулу как слагаемое и вычитаемое именно к $L_A$. Нижняя граница строгая, поэтому
            незаполненное значение нельзя подставлять нулём: при нулевом коньке формула вальмовой
            крыши вырождается в формулу шатровой и вернула бы площадь крыши другого типа.

            ### Открытые вопросы

            - **OQ-1. Контрольные примеры** — по одному на вальмовую и шатровую крышу. Значения
              предоставляет заказчик: расчёт контрольных чисел тем же алгоритмом, который проверяется,
              ничего не доказывает.

            Прежняя запись справочника «Четырехскатная» (`tr300`), если она осталась и на неё ссылаются
            модели, остаётся типом крыши, не поддержанным методикой, — как и `tr400` «Двускатная без
            нижнего перекрытия». Ни то, ни другое в объём этого CR не входит.
            """";

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

            В отличие от CR по односкатной крыше, здесь появляется новый исходный параметр, поэтому
            правка сквозная — от структуры БД до формы ввода. Ниже — фактическая цепочка,
            прослеженная по коду на примере соседнего атрибута «Уровень верхнего перекрытия»
            (`UpperFloorLevelM`), который проходит ровно те же слои.

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

            | Документ | Правка |
            |---|---|
            | {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Domain.ConsumptionModel.DOC_RoofModel>()}} | новый атрибут «Длина конька, м» в классе сущности — таблица свойств и диаграмма связей строятся по нему |
            | {{SdaidAnchors.RefTo<TeRoofCoverArea>("te.roof_cover_area")}} | правила валидации длины конька — внесены; источник данных для $L_{конька}$ — атрибут сущности ВОК |
            | {{SdaidAnchors.RefTo<TeAtticWallArea>("te.attic_wall_area")}} | правила валидации: перечень типов крыши |
            | {{SdaidAnchors.RefTo<DOC_CalcModel>()}} | блок `roof` модели входных данных: новое поле `ridgeLengthM` — без него параметр не доедет до расчёта |
            | {{SdaidAnchors.RefTo<DOC_TtrCalcInputMapping>()}} | соответствие `Roof.RidgeLength` ← «ВОК. Длина конька, м» в секции `TeInputSection.Roof` |
            | {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Domain.Reference.DOC_RoofType>()}} | состав справочника: записи `hipped` и `pyramidal` загружены из АИС; правок не требует, фиксируется как факт |
            | {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_ModelCardRoofModel>()}} | поле «Длина конька, м» в описании полей вкладки и в макете экрана — видимое только для вальмовой крыши |

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

            - `CmRoofModelEntity` — свойство `RidgeLengthM` (`decimal?`) и сеттер по образцу
              `SetUpperFloorLevelM`; копирование в методе клонирования модели.
            - `CmRoofModelEntityConfiguration` — `builder.Property(...).HasComment(...)`.
            - **Миграция EF** в `Ntr.Infrastructure/Migrations`: колонка `ridge_length_m` типа
              `numeric`, nullable, в таблице `ntr.cm_roof_models`. Данных не переносим: у существующих
              моделей конёк не заполнен.
            - `Infrastructure.Read/NtrModels/CmRoofModel` + конфигурация read-контекста —
              read-модель ведётся отдельно от доменной.

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

            - `RoofModelDetails` (модель ответа) и `RoofModelDetailsDto` в `ConsumptionModelDtos` —
              новое поле.
            - `GetConsumptionModelForEditQueryHandler` — проекция поля в ответ.
            - `CreateConsumptionModelCommandHandler`, `ApplyConsumptionModelEditCommandHandler`,
              `ConsumptionModelService` — запись значения в сущность.
            - `AisToNtrImportService` — **правок не требует**: в выгрузке АИС длины конька нет,
              импорт поле не трогает.
            - Swagger-контракт меняется ⇒ требуется перегенерация клиента фронтенда.

            ### Расчёт

            Цепочка передачи параметра до формулы: атрибут сущности ВОК → колонка БД → DTO ответа
            и команды → `InputModel.roof.ridgeLengthM` → `RoofTeInput.RidgeLength` → калькулятор
            секции `te.roof_cover_area`. Разрыв в любом звене оставит расчёт с пустым коньком,
            то есть с ошибкой валидации.

            - `RoofTeInput` — поле `RidgeLength`; сборка входной модели в `CalcInputModelFactory`.
            - `CalcInputModelFactory` — проброс длины конька и сопоставление кодов `hipped`
              и `pyramidal` с типами расчёта.
            - `RoofType` — новые значения перечисления.
            - `RoofCoverAreaCalculator` — ветки `hipped_rect` и `pyramidal_rect`;
              `AtticWallAreaCalculator` — обе ветки дают ноль.
            - Валидаторы обеих секций — допуск новых типов крыши и правило по $L_{конька}$.

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

            - `roof.util.ts` — контрол, заполнение при загрузке модели, дескриптор поля формы,
              сборка payload. Поле показывается только для четырёхскатной крыши — по образцу
              полей, зависящих от «Наличия чердака».
            - Сгенерированный клиент `roofModelDetails.ts` — после перегенерации по swagger.
            - Отображение атрибута в карточке модели в режиме просмотра.

            ### Тесты

            - Контрольные примеры на вальмовую и шатровую крышу в `src/Ntr.Calc.Tests`
              (значения — OQ-1).
            """";

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

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

            Шаги 2–5 разумно вести снизу вверх (домен → API → UI), чтобы контракт менялся один раз.
            """";

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

            - **AC-1.** Контрольные примеры на вальмовую и шатровую крышу в `src/Ntr.Calc.Tests`
              проходят: расчёт завершается со статусом `Success`, значения $A_{НП.Черд}$, $A_{ПК}$
              и $A_{ВОК}$ совпадают с контрольными.
            - **AC-2.** В карточке модели на вкладке верхних ограждающих конструкций можно ввести
              длину конька; значение сохраняется и после перезагрузки страницы отображается прежним.
            - **AC-3.** Модель потребления с четырёхскатной крышей рассчитывается из интерфейса
              и не падает с ошибкой «Необрабатываемый тип крыши».
            - **AC-4.** В журнале расчёта виден выбранный вариант методики — `hipped_rect`
              или `pyramidal_rect`.
            - **AC-5.** Повторный импорт данных из АИС ОИА ПЭО не затирает введённую длину конька.
            - **AC-6.** Поведение моделей с плоской, двускатной и односкатной крышей не изменилось:
              контрольные примеры 1–4 продолжают проходить.
            """";

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

            —
            """";
    }
}