feat: address QA feedback and limit Checko collection
Some checks failed
CI/CD Pipeline / Quality Gate (push) Failing after 25s
CI/CD Pipeline / Build and Push Images (push) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped
CI/CD Pipeline / Internal Notify (push) Successful in 0s

This commit is contained in:
2026-07-19 13:32:46 +02:00
parent 63f59b3799
commit 76aed1dca3
17 changed files with 730 additions and 41 deletions

View File

@@ -201,6 +201,18 @@ class StateCorpExchangeServiceTest(TestCase):
"Окончание подачи заявок": "20.02.2026",
},
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.CONTRACTS,
load_batch=1,
external_id="contract-not-a-procurement",
inn=organization.inn,
ogrn=organization.ogrn,
title="Исполненный контракт",
record_date="16.02.2026",
amount="1000000.00",
status="completed",
payload={"registry_number": "contract-not-a-procurement"},
)
GenericParserRecord.objects.create(
source=ParserLoadLog.Source.ARBITRATION,
load_batch=1,
@@ -294,6 +306,10 @@ class StateCorpExchangeServiceTest(TestCase):
self.assertEqual(package.payload_counts["labor_vacancies"], 1)
payload = _decode_package_payload(package)
self.assertNotIn(
"contract-not-a-procurement",
{row["purchase_number"] for row in payload["data"]["public_procurements"]},
)
self.assertEqual(payload["format"], StateCorpExchangeService.PAYLOAD_FORMAT)
self.assertEqual(payload["schema_version"], 3)

View File

@@ -2,6 +2,7 @@
from decimal import Decimal
from tempfile import NamedTemporaryFile
from unittest.mock import patch
from django.test import TestCase
from openpyxl import Workbook
@@ -61,6 +62,19 @@ class OrganizationDirectoryImportServiceTest(TestCase):
self.assertTrue(organization.goz_participation)
self.assertFalse(organization.opk_registry_membership)
def test_import_xlsx_invalidates_organization_api_cache_after_commit(self):
path = self._workbook_path([self._row(rn="10")])
with (
patch(
"organizations.directory_import.invalidate_organization_api_cache"
) as invalidate_cache,
self.captureOnCommitCallbacks(execute=True),
):
OrganizationDirectoryImportService.import_xlsx(path)
invalidate_cache.assert_called_once_with()
def test_import_xlsx_rejects_missing_required_column(self):
path = self._workbook_path([], headers=SOURCE_HEADERS[:-1])
@@ -111,9 +125,7 @@ class OrganizationDirectoryImportServiceTest(TestCase):
]
embedded_value = "\t".join(str(value or "") for value in first_tail)
embedded_value = (
embedded_value
+ "_x000D_\n"
+ "\t".join(["21", "0", "0", 'ООО "Вторая"'])
embedded_value + "_x000D_\n" + "\t".join(["21", "0", "0", 'ООО "Вторая"'])
)
second_row = self._row(

View File

@@ -3,7 +3,16 @@
from io import StringIO
from apps.exchange.state_corp_services import StateCorpExchangeService
from apps.parsers.models import ParserLoadLog
from apps.parsers.models import (
FinancialReport,
GenericParserRecord,
IndustrialCertificateRecord,
IndustrialProductRecord,
InspectionRecord,
ManufacturerRecord,
ParserLoadLog,
ProcurementRecord,
)
from django.core.management import call_command
from django.test import TestCase, override_settings
from organizations.models import (
@@ -79,6 +88,13 @@ class TestCompaniesCommandsTest(TestCase):
).count(),
20 * 4,
)
self.assertEqual(IndustrialCertificateRecord.objects.count(), 20)
self.assertEqual(IndustrialProductRecord.objects.count(), 20)
self.assertEqual(ManufacturerRecord.objects.count(), 20)
self.assertEqual(InspectionRecord.objects.count(), 20)
self.assertEqual(ProcurementRecord.objects.count(), 20)
self.assertEqual(FinancialReport.objects.count(), 20)
self.assertEqual(GenericParserRecord.objects.count(), 20 * 9)
def test_create_updates_the_fixed_dataset_without_duplicates(self):
call_command("create_test_companies", stdout=StringIO())
@@ -125,9 +141,7 @@ class TestCompaniesCommandsTest(TestCase):
.order_by("rn")
.values_list("inn", flat=True)
)
package = StateCorpExchangeService.build_package(
organization_inns=company_inns
)
package = StateCorpExchangeService.build_package(organization_inns=company_inns)
self.assertEqual(
package.payload_counts,
@@ -137,7 +151,7 @@ class TestCompaniesCommandsTest(TestCase):
"manufacturers": 20,
"industrial_products": 20,
"prosecutor_checks": 20,
"public_procurements": 80,
"public_procurements": 60,
"financial_reports": 20,
"arbitration_cases": 20,
"bankruptcy_procedures": 20,
@@ -166,3 +180,6 @@ class TestCompaniesCommandsTest(TestCase):
self.assertTrue(Organization.objects.filter(uid=untouched.uid).exists())
self.assertEqual(OrganizationSourceExtension.objects.count(), 0)
self.assertEqual(OrganizationSourceRecord.objects.count(), 0)
self.assertEqual(GenericParserRecord.objects.count(), 0)
self.assertEqual(IndustrialProductRecord.objects.count(), 0)
self.assertEqual(FinancialReport.objects.count(), 0)

View File

@@ -0,0 +1,92 @@
from datetime import date
from apps.parsers import tasks as parser_tasks
from apps.parsers.checko_collection import claim_monthly_collection, finish_collection
from apps.parsers.models import CheckoCollectionAttempt
from django.test import TestCase
from tests.apps.parsers.organization_helpers import create_directory_organization
class CheckoCollectionClaimTest(TestCase):
def setUp(self):
self.organization = create_directory_organization(
pn_name='ООО "Месячный лимит"',
mn_ogrn=1027700000199,
mn_inn=7701000199,
in_kpp=770101001,
mn_okpo="12345678",
)
def test_claim_is_unique_per_organization_source_and_calendar_month(self):
first = claim_monthly_collection(
organization_id=str(self.organization.id),
source=CheckoCollectionAttempt.Source.ARBITRATION,
at=date(2026, 7, 1),
)
duplicate = claim_monthly_collection(
organization_id=str(self.organization.id),
source=CheckoCollectionAttempt.Source.ARBITRATION,
at=date(2026, 7, 31),
)
other_source = claim_monthly_collection(
organization_id=str(self.organization.id),
source=CheckoCollectionAttempt.Source.INSPECTIONS,
at=date(2026, 7, 31),
)
next_month = claim_monthly_collection(
organization_id=str(self.organization.id),
source=CheckoCollectionAttempt.Source.ARBITRATION,
at=date(2026, 8, 1),
)
self.assertIsNotNone(first)
self.assertIsNone(duplicate)
self.assertIsNotNone(other_source)
self.assertIsNotNone(next_month)
def test_failed_attempt_keeps_monthly_claim(self):
attempt = claim_monthly_collection(
organization_id=str(self.organization.id),
source=CheckoCollectionAttempt.Source.CONTRACTS,
at=date(2026, 7, 19),
)
assert attempt is not None
finish_collection(attempt, records_count=0, error="quota exhausted")
repeated = claim_monthly_collection(
organization_id=str(self.organization.id),
source=CheckoCollectionAttempt.Source.CONTRACTS,
at=date(2026, 7, 20),
)
attempt.refresh_from_db()
self.assertIsNone(repeated)
self.assertEqual(attempt.status, CheckoCollectionAttempt.Status.FAILED)
self.assertEqual(attempt.error_message, "quota exhausted")
def test_due_filter_is_applied_before_lookup_limit(self):
self.organization.opk_registry_membership = True
self.organization.save(update_fields=["opk_registry_membership"])
second = create_directory_organization(
pn_name='ООО "Следующая организация"',
mn_ogrn=1027700000299,
mn_inn=7701000299,
in_kpp=770101001,
mn_okpo="87654321",
)
second.opk_registry_membership = True
second.save(update_fields=["opk_registry_membership"])
claimed = claim_monthly_collection(
organization_id=str(self.organization.id),
source=CheckoCollectionAttempt.Source.INSPECTIONS,
)
self.assertIsNotNone(claimed)
targets = parser_tasks._active_registry_lookup_targets(
limit=1,
checko_source=CheckoCollectionAttempt.Source.INSPECTIONS,
)
self.assertEqual([target.organization_id for target in targets], [str(second.id)])

View File

@@ -33,6 +33,7 @@ from apps.parsers.clients.minpromtorg.manufactures import (
from apps.parsers.clients.proverki.client import ProverkiClientError
from apps.parsers.clients.zakupki import ZakupkiClientError
from apps.parsers.models import (
CheckoCollectionAttempt,
FinancialReport,
ParserLoadLog,
)
@@ -316,6 +317,12 @@ class CheckoFedresursFallbackControlFlowTestCase(SimpleTestCase):
return_value=targets,
),
patch.object(parser_tasks, "CheckoClient", _RateLimitedCheckoClient),
patch.object(
parser_tasks,
"claim_monthly_collection",
return_value=SimpleNamespace(),
),
patch.object(parser_tasks, "finish_collection"),
):
records = parser_tasks._fetch_checko_bankruptcy_records(proxies=None)
@@ -801,6 +808,15 @@ class GenericSourceFetchTestCase(TestCase):
[request.inn for request in _CheckoClient.instances[0].requests],
[str(organization.mn_inn)],
)
second_result = parser_tasks.parse_arbitration_cases(limit=10, proxies=[])
self.assertEqual(second_result["status"], "skipped")
self.assertEqual(len(_CheckoClient.instances), 1)
attempt = CheckoCollectionAttempt.objects.get(
organization_id=organization.id,
source=CheckoCollectionAttempt.Source.ARBITRATION,
)
self.assertEqual(attempt.status, CheckoCollectionAttempt.Status.SUCCESS)
self.assertEqual(attempt.records_count, 1)
@override_settings(CHECKO_API_KEY="test-key")
def test_arbitration_skips_when_no_active_registry_organizations(self):
@@ -1842,11 +1858,13 @@ class MinpromtorgTasksTestCase(TestCase):
"parse_procurements_44fz",
"parse_procurements_223fz",
"parse_contracts",
"parse_registry_contracts",
"parse_unfair_suppliers",
"parse_fas_goz_evasion",
"sync_fns_financial_reports",
"parse_arbitration_cases",
"parse_fedresurs_bankruptcy",
"parse_registry_inspections",
"parse_fstec_registers",
"parse_trudvsem_vacancies",
)
@@ -1869,6 +1887,8 @@ class MinpromtorgTasksTestCase(TestCase):
"sync_fns_financial_reports-id",
)
delays["sync_fns_financial_reports"].assert_called_once_with(proxies=[])
delays["parse_registry_contracts"].assert_called_once_with(proxies=[])
delays["parse_registry_inspections"].assert_called_once_with(proxies=[])
def test_parse_all_minpromtorg_without_adapter(self):
with TestHTTPServer() as server:

View File

@@ -595,6 +595,17 @@ class TokenRefreshViewTest(APITestCase):
# New refresh token should be different
# Refresh token may be the same or different depending on implementation
def test_same_refresh_token_can_handle_parallel_refresh_requests(self):
data = {"refresh": self.tokens["refresh"]}
first_response = self.client.post(self.refresh_url, data, format="json")
second_response = self.client.post(self.refresh_url, data, format="json")
self.assertEqual(first_response.status_code, status.HTTP_200_OK)
self.assertEqual(second_response.status_code, status.HTTP_200_OK)
self.assertEqual(first_response.data["refresh"], self.tokens["refresh"])
self.assertEqual(second_response.data["refresh"], self.tokens["refresh"])
def test_refresh_token_invalid(self):
"""Test token refresh fails with invalid refresh token"""
data = {"refresh": fake.pystr(min_chars=20, max_chars=50)}