feat: complete published registry contracts and gated SRO ingestion
All checks were successful
Mostovik Backend CI/CD / Tests and lint (push) Successful in 3m55s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Successful in 3m43s
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped
Mostovik Backend CI/CD / Deploy dev (push) Successful in 1m45s

This commit is contained in:
Aleksandr Meshchryakov
2026-09-14 17:01:02 +02:00
parent 49cbfd265c
commit 18971d33ec
76 changed files with 33156 additions and 311 deletions

View File

@@ -0,0 +1,193 @@
"""Render synthetic API responses for the generated Orval/Zod contract gate."""
import json
import os
from pathlib import Path
from uuid import uuid4
import pytest
from apps.parsers.budget_registry import normalize_budget_record
from apps.parsers.models import ParserSourceArtifact
from apps.parsers.sme_support import support_payload
from apps.parsers.sro_membership import normalize_sro_membership
from django.urls import reverse
from django.utils import timezone
from lxml import etree
from organizations.models import Organization, OrganizationSourceRecord
from organizations.source_groups import get_source_group_descriptor
from rest_framework.test import APIClient
from tests.apps.parsers.test_registry_payload_contract import full_budget_fixture
from tests.apps.parsers.test_registry_snapshots import _xml
from tests.apps.user.factories import UserFactory
def _create_record(organization, source, normalized):
descriptor = get_source_group_descriptor(source)
extension = descriptor.extension_model.objects.create(organization=organization)
ParserSourceArtifact.objects.create(
source=source,
load_batch=9001,
status=ParserSourceArtifact.Status.PUBLISHED,
source_published_at="2026-08-15",
version="4.04" if source == "fns_sme_support_recipients" else "2026-08-15",
sha256="a" * 64,
original_name="synthetic-contract-fixture.zip",
published_count=1,
)
return OrganizationSourceRecord.objects.create(
extension=extension,
source=source,
record_type=descriptor.record_type,
external_id=f"synthetic-{source}",
load_batch=9001,
**normalized,
)
def _canonical_records():
organization = Organization.objects.create(
name="АО Синтетическая организация",
full_name="Акционерное общество Синтетическая организация",
inn="1234567890",
ogrn="1027700132195",
okpo="00123456",
directory_imported_at=timezone.now(),
opk_registry_membership=False,
)
raw_budget = full_budget_fixture()
# Unknown nested keys, leading zeroes, booleans and nulls must survive Zod.
raw_budget["future_block"] = [
{"future_key": {"code": "0007", "nested": [None, False, 0, {"keep": True}]}}
]
budget = _create_record(
organization, "budget_ubpandnubp", normalize_budget_record(raw_budget)
)
xml = etree.fromstring( # noqa: S320 - local synthetic fixture without entities
_xml(numbers=("synthetic-support",), units=("1", "2", "3", "4", "5"))
)
payload, amount = support_payload(
"synthetic-document", xml.find("Документ/СвПредПод"), "2026-08-15"
)
sme = _create_record(
organization,
"fns_sme_support_recipients",
{
"title": "Синтетическая мера поддержки",
"record_date": payload["decision_date"],
"amount": amount,
"status": "",
"url": "https://www.nalog.gov.ru/opendata/7707329152-rsmppp/",
"payload": payload,
},
)
sro = _create_record(
organization,
"sro_membership_check",
normalize_sro_membership(
{
"name": organization.name,
"full_name": organization.full_name,
"inn": organization.inn,
"ogrn": organization.ogrn,
"region": "Синтетический регион",
"sro_name": "Синтетическая СРО",
"membership_status_raw": "Является членом",
},
organization,
sro_id="90001",
sro_url="https://www.reestr-sro.ru/test-fixtures/sro-id-90001/",
source_url="https://www.reestr-sro.ru/test-fixtures/lookup/",
source_registry_date="2026-08-15",
admitted=None,
missing_reason="not_found",
resolution="row_link",
lookup_key="ogrn",
lookup_value=organization.ogrn,
),
)
return [budget, sme, sro], raw_budget
@pytest.mark.django_db
def test_source_record_runtime_fixtures():
client = APIClient()
client.force_authenticate(UserFactory.create_user())
records, raw_budget = _canonical_records()
list_url = reverse("api_v2:organizations:organization-source-records-list")
cases = []
def capture(name, operation, response, status=200):
assert response.status_code == status, (name, response.status_code)
# Rendered JSON is the actual wire representation, not DRF's .data.
body = response.json()
cases.append(
{"name": name, "operation": operation, "status": status, "body": body}
)
return body
mixed = capture("mixed-list", "list", client.get(list_url))
assert len(mixed["data"]) == 3
assert mixed["meta"]["pagination"]["total_count"] == 3
details = {}
for record in records:
listing = capture(
f"{record.source}-list",
"list",
client.get(list_url, {"source": record.source}),
)
assert len(listing["data"]) == 1
detail_url = reverse(
"api_v2:organizations:organization-source-records-detail",
kwargs={"uid": record.uid},
)
details[record.source] = capture(
f"{record.source}-detail", "detail", client.get(detail_url)
)
budget_payload = details["budget_ubpandnubp"]["payload"]
assert budget_payload["upstream"] == raw_budget
assert (
budget_payload["unclassified_blocks"]["future_block"]
== raw_budget["future_block"]
)
assert len(details["fns_sme_support_recipients"]["payload"]["support_sizes"]) == 5
assert details["sro_membership_check"]["payload"]["admission_date"] is None
empty = capture(
"empty-list", "list", client.get(list_url, {"search": "absent-fixture-value"})
)
assert empty["data"] == []
assert empty["meta"]["pagination"]["total_count"] == 0
assert empty["meta"]["pagination"]["total_pages"] == 0
capture(
"invalid-list",
"list",
client.get(list_url, {"page_size": "broken"}),
400,
)
for name, uid, status in (
("invalid-detail", "broken", 400),
("missing-detail", str(uuid4()), 404),
):
capture(
name,
"detail",
client.get(
reverse(
"api_v2:organizations:organization-source-records-detail",
kwargs={"uid": uid},
)
),
status,
)
# Ordinary pytest runs create no artifact. CI opts in with an explicit path.
destination = os.environ.get("OPENAPI_RUNTIME_FIXTURES")
if destination:
Path(destination).write_text(
json.dumps({"version": 1, "cases": cases}, ensure_ascii=False, indent=2)
+ "\n",
encoding="utf-8",
)