Compare commits

...

2 Commits

Author SHA1 Message Date
Raykov-MS
bf3d3f12e9 Restore column_registry test fix lost when parser was reset
test_column_registry.py expected the own_account_cash/foreign_card boundary
at column 97, but mapping_table.json (Fix Errors) already moved it to 96
(Код территории belongs to foreign_card, not own_account_cash). The test fix
for this had landed in a later commit that got reset off this branch during
an unrelated branch migration; reapplying it here.
2026-09-18 11:22:04 +03:00
Raykov-MS
6cc68862c1 Fix column 99: assemble foreign card address from structured components
The АдрМестаПриемаВыдача block under СведенияКартаИнБанк (code 1009) has no
single АдресСтрока tag, unlike the same-named block under
СведенияВнесениеПолучениеНалДС — column 99 was mapped directly onto that
nonexistent tag and stayed empty. It now assembles the value from the
individual address components (Индекс, КодОКСМ, КодСубъектаПоОКАТО, Район,
Пункт, Улица, Дом, Корп, Оф), the same way column 153 already does for
registration place. Also disabled allow_short_lookup for this column, since
it was previously able to pick up a same-named tag (e.g. Улица) from an
unrelated address elsewhere in the file.

Added regression tests at both the mapping level and a full parse_xml_content
-> build_fixed_row_by_index pipeline level with a synthetic code-1009 XML.
2026-09-18 11:20:07 +03:00
6 changed files with 137 additions and 7 deletions

View File

@ -47,6 +47,7 @@ COL_TRANSFER_STATUS = _index(62)
# Карты и место операции.
COL_TERRITORY_CODE_CARD = _index(96)
COL_FOREIGN_CARD_ADDRESS_STRUCTURED = _index(99)
COL_CARD_HOLDER_INFO = _index(103)
COL_EMPLOYEE_SIGN = _index(104)
COL_FOREIGN_BANK_NAME = _index(105)

View File

