from __future__ import annotations import xml.etree.ElementTree as ET from app.pipeline.parser import _extract_direct_fields, parse_xml_content def _base_xml(operations: str) -> str: return f""" <СообщОперКО xmlns="urn:test"> <СлужЧасть><ИдФайл>ID1 <ИнформЧасть> <ИнфБанк><БИК>123 <СведКО> {operations} """ def test_parse_xml_with_invalid_name_returns_fatal() -> None: result = parse_xml_content("broken.xml", "<СообщОперКО/>") assert result.is_fatal assert "Некорректное имя XML-файла" in (result.fatal_error or "") def test_parse_xml_with_malformed_xml_returns_fatal() -> None: result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", " None: xml = """<СообщОперКО>""" result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml) assert result.is_fatal assert "ИнформЧасть" in (result.fatal_error or "") def test_parse_xml_without_participants_creates_single_row() -> None: xml = _base_xml( "<Операция><ИдентификаторЗаписи>OP1<СуммаОпер>100" ) result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml) assert not result.is_fatal assert len(result.rows) == 1 assert result.rows[0].record_id == "OP1" def test_parse_xml_with_namespace_and_two_participants() -> None: xml = _base_xml( """ <Операция> <ИдентификаторЗаписи>OP2 <УчастникОп><Тип>01 <УчастникОп><Тип>02 """ ) result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml) assert not result.is_fatal assert len(result.rows) == 2 assert result.rows[0].participant_fields["Тип"] == "01" assert result.rows[1].participant_fields["Тип"] == "02" def test_parse_xml_operation_error_marks_partial(monkeypatch) -> None: xml = _base_xml( "<Операция><ИдентификаторЗаписи>OP3<УчастникОп><Тип>01" ) from app.pipeline import parser original = parser._extract_direct_fields def _boom(element, excluded_tags=None): # noqa: ANN001 tag_name = parser._normalize_tag(element.tag) if tag_name == "УчастникОп": raise ValueError("bad participant") return original(element, excluded_tags=excluded_tags) monkeypatch.setattr(parser, "_extract_direct_fields", _boom) result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml) assert not result.is_fatal assert result.operation_errors assert result.operation_errors[0].record_id == "OP3" def test_parse_xml_with_numeric_suffix_file_name_is_valid() -> None: xml = _base_xml( "<Операция><ИдентификаторЗаписи>OP4<СуммаОпер>100" ) result = parse_xml_content("SKO115FZ_01_044525111_20251224_000051.xml", xml) assert not result.is_fatal assert len(result.rows) == 1 def test_parse_xml_collects_legal_entity_registration_address_attributes() -> None: xml = _base_xml( """ <Операция> <ИдентификаторЗаписи>OP5 <УчастникОп> <УчастникЮЛ> <СведЮЛ> <АдрРегЮЛ Индекс="675520" КодОКСМ="643" КодСубъектаПоОКАТО="10" Район="Благовещенский р-н" Пункт="Чигири с" Улица="Зеленая ул" Дом="1" /> """ ) result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml) assert not result.is_fatal assert len(result.rows) == 1 participant_fields = result.rows[0].participant_fields assert participant_fields["УчастникЮЛ.СведЮЛ.АдрРегЮЛ.Индекс"] == "675520" assert participant_fields["УчастникЮЛ.СведЮЛ.АдрРегЮЛ.КодОКСМ"] == "643" assert participant_fields["УчастникЮЛ.СведЮЛ.АдрРегЮЛ.КодСубъектаПоОКАТО"] == "10" assert ( participant_fields["УчастникЮЛ.СведЮЛ.АдрРегЮЛ.Район"] == "Благовещенский р-н" ) def test_extract_direct_fields_indexes_repeated_siblings_at_every_level() -> None: participant = ET.fromstring( """ <УчастникОп> <Документ><НомДок>DOC-1 <Документ><НомДок>DOC-2 <УчастникЮЛ> <СведЕИО><ФЛЕИО><ИННФЛИП>111111111111 <СведЕИО><ФЛЕИО><ИННФЛИП>222222222222 """ ) fields = _extract_direct_fields(participant) assert fields["Документ[0].НомДок"] == "DOC-1" assert fields["Документ[1].НомДок"] == "DOC-2" assert fields["УчастникЮЛ.СведЕИО[0].ФЛЕИО.ИННФЛИП"] == "111111111111" assert fields["УчастникЮЛ.СведЕИО[1].ФЛЕИО.ИННФЛИП"] == "222222222222" assert all("; " not in value for value in fields.values()) def test_extract_direct_fields_keeps_single_sibling_path_unindexed() -> None: participant = ET.fromstring( "<УчастникОп><Документ><НомДок>DOC-1" ) fields = _extract_direct_fields(participant) assert fields == {"Документ.НомДок": "DOC-1"} def test_parse_xml_expands_documents_and_eio_into_additive_rows() -> None: xml = _base_xml( """ <Операция> <ИдентификаторЗаписи>OP6 <УчастникОп> <ТипУчастника>1 <Документ><НомДок>DOC-1 <Документ><НомДок>DOC-2 <Документ><НомДок>DOC-3 <УчастникЮЛ> <СведЕИО><ФЛЕИО><ИННФЛИП>111111111111 <СведЕИО><ЮЛЕИО><ИННЮЛ>2222222222 """ ) result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml) assert not result.is_fatal assert len(result.rows) == 4 base_row, second_document, third_document, second_eio = result.rows assert base_row.participant_fields["ТипУчастника"] == "1" assert base_row.participant_fields["Документ.НомДок"] == "DOC-1" assert ( base_row.participant_fields["УчастникЮЛ.СведЕИО.ФЛЕИО.ИННФЛИП"] == "111111111111" ) assert second_document.operation_fields == {} assert second_document.is_continuation assert second_document.participant_fields == {"Документ.НомДок": "DOC-2"} assert third_document.participant_fields == {"Документ.НомДок": "DOC-3"} assert second_eio.participant_fields == { "УчастникЮЛ.СведЕИО.ЮЛЕИО.ИННЮЛ": "2222222222" } def test_parse_xml_separates_single_eio_and_single_beneficiary() -> None: xml = _base_xml( """ <Операция> <ИдентификаторЗаписи>OP7 <УчастникОп> <ТипУчастника>1 <УчастникЮЛ> <СведЕИО><ЮЛЕИО><ИННЮЛ>2222222222 <БенефициарЮЛ> <ФЛБенефициар><ИННФЛИП>111111111111 """ ) result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml) assert len(result.rows) == 2 assert ( result.rows[0].participant_fields["УчастникЮЛ.СведЕИО.ЮЛЕИО.ИННЮЛ"] == "2222222222" ) assert result.rows[1].participant_fields == { "УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ИННФЛИП": "111111111111" }