fix(statements): валидация units при правке шапки и comment в аудите сброса акцепта (T6.2, T6.4)
- HeaderPatch.units: обрезка пробелов и min_length=1, пробельная строка — 422; null — «не менять» (docstring); - запись аудита сброса акцепта содержит comment сброшенного акцепта. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
e45502b2ea
commit
fb457fc06b
@ -9,8 +9,9 @@ import datetime
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, StringConstraints
|
||||
|
||||
from src.domain.statement_checks import ChecksReport, FailedCheck
|
||||
from src.models.enums import DocumentCheckResult, DocumentStatus, DocumentType, PeriodQuarter
|
||||
@ -237,13 +238,16 @@ class CellPatch(BaseModel):
|
||||
|
||||
|
||||
class HeaderPatch(BaseModel):
|
||||
"""Отсутствующее поле не меняется; `null` у `report_date`/`inn` — очистить значение (`model_fields_set`)."""
|
||||
"""Отсутствующее поле не меняется; `null` у `report_date`/`inn` — очистить значение (`model_fields_set`).
|
||||
|
||||
`units: null` — «не менять» (единицы измерения очищать нельзя); непустая строка после обрезки пробелов, до 50
|
||||
символов (пробельная строка — 422)."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
inn: str | None = None
|
||||
report_date: datetime.date | None = None
|
||||
units: str | None = Field(None, min_length=1, max_length=50)
|
||||
units: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=50)] | None = None
|
||||
|
||||
|
||||
class StatementPatch(BaseModel):
|
||||
|
||||
@ -202,7 +202,11 @@ class StatementService:
|
||||
if not after:
|
||||
return await self.build_out(document, meta)
|
||||
if meta.checks_accepted_at is not None:
|
||||
before["checks_accepted"] = {"by": meta.checks_accepted_by, "at": meta.checks_accepted_at}
|
||||
before["checks_accepted"] = {
|
||||
"by": meta.checks_accepted_by,
|
||||
"at": meta.checks_accepted_at,
|
||||
"comment": meta.checks_accepted_comment,
|
||||
}
|
||||
after["checks_accepted"] = None
|
||||
meta.checks_accepted_by = meta.checks_accepted_at = meta.checks_accepted_comment = None
|
||||
await self._session.flush()
|
||||
|
||||
@ -410,6 +410,7 @@ async def test_patch_rereads_meta_under_lock(
|
||||
assert held.checks_accepted_at is None and held.checks_accepted_comment is None
|
||||
event = (await seeded.execute(select(AuditEvent).where(AuditEvent.object_id == document.id))).scalars().one()
|
||||
assert event.before_value["checks_accepted"]["by"] == str(user_id) and event.after_value["checks_accepted"] is None
|
||||
assert event.before_value["checks_accepted"]["comment"] == "x" # T6.4: сброшенный комментарий акцепта сохранён
|
||||
|
||||
|
||||
async def test_patch_duplicate_cell_rejected(
|
||||
@ -713,3 +714,27 @@ async def test_confirmed_form_in_checklist(
|
||||
)
|
||||
).scalar_one()
|
||||
assert meta.reviewed_by == user.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize("units", [" ", "", "я" * 51])
|
||||
async def test_patch_units_invalid_is_422(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory, units: str
|
||||
) -> None:
|
||||
"""T6.2: пробельная, пустая и длиннее 50 символов единицы измерения отклоняются; `null` — «не менять»."""
|
||||
document = await form(seeded, fixtures_dir)
|
||||
client = await auth_client(KA_RF)
|
||||
url = f"/documents/{document.id}/statement"
|
||||
response = await client.patch(url, json={"header": {"units": units}})
|
||||
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY, response.text
|
||||
unchanged = await client.patch(url, json={"header": {"units": None}})
|
||||
assert unchanged.status_code == HTTPStatus.OK
|
||||
|
||||
|
||||
async def test_patch_units_are_stripped(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
document = await form(seeded, fixtures_dir)
|
||||
client = await auth_client(KA_RF)
|
||||
response = await client.patch(f"/documents/{document.id}/statement", json={"header": {"units": " тыс. руб. "}})
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["header"]["units"]["value"] == "тыс. руб."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user