feat(registry): add new endpoints for registers, exchange, and backups; update routing and configurations
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 3m10s
CI/CD Pipeline / Run Tests (push) Successful in 3m35s
CI/CD Pipeline / Telegram Notify Success (push) Has been skipped
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m26s
CI/CD Pipeline / Run Tests (pull_request) Successful in 2m46s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped

This commit is contained in:
2026-03-04 15:36:57 +01:00
parent 052389d921
commit a91ed1f1ae
90 changed files with 5488 additions and 622 deletions

View File

@@ -1,9 +1,5 @@
"""Tests for parsers admin configurations."""
from django.contrib.admin.sites import AdminSite
from django.contrib.messages.storage.fallback import FallbackStorage
from django.test import RequestFactory, TestCase
from apps.parsers.admin import (
FinancialReportAdmin,
HasCertificateNumberFilter,
@@ -24,6 +20,10 @@ from apps.parsers.models import (
ProcurementRecord,
Proxy,
)
from django.contrib.admin.sites import AdminSite
from django.contrib.messages.storage.fallback import FallbackStorage
from django.test import RequestFactory, TestCase
from tests.apps.parsers.factories import (
IndustrialCertificateRecordFactory,
InspectionRecordFactory,
@@ -34,7 +34,6 @@ from tests.apps.parsers.factories import (
from tests.apps.user.factories import UserFactory
from tests.utils.fixtures import fake
_CYRILLIC_FINISHED = "\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d"
_CYRILLIC_PUBLISHED = "\u043e\u043f\u0443\u0431\u043b\u0438\u043a"
@@ -121,7 +120,9 @@ class ParsersAdminTest(TestCase):
filter_none = HasCertificateNumberFilter(
request, {}, IndustrialCertificateRecord, admin
)
qs_none = filter_none.queryset(request, IndustrialCertificateRecord.objects.all())
qs_none = filter_none.queryset(
request, IndustrialCertificateRecord.objects.all()
)
self.assertIn(record_good, qs_none)
def test_manufacturer_admin_helpers(self):
@@ -141,13 +142,17 @@ class ParsersAdminTest(TestCase):
record = InspectionRecordFactory(
organisation_name="Org" * 30,
control_authority="Auth" * 20,
status=f"{_CYRILLIC_FINISHED}"
status=f"{_CYRILLIC_FINISHED}",
)
self.assertTrue(admin.organisation_name_short(record).endswith("..."))
self.assertTrue(admin.control_authority_short(record).endswith("..."))
self.assertIn("span", str(admin.status_badge(record)))
record_progress = InspectionRecordFactory(status="\u0432 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0435")
record_cancel = InspectionRecordFactory(status="\u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430")
record_progress = InspectionRecordFactory(
status="\u0432 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0435"
)
record_cancel = InspectionRecordFactory(
status="\u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430"
)
record_other = InspectionRecordFactory(status="unknown")
self.assertIn("span", str(admin.status_badge(record_progress)))
self.assertIn("span", str(admin.status_badge(record_cancel)))

View File

@@ -3,49 +3,47 @@
from __future__ import annotations
import json
import requests
from urllib.parse import parse_qs
from django.test import SimpleTestCase
from requests.adapters import BaseAdapter
import requests
from apps.parsers.clients.checko import (
CheckoClient,
BankRequest,
CaseRole,
CheckoAPIError,
CheckoClient,
CheckoConnectionError,
CheckoNotFoundError,
CheckoRateLimitError,
CheckoValidationError,
CheckoConnectionError,
BankRequest,
CompanyRequest,
ContractLaw,
ContractRole,
ContractsRequest,
EntrepreneurRequest,
EnforcementsRequest,
EntrepreneurRequest,
FinancesRequest,
InspectionsRequest,
LegalCasesRequest,
ObjectType,
PersonRequest,
SearchRequest,
CaseRole,
ContractRole,
SearchType,
ObjectType,
ContractLaw,
SortOrder,
)
from apps.parsers.clients.checko.datasets import (
OKVED2,
OKFS,
OKOPF,
OKPD,
OKPD2,
OKVED2,
AccountCodes,
CompanyStatuses,
EntrepreneurStatuses,
)
from django.test import SimpleTestCase
from requests.adapters import BaseAdapter
from tests.utils import TestHTTPServer, Response
from tests.utils import Response, TestHTTPServer
from tests.utils.fixtures import fake
@@ -125,7 +123,9 @@ class CheckoClientValidationTest(SimpleTestCase):
def test_search_request_min_query_length(self):
with self.assertRaises(CheckoValidationError) as context:
self.client.search(
SearchRequest(by=SearchType.NAME, obj=ObjectType.ORGANIZATION, query="abc")
SearchRequest(
by=SearchType.NAME, obj=ObjectType.ORGANIZATION, query="abc"
)
)
self.assertIn("4", str(context.exception))
@@ -241,6 +241,7 @@ class CheckoRequestParamsTest(SimpleTestCase):
bank = BankRequest(bic=_digits(9))
self.assertEqual(bank.to_params()["bic"], bank.bic)
class CheckoClientApiTest(SimpleTestCase):
def test_get_company_success(self):
inn = "".join(str(fake.random_int(0, 9)) for _ in range(10))
@@ -279,7 +280,11 @@ class CheckoClientApiTest(SimpleTestCase):
)
client = _client_for(server)
with self.assertRaises(CheckoNotFoundError):
client.get_company(CompanyRequest(inn="".join(str(fake.random_int(0, 9)) for _ in range(10))))
client.get_company(
CompanyRequest(
inn="".join(str(fake.random_int(0, 9)) for _ in range(10))
)
)
def test_get_entrepreneur_success(self):
ogrnip = "".join(str(fake.random_int(0, 9)) for _ in range(15))
@@ -468,7 +473,11 @@ class CheckoClientApiTest(SimpleTestCase):
)
client = _client_for(server)
with self.assertRaises(CheckoAPIError):
client.get_company(CompanyRequest(inn="".join(str(fake.random_int(0, 9)) for _ in range(10))))
client.get_company(
CompanyRequest(
inn="".join(str(fake.random_int(0, 9)) for _ in range(10))
)
)
def test_rate_limit_error_handling(self):
with TestHTTPServer() as server:
@@ -478,7 +487,11 @@ class CheckoClientApiTest(SimpleTestCase):
)
client = _client_for(server)
with self.assertRaises(CheckoRateLimitError):
client.get_company(CompanyRequest(inn="".join(str(fake.random_int(0, 9)) for _ in range(10))))
client.get_company(
CompanyRequest(
inn="".join(str(fake.random_int(0, 9)) for _ in range(10))
)
)
def test_not_found_error_handling(self):
with TestHTTPServer() as server:
@@ -488,7 +501,11 @@ class CheckoClientApiTest(SimpleTestCase):
)
client = _client_for(server)
with self.assertRaises(CheckoNotFoundError):
client.get_company(CompanyRequest(inn="".join(str(fake.random_int(0, 9)) for _ in range(10))))
client.get_company(
CompanyRequest(
inn="".join(str(fake.random_int(0, 9)) for _ in range(10))
)
)
class CheckoClientExtraEndpointsTest(SimpleTestCase):
@@ -564,9 +581,7 @@ class CheckoClientExtraEndpointsTest(SimpleTestCase):
with TestHTTPServer() as server:
server.add_route("GET", "/v2/inspections", inspections_handler)
client = _client_for(server)
inspections = list(
client.iter_inspections(InspectionsRequest(inn=inn))
)
inspections = list(client.iter_inspections(InspectionsRequest(inn=inn)))
self.assertTrue(inspections)
@@ -599,9 +614,7 @@ class CheckoClientExtraEndpointsTest(SimpleTestCase):
with TestHTTPServer() as server:
server.add_route("GET", "/v2/inspections", inspections_handler)
client = _client_for(server)
inspections = list(
client.iter_inspections(InspectionsRequest(inn=inn))
)
inspections = list(client.iter_inspections(InspectionsRequest(inn=inn)))
self.assertEqual(len(inspections), 1)
@@ -642,9 +655,7 @@ class CheckoClientExtraEndpointsTest(SimpleTestCase):
page = int(params.get("page", ["1"])[0])
payload = {
"data": {
"enforcements": [
{"number": fake.bothify(text="##-####")}
],
"enforcements": [{"number": fake.bothify(text="##-####")}],
"pagination": {
"total_records": 2,
"total_pages": 2,
@@ -664,9 +675,7 @@ class CheckoClientExtraEndpointsTest(SimpleTestCase):
with TestHTTPServer() as server:
server.add_route("GET", "/v2/enforcements", enforcements_handler)
client = _client_for(server)
enforcements = list(
client.iter_enforcements(EnforcementsRequest(inn=inn))
)
enforcements = list(client.iter_enforcements(EnforcementsRequest(inn=inn)))
self.assertTrue(enforcements)
@@ -676,9 +685,7 @@ class CheckoClientExtraEndpointsTest(SimpleTestCase):
def enforcements_handler(_req, _body):
payload = {
"data": {
"enforcements": [
{"number": fake.bothify(text="##-####")}
],
"enforcements": [{"number": fake.bothify(text="##-####")}],
"pagination": {
"total_records": 1,
"total_pages": 1,
@@ -696,9 +703,7 @@ class CheckoClientExtraEndpointsTest(SimpleTestCase):
with TestHTTPServer() as server:
server.add_route("GET", "/v2/enforcements", enforcements_handler)
client = _client_for(server)
enforcements = list(
client.iter_enforcements(EnforcementsRequest(inn=inn))
)
enforcements = list(client.iter_enforcements(EnforcementsRequest(inn=inn)))
self.assertEqual(len(enforcements), 1)

View File

@@ -2,9 +2,9 @@
from __future__ import annotations
from apps.parsers.clients.checko.client import CheckoClient, _map_ru_keys
from django.test import SimpleTestCase
from apps.parsers.clients.checko.client import CheckoClient, _map_ru_keys
from tests.utils.fixtures import fake
@@ -32,7 +32,10 @@ class CheckoClientParsingTest(SimpleTestCase):
self.assertEqual(mapped["inn"], data["\u0418\u041d\u041d"])
self.assertIn("legal_address", mapped)
self.assertIn("records", mapped)
self.assertEqual(mapped["records"][0]["ogrn"], data["\u0417\u0430\u043f\u0438\u0441\u0438"][0]["\u041e\u0413\u0420\u041d"])
self.assertEqual(
mapped["records"][0]["ogrn"],
data["\u0417\u0430\u043f\u0438\u0441\u0438"][0]["\u041e\u0413\u0420\u041d"],
)
def test_parse_okved_info_with_additional(self):
info = self.client._parse_okved_info(
@@ -96,7 +99,11 @@ class CheckoClientParsingTest(SimpleTestCase):
"reg_date": str(fake.date()),
"short_name": fake.company(),
"full_name": fake.company(),
"status": {"code": "100", "name": "Active", "record_date": str(fake.date())},
"status": {
"code": "100",
"name": "Active",
"record_date": str(fake.date()),
},
"legal_address": {
"full_address": fake.address(),
"region": {"code": "77", "name": "Moscow"},
@@ -168,7 +175,9 @@ class CheckoClientParsingTest(SimpleTestCase):
"full_name": fake.company(),
}
],
"branches": {"branch": [{"full_name": fake.company(), "address": fake.address()}]},
"branches": {
"branch": [{"full_name": fake.company(), "address": fake.address()}]
},
"licenses": [{"number": "L-1", "activities": [fake.word()]}],
"trademarks": [{"id": 1, "url": fake.url()}],
"tax_debt": {"total": 123, "date": str(fake.date())},
@@ -239,9 +248,7 @@ class CheckoClientParsingTest(SimpleTestCase):
"companies_as_founder": [
{"ogrn": _digits(13), "short_name": fake.company()}
],
"entrepreneurs": [
{"ogrnip": _digits(15), "full_name": fake.name()}
],
"entrepreneurs": [{"ogrnip": _digits(15), "full_name": fake.name()}],
}
)
self.assertEqual(len(person.disqualifications), 1)

View File

@@ -5,8 +5,6 @@ from __future__ import annotations
from urllib.parse import urlparse
import requests
from requests.adapters import BaseAdapter
from apps.parsers.clients.base import (
BaseHTTPClient,
ConnectionError,
@@ -19,6 +17,7 @@ from apps.parsers.clients.minpromtorg.schemas import IndustrialCertificate, Manu
from apps.parsers.clients.proverki import ProverkiClient
from apps.parsers.clients.proverki.schemas import Inspection
from django.test import TestCase, tag
from requests.adapters import BaseAdapter
from tests.utils import Response, TestHTTPServer
from tests.utils.fixtures import (
@@ -110,7 +109,9 @@ class BaseHTTPClientTest(TestCase):
with TestHTTPServer() as server:
server.add_route("POST", "/echo", echo_handler)
server.add_route("GET", "/missing", lambda _req, _body: Response(status=404))
server.add_route(
"GET", "/missing", lambda _req, _body: Response(status=404)
)
client = BaseHTTPClient(base_url=server.base_url, adapter=server.adapter)
result = client.post("/echo", data=b"ping")
self.assertEqual(result, b"ping")
@@ -123,7 +124,9 @@ class BaseHTTPClientTest(TestCase):
def test_download_file_error(self):
with TestHTTPServer() as server:
server.add_route("GET", "/missing.bin", lambda _req, _body: Response(status=404))
server.add_route(
"GET", "/missing.bin", lambda _req, _body: Response(status=404)
)
client = BaseHTTPClient(base_url=server.base_url, adapter=server.adapter)
with self.assertRaises(HTTPError):
client.download_file("/missing.bin")
@@ -136,7 +139,9 @@ class BaseHTTPClientTest(TestCase):
def test_context_manager_closes_session(self):
with TestHTTPServer() as server:
server.add_json("/ping", {"ok": True})
with BaseHTTPClient(base_url=server.base_url, adapter=server.adapter) as client:
with BaseHTTPClient(
base_url=server.base_url, adapter=server.adapter
) as client:
client.get_json("/ping")
self.assertIsNotNone(client._session)
self.assertIsNone(client._session)
@@ -280,10 +285,7 @@ class IndustrialProductionClientTest(TestCase):
def test_get_latest_file_url_selects_newest(self):
client = IndustrialProductionClient()
dates = sorted(
{
fake.date_between(start_date="-90d", end_date="today")
for _ in range(3)
}
{fake.date_between(start_date="-90d", end_date="today") for _ in range(3)}
)
files = []
for date in dates:
@@ -397,10 +399,7 @@ class ManufacturesClientTest(TestCase):
def test_get_latest_file_url_selects_newest(self):
client = ManufacturesClient()
dates = sorted(
{
fake.date_between(start_date="-90d", end_date="today")
for _ in range(3)
}
{fake.date_between(start_date="-90d", end_date="today") for _ in range(3)}
)
files = []
for date in dates:
@@ -565,7 +564,7 @@ class ProverkiClientTest(TestCase):
f"<КонтрольныйОрган>{authority}</КонтрольныйОрган>"
"</КНМ>"
"</Проверки>"
).encode("utf-8")
).encode()
inspections = client._parse_xml_content(xml_content, None)
@@ -580,7 +579,7 @@ class ProverkiClientTest(TestCase):
row_inn = "".join(str(fake.random_int(0, 9)) for _ in range(10))
reg_num = "".join(str(fake.random_int(0, 9)) for _ in range(12))
element = ET.fromstring(
f"<inspection inn=\"{row_inn}\" registration_number=\"{reg_num}\" />"
f'<inspection inn="{row_inn}" registration_number="{reg_num}" />'
) # noqa: S314
client = ProverkiClient()
@@ -602,7 +601,7 @@ class ProverkiClientTest(TestCase):
inn = "".join(str(fake.random_int(0, 9)) for _ in range(10))
reg_num = "".join(str(fake.random_int(0, 9)) for _ in range(12))
xml_content = (
"<?xml version=\"1.0\" encoding=\"windows-1251\"?>"
'<?xml version="1.0" encoding="windows-1251"?>'
"<inspections>"
"<inspection>"
f"<inn>{inn}</inn>"

View File

@@ -27,7 +27,9 @@ def _build_fns_excel_bytes() -> bytes:
year = fake.random_int(min=2020, max=2025)
ws.append(["Форма №1", None, year, None])
ws.append([None, "Код", "Начало", "Конец"])
ws.append([fake.word(), _digits(4), fake.random_int(10, 999), fake.random_int(10, 999)])
ws.append(
[fake.word(), _digits(4), fake.random_int(10, 999), fake.random_int(10, 999)]
)
buf = io.BytesIO()
wb.save(buf)
wb.close()
@@ -80,9 +82,7 @@ class FNSUploadIntegrationTest(APITestCase):
report = FinancialReport.objects.first()
self.assertEqual(report.external_id, external_id)
self.assertEqual(report.ogrn, ogrn)
self.assertTrue(
FinancialReportLine.objects.filter(report=report).exists()
)
self.assertTrue(FinancialReportLine.objects.filter(report=report).exists())
processed_path = os.path.join(processed_dir, filename)
self.assertTrue(os.path.exists(processed_path))

View File

@@ -1,13 +1,17 @@
"""Tests for parsers services."""
from urllib.parse import urlparse
from apps.parsers.clients.minpromtorg.industrial import IndustrialProductionClient
from apps.parsers.clients.minpromtorg.schemas import IndustrialCertificate, Manufacturer
from apps.parsers.clients.proverki.schemas import Inspection
from apps.parsers.clients.zakupki.schemas import Procurement
from apps.parsers.models import (
IndustrialCertificateRecord,
InspectionRecord,
ManufacturerRecord,
ParserLoadLog,
ProcurementRecord,
Proxy,
)
from apps.parsers.services import (
@@ -15,12 +19,14 @@ from apps.parsers.services import (
InspectionService,
ManufacturerService,
ParserLoadLogService,
ProcurementService,
ProxyService,
)
from apps.registers.models import Organization
from django.test import TestCase, tag
from tests.utils import TestHTTPServer
from tests.utils.fixtures import build_minpromtorg_certificates_excel, fake
from urllib.parse import urlparse
from .factories import (
IndustrialCertificateRecordFactory,
@@ -38,6 +44,17 @@ def _digits(length: int) -> str:
def _proxy_address() -> str:
return f"http://{fake.ipv4()}:{fake.port_number()}"
def _create_registry_organization(*, inn: str, ogrn: str) -> Organization:
return Organization.objects.create(
pn_name=fake.company(),
mn_ogrn=int(ogrn),
mn_inn=int(inn),
in_kpp=int(_digits(9)),
mn_okpo=_digits(8),
)
class ProxyServiceTest(TestCase):
"""Tests for ProxyService."""
@@ -248,6 +265,59 @@ class IndustrialCertificateServiceTest(TestCase):
self.assertEqual(count, 5)
self.assertEqual(IndustrialCertificateRecord.objects.count(), 5)
record = IndustrialCertificateRecord.objects.first()
self.assertIsNotNone(record.issue_date_normalized)
self.assertIsNotNone(record.expiry_date_normalized)
def test_save_certificates_links_registry_organization_when_exists(self):
"""Test linking to registers organization is created when identifiers match."""
inn = _digits(10)
ogrn = _digits(13)
organization = _create_registry_organization(inn=inn, ogrn=ogrn)
certificate_number = fake.bothify(text="??-####-#####")
certificates = [
IndustrialCertificate(
issue_date=str(fake.date()),
certificate_number=certificate_number,
expiry_date=str(fake.date()),
certificate_file_url=fake.url(),
organisation_name=fake.company(),
inn=inn,
ogrn=ogrn,
)
]
saved = IndustrialCertificateService.save_certificates(certificates, batch_id=1)
self.assertEqual(saved, 1)
record = IndustrialCertificateRecord.objects.get(
certificate_number=certificate_number
)
self.assertEqual(record.registry_organization_id, organization.id)
def test_save_certificates_keeps_null_registry_organization_when_not_found(self):
"""Test parser save does not fail and keeps null when organization is absent."""
certificate_number = fake.bothify(text="??-####-#####")
certificates = [
IndustrialCertificate(
issue_date=str(fake.date()),
certificate_number=certificate_number,
expiry_date=str(fake.date()),
certificate_file_url=fake.url(),
organisation_name=fake.company(),
inn=_digits(10),
ogrn=_digits(13),
)
]
saved = IndustrialCertificateService.save_certificates(certificates, batch_id=1)
self.assertEqual(saved, 1)
record = IndustrialCertificateRecord.objects.get(
certificate_number=certificate_number
)
self.assertIsNone(record.registry_organization_id)
def test_save_certificates_with_chunk_size(self):
"""Test saving certificates in chunks."""
@@ -287,16 +357,16 @@ class IndustrialCertificateServiceTest(TestCase):
results = IndustrialCertificateService.find_by_inn(inn_a)
self.assertEqual(results.count(), 2)
results_batch1 = IndustrialCertificateService.find_by_inn(
inn_a, batch_id=1
)
results_batch1 = IndustrialCertificateService.find_by_inn(inn_a, batch_id=1)
self.assertEqual(results_batch1.count(), 1)
def test_find_by_certificate_number(self):
"""Test finding certificate by number."""
unique_number = fake.bothify(text="CERT-#####")
IndustrialCertificateRecordFactory(certificate_number=unique_number)
IndustrialCertificateRecordFactory(certificate_number=fake.bothify(text="CERT-#####"))
IndustrialCertificateRecordFactory(
certificate_number=fake.bothify(text="CERT-#####")
)
results = IndustrialCertificateService.find_by_certificate_number(unique_number)
self.assertEqual(results.count(), 1)
@@ -332,9 +402,10 @@ class IndustrialCertificateServiceTest(TestCase):
ogrn=_digits(13),
)
]
IndustrialCertificateService.save_certificates(duplicate, batch_id=2)
count2 = IndustrialCertificateService.save_certificates(duplicate, batch_id=2)
# Should still be 1 record (duplicate skipped)
self.assertEqual(count2, 0)
self.assertEqual(IndustrialCertificateRecord.objects.count(), 1)
# Verify original data preserved
@@ -369,6 +440,27 @@ class ManufacturerServiceTest(TestCase):
self.assertEqual(count, 5)
self.assertEqual(ManufacturerRecord.objects.count(), 5)
def test_save_manufacturers_links_registry_organization_when_exists(self):
"""Test linking manufacturer to registers organization by INN/ОГРН."""
inn = _digits(10)
ogrn = _digits(13)
organization = _create_registry_organization(inn=inn, ogrn=ogrn)
manufacturers = [
Manufacturer(
full_legal_name=fake.company(),
inn=inn,
ogrn=ogrn,
address=fake.address().replace("\n", ", "),
)
]
saved = ManufacturerService.save_manufacturers(manufacturers, batch_id=1)
self.assertEqual(saved, 1)
record = ManufacturerRecord.objects.get(inn=inn)
self.assertEqual(record.registry_organization_id, organization.id)
def test_save_manufacturers_with_chunk_size(self):
"""Test saving manufacturers in chunks."""
manufacturers = [
@@ -448,9 +540,10 @@ class ManufacturerServiceTest(TestCase):
address=fake.address().replace("\n", ", "),
)
]
ManufacturerService.save_manufacturers(duplicate, batch_id=2)
count2 = ManufacturerService.save_manufacturers(duplicate, batch_id=2)
# Should still be 1 record (duplicate skipped)
self.assertEqual(count2, 0)
self.assertEqual(ManufacturerRecord.objects.count(), 1)
# Verify original data preserved
@@ -493,6 +586,39 @@ class InspectionServiceTest(TestCase):
self.assertEqual(count, 5)
self.assertEqual(InspectionRecord.objects.count(), 5)
record = InspectionRecord.objects.first()
self.assertIsNotNone(record.start_date_normalized)
self.assertIsNotNone(record.end_date_normalized)
def test_save_inspections_links_registry_organization_when_exists(self):
"""Test linking inspection to registers organization by INN/ОГРН."""
inn = _digits(10)
ogrn = _digits(13)
organization = _create_registry_organization(inn=inn, ogrn=ogrn)
registration_number = _digits(12)
inspections = [
Inspection(
registration_number=registration_number,
inn=inn,
ogrn=ogrn,
organisation_name=fake.company(),
control_authority=fake.company(),
inspection_type=fake.word(),
inspection_form=fake.word(),
start_date=str(fake.date()),
end_date=str(fake.date()),
status=fake.word(),
legal_basis=fake.sentence(nb_words=3),
result=fake.sentence(nb_words=3),
)
]
saved = InspectionService.save_inspections(inspections, batch_id=1)
self.assertEqual(saved, 1)
record = InspectionRecord.objects.get(registration_number=registration_number)
self.assertEqual(record.registry_organization_id, organization.id)
def test_save_inspections_with_chunk_size(self):
"""Test saving inspections in chunks."""
@@ -612,9 +738,10 @@ class InspectionServiceTest(TestCase):
result=fake.sentence(nb_words=3),
)
]
InspectionService.save_inspections(duplicate, batch_id=2)
count2 = InspectionService.save_inspections(duplicate, batch_id=2)
# Should still be 1 record (duplicate skipped)
self.assertEqual(count2, 0)
self.assertEqual(InspectionRecord.objects.count(), 1)
# Verify original data preserved
@@ -626,6 +753,57 @@ class InspectionServiceTest(TestCase):
self.assertEqual(record.load_batch, 1) # Original batch
class ProcurementServiceTest(TestCase):
"""Tests for ProcurementService."""
def _build_procurement(self, **overrides) -> Procurement:
data = {
"purchase_number": _digits(19),
"purchase_name": fake.sentence(nb_words=4),
"customer_inn": _digits(10),
"customer_kpp": _digits(9),
"customer_ogrn": _digits(13),
"customer_name": fake.company(),
"max_price": "1 234 567,89",
"currency_code": "RUB",
"placement_method": fake.word(),
"publish_date": "01.03.2026",
"end_date": "2026-03-15",
"status": fake.word(),
"law_type": "44-FZ",
"purchase_object_info": fake.sentence(nb_words=4),
"href": fake.url(),
}
data.update(overrides)
return Procurement(**data)
def test_save_procurements_sets_normalized_fields(self):
procurement = self._build_procurement()
saved = ProcurementService.save_procurements([procurement], batch_id=1)
self.assertEqual(saved, 1)
record = ProcurementRecord.objects.get(purchase_number=procurement.purchase_number)
self.assertEqual(str(record.max_price_amount), "1234567.89")
self.assertEqual(str(record.publish_date_normalized), "2026-03-01")
self.assertEqual(str(record.end_date_normalized), "2026-03-15")
def test_save_procurements_duplicate_returns_zero(self):
purchase_number = _digits(19)
first = self._build_procurement(purchase_number=purchase_number)
duplicate = self._build_procurement(
purchase_number=purchase_number,
customer_name=fake.company(),
)
saved_first = ProcurementService.save_procurements([first], batch_id=1)
saved_second = ProcurementService.save_procurements([duplicate], batch_id=2)
self.assertEqual(saved_first, 1)
self.assertEqual(saved_second, 0)
self.assertEqual(ProcurementRecord.objects.count(), 1)
@tag("integration", "slow", "e2e")
class EndToEndIntegrationTest(TestCase):
"""
@@ -668,9 +846,7 @@ class EndToEndIntegrationTest(TestCase):
)
server.add_bytes(f"/files/{file_name}", excel_bytes)
host = urlparse(server.base_url)
client_host = (
f"{host.hostname}:{host.port}" if host.port else host.hostname
)
client_host = f"{host.hostname}:{host.port}" if host.port else host.hostname
with IndustrialProductionClient(
host=client_host,
scheme="http",

View File

@@ -6,13 +6,13 @@ import io
import os
import tempfile
from apps.parsers.models import FinancialReport, FinancialReportLine, ProcurementRecord
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from openpyxl import Workbook
from rest_framework import status
from rest_framework.test import APITestCase
from apps.parsers.models import FinancialReport, FinancialReportLine, ProcurementRecord
from tests.apps.parsers.factories import (
IndustrialCertificateRecordFactory,
InspectionRecordFactory,
@@ -34,7 +34,9 @@ def _build_fns_excel_bytes() -> bytes:
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)])
ws.append(
[fake.word(), _digits(4), fake.random_int(10, 999), fake.random_int(10, 999)]
)
buf = io.BytesIO()
wb.save(buf)
wb.close()
@@ -130,6 +132,7 @@ class ParsersViewSetTest(APITestCase):
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)
detail = self.client.get(
reverse("api_v1:fns:fns-reports-detail", args=[report.id])
)
@@ -184,5 +187,7 @@ class ParsersViewSetTest(APITestCase):
FNS_FAILED_DIRECTORY=failed_dir,
):
url = reverse("api_v1:fns:fns-upload")
response = self.client.post(url, {"files": [upload]}, format="multipart")
response = self.client.post(
url, {"files": [upload]}, format="multipart"
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)