# sheet-cell-edit Specification ## Purpose Ответ сервера на правку ячеек листа бюджетной формы (HTTP `PATCH /form/{id}/sheet/{sheet}/cell(s)` и WebSocket `cell_updated`): какие строки в нём приходят, какими они должны быть относительно полного GET листа и сколько времени правка может стоить на сервере. ## Requirements ### Requirement: Ответ на правку содержит затронутые строки целиком Ответ на правку ячеек листов AHR/CAP/OPER форм FORM_1, FORM_2 и FORM_4 SHALL содержать каждую изменённую строку INPUT и все её строки-агрегаты, то есть всех предков по дереву статей до ROOT включительно. Для FORM_4 предками считаются только ROOT раздела, GROUP программы и ITEM проекта изменённой строки. Агрегат SHALL входить в ответ, даже если его значения не изменились. Ответ SHALL NOT содержать строку META и строки других ветвей дерева. Поле `data` каждой строки SHALL быть полной строкой в том же виде, что и в GET листа с теми же `direction` и `sections`: пустые значения передаются явным `null`. Если задан `sections`, ключи верхнего уровня у исключённых разделов SHALL отсутствовать в `data`. #### Scenario: Правка числовой ячейки возвращает строку и её предков - **WHEN** исполнитель меняет `plan.q1` у строки листа FORM_2/AHR - **THEN** ответ содержит эту строку INPUT и по одной строке-агрегату на каждый уровень её пути в дереве статей - **AND** `data` каждой строки ответа равно `data` строки того же `sort_order` из последующего GET листа #### Scenario: Правка нечисловой ячейки всё равно возвращает предков - **WHEN** исполнитель меняет `header.justification` у строки FORM_2/AHR - **THEN** ответ содержит строку INPUT и всех её предков, хотя их суммы не изменились #### Scenario: Очистка значения - **WHEN** исполнитель записывает `null` в `plan.q1` - **THEN** в строке INPUT ответа ключ `plan.q1` присутствует со значением `null` (или 0 — так же, как в GET) - **AND** суммы предков в ответе уменьшены на прежнее значение ячейки #### Scenario: Правка с фильтром разделов - **WHEN** правка выполняется с `sections=plan` - **THEN** ни одна строка ответа не содержит ключей разделов, отличных от `plan`, а также служебных ключей, которых нет в GET с тем же `sections` ### Requirement: sort_order в ответе совпадает с полным листом Значения `row_type`, `depth` и `sort_order` каждой строки ответа на правку SHALL совпадать со значениями этой же строки в полном GET листа с теми же параметрами, выполненном сразу после правки. `sort_order` — сквозной номер строки во всём листе, а не номер внутри ответа. Это SHALL выполняться для первой и последней строки листа, после добавления и удаления строк в листе, а также при совпадении ключа сортировки у строки INPUT и узла дерева. Порядок строк в GET при совпадении ключа SHALL быть детерминированным и одинаковым в GET и в ответе на правку. #### Scenario: Первая и последняя строка листа - **WHEN** правится первая по порядку строка INPUT листа, а затем последняя - **THEN** у каждой строки обоих ответов есть строка с тем же `(row_type, depth, sort_order)` в GET листа, и её `data` совпадает #### Scenario: После добавления и удаления строк - **WHEN** в лист добавлены строки (POST line), одна из строк в середине листа удалена (DELETE line), после чего правится строка, расположенная ниже удалённой - **THEN** `sort_order` каждой строки ответа равен её `sort_order` в GET листа после этих операций #### Scenario: Пакет правок по разным ветвям дерева - **WHEN** через `PATCH /cells` одним запросом правятся строки двух разных ветвей дерева статей - **THEN** ответ содержит обе строки INPUT и объединение их предков без дублей, и каждая строка совпадает со строкой GET того же `sort_order` #### Scenario: Направление FORM_1 - **WHEN** правится строка FORM_1/AHR с `direction=Support` - **THEN** `sort_order` строк ответа равен `sort_order` в GET FORM_1/AHR с `direction=Support` ### Requirement: Состав ответа на листах сателлитов, AHR_LIMIT и OTCH9F На листах FORM_2 AHR_RENT, AHR_UTILITY и AHR_SECURITY ответ на правку SHALL содержать только изменённые строки. На AHR_LIMIT ответ SHALL содержать изменённые строки LEAF шаблона и их предков по шаблону (GROUP и SECTION). На OTCH9F ответ SHALL содержать все строки (все месяцы) того же сочетания статьи и оборудования, что и изменённая строка: остатки на начало месяца зависят от предыдущих месяцев. Для всех этих листов SHALL выполняться требование о совпадении `data` и `sort_order` с полным GET. #### Scenario: Правка строки аренды - **WHEN** исполнитель меняет `plan.q1` у строки AHR_RENT - **THEN** ответ содержит ровно одну строку с `data.id` этой строки, и она совпадает со строкой GET AHR_RENT того же `sort_order` #### Scenario: Правка количества в лимитах - **WHEN** исполнитель меняет `qty_q1` у строки LEAF листа AHR_LIMIT - **THEN** ответ содержит эту строку и строки её группы и раздела с пересчитанными `amount_q1`, и они совпадают с GET AHR_LIMIT #### Scenario: Правка месяца в OTCH9F - **WHEN** исполнитель меняет поступление за месяц у строки OTCH9F - **THEN** ответ содержит строки всех месяцев того же оборудования той же статьи с пересчитанными остатками, и они совпадают с GET OTCH9F ### Requirement: Обновление АХР после правки сателлита После правки или добавления строки на AHR_RENT, AHR_UTILITY, AHR_SECURITY или AHR_LIMIT формы FORM_2 сервер SHALL отправить подключённым к листу AHR событие (`cell_updated` или `row_added`). Событие SHALL содержать синхронизированные строки AHR и всех их предков, включая уровень SUB_SUB_ITEM. Каждая из этих строк SHALL совпадать со строкой того же `sort_order` полного GET листа AHR. Если синхронизированных строк нет, событие SHALL NOT содержать строк AHR, то есть ответ пуст, а не весь лист. Строки AHR_LIMIT SHALL сопоставляться и со статьями AHR глубины 4 по `num_group_id` (поведение миграции 0037). #### Scenario: Правка аренды обновляет АХР - **WHEN** исполнитель меняет `plan.q1` у строки AHR_RENT, связанной со строкой AHR - **THEN** строки, которые сервер готовит для события на лист AHR, совпадают со строками GET AHR того же `sort_order`, и среди них есть синхронизированная строка INPUT с новой суммой #### Scenario: Источник без синхронизированных строк - **WHEN** запрашивается обновление АХР для строки сателлита, у которой нет связанной строки AHR - **THEN** набор строк для события пуст #### Scenario: Лимит на статье глубины 4 - **WHEN** правится строка AHR_LIMIT, код статьи которой совпадает с `num_group_id` статьи AHR глубины 4 - **THEN** набор строк для события содержит синхронизированную строку AHR этой статьи ### Requirement: Пакет правок атомарен Если хотя бы одна правка в пакете отклонена (неизвестная колонка, чужая строка, нет прав, окно закрыто), SHALL NOT применяться ни одна правка пакета, и ответ SHALL быть ошибкой. #### Scenario: Ошибочная колонка в пакете - **WHEN** `PATCH /cells` содержит одну корректную правку и одну с несуществующей колонкой - **THEN** ответ — ошибка 4xx, и GET листа после запроса совпадает с GET до него ### Requirement: GET листа не меняется Содержимое GET листов FORM_1 и FORM_2 (строки, `data`, `sort_order`) после изменения SHALL совпадать с содержимым до него на тех же данных. Исключение — порядок строк с совпадающим ключом сортировки: до изменения он не был определён. Время GET FORM_2/AHR SHALL NOT вырасти больше чем на 10 %. #### Scenario: Снимок GET до и после миграции - **WHEN** на стенде профилирования снят GET всех листов FORM_1 и FORM_2 из манифеста до применения миграции и после неё - **THEN** ответы попарно совпадают #### Scenario: Время GET после смешанной нагрузки - **WHEN** в одном соединении с БД чередуются правки ячеек и GET листа FORM_2/AHR - **THEN** среднее время GET не больше базового + 10 % ### Requirement: Время правки ячейки на FORM_1 и FORM_2 Правка одной ячейки листа FORM_1 или FORM_2 SHALL NOT собирать лист целиком: на правку SHALL приходиться 0 вложенных полных сборок листа. Среднее серверное время `v3.upd_form_cell` на FORM_2/AHR SHALL быть не больше 50 мс как на листе в 1154 строки, так и на листе в 2092 строки. Требование не распространяется на FORM_4 до слияния AURORA-1846. #### Scenario: Соавтор на листе масштаба 1 - **WHEN** на стенде профилирования (масштаб 1, без grow, FORM_2/AHR 1154 строки) выполняется шаг `coauthor` - **THEN** в `sql/coauthor/sql_top.json` `mean_exec_time` вызова `v3.upd_form_cell` ≤ 50 мс - **AND** вызовов `v3.v_form_view` не верхнего уровня (`toplevel = false`) нет, а число вызовов `v3.v_form2_view(p_form_id, p_sheet, p_sections)` равно числу GET листа и не зависит от числа правок #### Scenario: Соавтор на выросшем листе - **WHEN** после `perfkit.gen.grow` (FORM_2/AHR 2092 строки) шаг `coauthor` выполняется повторно - **THEN** `mean_exec_time` вызова `v3.upd_form_cell` ≤ 50 мс ### Requirement: Ручной ввод без очереди Сервер SHALL успевать подтверждать правки одного соединения с частотой ручного ввода (5 правок/с), не накапливая очередь. #### Scenario: Соавтор с частотой 5 правок в секунду - **WHEN** бот соавтора на FORM_2/AHR отправляет 5 правок/с в течение шага `coauthor` - **THEN** число полученных ответов (эхо и ошибки) составляет не меньше 95 % от числа отправленных правок