feat: update finance exchange and source refresh jobs
This commit is contained in:
@@ -61,6 +61,20 @@ class BackgroundJobModelTest(TestCase):
|
||||
self.assertEqual(job.result, result)
|
||||
self.assertIsNotNone(job.completed_at)
|
||||
|
||||
def test_complete_clears_previous_failure_details(self):
|
||||
job = BackgroundJob.objects.create(
|
||||
task_id=fake.uuid4(),
|
||||
task_name="test.task",
|
||||
error="stale error",
|
||||
traceback="stale traceback",
|
||||
)
|
||||
|
||||
job.complete(result={"processed": 1})
|
||||
|
||||
self.assertEqual(job.status, JobStatus.SUCCESS)
|
||||
self.assertEqual(job.error, "")
|
||||
self.assertEqual(job.traceback, "")
|
||||
|
||||
def test_fail(self):
|
||||
"""Тест завершения с ошибкой."""
|
||||
job = BackgroundJob.objects.create(
|
||||
@@ -288,7 +302,7 @@ class BackgroundJobServiceTest(TestCase):
|
||||
task_name="apps.other.tasks.task",
|
||||
meta={"source": "industrial_products"},
|
||||
)
|
||||
old_timestamp = timezone.now() - timedelta(hours=3)
|
||||
old_timestamp = timezone.now() - timedelta(hours=25)
|
||||
BackgroundJob.objects.filter(
|
||||
task_id__in=[stale_job.task_id, unrelated_job.task_id]
|
||||
).update(created_at=old_timestamp, updated_at=timezone.now())
|
||||
@@ -307,3 +321,37 @@ class BackgroundJobServiceTest(TestCase):
|
||||
self.assertIn("Stale background job", stale_job.error)
|
||||
self.assertEqual(fresh_job.status, JobStatus.PENDING)
|
||||
self.assertEqual(unrelated_job.status, JobStatus.PENDING)
|
||||
|
||||
def test_mark_stale_active_jobs_uses_updated_at_for_started_job(self):
|
||||
job = BackgroundJobService.create_job(
|
||||
task_id="job-heartbeat",
|
||||
task_name="apps.parsers.tasks.parse_industrial_products",
|
||||
meta={"source": "industrial_products"},
|
||||
)
|
||||
job.mark_started()
|
||||
old_timestamp = timezone.now() - timedelta(hours=5)
|
||||
BackgroundJob.objects.filter(pk=job.pk).update(
|
||||
started_at=old_timestamp,
|
||||
updated_at=timezone.now(),
|
||||
)
|
||||
|
||||
updated = BackgroundJobService.mark_stale_active_jobs_failed(
|
||||
max_age_minutes=240,
|
||||
task_names={"apps.parsers.tasks.parse_industrial_products"},
|
||||
meta_sources={"industrial_products"},
|
||||
)
|
||||
|
||||
self.assertEqual(updated, 0)
|
||||
job.refresh_from_db()
|
||||
self.assertEqual(job.status, JobStatus.STARTED)
|
||||
|
||||
BackgroundJob.objects.filter(pk=job.pk).update(updated_at=old_timestamp)
|
||||
updated = BackgroundJobService.mark_stale_active_jobs_failed(
|
||||
max_age_minutes=240,
|
||||
task_names={"apps.parsers.tasks.parse_industrial_products"},
|
||||
meta_sources={"industrial_products"},
|
||||
)
|
||||
|
||||
self.assertEqual(updated, 1)
|
||||
job.refresh_from_db()
|
||||
self.assertEqual(job.status, JobStatus.FAILURE)
|
||||
|
||||
@@ -249,10 +249,12 @@ class StateCorpExchangeServiceTest(TestCase):
|
||||
ogrn=organization.ogrn,
|
||||
title="А40-1/2026",
|
||||
record_date="2026-03-25",
|
||||
amount="1250000.50",
|
||||
status="in_progress",
|
||||
payload={
|
||||
"case_number": "А40-1/2026",
|
||||
"court_name": "АС города Москвы",
|
||||
"claim_amount": "1.00",
|
||||
"target": {"role": "ответчик"},
|
||||
},
|
||||
)
|
||||
@@ -429,6 +431,10 @@ class StateCorpExchangeServiceTest(TestCase):
|
||||
payload["data"]["arbitration_cases"][0]["case_number"],
|
||||
"А40-1/2026",
|
||||
)
|
||||
self.assertEqual(
|
||||
payload["data"]["arbitration_cases"][0]["claim_amount"],
|
||||
"1250000.50",
|
||||
)
|
||||
self.assertEqual(
|
||||
payload["data"]["bankruptcy_procedures"][0]["case_number"],
|
||||
"А40-555/2026",
|
||||
|
||||
@@ -15,6 +15,7 @@ from apps.parsers.models import (
|
||||
)
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
from organizations.models import (
|
||||
Organization,
|
||||
OrganizationSourceExtension,
|
||||
@@ -88,6 +89,14 @@ class TestCompaniesCommandsTest(TestCase):
|
||||
).count(),
|
||||
20 * 4,
|
||||
)
|
||||
self.assertEqual(
|
||||
set(
|
||||
OrganizationSourceFinancialLine.objects.filter(
|
||||
source_record__extension__organization__in=companies
|
||||
).values_list("year", flat=True)
|
||||
),
|
||||
{timezone.localdate().year},
|
||||
)
|
||||
self.assertEqual(IndustrialCertificateRecord.objects.count(), 20)
|
||||
self.assertEqual(IndustrialProductRecord.objects.count(), 20)
|
||||
self.assertEqual(ManufacturerRecord.objects.count(), 20)
|
||||
|
||||
@@ -441,8 +441,8 @@ class ParserLoadLogServiceTest(TestCase):
|
||||
self.assertEqual(updated, 0)
|
||||
self.assertEqual(log.status, ParserLoadLog.Status.IN_PROGRESS)
|
||||
|
||||
def test_mark_stale_in_progress_failed_closes_precreated_job_without_batch(self):
|
||||
"""Pre-created source-card jobs without batch_id are still linked by source."""
|
||||
def test_mark_stale_in_progress_failed_keeps_recent_heartbeat_without_batch(self):
|
||||
"""A recent heartbeat keeps a pre-created source-card job alive."""
|
||||
log = ParserLoadLogFactory(
|
||||
source=ParserLoadLog.Source.INDUSTRIAL_PRODUCTS,
|
||||
batch_id=2,
|
||||
@@ -465,9 +465,9 @@ class ParserLoadLogServiceTest(TestCase):
|
||||
|
||||
log.refresh_from_db()
|
||||
job.refresh_from_db()
|
||||
self.assertEqual(updated, 1)
|
||||
self.assertEqual(log.status, ParserLoadLog.Status.FAILED)
|
||||
self.assertEqual(job.status, JobStatus.FAILURE)
|
||||
self.assertEqual(updated, 0)
|
||||
self.assertEqual(log.status, ParserLoadLog.Status.IN_PROGRESS)
|
||||
self.assertEqual(job.status, JobStatus.STARTED)
|
||||
|
||||
|
||||
@unittest.skip(
|
||||
|
||||
@@ -392,7 +392,7 @@ class SourceCardServiceUnitTest(SimpleTestCase):
|
||||
)
|
||||
stale_in_progress_load = SimpleNamespace(
|
||||
status="in_progress",
|
||||
updated_at=timezone.now() - timedelta(hours=3),
|
||||
updated_at=timezone.now() - timedelta(hours=5),
|
||||
)
|
||||
self.assertEqual(
|
||||
SourceCardService._get_status(
|
||||
@@ -618,7 +618,7 @@ class SourceCardServiceDatabaseTest(TestCase):
|
||||
)
|
||||
self.assertEqual(procurements_card["records_count"], 1)
|
||||
|
||||
def test_get_active_tasks_ignores_old_jobs_even_when_updated_recently(self):
|
||||
def test_get_active_tasks_keeps_old_job_with_recent_heartbeat(self):
|
||||
job = BackgroundJob.objects.create(
|
||||
task_id="old-source-task",
|
||||
task_name="apps.parsers.tasks.parse_industrial_products",
|
||||
@@ -637,7 +637,8 @@ class SourceCardServiceDatabaseTest(TestCase):
|
||||
SourceCardService.get_definition("manufacturers-and-products")
|
||||
)
|
||||
|
||||
self.assertEqual(tasks, [])
|
||||
self.assertEqual(len(tasks), 1)
|
||||
self.assertEqual(tasks[0]["task_id"], job.task_id)
|
||||
|
||||
def test_get_active_tasks_keeps_recent_pending_jobs(self):
|
||||
BackgroundJob.objects.create(
|
||||
|
||||
@@ -133,6 +133,11 @@ class SourcesApiE2ETest(APITestCase):
|
||||
self.assertEqual(minprom_card["records_count"], 3)
|
||||
self.assertEqual(minprom_card["organizations_count"], 1)
|
||||
self.assertEqual(len(minprom_card["source_items"]), 3)
|
||||
minprom_items = {item["code"]: item for item in minprom_card["source_items"]}
|
||||
self.assertEqual(
|
||||
minprom_items["industrial_products"]["refresh_key"],
|
||||
"mpt_products",
|
||||
)
|
||||
|
||||
statuses = {item["slug"]: item for item in statuses_response.data["data"]}
|
||||
self.assertEqual(statuses["planned-inspections"]["progress"], 55)
|
||||
|
||||
@@ -1086,9 +1086,16 @@ class GenericSourceFetchTestCase(TestCase):
|
||||
) as jobs_cleanup_mock:
|
||||
result = parser_tasks.cleanup_stale_parser_loads(max_age_minutes=45)
|
||||
|
||||
cleanup_mock.assert_called_once_with(max_age_minutes=45)
|
||||
cleanup_mock.assert_called_once_with(
|
||||
max_age_minutes=45,
|
||||
pending_max_age_minutes=24 * 60,
|
||||
)
|
||||
jobs_cleanup_mock.assert_called_once()
|
||||
self.assertEqual(jobs_cleanup_mock.call_args.kwargs["max_age_minutes"], 45)
|
||||
self.assertEqual(
|
||||
jobs_cleanup_mock.call_args.kwargs["pending_max_age_minutes"],
|
||||
24 * 60,
|
||||
)
|
||||
self.assertIn(
|
||||
"apps.parsers.tasks.scan_fns_directory",
|
||||
jobs_cleanup_mock.call_args.kwargs["task_names"],
|
||||
@@ -1101,6 +1108,7 @@ class GenericSourceFetchTestCase(TestCase):
|
||||
self.assertEqual(result["marked_failed"], 2)
|
||||
self.assertEqual(result["marked_jobs_failed"], 3)
|
||||
self.assertEqual(result["max_age_minutes"], 45)
|
||||
self.assertEqual(result["pending_max_age_minutes"], 24 * 60)
|
||||
|
||||
def test_get_or_create_background_job_merges_meta_for_precreated_job(self):
|
||||
BackgroundJobService.create_job(
|
||||
|
||||
@@ -11,6 +11,7 @@ import zipfile
|
||||
from datetime import date
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from apps.core.models import BackgroundJob
|
||||
from apps.parsers.models import (
|
||||
FinancialReport,
|
||||
FinancialReportLine,
|
||||
@@ -1411,3 +1412,19 @@ class ParsersViewSetTest(APITestCase):
|
||||
for key, value in payload.items():
|
||||
self.assertEqual(task_kwargs[key], value)
|
||||
self.assertEqual(task_kwargs["requested_by_id"], self.user.id)
|
||||
|
||||
def test_run_industrial_products_uses_legacy_alias(self):
|
||||
self.client.force_authenticate(self.user)
|
||||
url = reverse("api_v1:parsers:run-parser", args=["industrial_products"])
|
||||
|
||||
with patch(
|
||||
"apps.parsers.views.tasks.parse_industrial_products.apply_async",
|
||||
return_value=Mock(id="task-products"),
|
||||
) as apply_async_mock:
|
||||
response = self.client.post(url, {}, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
|
||||
apply_async_mock.assert_called_once()
|
||||
queued_task_id = apply_async_mock.call_args.kwargs["task_id"]
|
||||
job = BackgroundJob.objects.get(task_id=queued_task_id)
|
||||
self.assertEqual(job.meta["source_key"], "mpt_products")
|
||||
|
||||
Reference in New Issue
Block a user