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

View File

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

View File

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