19 KiB
Purpose
Контракт API свода 2 (/api/v1/svod/2/...): постраничная выдача строк листа GO или RF с общим числом строк, серверные поиск и колоночные фильтры с той же семантикой, что у экрана до изменения, варианты колоночных фильтров, компактное представление строки и совместимость прежнего ответа для выгрузки, тестов и стенда замеров.
ADDED Requirements
Requirement: Прежний ответ без новых параметров
Запрос GET /api/v1/svod/2/{sheet}/{year}, в котором нет ни одного из параметров view, search, smeta_type, smeta_direction, org_name, SHALL возвращать ответ, байт-в-байт совпадающий с ответом до изменения на тех же данных и с теми же org_ids, offset, limit, sort_by, sort_direction. Совпадать должны набор и порядок строк, sort_order, содержимое data со всеми ключами, включая null, и верхний уровень success/message/result/count. Поля total в таком ответе SHALL NOT быть. Excel-выгрузка свода 2 SHALL выдавать то же содержимое, что до изменения. Это условие слияния.
Scenario: Полный ответ «все организации» не изменился
- WHEN на одних и тех же данных стенда выполняются
GET /api/v1/svod/2/GO/{year}иGET /api/v1/svod/2/RF/{year}без параметров до и после изменения (роли admin и dfip, все организации и одна организация,offset=0&limit=50) - THEN тела ответов после распаковки совпадают побайтно (одинаковый sha256)
Scenario: Прежняя пагинация без total
- WHEN выполняется
GET /api/v1/svod/2/GO/{year}?offset=0&limit=2 - THEN ответ содержит
countне больше 2,resultне длиннее 2 строк и не содержит ключаtotal
Scenario: Невалидные прежние параметры
- WHEN передан
offset=-1,limit=0,sort_by=badилиsort_direction=bad - THEN ответ — 422, как до изменения
Requirement: Постраничная выдача с общим числом строк
Если в запросе есть хотя бы один из параметров view, search, smeta_type, smeta_direction, org_name (это «новый режим»), ответ SHALL содержать поле total. total — число строк, удовлетворяющих всем условиям запроса (год, org_ids, поиск, колоночные фильтры), без учёта offset и limit. count SHALL по-прежнему равняться длине result. Строки SHALL идти в том же порядке, что в полном ответе с теми же условиями: sort_order — номер строки в этом порядке, начиная с 1. Страницы с последовательными offset при limit = L SHALL в объединении давать ровно полный список без пропусков и повторов, если данные между запросами не менялись. Если offset не меньше total, result SHALL быть пустым, а total — верным. Это условие слияния.
Scenario: Склейка страниц равна полному списку
- WHEN при неизменных данных запрашиваются страницы
offset = 0, L, 2L, …сlimit = Lиview=full, пока страница не станет корочеL - THEN последовательность
data.line_idпо всем страницам совпадает с последовательностью в ответе безoffset/limitс теми же условиями, аtotalна каждой странице равен длине этого полного списка
Scenario: Страница за концом списка
- WHEN в новом режиме запрашивается
offset = total - THEN
resultпустой,count= 0,totalравен числу строк по условиям запроса
Scenario: Явный view=full включает total
- WHEN выполняется
GET /api/v1/svod/2/RF/{year}?view=full&offset=0&limit=50 - THEN строки в
dataсовпадают с прежним полным представлением тех же строк, а в ответе естьtotal
Requirement: Серверный поиск с семантикой экрана
Параметр search SHALL отбирать ровно те строки, которые до изменения оставлял поиск экрана:
- Запрос нормализуется как
search.trim().toLowerCase()в JavaScript (обрезка пробельных символов по правилам ECMAScriptString.prototype.trim). Если после нормализации строка пустая, выборка SHALL NOT сужаться. - Строка проходит отбор, если нормализованный запрос входит подстрокой хотя бы в одно значение объекта, полученного слиянием
{...data.header, ...data.contract_summary, ...data.contract_detail}(поздние группы перекрывают одноимённые ключи ранних). Поляcontract_summary, одноимённые полямcontract_detail(counterparty,reference,deadline,comment), поэтому SHALL NOT участвовать в поиске. - Каждое значение приводится к тексту как
String(value ?? '').toLowerCase()в JavaScript:null— пустая строка;- строка — как есть;
- число — по правилам
Number.prototype.toStringдля двойной точности, которую экран получил из JSON ответа (например,1234.50→1234.5,100.00→100,1e-7→1e-7).
- Значения берутся такими, какими они приходят в ответе этого листа. Например, на листе RF
contract_detail.expenses_q1..4— это значения ответа (COALESCEдоговора и ЦКК). - Приведение к нижнему регистру SHALL NOT зависеть от локали базы данных. Для латиницы ASCII и кириллицы (включая Ё и буквы U+0400–U+042F, U+0490) результат SHALL совпадать с
toLowerCase().
Поиск сочетается с остальными условиями по «И». Длина search API не ограничивает: любой запрос даёт тот же результат, что поиск экрана (совпадения есть или их нет), без ошибки.
Scenario: Поиск без учёта регистра по кириллице независимо от локали
- WHEN в данных есть строка с
header.name = «Аренда помещения», база работает сLC_CTYPE=Cилиen_US.utf8, и выполняется запрос сsearch=аРЕНДА - THEN в обоих случаях строка есть в выдаче, а
totalравен числу строк, отобранных поиском экрана до изменения
Scenario: Перекрытые поля contract_summary не ищутся
- WHEN у строки
contract_summary.counterparty = «ООО Ромашка», аcontract_detail.counterpartyпустой, и ни одно другое поле не содержит «ромаш» - THEN при
search=ромашстроки нет в выдаче, как и на экране до изменения
Scenario: Поиск по договору
- WHEN у строки
contract_detail.counterparty = «ООО Ромашка»иsearch=ромаш - THEN строка есть в выдаче
Scenario: Числа ищутся в записи JavaScript
- WHEN у строки
contract_detail.ceiling_amount = 1234.50в БД - THEN
search=1234.5находит строку, аsearch=1234.50не находит
Scenario: Очень длинный поиск без ошибки
- WHEN передан
searchдлиной 5000 символов, которого нет ни в одном значении - THEN ответ — 200 с пустым
resultиtotal= 0, как пустой результат поиска на экране
Scenario: Пустой поиск не сужает
- WHEN в новом режиме передан
searchиз одних пробельных символов - THEN
totalравенtotalтого же запроса безsearch
Scenario: Сверка с поиском экрана на данных стенда
- WHEN для GO и RF масштаба 1 по набору запросов (
аренда,АРЕНДА,1,,, подстрока названия организации, подстрока контрагента, дробное число из данных) серверная выдача сравнивается с функциейmatchesSearchэкрана до изменения, применённой в Node.js к прежнему полному ответу - THEN множества и порядок
line_idсовпадают для каждого запроса
Requirement: Колоночные фильтры с семантикой экрана
Параметры smeta_type, smeta_direction и org_name SHALL оставлять строки, у которых соответствующее значение (data.header.smeta_type, data.header.smeta_direction, data.header.org_name) не null и после toLowerCase().trim() равно значению параметра после toLowerCase().trim(). Это семантика фильтра equals таблицы экрана до изменения. Пустое значение параметра SHALL NOT сужать выборку. Длина значений API не ограничивает, ошибки из-за длины нет. Все условия, включая org_ids и search, сочетаются по «И». org_ids, как и раньше, пересекается с организациями, разрешёнными пользователю.
Scenario: Фильтр по виду сметы
- WHEN запрос с
smeta_type=КВ - THEN у всех строк выдачи
data.header.smeta_type = «КВ», аtotalравен числу таких строк в полном ответе с теми же организациями
Scenario: Организации с одинаковым названием
- WHEN у двух организаций одинаковое название и запрос передаёт
org_nameс этим названием - THEN в выдаче строки обеих организаций, как при выборе этого значения в фильтре «ССП» на экране до изменения
Scenario: Комбинация условий
- WHEN запрос с
search=X,smeta_direction=Развитиеиorg_name=<название> - THEN выдача — пересечение трёх условий, её состав совпадает с тем, что экран до изменения показывал при тех же поиске и фильтрах
Requirement: Варианты колоночных фильтров с семантикой экрана
GET /api/v1/svod/2/{sheet}/{year}/facets принимает org_ids, search, smeta_type, smeta_direction, org_name с тем же смыслом, что у страниц. Ответ SHALL содержать:
total— число строк по году иorg_idsбез поиска и фильтров (M в «Найдено N из M»);search_total— число строк по году,org_idsи поиску без колоночных фильтров (N в «Найдено N из M»);- для каждой из колонок
smeta_type,smeta_direction,org_name— пары «значение → число строк». Они считаются по строкам, отобранным годом,org_ids, поиском и фильтрами двух других колонок; фильтр самой колонки не применяется. Значения различаются точным совпадением строк,nullне выдаётся.
Это семантика вариантов (getFacetedUniqueValues) таблицы экрана до изменения, где поиск применялся к данным до таблицы. Неизвестный sheet SHALL давать 404. Это условие слияния.
Scenario: Варианты с учётом поиска и других фильтров
- WHEN задан поиск
Xи фильтрsmeta_type=КВ - THEN варианты
smeta_directionиorg_nameс числами строк посчитаны по строкам, найденным поXс видом сметы «КВ», а вариантыsmeta_type— по строкам, найденным поX, без фильтра вида сметы
Scenario: Варианты совпадают с вычисленными по полному ответу
- WHEN для набора сочетаний поиска и фильтров запрошены
/facetsи прежний полный ответ того же листа, года иorg_ids - THEN
total,search_totalи пары «значение → число» совпадают с вычисленными по полному ответу по правилам выше
Scenario: Неизвестный лист
- WHEN запрошен
/api/v1/svod/2/XX/{year}/facets - THEN ответ — 404
Requirement: Компактное представление строки
При view=compact поле data каждой строки SHALL содержать только поля ниже, со значениями, равными значениям тех же путей в полном представлении:
| группа | поля |
|---|---|
| верхний уровень | line_id |
header |
source_form, source_id, smeta_type, smeta_direction, org_name, name, item_id, num_group, justification |
plan |
plan_q1..4 |
allocation |
internal_order, property_object |
sequestration.DFIP |
adj_q1..4 |
reserve |
amount_q1..4 |
approved |
approved_q1..4 |
contract_detail |
counterparty, reference, subject, currency, ceiling_amount, expenses_q1..4, vat_rate, deadline |
q1–q4 |
adj_current, adj_ssp, adj_rf, adj_reserve, payment_date, payment_amount, payment_amount_ho, payment_amount_rf, payment_act, booking_amount, actual_m1..3 |
только q4 |
actual_spod |
q1–q3 |
transfer_to_q<k> для всех k больше номера квартала, transfer_to_economy |
q2–q4 |
plan_revision_eco_change, plan_revision_item_adj, plan_revision_increase, plan_revision_sequester |
Ключи со значением null SHALL отсутствовать. Нулевые числа SHALL сохраняться. Обёртка строки (row_type, depth, sort_order) SHALL совпадать с полным представлением.
Scenario: Компактная строка — проекция полной
- WHEN одна и та же страница запрошена с
view=fullи сview=compact - THEN строки идут в одном порядке, и для каждой строки компактное
dataравно проекции полногоdataна перечисленные пути без ключей со значениемnull
Scenario: Ноль не теряется
- WHEN в полном представлении
plan.plan_q1 = 0 - THEN в компактном
plan.plan_q1 = 0
Requirement: Права доступа на страницы и варианты
Для не-администратора выдача страниц, total и варианты фильтров SHALL строиться только по организациям, привязанным к пользователю. Запрошенные org_ids вне этого множества SHALL отбрасываться, как в прежнем режиме. Если пересечение пусто, ответ SHALL быть пустым: result пустой, total = 0, search_total = 0, списки вариантов пусты.
Scenario: Чужая организация в выборке
- WHEN исполнитель, привязанный к организации A, запрашивает страницу и
/facetsсorg_ids=<B> - THEN
resultпустой,total= 0, варианты пусты
Requirement: Замер размера и стоимости ответа
Для GO и RF на масштабах 1 и 2 SHALL быть сняты и приведены в PR цифры «до/после». Цифры условием слияния не являются: недобор оформляется follow-up задачей. Что снимается:
- распакованный размер страницы
view=compactи той же страницыview=full; - распакованный размер первой страницы и вариантов против прежнего полного ответа;
- медиана времени прежнего полного запроса.
Целевые значения:
compactне больше 40 %full;- первая страница вместе с вариантами не больше 5 % прежнего полного ответа;
- время прежнего запроса не больше 1,10 значения «до».
Scenario: Замер размеров на стенде
- WHEN
perf/analysis/api_split.py --scale <N>выполняет для GO и RF полный прежний запрос, страницуoffset=0&limit=<размер страницы>сview=fullи сview=compactи запрос/facets - THEN в отчёте PR есть отношения размеров и времён к целевым значениям, а каждый недобор записан как follow-up