refactor(services): общий utc_now вместо локальных _now в сервисах справочников

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-04 22:08:04 +04:00
parent 69d641c14e
commit e3ea074e4d
4 changed files with 20 additions and 19 deletions

8
api/src/common/clock.py Normal file
View File

@ -0,0 +1,8 @@
"""Текущее время сервисов: одно место для «сейчас» (UTC, с часовым поясом) — `valid_from`/`valid_to`, `updated_at`."""
import datetime
def utc_now() -> datetime.datetime:
"""Текущий момент в UTC (aware)."""
return datetime.datetime.now(datetime.UTC)

View File

@ -4,12 +4,12 @@
Аудит `LEGAL_ENTITY_CHANGE` / `object_type = legal_entity_alias` пишется в той же транзакции запроса.
"""
import datetime
import uuid
from sqlalchemy.ext.asyncio import AsyncSession
from src.api.dependencies.audit import RequestAudit
from src.common.clock import utc_now
from src.common.exceptions import ConflictError, DomainValidationError, NotFoundError
from src.domain.aliases import normalize_alias
from src.domain.policy import VisibilityScope
@ -50,7 +50,7 @@ class AliasService:
alias=data.alias,
normalized_alias=normalized,
created_by=self._audit.user_id,
created_at=datetime.datetime.now(datetime.UTC),
created_at=utc_now(),
)
self._session.add(alias)
await self._session.flush()

View File

@ -4,13 +4,13 @@
`commit` делает `SessionDep`; `audit_event` пишется в той же транзакции через `RequestAudit`.
"""
import datetime
import uuid
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from src.api.dependencies.audit import RequestAudit
from src.common.clock import utc_now
from src.common.exceptions import ConflictError, DomainValidationError, NotFoundError
from src.domain.policy import VisibilityScope, office_for_create
from src.models.directories import COMPANY_GROUP_CODE_SEQ, CompanyGroup, CompanyGroupHistory
@ -25,10 +25,6 @@ OBJECT_TYPE = "company_group"
TRACKED_FIELDS = ("name", "crm_id", "base_regional_office_id") # поля ГК, изменяемые PATCH и попадающие в историю
def _now() -> datetime.datetime:
return datetime.datetime.now(datetime.UTC)
class CompanyGroupService:
"""Операции над ГК от имени текущего пользователя в его области видимости."""
@ -43,7 +39,7 @@ class CompanyGroupService:
await self._check_office_exists(office_id)
if data.crm_id is not None:
await self._check_crm_id_free(data.crm_id)
now = _now()
now = utc_now()
group = CompanyGroup(
code=await repo.next_code(self._session, COMPANY_GROUP_CODE_SEQ, COMPANY_GROUP_CODE_PREFIX),
crm_id=data.crm_id,
@ -77,7 +73,7 @@ class CompanyGroupService:
if not changes:
return group
before = {field: getattr(group, field) for field in changes}
now = _now()
now = utc_now()
self._session.add(
CompanyGroupHistory(
company_group_id=group.id,
@ -112,7 +108,7 @@ class CompanyGroupService:
if group.is_archived == archived:
return group
group.is_archived = archived
group.updated_at = _now()
group.updated_at = utc_now()
group.updated_by = self._audit.user_id
await self._session.flush()
await self._audit.record(

View File

@ -13,6 +13,7 @@ from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from src.api.dependencies.audit import RequestAudit
from src.common.clock import utc_now
from src.common.exceptions import ConflictError, DomainValidationError, NotFoundError
from src.domain.inn import validate_inn
from src.domain.policy import VisibilityScope, office_for_create
@ -38,13 +39,9 @@ MEMBER_OBJECT_TYPE = "related_group_member"
TRACKED_FIELDS = ("name", "entity_type", "crm_id", "regional_office_id") # поля ЮЛ, изменяемые PATCH
def _now() -> datetime.datetime:
return datetime.datetime.now(datetime.UTC)
def _today() -> datetime.date:
"""«Сегодня» для проверки `effective_date` — дата UTC (часовой пояс пользователя в Срезе 1 не известен)."""
return _now().date()
return utc_now().date()
class LegalEntityService:
@ -69,7 +66,7 @@ class LegalEntityService:
# Филиал не может завести ЮЛ (в т.ч. монозаёмщика) на чужой филиал; без филиала — свой (§10.1)
office_id = office_for_create(self._scope, requested_office)
await self._check_office_exists(office_id)
now = _now()
now = utc_now()
entity = LegalEntity(
inn=inn,
crm_id=data.crm_id,
@ -107,7 +104,7 @@ class LegalEntityService:
if not changes:
return entity
before = {field: getattr(entity, field) for field in changes}
now = _now()
now = utc_now()
self._snapshot(entity, LeHistoryEvent.UPDATE, now, effective_date=None)
for field, value in changes.items():
setattr(entity, field, value)
@ -133,7 +130,7 @@ class LegalEntityService:
if entity.company_group_id == target.id:
raise ConflictError("SAME_COMPANY_GROUP", "ЮЛ уже входит в эту ГК")
before = {"company_group_id": entity.company_group_id}
now = _now()
now = utc_now()
self._snapshot(entity, LeHistoryEvent.MOVE_TO_GROUP, now, effective_date=data.effective_date)
entity.company_group_id = target.id
await self._touch(entity, now)
@ -164,7 +161,7 @@ class LegalEntityService:
changes["close_date"] = data.effective_date
changes = {field: value for field, value in changes.items() if getattr(entity, field) != value}
before = {field: getattr(entity, field) for field in changes}
now = _now()
now = utc_now()
self._snapshot(entity, event, now, effective_date=data.effective_date)
for field, value in changes.items():
setattr(entity, field, value)