From c481d35b8c8b2e49b5caceda6fd2471ef457bee5 Mon Sep 17 00:00:00 2001 From: Aleksandr Meshchriakov Date: Tue, 4 Aug 2026 09:07:08 +0200 Subject: [PATCH] fix: exclude provider records from source exports --- docs/source-record-export-matrix-ru.md | 3 ++ .../external_data/source_record_export.py | 33 +++++++++++++++++ .../test_source_record_export.py | 37 +++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/docs/source-record-export-matrix-ru.md b/docs/source-record-export-matrix-ru.md index 975d3ad..418d828 100644 --- a/docs/source-record-export-matrix-ru.md +++ b/docs/source-record-export-matrix-ru.md @@ -38,6 +38,9 @@ JavaScript. Ticket не попадает в URL и после первого з объединяет три таблицы, а поле `record_type` различает тип строки. Все строки содержат реквизиты организации, включая ОКПО. +Записи технического внешнего поставщика в публичные файлы не включаются; в БД +они сохраняются для работы интеграции и дедупликации. + Физических XLSX-файлов может быть больше: по умолчанию один файл содержит не более 100 000 строк данных и получает суффикс `-part-001`, `-part-002` и далее. diff --git a/src/apps/external_data/source_record_export.py b/src/apps/external_data/source_record_export.py index 417cd14..989a1e5 100644 --- a/src/apps/external_data/source_record_export.py +++ b/src/apps/external_data/source_record_export.py @@ -54,6 +54,11 @@ DEFAULT_XLSX_DATA_ROWS_PER_FILE = 100_000 DEFAULT_DOWNLOAD_TICKET_TTL_SECONDS = 5 * 60 SOURCE_RECORD_EXPORT_TICKET_CACHE_PREFIX = "external-data:source-record-exports:ticket" SOURCE_RECORD_EXPORT_TICKET_PATTERN = re.compile(r"[A-Za-z0-9_-]{43}") +EXCLUDED_EXPORT_PROVIDER_PATTERN = re.compile( + r"(? bool: + """Return whether a provider-backed record must stay out of public files.""" + return any( + _contains_excluded_export_provider(getattr(record, field_name)) + for field_name in model_spec.fields + ) + + +def _contains_excluded_export_provider(value: object) -> bool: + if isinstance(value, str): + return EXCLUDED_EXPORT_PROVIDER_PATTERN.search(value) is not None + if isinstance(value, dict): + return any( + _contains_excluded_export_provider(key) + or _contains_excluded_export_provider(item) + for key, item in value.items() + ) + if isinstance(value, list | tuple): + return any(_contains_excluded_export_provider(item) for item in value) + return False + + def _serialize_flat_value(value: Any) -> str | int | float | bool: if value is None: return "" diff --git a/tests/apps/external_data/test_source_record_export.py b/tests/apps/external_data/test_source_record_export.py index ce253d7..26aa0f6 100644 --- a/tests/apps/external_data/test_source_record_export.py +++ b/tests/apps/external_data/test_source_record_export.py @@ -16,6 +16,7 @@ from rest_framework import status from rest_framework.test import APITestCase from tests.apps.external_data.factories import ( + BankruptcyProcedureFactory, FinancialReportFactory, FinancialReportLineFactory, IndustrialCertificateFactory, @@ -106,6 +107,42 @@ class SourceRecordExportApiTest(APITestCase): financial_rows = json.loads(financial_path.read_text(encoding="utf-8")) self.assertEqual(financial_rows[0]["financial_lines"][0]["line_code"], "1600") + def test_generation_excludes_provider_records_and_keeps_okpo(self): + organization = OrganizationFactory.create(okpo="11223344") + excluded_record = BankruptcyProcedureFactory.create( + organization=organization, + external_id="checko-fedresurs:123", + source_url="https://api.checko.ru/v2/bankruptcy/123", + ) + included_record = BankruptcyProcedureFactory.create( + organization=organization, + external_id="checkout-reference:456", + source_url="https://fedresurs.ru/message/456", + ) + + generation = build_source_record_export_artifacts() + + self.assertEqual(generation.records_count, 1) + artifacts = [ + artifact + for artifact in generation.artifacts + if artifact.source_group == "bankruptcy" + ] + self.assertEqual( + {artifact.file_format for artifact in artifacts}, {"csv", "xlsx", "json"} + ) + for artifact in artifacts: + if artifact.file_format == "json": + exported_values = json.loads(artifact.path.read_text(encoding="utf-8")) + elif artifact.file_format == "csv": + exported_values = artifact.path.read_text(encoding="utf-8-sig") + else: + workbook = load_workbook(artifact.path, read_only=True) + exported_values = list(workbook["data"].iter_rows(values_only=True)) + self.assertNotIn(str(excluded_record.id), str(exported_values)) + self.assertIn(str(included_record.id), str(exported_values)) + self.assertIn("11223344", str(exported_values)) + def test_management_command_bootstraps_first_generation(self): command_output = StringIO()