fix(svod2): серверный поиск совпадает с экраном — пустое поле contract_detail не затирает contract_summary (R-30, решение пользователя 08.11)
В v3.svod2_lines counterparty/reference/deadline/comment берутся как COALESCE(detail, summary). Оракулы (Python, JS, векторы) отбрасывают null при слиянии, legacy_full() снова читает ответ без параметров. Тесты: пустое поле detail, отсутствие строки detail, перекрытие непустым detail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7d71001b7c
commit
5db0bd4cee
@ -298,6 +298,8 @@ $function$;
|
|||||||
-- у contract_summary остаются поля, которых нет у contract_detail, а counterparty, reference,
|
-- у contract_summary остаются поля, которых нет у contract_detail, а counterparty, reference,
|
||||||
-- deadline и comment берутся только из contract_detail (отсутствие строки contract_detail
|
-- deadline и comment берутся только из contract_detail (отсутствие строки contract_detail
|
||||||
-- затирает их у contract_summary значением null). Состав колонок сверяет тест.
|
-- затирает их у contract_summary значением null). Состав колонок сверяет тест.
|
||||||
|
-- Поправка R-30 (решение пользователя 08.11): экран получает data без null-полей (AURORA-1848), поэтому
|
||||||
|
-- пустое поле contract_detail НЕ затирает contract_summary: берётся COALESCE(detail, summary).
|
||||||
CREATE OR REPLACE FUNCTION v3.svod2_lines(
|
CREATE OR REPLACE FUNCTION v3.svod2_lines(
|
||||||
p_sheet varchar,
|
p_sheet varchar,
|
||||||
p_year integer,
|
p_year integer,
|
||||||
@ -467,9 +469,13 @@ WHERE q.s IS NULL OR EXISTS (
|
|||||||
el.form_id::text, el.org_unit_id::text]
|
el.form_id::text, el.org_unit_id::text]
|
||||||
END AS ints,
|
END AS ints,
|
||||||
CASE WHEN has_contract.v
|
CASE WHEN has_contract.v
|
||||||
THEN ARRAY[cs.centralized_flag, cd.counterparty, cd.reference, cd.addenda, cd.subject,
|
-- counterparty, reference, deadline, comment есть в обеих секциях; экран получает data без
|
||||||
cd.currency, cd.rf_schedule, cd.vat_rate, cd.deadline, cd.payment_scheme,
|
-- null-полей, поэтому пустое поле contract_detail не затирает contract_summary (R-30).
|
||||||
cd.act, cd.comment, to_jsonb(cd.contract_date) #>> '{}']::text[]
|
THEN ARRAY[cs.centralized_flag, COALESCE(cd.counterparty, cs.counterparty),
|
||||||
|
COALESCE(cd.reference, cs.reference), cd.addenda, cd.subject,
|
||||||
|
cd.currency, cd.rf_schedule, cd.vat_rate, COALESCE(cd.deadline, cs.deadline),
|
||||||
|
cd.payment_scheme, cd.act, COALESCE(cd.comment, cs.comment),
|
||||||
|
to_jsonb(cd.contract_date) #>> '{}']::text[]
|
||||||
END AS rest_s,
|
END AS rest_s,
|
||||||
CASE WHEN has_contract.v AND q.num_ok
|
CASE WHEN has_contract.v AND q.num_ok
|
||||||
THEN ARRAY[cs.total_amount, cs.future_payments_y1, cs.future_payments_y2, cs.other_ssp_amount,
|
THEN ARRAY[cs.total_amount, cs.future_payments_y1, cs.future_payments_y2, cs.other_ssp_amount,
|
||||||
|
|||||||
15
api/tests/fixtures/svod2_screen_vectors.json
vendored
15
api/tests/fixtures/svod2_screen_vectors.json
vendored
@ -360,32 +360,41 @@
|
|||||||
"search": "ромаш",
|
"search": "ромаш",
|
||||||
"filters": {},
|
"filters": {},
|
||||||
"line_ids": [
|
"line_ids": [
|
||||||
|
1,
|
||||||
2,
|
2,
|
||||||
9
|
9
|
||||||
],
|
],
|
||||||
"found": {
|
"found": {
|
||||||
"found": 2,
|
"found": 3,
|
||||||
"total": 11
|
"total": 11
|
||||||
},
|
},
|
||||||
"facets": {
|
"facets": {
|
||||||
"total": 11,
|
"total": 11,
|
||||||
"search_total": 2,
|
"search_total": 3,
|
||||||
"smeta_type": [
|
"smeta_type": [
|
||||||
[
|
[
|
||||||
"АХР",
|
"АХР",
|
||||||
2
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"КВ",
|
||||||
|
1
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"smeta_direction": [
|
"smeta_direction": [
|
||||||
[
|
[
|
||||||
"Поддержка",
|
"Поддержка",
|
||||||
2
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Развитие",
|
||||||
|
1
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"org_name": [
|
"org_name": [
|
||||||
[
|
[
|
||||||
"Филиал Ёлки",
|
"Филиал Ёлки",
|
||||||
1
|
2
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Центр",
|
"Центр",
|
||||||
|
|||||||
@ -118,30 +118,18 @@ SVOD2_YEAR = 2031
|
|||||||
SVOD2_SEED_PATH = os.path.join(os.path.dirname(__file__), "..", "sqls", "svod2_seed.sql")
|
SVOD2_SEED_PATH = os.path.join(os.path.dirname(__file__), "..", "sqls", "svod2_seed.sql")
|
||||||
|
|
||||||
|
|
||||||
def legacy_full(client, sheet: str, headers: dict = None, org_ids=None) -> list[dict]:
|
def legacy_full(client, sheet: str, headers: dict, org_ids=None) -> list[dict]:
|
||||||
"""Прежний полный ответ свода 2 без параметров нового режима (оракульная база).
|
"""Ответ свода 2 без параметров нового режима - то, что получает экран (оракульная база).
|
||||||
|
|
||||||
Строится прямым вызовом функции представления, а не через GET без параметров: после AURORA-1848
|
После AURORA-1848 data в нём без null-полей: оракул экрана «до» работает именно с такими строками
|
||||||
этот GET отдаёт data без null-полей (jsonb_strip_nulls), а оракул экрана «до» работает с полным
|
(решение пользователя 08.11, R-30: поиск совпадает с тем, что видно на экране).
|
||||||
data - в нём пустая секция contract_detail затирает поля contract_summary значением null.
|
|
||||||
Запрос идёт от имени администратора, headers оставлен для совместимости вызовов.
|
|
||||||
"""
|
"""
|
||||||
function_name = {"GO": "v3.v_svod2_go", "RF": "v3.v_svod2_rf"}[sheet]
|
url = f"/api/v1/svod/2/{sheet}/{SVOD2_YEAR}"
|
||||||
|
if org_ids is not None:
|
||||||
async def _fetch():
|
url += f"?org_ids={org_ids}"
|
||||||
result = await client.db_session.execute(
|
response = client.get(url, headers=headers)
|
||||||
text(
|
assert response.status_code == 200, response.text
|
||||||
f"SELECT sort_order, data FROM {function_name}("
|
return response.json()["result"]
|
||||||
"CAST(:year AS INT), CAST(:org_ids AS INT[]), NULL, NULL, 'org_name', 'asc')"
|
|
||||||
),
|
|
||||||
{"year": SVOD2_YEAR, "org_ids": org_ids},
|
|
||||||
)
|
|
||||||
return [
|
|
||||||
{"row_type": "INPUT", "depth": 3, "sort_order": sort_order, "data": data}
|
|
||||||
for sort_order, data in result.fetchall()
|
|
||||||
]
|
|
||||||
|
|
||||||
return client.portal.call(_fetch)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@ -39,6 +39,14 @@ def _url(sheet: str, suffix: str = "") -> str:
|
|||||||
return f"/api/v1/svod/2/{sheet}/{SVOD2_YEAR}{suffix}"
|
return f"/api/v1/svod/2/{sheet}/{SVOD2_YEAR}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_nulls(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {k: _strip_nulls(v) for k, v in value.items() if v is not None}
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [_strip_nulls(v) for v in value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _line_ids(rows: list[dict]) -> list[int]:
|
def _line_ids(rows: list[dict]) -> list[int]:
|
||||||
return [r["data"]["line_id"] for r in rows]
|
return [r["data"]["line_id"] for r in rows]
|
||||||
|
|
||||||
@ -127,7 +135,8 @@ def test_view_full_rows_equal_legacy_rows(client, svod2_data, headers):
|
|||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
body = r.json()
|
body = r.json()
|
||||||
assert "total" in body
|
assert "total" in body
|
||||||
assert body["result"] == full[:50]
|
# Режим страниц отдаёт data целиком (с null), ответ без параметров - без null-полей (AURORA-1848).
|
||||||
|
assert _strip_nulls(body["result"]) == full[:50]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("sheet", SHEETS)
|
@pytest.mark.parametrize("sheet", SHEETS)
|
||||||
@ -141,33 +150,53 @@ def test_search_matches_screen_oracle(client, svod2_data, headers, sheet, search
|
|||||||
assert all(t == len(expected) for t in totals)
|
assert all(t == len(expected) for t in totals)
|
||||||
|
|
||||||
|
|
||||||
# Строка 910004 имеет contract_summary (ООО Лютик, ДС-2, 70), но НЕ имеет contract_detail:
|
# Решение пользователя 08.11 (R-30): серверный поиск совпадает с тем, что видно на экране. Пустое (null)
|
||||||
# пустая секция contract_detail перекрывает counterparty/reference из contract_summary
|
# поле contract_detail не затирает такое же поле contract_summary: экран получает data без null-полей,
|
||||||
# (семантика экрана), а неперекрытое поле total_amount остаётся доступным для поиска.
|
# и в {...header, ...contract_summary, ...contract_detail} побеждает последнее НЕпустое значение.
|
||||||
|
# Строка 910004 имеет contract_summary (ООО Лютик, ДС-2, 70), но НЕ имеет contract_detail.
|
||||||
LINE_NO_DETAIL = 910004
|
LINE_NO_DETAIL = 910004
|
||||||
|
# Строка 910001: contract_summary (ООО Ромашка, ДС-1), в contract_detail counterparty = NULL, reference = Д-1.
|
||||||
|
LINE_EMPTY_DETAIL_FIELD = 910001
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("sheet", SHEETS)
|
@pytest.mark.parametrize("sheet", SHEETS)
|
||||||
@pytest.mark.parametrize("search", ["ооо лютик", "дс-2"])
|
@pytest.mark.parametrize("search", ["ооо лютик", "дс-2", "70"])
|
||||||
def test_search_overlapped_contract_summary_field_not_found(
|
def test_search_summary_value_found_when_detail_row_missing(
|
||||||
client, svod2_data, headers, sheet, search
|
client, svod2_data, headers, sheet, search
|
||||||
):
|
):
|
||||||
full = legacy_full(client, sheet, headers)
|
full = legacy_full(client, sheet, headers)
|
||||||
expected = screen_rows(full, search)
|
expected = screen_rows(full, search)
|
||||||
assert LINE_NO_DETAIL not in _line_ids(expected)
|
|
||||||
rows, _ = _all_pages(client, sheet, headers, 3, view="compact", search=search)
|
rows, _ = _all_pages(client, sheet, headers, 3, view="compact", search=search)
|
||||||
assert LINE_NO_DETAIL not in _line_ids(rows)
|
assert _line_ids(rows) == _line_ids(expected)
|
||||||
|
if sheet == "GO":
|
||||||
|
assert LINE_NO_DETAIL in _line_ids(rows)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("sheet", ["GO"])
|
@pytest.mark.parametrize("search,found", [("ооо ромашка", True), ("дс-1", False), ("д-1", True)])
|
||||||
def test_search_non_overlapped_contract_summary_field_found(
|
def test_search_summary_value_found_when_detail_field_is_empty(
|
||||||
client, svod2_data, headers, sheet
|
client, svod2_data, headers, search, found
|
||||||
):
|
):
|
||||||
full = legacy_full(client, sheet, headers)
|
"""detail.counterparty = NULL, summary.counterparty = «ООО Ромашка»: строка находится по значению summary.
|
||||||
expected = screen_rows(full, "70")
|
|
||||||
assert LINE_NO_DETAIL in _line_ids(expected)
|
reference у detail непустой (Д-1) и перекрывает summary (ДС-1): по «дс-1» строка не находится.
|
||||||
rows, _ = _all_pages(client, sheet, headers, 3, view="compact", search="70")
|
"""
|
||||||
assert LINE_NO_DETAIL in _line_ids(rows)
|
full = legacy_full(client, "GO", headers)
|
||||||
|
assert (LINE_EMPTY_DETAIL_FIELD in _line_ids(screen_rows(full, search))) is found
|
||||||
|
rows, _ = _all_pages(client, "GO", headers, 3, view="compact", search=search)
|
||||||
|
assert (LINE_EMPTY_DETAIL_FIELD in _line_ids(rows)) is found
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_detail_value_overrides_summary_value(client, svod2_data, svod2_sql, headers):
|
||||||
|
"""Непустое поле contract_detail по-прежнему перекрывает contract_summary: старое значение не находится."""
|
||||||
|
svod2_sql(
|
||||||
|
"INSERT INTO v3.contract_summary (line_id, counterparty, reference) "
|
||||||
|
"VALUES (910002, 'Старый контрагент', 'Старая ссылка')"
|
||||||
|
)
|
||||||
|
for search, found in (("старый контрагент", False), ("ромашка плюс", True), ("старая ссылка", False)):
|
||||||
|
full = legacy_full(client, "GO", headers)
|
||||||
|
assert (910002 in _line_ids(screen_rows(full, search))) is found
|
||||||
|
rows, _ = _all_pages(client, "GO", headers, 3, view="compact", search=search)
|
||||||
|
assert (910002 in _line_ids(rows)) is found, search
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("sheet", SHEETS)
|
@pytest.mark.parametrize("sheet", SHEETS)
|
||||||
|
|||||||
@ -136,7 +136,7 @@ def test_facets_search_subplan_is_not_correlated(svod2_sql):
|
|||||||
|
|
||||||
# --- Поиск в svod2_lines (ускорение AURORA-1843): результат равен прежнему поиску по jsonb ---
|
# --- Поиск в svod2_lines (ускорение AURORA-1843): результат равен прежнему поиску по jsonb ---
|
||||||
|
|
||||||
# Прежний поиск дословно: значения header + contract_summary + contract_detail через jsonb_each,
|
# Поиск экрана: data без null-полей (AURORA-1848, R-30), значения значения header + contract_summary + contract_detail через jsonb_each,
|
||||||
# регистр svod2_casefold по каждому значению, числа через svod2_js_number_text.
|
# регистр svod2_casefold по каждому значению, числа через svod2_js_number_text.
|
||||||
REFERENCE_SEARCH_SQL = """
|
REFERENCE_SEARCH_SQL = """
|
||||||
SELECT l.line_id
|
SELECT l.line_id
|
||||||
@ -176,7 +176,8 @@ WHERE EXISTS (
|
|||||||
ELSE COALESCE(to_jsonb(cd) - 'line_id', e.cd0)
|
ELSE COALESCE(to_jsonb(cd) - 'line_id', e.cd0)
|
||||||
END AS cd_j
|
END AS cd_j
|
||||||
) s
|
) s
|
||||||
CROSS JOIN LATERAL jsonb_each(s.header_j || s.cs_j || s.cd_j) kv
|
CROSS JOIN LATERAL jsonb_each(
|
||||||
|
jsonb_strip_nulls(s.header_j) || jsonb_strip_nulls(s.cs_j) || jsonb_strip_nulls(s.cd_j)) kv
|
||||||
WHERE bl.id = l.line_id
|
WHERE bl.id = l.line_id
|
||||||
AND strpos(
|
AND strpos(
|
||||||
v3.svod2_casefold(
|
v3.svod2_casefold(
|
||||||
|
|||||||
@ -70,10 +70,15 @@ def js_string(value: object) -> str:
|
|||||||
|
|
||||||
def matches_search(row: dict, query: str) -> bool:
|
def matches_search(row: dict, query: str) -> bool:
|
||||||
data = row.get("data") or {}
|
data = row.get("data") or {}
|
||||||
|
# Экран получает data без null-полей (AURORA-1848): пустое поле contract_detail не затирает
|
||||||
|
# contract_summary (решение пользователя 08.11, R-30). Null отбрасываются, как на сервере.
|
||||||
|
def present(section):
|
||||||
|
return {k: v for k, v in (section or {}).items() if v is not None}
|
||||||
|
|
||||||
merged = {
|
merged = {
|
||||||
**(data.get("header") or {}),
|
**present(data.get("header")),
|
||||||
**(data.get("contract_summary") or {}),
|
**present(data.get("contract_summary")),
|
||||||
**(data.get("contract_detail") or {}),
|
**present(data.get("contract_detail")),
|
||||||
}
|
}
|
||||||
return any(query in js_string(v).lower() for v in merged.values())
|
return any(query in js_string(v).lower() for v in merged.values())
|
||||||
|
|
||||||
|
|||||||
@ -700,9 +700,11 @@ PR создаются по готовности, merge — по команде
|
|||||||
Цена ошибки: форма `data` в `view=full` и в прежнем ответе различается только null-полями; фронт PR 2 использует compact. Исправление - `jsonb_strip_nulls` и в full.
|
Цена ошибки: форма `data` в `view=full` и в прежнем ответе различается только null-полями; фронт PR 2 использует compact. Исправление - `jsonb_strip_nulls` и в full.
|
||||||
46. **2026-11-08 (R-26) — эталон `svod2_legacy_golden.json` перезаписан на ответе test-кода (без null-полей, порядок ключей `success, message, result, count`).** Содержимое сверено с прежним эталоном после удаления null-полей: совпало для всех 10 случаев. Перезапись сделана на БД до миграции 0057, затем проверка пройдена с 0057.
|
46. **2026-11-08 (R-26) — эталон `svod2_legacy_golden.json` перезаписан на ответе test-кода (без null-полей, порядок ключей `success, message, result, count`).** Содержимое сверено с прежним эталоном после удаления null-полей: совпало для всех 10 случаев. Перезапись сделана на БД до миграции 0057, затем проверка пройдена с 0057.
|
||||||
Цена ошибки: эталон перестаёт ловить регрессию, если перезаписан на сломанном коде; снижено сверкой с прежним эталоном.
|
Цена ошибки: эталон перестаёт ловить регрессию, если перезаписан на сломанном коде; снижено сверкой с прежним эталоном.
|
||||||
47. **2026-11-08 (R-27) — `legacy_full` в тестах оракулов строится прямым вызовом `v_svod2_go/rf` (с null-полями), а не GET без параметров.** После AURORA-1848 GET отдаёт `data` без null-полей, и пустая секция `contract_detail` перестаёт затирать поля `contract_summary` значением null, как это делал экран «до» и как реализован серверный поиск.
|
47. **2026-11-08 (R-27, заменено R-30) — `legacy_full` в тестах оракулов строился прямым вызовом `v_svod2_go/rf` (с null-полями), а не GET без параметров.** После AURORA-1848 GET отдаёт `data` без null-полей, и пустая секция `contract_detail` перестаёт затирать поля `contract_summary` значением null, как это делал экран «до» и как реализован серверный поиск.
|
||||||
Цена ошибки: семантика поиска «пустой contract_detail затирает contract_summary» остаётся от экрана «до», живой экран test (на данных без null) её уже не воспроизводит; решение за человеком (deferred).
|
Цена ошибки: семантика поиска «пустой contract_detail затирает contract_summary» остаётся от экрана «до», живой экран test (на данных без null) её уже не воспроизводит; решение за человеком (deferred).
|
||||||
48. **2026-11-08 (R-28) — `svod2_paging_check.py` перед сверкой склейки `view=full` с прежним ответом убирает null-поля.** Иначе проверка всегда расходилась бы с ответом без null-полей.
|
48. **2026-11-08 (R-28) — `svod2_paging_check.py` перед сверкой склейки `view=full` с прежним ответом убирает null-поля.** Иначе проверка всегда расходилась бы с ответом без null-полей.
|
||||||
Цена ошибки: проверка не видит расхождений только в null-полях.
|
Цена ошибки: проверка не видит расхождений только в null-полях.
|
||||||
49. **2026-11-08 (R-29) — на фронте (PR 2) измерение высоты строки `offsetHeight` заменено фиксированной высотой `2.75rem` (AURORA-1836); px для окна страниц считается из `getRootFontSizePx()`, заглушки получают высоту в rem.**
|
49. **2026-11-08 (R-29) — на фронте (PR 2) измерение высоты строки `offsetHeight` заменено фиксированной высотой `2.75rem` (AURORA-1836); px для окна страниц считается из `getRootFontSizePx()`, заглушки получают высоту в rem.**
|
||||||
Цена ошибки: если реальная высота строки с данными отличается от 2.75rem (длинный перенос), окно страниц рассчитается неточно; в test строки так же имеют фиксированную высоту.
|
Цена ошибки: если реальная высота строки с данными отличается от 2.75rem (длинный перенос), окно страниц рассчитается неточно; в test строки так же имеют фиксированную высоту.
|
||||||
|
50. **2026-11-08 (R-30, решение пользователя 08.11; в поручении названо R-10, номер занят) — серверный поиск совпадает с тем, что показывает экран: пустое (null) поле `contract_detail` НЕ затирает такое же поле `contract_summary`.** Экран получает `data` без null-полей (AURORA-1848), и в `{...header, ...contract_summary, ...contract_detail}` побеждает последнее непустое значение. В `v3.svod2_lines` значения `counterparty`, `reference`, `deadline`, `comment` берутся как `COALESCE(cd.x, cs.x)`; оракулы (`svod2_oracle.py`, `svod2_search_oracle.mjs`, векторы `svod2_screen_vectors.json`) отбрасывают null при слиянии, `legacy_full()` снова читает ответ без параметров. Новые тесты: строка 910001 (`contract_detail.counterparty = NULL`, у `contract_summary` «ООО Ромашка») находится по «ооо ромашка», по «дс-1» не находится (непустой `reference` detail перекрывает); строка без `contract_detail` находится по значениям summary; непустое поле detail перекрывает summary.
|
||||||
|
Цена ошибки: если пользователь имел в виду иное, поиск расходится с экраном в этих строках; правка - вернуть `cd.x` вместо `COALESCE` в одном месте SQL и убрать отбрасывание null в оракулах. Замечание: R-25 про `view=full` с null остаётся в силе.
|
||||||
|
|||||||
@ -4,11 +4,16 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
// Экран получает data без null-полей (jsonb_strip_nulls, AURORA-1848), поэтому пустое поле contract_detail
|
||||||
|
// не затирает такое же поле contract_summary (решение пользователя 08.11, R-30). Строки оракула могут
|
||||||
|
// содержать null (полный data режима страниц) - они отбрасываются так же, как это делает сервер.
|
||||||
|
const withoutNulls = (section) => Object.fromEntries(Object.entries(section ?? {}).filter(([, value]) => value !== null));
|
||||||
|
|
||||||
const matchesSearch = (row, query) => {
|
const matchesSearch = (row, query) => {
|
||||||
const searchableData = {
|
const searchableData = {
|
||||||
...row.data?.header,
|
...withoutNulls(row.data?.header),
|
||||||
...row.data?.contract_summary,
|
...withoutNulls(row.data?.contract_summary),
|
||||||
...row.data?.contract_detail,
|
...withoutNulls(row.data?.contract_detail),
|
||||||
};
|
};
|
||||||
return Object.values(searchableData).some((value) =>
|
return Object.values(searchableData).some((value) =>
|
||||||
String(value ?? '')
|
String(value ?? '')
|
||||||
|
|||||||
@ -14,9 +14,14 @@ const mk = (id, type, dir, org) => ({ data: { line_id: id, header: { smeta_type:
|
|||||||
const rows2 = [mk(1, 'КВ', 'Развитие', 'А'), mk(2, 'АХР', 'Поддержка', 'Б')];
|
const rows2 = [mk(1, 'КВ', 'Развитие', 'А'), mk(2, 'АХР', 'Поддержка', 'Б')];
|
||||||
const rows3 = [mk(1, 'КВ', 'Р', 'Филиал Ёлки'), mk(2, 'КВ', 'Р', 'Филиал Ёлки'), mk(3, 'КВ', 'Р', null)];
|
const rows3 = [mk(1, 'КВ', 'Р', 'Филиал Ёлки'), mk(2, 'КВ', 'Р', 'Филиал Ёлки'), mk(3, 'КВ', 'Р', null)];
|
||||||
|
|
||||||
test('перекрытое поле contract_summary не ищется', () => {
|
test('пустое поле contract_detail не затирает contract_summary (R-30)', () => {
|
||||||
const row = { data: { header: { name: 'x' }, contract_summary: { counterparty: 'ООО Ромашка' }, contract_detail: { counterparty: null } } };
|
const row = { data: { header: { name: 'x' }, contract_summary: { counterparty: 'ООО Ромашка' }, contract_detail: { counterparty: null } } };
|
||||||
|
assert.equal(matchesSearch(row, 'ромаш'), true);
|
||||||
|
});
|
||||||
|
test('непустое поле contract_detail перекрывает contract_summary', () => {
|
||||||
|
const row = { data: { header: {}, contract_summary: { counterparty: 'ООО Ромашка' }, contract_detail: { counterparty: 'ИП Ёж' } } };
|
||||||
assert.equal(matchesSearch(row, 'ромаш'), false);
|
assert.equal(matchesSearch(row, 'ромаш'), false);
|
||||||
|
assert.equal(matchesSearch(row, 'ёж'), true);
|
||||||
});
|
});
|
||||||
test('число ищется в записи JS', () => {
|
test('число ищется в записи JS', () => {
|
||||||
const row = { data: { header: {}, contract_summary: {}, contract_detail: JSON.parse('{"ceiling_amount": 1234.50}') } };
|
const row = { data: { header: {}, contract_summary: {}, contract_detail: JSON.parse('{"ceiling_amount": 1234.50}') } };
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user