@ -31,6 +31,7 @@ from .column_constants import (
COL_EXTRA_INFO,
COL_FILE_NAME,
COL_FOREIGN_BANK_NAME,
COL_FOREIGN_CARD_ADDRESS_STRUCTURED,
COL_FTR_SIGN,
COL_IDENT_FL,
COL_INN,
@ -290,6 +291,32 @@ def _build_rules(mapping_data: tuple[ColumnMeta, ...]) -> tuple[MappingRule, ...
candidates = [
f"{base_path}.{tag}" for base_path in base_paths for tag in address_tags
]
elif index == COL_FOREIGN_CARD_ADDRESS_STRUCTURED:
# legacy_099: адрес места приёма/выдачи по карте иностранного
# банка (код 1009) приходит в XML разложенным по тем же тегам
# АдрМестаПриемаВыдача, что и другие адресные блоки (см.
# COL_REG_PLACE_STRUCTURED выше) — АдресСтрока как единого поля
# для этого блока в схеме нет, поэтому колонку нужно собирать
# из компонентов, а не мапить напрямую на несуществующий тег.
base_paths = (
"СведенияКартаИнБанк.СведМестоОперация.АдрМестаПриемаВыдача",
"СообщОперКО.ИнформЧасть.СведКО.Операция.СведенияКартаИнБанк."
"СведМестоОперация.АдрМестаПриемаВыдача",
)
address_tags = (
"Индекс",
"КодОКСМ",
"КодСубъектаПоОКАТО",
"Район",
"Пункт",
"Улица",
"Дом",
"Корп",
"Оф",
)
candidates = [
f"{base_path}.{tag}" for base_path in base_paths for tag in address_tags
]
elif index == COL_CP_ISSUER_INN:
candidates = [
candidate

View File

@ -1965,10 +1965,10 @@
"column_name": "Адрес места приема (выдачи) наличных денежных средств (по структуре полный)",
"block": "foreign_card",
"report_group": "foreign_card_operation",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияКартаИнБанк/СведМестоОперация/АдрМестаПриемаВыдача/АдресСтрока",
"xml_tag": "Индекс|КодОКСМ|КодСубъектаПоОКАТО|Район|Пункт|Улица|Дом|Корп|Оф",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияКартаИнБанк/СведМестоОперация/АдрМестаПриемаВыдача/",
"source_scope": "any",
"allow_short_lookup": true,
"allow_short_lookup": false,
"allow_direct_mapping": true,
"structured_value": true,
"structured_group": "",
@ -1977,7 +1977,7 @@
"join_with_space": false,
"multiplies": "нет",
"required": "необязательно",
"comment": "текстовый показатель, поле может остаться пустым и не передаваться в XML"
"comment": "У блока АдрМестаПриемаВыдача в СведенияКартаИнБанк нет единого тега АдресСтрока (в отличие от одноимённого блока в СведенияВнесениеПолучениеНалДС) — значение собирается из компонентов адреса (см. COL_FOREIGN_CARD_ADDRESS_STRUCTURED в mapping.py), поле может остаться пустым"
},
{
"index": 100,

View File

@ -28,7 +28,7 @@ def test_registry_covers_all_report_columns_without_gaps() -> None:
def test_registry_assigns_card_columns_missing_from_original_plan() -> None:
assert COLUMNS.block("foreign_card") == tuple(range(97, 107))
assert COLUMNS.block("foreign_card") == tuple(range(96, 107))
def test_registry_explicitly_preserves_structured_lookup_compatibility() -> None:
@ -75,14 +75,14 @@ def test_registry_splits_money_and_participant_header_groups() -> None:
(
"own_account_cash",
79,
96,
95,
"Сведения о внесении наличных денежных средств на свой банковский счет "
"или о получении наличных денежных средств со своего банковского счета "
"у одного оператора по переводу денежных средств",
),
(
"foreign_card_operation",
97,
96,
106,
"Сведения об операции с использованием платежной карты иностранного банка",
),

View File

@ -1720,6 +1720,48 @@ def test_col99_foreign_card_address_header_renamed() -> None:
)
def test_col99_foreign_card_address_assembled_from_structured_components() -> None:
"""Регрессия: у блока АдрМестаПриемаВыдача в СведенияКартаИнБанк (код
1009) нет единого тега АдресСтрока, в отличие от одноимённого блока в
СведенияВнесениеПолучениеНалДС — колонка 99 раньше мапилась напрямую на
несуществующий АдресСтрока и оставалась пустой. Значение должно
собираться из компонентов адреса, разложенных в XML по структуре."""
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"КодОперации": "1009",
"СведенияКартаИнБанк.СведМестоОперация.АдрМестаПриемаВыдача.КодОКСМ": "840",
"СведенияКартаИнБанк.СведМестоОперация.АдрМестаПриемаВыдача.Пункт": "Нью-Йорк",
"СведенияКартаИнБанк.СведМестоОперация.АдрМестаПриемаВыдача.Улица": "Уолл-стрит",
"СведенияКартаИнБанк.СведМестоОперация.АдрМестаПриемаВыдача.Дом": "11",
},
participant_fields={},
)
assert row[99] == "840, Нью-Йорк, Уолл-стрит, 11"
def test_col99_foreign_card_address_not_leaked_from_participant_address() -> None:
"""Регрессия: allow_short_lookup для колонки 99 должен быть выключен —
иначе адрес участника операции (другой блок, тот же тег 'Улица') может
случайно попасть в адрес места приёма/выдачи по карте ин. банка."""
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"КодОперации": "1009",
},
participant_fields={
"УчастникЮЛ.СведЮЛ.АдрРегЮЛ.Улица": "Совсем другая улица",
},
)
assert row[99] == ""
def test_col96_territory_code_card_belongs_to_foreign_card_group() -> None:
"""Регрессия: колонка 96 раньше входила в группу заголовков блока
'своих' кассовых операций (cash_operation/own_account_cash), из-за чего

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import xml.etree.ElementTree as ET
from app.pipeline.mapping import build_fixed_row_by_index
from app.pipeline.parser import _extract_direct_fields, parse_xml_content
@ -358,6 +359,65 @@ def test_mixed_operation_and_eio_repeats_are_additive_not_cartesian() -> None:
assert result.rows[0].operation_fields["ОснованиеОп.НомДок"] == "BASE-1; BASE-2"
def test_1009_foreign_card_address_survives_full_pipeline() -> None:
"""Сквозная проверка на синтетическом файле: реальный XML -> parse_xml_content
-> build_fixed_row_by_index. Адрес места приёма/выдачи по карте
иностранного банка (код 1009) должен собраться из компонентов
АдрМестаПриемаВыдача в колонку 99, а не потеряться и не подхватить
чужой адрес участника операции (тот же тег 'Улица' в другом блоке)."""
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP1</ИдентификаторЗаписи>
<КодОперации>1009</КодОперации>
<СведенияКартаИнБанк>
<НомерКарта>5536********1234</НомерКарта>
<НаимИнБанк>Bank of Testland</НаимИнБанк>
<СведМестоОперация>
<ИдТерминал>ATM00042</ИдТерминал>
<НаимКО>First Test Bank</НаимКО>
<АдрМестаПриемаВыдача>
<Индекс>10001</Индекс>
<КодОКСМ>840</КодОКСМ>
<Пункт>Нью-Йорк</Пункт>
<Улица>Уолл-стрит</Улица>
<Дом>11</Дом>
</АдрМестаПриемаВыдача>
</СведМестоОперация>
</СведенияКартаИнБанк>
<УчастникОп>
<Тип>01</Тип>
<УчастникЮЛ>
<СведЮЛ>
<АдрРегЮЛ>
<Улица>Совсем другая улица участника</Улица>
</АдрРегЮЛ>
</СведЮЛ>
</УчастникЮЛ>
</УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert not result.is_fatal
assert len(result.rows) == 1
row = result.rows[0]
fixed_row = build_fixed_row_by_index(
file_name="SKO115FZ_01_123456789_20260616_X00001.xml",
record_id=row.record_id,
operation_index=1,
operation_fields=row.operation_fields,
participant_fields=row.participant_fields,
)
assert fixed_row[97] == "ATM00042"
assert fixed_row[98] == "840"
assert fixed_row[99] == "10001, 840, Нью-Йорк, Уолл-стрит, 11"
assert fixed_row[100] == "First Test Bank"
def test_repeated_scalar_leaves_are_joined_without_continuation_rows() -> None:
operation = ET.fromstring(
"""