"""Integration tests for parsers API views (no mocks).""" from __future__ import annotations import csv import io import os import tempfile import zipfile from unittest.mock import Mock, patch from apps.parsers.models import ( FinancialReport, FinancialReportLine, GenericParserRecord, ParserLoadLog, ProcurementRecord, ) from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse from openpyxl import Workbook from organizations.models import Organization from rest_framework import status from rest_framework.test import APITestCase from tests.apps.parsers.factories import ( IndustrialCertificateRecordFactory, IndustrialProductRecordFactory, InspectionRecordFactory, ManufacturerRecordFactory, ParserLoadLogFactory, ) from tests.apps.registers.factories import ( OrganizationFactory as RegisterOrganizationFactory, ) from tests.apps.registers.factories import ( RegistryMembershipPeriodFactory, ) from tests.apps.user.factories import UserFactory from tests.utils.fixtures import fake def _digits(length: int) -> str: return "".join(str(fake.random_int(0, 9)) for _ in range(length)) def _build_fns_excel_bytes() -> bytes: wb = Workbook() ws = wb.active year = fake.random_int(min=2020, max=2025) ws.append(["Form", None, year, None]) ws.append([None, "Code", "Start", "End"]) ws.append( [fake.word(), _digits(4), fake.random_int(10, 999), fake.random_int(10, 999)] ) buf = io.BytesIO() wb.save(buf) wb.close() return buf.getvalue() def _build_fns_zip_bytes(file_map: dict[str, bytes]) -> bytes: buf = io.BytesIO() with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as archive: for file_name, content in file_map.items(): archive.writestr(file_name, content) return buf.getvalue() def _create_procurement_record() -> ProcurementRecord: return ProcurementRecord.objects.create( load_batch=fake.random_int(min=1, max=1000), purchase_number=_digits(19), purchase_name=fake.sentence(nb_words=6), customer_inn=_digits(10), customer_kpp=_digits(9), customer_ogrn=_digits(13), customer_name=fake.company(), max_price=str(fake.pydecimal(left_digits=7, right_digits=2, positive=True)), status=fake.word(), law_type="44-FZ", href=fake.url(), region_code=f"{fake.random_int(min=1, max=99):02d}", ) class ParsersViewSetTest(APITestCase): def setUp(self): self.user = UserFactory.create_user() self.admin = UserFactory.create_superuser() def test_certificates_list_and_retrieve(self): record = IndustrialCertificateRecordFactory() self.client.force_authenticate(self.user) url = reverse("api_v1:minpromtorg:certificates-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) detail = self.client.get( reverse("api_v1:minpromtorg:certificates-detail", args=[record.id]) ) self.assertEqual(detail.status_code, status.HTTP_200_OK) def test_v2_source_csv_download_exports_source_rows(self): record = IndustrialCertificateRecordFactory( certificate_number="CERT-CSV-1", organisation_name='АО "CSV"', inn="7701000101", ogrn="1027700000001", ) self.client.force_authenticate(self.user) response = self.client.get( reverse("api_v2:parser_sources:minpromtorg-certificates-download") ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response["Content-Type"], "text/csv; charset=utf-8") self.assertIn( 'attachment; filename="minpromtorg-certificates.csv"', response["Content-Disposition"], ) rows = list(csv.DictReader(io.StringIO(response.content.decode("utf-8")))) self.assertEqual(len(rows), 1) self.assertEqual(rows[0]["id"], str(record.id)) self.assertEqual(rows[0]["source"], ParserLoadLog.Source.INDUSTRIAL) self.assertEqual(rows[0]["external_id"], "CERT-CSV-1") self.assertEqual(rows[0]["organisation_name"], 'АО "CSV"') self.assertEqual(rows[0]["inn"], "7701000101") self.assertIn("CERT-CSV-1", rows[0]["payload"]) def test_v2_source_csv_download_is_not_registered_for_financial_reports(self): self.client.force_authenticate(self.user) response = self.client.get("/api/v2/sources/fns/reports/download/") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_manufacturers_list_and_retrieve(self): record = ManufacturerRecordFactory() second_record = ManufacturerRecordFactory() self.client.force_authenticate(self.user) url = reverse("api_v1:minpromtorg:manufacturers-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) detail = self.client.get( reverse("api_v1:minpromtorg:manufacturers-detail", args=[record.id]) ) self.assertEqual(detail.status_code, status.HTTP_200_OK) first_page_response = self.client.get( "/api/v1/parsers/results/manufactures/", {"page": 1, "page_size": 1}, ) second_page_response = self.client.get( "/api/v1/parsers/results/manufactures/", {"page": 2, "page_size": 1}, ) self.assertEqual(first_page_response.status_code, status.HTTP_200_OK) self.assertEqual(second_page_response.status_code, status.HTTP_200_OK) self.assertEqual(first_page_response.data["meta"]["pagination"]["count"], 2) self.assertEqual(second_page_response.data["meta"]["pagination"]["count"], 2) self.assertNotEqual( first_page_response.data["data"][0]["id"], second_page_response.data["data"][0]["id"], ) self.assertEqual( { first_page_response.data["data"][0]["id"], second_page_response.data["data"][0]["id"], }, {record.id, second_record.id}, ) def test_products_list_and_retrieve(self): record = IndustrialProductRecordFactory() self.client.force_authenticate(self.user) url = reverse("api_v1:minpromtorg:industrial-products-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) detail = self.client.get( reverse("api_v1:minpromtorg:industrial-products-detail", args=[record.id]) ) self.assertEqual(detail.status_code, status.HTTP_200_OK) def test_inspections_list_and_retrieve(self): record = InspectionRecordFactory() self.client.force_authenticate(self.user) url = reverse("api_v1:proverki:inspections-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) detail = self.client.get( reverse("api_v1:proverki:inspections-detail", args=[record.id]) ) self.assertEqual(detail.status_code, status.HTTP_200_OK) def test_procurements_list_and_retrieve(self): record = _create_procurement_record() self.client.force_authenticate(self.user) url = reverse("api_v1:zakupki:procurements-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) detail = self.client.get( reverse("api_v1:zakupki:procurements-detail", args=[record.id]) ) self.assertEqual(detail.status_code, status.HTTP_200_OK) def test_procurements_v1_enriches_missing_customer_fields_from_canonical_organization( self, ): organization = Organization.objects.create( name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"', inn="7701000101", kpp="770101001", ogrn="1027700000001", ) record = ProcurementRecord.objects.create( load_batch=1, purchase_number="0320100000126000001", purchase_name="Поставка оборудования", customer_inn="", customer_kpp="", customer_ogrn=organization.ogrn, customer_name="", max_price="1000.00", status="published", law_type="44-FZ", href="https://zakupki.gov.ru/example", region_code="77", ) self.client.force_authenticate(self.user) list_response = self.client.get(reverse("api_v1:zakupki:procurements-list")) detail_response = self.client.get( reverse("api_v1:zakupki:procurements-detail", args=[record.id]) ) self.assertEqual(list_response.status_code, status.HTTP_200_OK) self.assertEqual(detail_response.status_code, status.HTTP_200_OK) list_row = list_response.data["data"][0] detail_row = detail_response.data self.assertEqual(list_row["customer_name"], organization.name) self.assertEqual(list_row["customer_inn"], organization.inn) self.assertEqual(list_row["customer_kpp"], organization.kpp) self.assertEqual(list_row["customer_ogrn"], organization.ogrn) self.assertEqual(detail_row["customer_name"], organization.name) self.assertEqual(detail_row["customer_inn"], organization.inn) self.assertEqual(detail_row["customer_kpp"], organization.kpp) self.assertEqual(detail_row["customer_ogrn"], organization.ogrn) def test_procurements_v1_uses_customer_kpp_for_branch_enrichment(self): head = Organization.objects.create( name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"', inn="7701000101", kpp="770101001", ogrn="1027700000001", ) branch = Organization.objects.create( name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК" ФИЛИАЛ', inn=head.inn, kpp="780101001", ogrn=head.ogrn, ) record = ProcurementRecord.objects.create( load_batch=1, purchase_number="0320100000126000002", purchase_name="Поставка оборудования", customer_inn=head.inn, customer_kpp=branch.kpp, customer_ogrn=head.ogrn, customer_name="", max_price="1000.00", status="published", law_type="44-FZ", href="https://zakupki.gov.ru/example", region_code="77", ) self.client.force_authenticate(self.user) response = self.client.get( reverse("api_v1:zakupki:procurements-detail", args=[record.id]) ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["customer_name"], branch.name) self.assertEqual(response.data["customer_kpp"], branch.kpp) def test_procurements_v1_does_not_guess_branch_when_customer_kpp_is_missing(self): head = Organization.objects.create( name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК"', inn="7701000101", kpp="770101001", ogrn="1027700000001", ) Organization.objects.create( name='ООО "КАНОНИЧЕСКИЙ ЗАКАЗЧИК" ФИЛИАЛ', inn=head.inn, kpp="780101001", ogrn=head.ogrn, ) record = ProcurementRecord.objects.create( load_batch=1, purchase_number="0320100000126000003", purchase_name="Поставка оборудования", customer_inn=head.inn, customer_kpp="", customer_ogrn=head.ogrn, customer_name="", max_price="1000.00", status="published", law_type="44-FZ", href="https://zakupki.gov.ru/example", region_code="77", ) self.client.force_authenticate(self.user) response = self.client.get( reverse("api_v1:zakupki:procurements-detail", args=[record.id]) ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["customer_name"], "") self.assertEqual(response.data["customer_kpp"], "") def test_eis_result_endpoint_uses_generic_records_without_breaking_old_zakupki_api( self, ): old_record = _create_procurement_record() generic_record = GenericParserRecord.objects.create( load_batch=1, source=ParserLoadLog.Source.PROCUREMENTS_44FZ, external_id="eis-44fz-1", inn=_digits(10), organisation_name="EIS Customer", title="EIS 44-FZ notice", status="published", payload={"registry": "44fz"}, ) self.client.force_authenticate(self.user) old_response = self.client.get(reverse("api_v1:zakupki:procurements-list")) new_response = self.client.get("/api/v1/eis/procurements-44fz/") detail_response = self.client.get( f"/api/v1/eis/procurements-44fz/{generic_record.id}/" ) self.assertEqual(old_response.status_code, status.HTTP_200_OK) self.assertEqual(old_response.data["data"][0]["id"], old_record.id) self.assertEqual(new_response.status_code, status.HTTP_200_OK) self.assertEqual(new_response.data["meta"]["pagination"]["count"], 1) self.assertEqual(new_response.data["data"][0]["id"], generic_record.id) self.assertEqual(detail_response.status_code, status.HTTP_200_OK) self.assertEqual(detail_response.data["data"]["payload"], {"registry": "44fz"}) def test_parser_results_v1_enrich_missing_organization_fields_without_contract_change( self, ): organization = Organization.objects.create( name='ООО "КАНОНИЧЕСКИЙ ПОСТАВЩИК"', inn="7701000102", kpp="770102001", ogrn="1027700000002", ) generic_record = GenericParserRecord.objects.create( load_batch=1, source=ParserLoadLog.Source.PROCUREMENTS_44FZ, external_id="eis-44fz-enrichment", inn="", ogrn=organization.ogrn, organisation_name="", title="EIS notice", status="published", payload={"registry": "44fz"}, ) self.client.force_authenticate(self.user) response = self.client.get("/api/v1/parsers/results/procurements_44fz/") detail_response = self.client.get( f"/api/v1/parsers/results/procurements_44fz/{generic_record.id}/" ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(detail_response.status_code, status.HTTP_200_OK) row = response.data["data"][0] detail = detail_response.data["data"] self.assertEqual(row["organisation_name"], organization.name) self.assertEqual(row["inn"], organization.inn) self.assertEqual(row["ogrn"], organization.ogrn) self.assertNotIn("kpp", row) self.assertEqual(detail["organisation_name"], organization.name) self.assertEqual(detail["inn"], organization.inn) self.assertEqual(detail["ogrn"], organization.ogrn) self.assertNotIn("kpp", detail) def test_dashboard_data_exposes_source_groups_for_page(self): GenericParserRecord.objects.create( load_batch=1, source=ParserLoadLog.Source.PROCUREMENTS_44FZ, external_id="eis-44fz-1", title="EIS 44-FZ notice 1", payload={"registry": "44fz"}, ) GenericParserRecord.objects.create( load_batch=1, source=ParserLoadLog.Source.PROCUREMENTS_44FZ, external_id="eis-44fz-2", title="EIS 44-FZ notice 2", payload={"registry": "44fz"}, ) GenericParserRecord.objects.create( load_batch=1, source=ParserLoadLog.Source.TRUDVSEM, external_id="trudvsem-1", title="Vacancy", payload={"registry": "trudvsem"}, ) FinancialReport.objects.create( external_id=_digits(5), ogrn=_digits(13), file_name=f"fin_{_digits(5)}_{_digits(13)}.xlsx", file_hash=fake.sha256(raw_output=False), load_batch=1, status=FinancialReport.Status.SUCCESS, source=FinancialReport.SourceType.API, ) self.client.force_authenticate(self.user) response = self.client.get("/api/v1/parsers/dashboard/") self.assertEqual(response.status_code, status.HTTP_200_OK) payload = response.data["data"] self.assertIn("groups", payload) self.assertIn("api", payload["groups"]) self.assertIn("uploads", payload["groups"]) self.assertEqual(payload["api_sources"], payload["groups"]["api"]) self.assertEqual(payload["file_sources"], payload["groups"]["uploads"]) sources = {item["key"]: item for item in payload["sources"]} self.assertEqual( sources["procurements_44fz"]["result_list_url"], "/api/v1/parsers/results/procurements_44fz/", ) self.assertEqual( sources["procurements_44fz"]["api_route"], "eis/procurements-44fz", ) self.assertEqual( sources["procurements_223fz"]["result_list_url"], "/api/v1/parsers/results/procurements_223fz/", ) self.assertEqual( sources["contracts"]["result_list_url"], "/api/v1/parsers/results/contracts/", ) self.assertEqual( payload["source_counts"][ParserLoadLog.Source.PROCUREMENTS_44FZ], 2, ) self.assertEqual(payload["source_counts"][ParserLoadLog.Source.TRUDVSEM], 1) self.assertEqual(payload["source_counts"][ParserLoadLog.Source.FNS_REPORTS], 1) def test_dashboard_data_exposes_registry_data_coverage(self): registry_organization = RegisterOrganizationFactory( mn_inn=7720699480, mn_ogrn=1107746880031, ) RegistryMembershipPeriodFactory(organization=registry_organization) FinancialReport.objects.create( external_id=_digits(5), ogrn=str(registry_organization.mn_ogrn), file_name=f"fin_{_digits(5)}_{registry_organization.mn_ogrn}.xlsx", file_hash=fake.sha256(raw_output=False), load_batch=1, status=FinancialReport.Status.SUCCESS, source=FinancialReport.SourceType.API, ) GenericParserRecord.objects.create( load_batch=1, source=ParserLoadLog.Source.UNFAIR_SUPPLIERS, external_id="unfair-1", title="Unfair supplier record", inn=str(registry_organization.mn_inn), ogrn=str(registry_organization.mn_ogrn), ) self.client.force_authenticate(self.user) response = self.client.get("/api/v1/parsers/dashboard/") self.assertEqual(response.status_code, status.HTTP_200_OK) coverage = response.data["data"]["registry_data_coverage"] self.assertEqual(coverage["total_organizations"], 1) items = {item["source"]: item for item in coverage["items"]} self.assertEqual(items[ParserLoadLog.Source.FNS_REPORTS]["organizations_count"], 1) self.assertEqual(items[ParserLoadLog.Source.FNS_REPORTS]["coverage_percent"], 100) self.assertNotIn(ParserLoadLog.Source.UNFAIR_SUPPLIERS, items) def test_dashboard_data_exposes_registry_enrichment_analytics(self): rosatom_organization = RegisterOrganizationFactory( mn_inn=7720699480, mn_ogrn=1107746880031, ) roscosmos_organization = RegisterOrganizationFactory( mn_inn=7730239877, mn_ogrn=1087302001797, ) rosatom_membership = RegistryMembershipPeriodFactory( organization=rosatom_organization ) roscosmos_membership = RegistryMembershipPeriodFactory( organization=roscosmos_organization ) FinancialReport.objects.create( external_id=_digits(5), ogrn=str(rosatom_organization.mn_ogrn), file_name=f"fin_{_digits(5)}_{rosatom_organization.mn_ogrn}.xlsx", file_hash=fake.sha256(raw_output=False), load_batch=1, status=FinancialReport.Status.SUCCESS, source=FinancialReport.SourceType.API, ) IndustrialCertificateRecordFactory( inn=str(rosatom_organization.mn_inn), ogrn=str(rosatom_organization.mn_ogrn), ) GenericParserRecord.objects.create( load_batch=1, source=ParserLoadLog.Source.UNFAIR_SUPPLIERS, external_id="unfair-registry-analytics", title="Risk signal", inn=str(roscosmos_organization.mn_inn), ogrn=str(roscosmos_organization.mn_ogrn), ) GenericParserRecord.objects.create( load_batch=1, source=ParserLoadLog.Source.FEDRESURS_BANKRUPTCY, external_id="bankruptcy-registry-analytics", title="Bankruptcy risk signal", inn=str(roscosmos_organization.mn_inn), ogrn=str(roscosmos_organization.mn_ogrn), ) InspectionRecordFactory( inn=str(rosatom_organization.mn_inn), ogrn=str(rosatom_organization.mn_ogrn), ) self.client.force_authenticate(self.user) response = self.client.get("/api/v1/parsers/dashboard/") self.assertEqual(response.status_code, status.HTTP_200_OK) analytics = response.data["data"]["registry_enrichment_analytics"] self.assertEqual( analytics["population"]["active_registry_organizations"], 2, ) self.assertEqual(analytics["coverage_summary"]["with_any_enrichment"], 1) self.assertEqual(analytics["coverage_summary"]["core_profile_complete"], 1) self.assertEqual(analytics["coverage_summary"]["requires_attention"], 1) source_coverage = { item["source"]: item for item in analytics["source_coverage"] } self.assertEqual( source_coverage[ParserLoadLog.Source.FNS_REPORTS][ "organizations_count" ], 1, ) self.assertEqual( source_coverage[ParserLoadLog.Source.INDUSTRIAL][ "organizations_count" ], 1, ) self.assertNotIn(ParserLoadLog.Source.UNFAIR_SUPPLIERS, source_coverage) self.assertNotIn(ParserLoadLog.Source.FEDRESURS_BANKRUPTCY, source_coverage) self.assertNotIn(ParserLoadLog.Source.INSPECTIONS, source_coverage) risk_signals = {item["source"]: item for item in analytics["risk_signals"]} self.assertEqual( risk_signals[ParserLoadLog.Source.UNFAIR_SUPPLIERS][ "organizations_count" ], 1, ) self.assertEqual( risk_signals[ParserLoadLog.Source.UNFAIR_SUPPLIERS]["risk_severity"], "critical", ) self.assertEqual( risk_signals[ParserLoadLog.Source.FEDRESURS_BANKRUPTCY][ "risk_severity" ], "critical", ) self.assertEqual( risk_signals[ParserLoadLog.Source.INSPECTIONS]["risk_severity"], "soft", ) self.assertEqual( analytics["risk_summary"]["critical"]["organizations_count"], 1, ) self.assertEqual( analytics["risk_summary"]["soft"]["organizations_count"], 1, ) matrix = { item["registry_id"]: item for item in analytics["registry_source_matrix"] } rosatom_matrix = matrix[str(rosatom_membership.registry_id)] roscosmos_matrix = matrix[str(roscosmos_membership.registry_id)] self.assertEqual(rosatom_matrix["active_organizations"], 1) self.assertEqual( rosatom_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][ "organizations_count" ], 1, ) self.assertEqual( roscosmos_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][ "organizations_count" ], 0, ) total_matrix = analytics["registry_source_matrix"][-1] self.assertEqual(total_matrix["registry_id"], "__all__") self.assertEqual(total_matrix["registry_name"], "Все организации") self.assertEqual(total_matrix["active_organizations"], 2) self.assertEqual( total_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][ "organizations_count" ], 1, ) self.assertEqual( total_matrix["sources"][ParserLoadLog.Source.FNS_REPORTS][ "coverage_percent" ], 50, ) self.assertEqual( total_matrix["sources"][ParserLoadLog.Source.INDUSTRIAL][ "organizations_count" ], 1, ) self.assertEqual( total_matrix["sources"][ParserLoadLog.Source.INDUSTRIAL][ "coverage_percent" ], 50, ) def test_financial_reports_list_and_retrieve(self): report = FinancialReport.objects.create( external_id=_digits(5), ogrn=_digits(13), file_name=f"fin_{_digits(5)}_{_digits(13)}.xlsx", file_hash=fake.sha256(raw_output=False), load_batch=fake.random_int(min=1, max=1000), status=FinancialReport.Status.SUCCESS, source=FinancialReport.SourceType.API, ) FinancialReportLine.objects.create( report=report, form_code="1", line_code=_digits(4), line_name=fake.word(), year=fake.random_int(min=2020, max=2025), period_start=fake.random_int(min=1, max=999), period_end=fake.random_int(min=1, max=999), ) self.client.force_authenticate(self.user) url = reverse("api_v1:fns:fns-reports-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["data"][0]["lines_count"], 1) alias_response = self.client.get(url, {"source": "fns_reports"}) self.assertEqual(alias_response.status_code, status.HTTP_200_OK) self.assertEqual(alias_response.data["data"][0]["id"], report.id) source_type_response = self.client.get( url, {"source": FinancialReport.SourceType.API}, ) self.assertEqual(source_type_response.status_code, status.HTTP_200_OK) self.assertEqual(source_type_response.data["data"][0]["id"], report.id) invalid_source_response = self.client.get(url, {"source": "unknown"}) self.assertEqual(invalid_source_response.status_code, status.HTTP_200_OK) self.assertEqual(invalid_source_response.data["data"], []) detail = self.client.get( reverse("api_v1:fns:fns-reports-detail", args=[report.id]) ) self.assertEqual(detail.status_code, status.HTTP_200_OK) self.assertIn("lines", detail.data) unified_response = self.client.get("/api/v1/parsers/results/fns_financial/") self.assertEqual(unified_response.status_code, status.HTTP_200_OK) self.assertEqual(unified_response.data["meta"]["pagination"]["count"], 1) self.assertEqual(unified_response.data["data"][0]["id"], report.id) self.assertEqual(unified_response.data["data"][0]["title"], report.file_name) def test_fns_financial_results_searches_and_orders_by_registry_organization(self): alpha_org = RegisterOrganizationFactory( pn_name='alpha "ФНС"', mn_ogrn=1027700000001, mn_inn=7701000001, ) beta_org = RegisterOrganizationFactory( pn_name='Beta "ФНС"', mn_ogrn=1027700000002, mn_inn=7701000002, ) alpha_report = FinancialReport.objects.create( external_id="fns-alpha", ogrn=str(alpha_org.mn_ogrn), registry_organization=alpha_org, file_name=f"fin_fns-alpha_{alpha_org.mn_ogrn}.xlsx", file_hash=fake.sha256(raw_output=False), load_batch=1, status=FinancialReport.Status.SUCCESS, source=FinancialReport.SourceType.API, ) beta_report = FinancialReport.objects.create( external_id="fns-beta", ogrn=str(beta_org.mn_ogrn), registry_organization=beta_org, file_name=f"fin_fns-beta_{beta_org.mn_ogrn}.xlsx", file_hash=fake.sha256(raw_output=False), load_batch=1, status=FinancialReport.Status.SUCCESS, source=FinancialReport.SourceType.API, ) self.client.force_authenticate(self.user) name_response = self.client.get( "/api/v1/parsers/results/fns_financial/", {"search": "alpha"}, ) inn_response = self.client.get( "/api/v1/parsers/results/fns_financial/", {"search": str(beta_org.mn_inn)}, ) ordered_response = self.client.get( "/api/v1/parsers/results/fns_financial/", {"ordering": "organisation_name"}, ) self.assertEqual(name_response.status_code, status.HTTP_200_OK) self.assertEqual( [item["id"] for item in name_response.data["data"]], [alpha_report.id], ) self.assertEqual( name_response.data["data"][0]["organisation_name"], alpha_org.pn_name, ) self.assertEqual(name_response.data["data"][0]["inn"], str(alpha_org.mn_inn)) self.assertEqual(inn_response.status_code, status.HTTP_200_OK) self.assertEqual( [item["id"] for item in inn_response.data["data"]], [beta_report.id], ) self.assertEqual(ordered_response.status_code, status.HTTP_200_OK) self.assertEqual( [item["id"] for item in ordered_response.data["data"]], [alpha_report.id, beta_report.id], ) def test_eis_results_order_text_case_insensitively_and_by_amount(self): low_amount = GenericParserRecord.objects.create( load_batch=1, source=ParserLoadLog.Source.PROCUREMENTS_44FZ, external_id="amount-low", organisation_name="alpha customer", title="Low amount", amount="10.00", ) high_amount = GenericParserRecord.objects.create( load_batch=1, source=ParserLoadLog.Source.PROCUREMENTS_44FZ, external_id="amount-high", organisation_name="Beta customer", title="High amount", amount="20.00", ) self.client.force_authenticate(self.user) amount_response = self.client.get( "/api/v1/eis/procurements-44fz/", {"ordering": "amount"}, ) name_response = self.client.get( "/api/v1/eis/procurements-44fz/", {"ordering": "-organisation_name"}, ) self.assertEqual(amount_response.status_code, status.HTTP_200_OK) self.assertEqual( [item["id"] for item in amount_response.data["data"]], [low_amount.id, high_amount.id], ) self.assertEqual(name_response.status_code, status.HTTP_200_OK) self.assertEqual( [item["id"] for item in name_response.data["data"]], [high_amount.id, low_amount.id], ) def test_system_logs_admin_only(self): log = ParserLoadLogFactory() url_logs = reverse("api_v1:system:parser-logs-list") response = self.client.get(url_logs) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.client.force_authenticate(self.user) response = self.client.get(url_logs) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.client.force_authenticate(self.admin) response = self.client.get(url_logs) self.assertEqual(response.status_code, status.HTTP_200_OK) detail = self.client.get( reverse("api_v1:system:parser-logs-detail", args=[log.id]) ) self.assertEqual(detail.status_code, status.HTTP_200_OK) def test_system_logs_support_search_and_organizations_count(self): search_marker = "manufactures-unique-search-marker" first_log = ParserLoadLogFactory( source="manufactures", batch_id=101, status="success", error_message=search_marker, ) ParserLoadLogFactory( source="inspections", batch_id=202, status="failed", error_message="timeout", ) ManufacturerRecordFactory(load_batch=101, inn="7701000001") ManufacturerRecordFactory(load_batch=101, inn="7701000002") self.client.force_authenticate(self.admin) response = self.client.get( reverse("api_v1:system:parser-logs-list"), {"search": search_marker}, ) self.assertEqual(response.status_code, status.HTTP_200_OK) rows = response.data["results"] self.assertEqual(len(rows), 1) self.assertEqual(rows[0]["id"], first_log.id) self.assertEqual(rows[0]["organizations_count"], 2) def test_system_logs_export_returns_csv(self): ParserLoadLogFactory( source="manufactures", batch_id=333, status="success", records_count=4, ) ManufacturerRecordFactory(load_batch=333, inn="7701000001") self.client.force_authenticate(self.admin) response = self.client.get( reverse("api_v1:system:parser-logs-export"), {"search": "333"}, ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response["Content-Type"], "text/csv; charset=utf-8") content = response.content.decode("utf-8") self.assertIn("organizations_count", content) self.assertIn("333", content) def test_system_logs_support_search_by_source_label(self): ParserLoadLogFactory( source="fns_reports", batch_id=909, status="success", ) self.client.force_authenticate(self.admin) response = self.client.get( reverse("api_v1:system:parser-logs-list"), {"search": "финансово"}, ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data["results"]), 1) self.assertEqual(response.data["results"][0]["source"], "financial-indicators") def test_fns_upload_invalid_filename(self): self.client.force_authenticate(self.admin) with tempfile.TemporaryDirectory() as tmpdir: watch_dir = os.path.join(tmpdir, "watch") processed_dir = os.path.join(tmpdir, "processed") failed_dir = os.path.join(tmpdir, "failed") content = _build_fns_excel_bytes() upload = SimpleUploadedFile( f"bad_{fake.random_int()}.xlsx", content, content_type=( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ), ) with self.settings( FNS_WATCH_DIRECTORY=watch_dir, FNS_PROCESSED_DIRECTORY=processed_dir, FNS_FAILED_DIRECTORY=failed_dir, ): url = reverse("api_v1:fns:fns-upload") response = self.client.post( url, {"files": [upload]}, format="multipart" ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_fns_upload_forbidden_for_regular_user(self): self.client.force_authenticate(self.user) with tempfile.TemporaryDirectory() as tmpdir: watch_dir = os.path.join(tmpdir, "watch") processed_dir = os.path.join(tmpdir, "processed") failed_dir = os.path.join(tmpdir, "failed") content = _build_fns_excel_bytes() upload = SimpleUploadedFile( f"fin_{_digits(5)}_{_digits(13)}.xlsx", content, content_type=( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ), ) with self.settings( FNS_WATCH_DIRECTORY=watch_dir, FNS_PROCESSED_DIRECTORY=processed_dir, FNS_FAILED_DIRECTORY=failed_dir, ): url = reverse("api_v1:fns:fns-upload") response = self.client.post( url, {"files": [upload]}, format="multipart" ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_fns_upload_accepts_single_file_payload(self): self.client.force_authenticate(self.admin) with tempfile.TemporaryDirectory() as tmpdir: watch_dir = os.path.join(tmpdir, "watch") processed_dir = os.path.join(tmpdir, "processed") failed_dir = os.path.join(tmpdir, "failed") content = _build_fns_excel_bytes() upload = SimpleUploadedFile( f"fin_{_digits(5)}_{_digits(13)}.xlsx", content, content_type=( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ), ) with self.settings( FNS_WATCH_DIRECTORY=watch_dir, FNS_PROCESSED_DIRECTORY=processed_dir, FNS_FAILED_DIRECTORY=failed_dir, ): url = reverse("api_v1:fns:fns-upload") response = self.client.post(url, {"file": upload}, format="multipart") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["success"], True) self.assertIn("message", response.data) def test_fns_upload_accepts_zip_payload(self): self.client.force_authenticate(self.admin) external_id = _digits(5) ogrn = _digits(13) filename = f"fin_{external_id}_{ogrn}.xlsx" upload = SimpleUploadedFile( "fns_reports.zip", _build_fns_zip_bytes({filename: _build_fns_excel_bytes()}), content_type="application/zip", ) with tempfile.TemporaryDirectory() as tmpdir: watch_dir = os.path.join(tmpdir, "watch") processed_dir = os.path.join(tmpdir, "processed") failed_dir = os.path.join(tmpdir, "failed") with self.settings( FNS_WATCH_DIRECTORY=watch_dir, FNS_PROCESSED_DIRECTORY=processed_dir, FNS_FAILED_DIRECTORY=failed_dir, ): url = reverse("api_v1:fns:fns-upload") response = self.client.post(url, {"file": upload}, format="multipart") self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) self.assertEqual(response.data["queued"], 1) self.assertEqual(response.data["skipped"], 0) self.assertEqual(response.data["invalid"], 0) self.assertEqual(FinancialReport.objects.count(), 1) def test_parsing_settings_get_and_patch(self): self.client.force_authenticate(self.admin) url = reverse("api_v1:parsing:parsing-settings") initial = self.client.get(url) self.assertEqual(initial.status_code, status.HTTP_200_OK) self.assertEqual(initial.data["planned_inspections"], "monthly") updated = self.client.patch( url, {"planned_inspections": "weekly"}, format="json", ) self.assertEqual(updated.status_code, status.HTTP_200_OK) self.assertEqual(updated.data["planned_inspections"], "weekly") def test_run_sync_inspections_accepts_limited_sync_params(self): self.client.force_authenticate(self.user) url = reverse("api_v1:parsers:run-parser", args=["sync_inspections"]) payload = { "max_months_per_law": 1, "start_year": 2026, "start_month": 4, "include_fz294": True, "include_fz248": False, "current_year": 2026, "current_month": 4, } with patch( "apps.parsers.views.tasks.sync_inspections.apply_async", return_value=Mock(id="task-123"), ) as apply_async_mock: response = self.client.post(url, payload, format="json") self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) task_kwargs = apply_async_mock.call_args.kwargs["kwargs"] for key, value in payload.items(): self.assertEqual(task_kwargs[key], value) self.assertEqual(task_kwargs["requested_by_id"], self.user.id)