Files
mostovik-backend/tests/apps/parsers/test_views.py
Aleksandr Meshchriakov 89607356b7
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 24s
CI/CD Pipeline / Build and Push Images (push) Successful in 10s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Successful in 1s
Add organization stats endpoint
2026-05-12 17:48:54 +02:00

1037 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)