feat(parsers): implement arbitration and upload fixes
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 19s
CI/CD Pipeline / Build and Push Images (push) Successful in 6s
CI/CD Pipeline / Internal Notify (push) Successful in 1s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Successful in 2s

This commit is contained in:
2026-04-29 00:52:12 +02:00
parent db824bf4fa
commit 8e29b9902d
7 changed files with 578 additions and 16 deletions

View File

@@ -31,6 +31,7 @@ from apps.parsers.clients.proverki.client import ProverkiClientError
from apps.parsers.clients.zakupki import ZakupkiClientError
from apps.parsers.models import (
FinancialReport,
GenericParserRecord,
IndustrialCertificateRecord,
IndustrialProductRecord,
InspectionRecord,
@@ -66,6 +67,7 @@ from registers.models import Organization
from tests.apps.parsers.factories import (
InspectionRecordFactory,
ManufacturerRecordFactory,
ParserLoadLogFactory,
ProcurementRecordFactory,
ProxyFactory,
@@ -246,6 +248,124 @@ class GenericSourceFetchTestCase(TestCase):
self.assertEqual(records[0].record_date, "2026-04-01")
self.assertEqual(records[0].payload["provider"], "checko")
@override_settings(CHECKO_API_KEY="test-key", ARBITRATION_CHECKO_LIMIT=10)
def test_arbitration_fetches_checko_legal_cases_for_registry_organizations(self):
organization = Organization.objects.create(
pn_name='ООО "Арбитраж"',
mn_ogrn=1027700000001,
mn_inn=7701000002,
in_kpp=770101001,
mn_okpo="12345678",
)
class _CheckoClient:
instances = []
def __init__(self, **kwargs):
self.kwargs = kwargs
self.requests = []
self.instances.append(self)
def iter_legal_cases(self, request):
self.requests.append(request)
return iter(
[
SimpleNamespace(
case_number="А40-1/2026",
court_name="АС города Москвы",
type="civil",
category="Взыскание задолженности",
status="Рассмотрено",
filing_date="2026-04-01",
result_date="2026-04-20",
claim_amount=150000,
awarded_amount=100000,
plaintiffs=(
SimpleNamespace(
name=organization.pn_name,
inn=str(organization.mn_inn),
ogrn=str(organization.mn_ogrn),
role="plaintiff",
),
),
defendants=(),
third_parties=(),
instances=(),
url="https://kad.arbitr.ru/Card/1",
)
]
)
with patch.object(parser_tasks, "CheckoClient", _CheckoClient):
result = parser_tasks.parse_arbitration_cases(limit=10, proxies=[])
self.assertEqual(result["status"], "success")
self.assertEqual(result["saved"], 1)
record = GenericParserRecord.objects.get(
source=ParserLoadLog.Source.ARBITRATION
)
self.assertEqual(record.registry_organization_id, organization.id)
self.assertEqual(record.inn, str(organization.mn_inn))
self.assertEqual(record.ogrn, str(organization.mn_ogrn))
self.assertEqual(record.amount, 150000)
self.assertEqual(record.payload["provider"], "checko")
self.assertEqual(record.payload["target"]["role"], "plaintiff")
self.assertEqual(_CheckoClient.instances[0].requests[0].inn, record.inn)
@override_settings(CHECKO_API_KEY="test-key")
def test_arbitration_uses_loaded_parser_identifiers_when_registry_empty(self):
manufacturer = ManufacturerRecordFactory(
inn="7701000003",
ogrn="1027700000002",
full_legal_name='ООО "Источник"',
)
class _CheckoClient:
def __init__(self, **_kwargs):
return
def iter_legal_cases(self, request):
self.request = request
return iter(
[
SimpleNamespace(
case_number="А40-2/2026",
court_name="АС города Москвы",
type=None,
category=None,
status="Принято",
filing_date="2026-04-02",
result_date=None,
claim_amount=None,
awarded_amount=None,
plaintiffs=(),
defendants=(
SimpleNamespace(
name=manufacturer.full_legal_name,
inn=manufacturer.inn,
ogrn=manufacturer.ogrn,
role="defendant",
),
),
third_parties=(),
instances=(),
url="https://kad.arbitr.ru/Card/2",
)
]
)
with patch.object(parser_tasks, "CheckoClient", _CheckoClient):
records = parser_tasks._fetch_checko_arbitration_records(
limit=1,
proxies=[],
)
self.assertEqual(len(records), 1)
self.assertEqual(records[0].source, "arbitration")
self.assertEqual(records[0].inn, manufacturer.inn)
self.assertEqual(records[0].ogrn, manufacturer.ogrn)
self.assertEqual(records[0].payload["target"]["role"], "defendant")
@override_settings(CHECKO_API_KEY="")
def test_fedresurs_skips_when_official_blocked_and_fallback_empty(self):
with patch.object(

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import io
import os
import tempfile
import zipfile
from unittest.mock import Mock, patch
from apps.parsers.models import (
@@ -50,6 +51,14 @@ def _build_fns_excel_bytes() -> bytes:
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),
@@ -411,6 +420,34 @@ class ParsersViewSetTest(APITestCase):
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")