diff --git a/api/src/common/clock.py b/api/src/common/clock.py new file mode 100644 index 0000000..c7bf2e5 --- /dev/null +++ b/api/src/common/clock.py @@ -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) diff --git a/api/src/services/aliases.py b/api/src/services/aliases.py index e46de82..f539c3d 100644 --- a/api/src/services/aliases.py +++ b/api/src/services/aliases.py @@ -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() diff --git a/api/src/services/company_groups.py b/api/src/services/company_groups.py index 17c4af1..ce072be 100644 --- a/api/src/services/company_groups.py +++ b/api/src/services/company_groups.py @@ -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( diff --git a/api/src/services/legal_entities.py b/api/src/services/legal_entities.py index dbab8c4..13b7572 100644 --- a/api/src/services/legal_entities.py +++ b/api/src/services/legal_entities.py @@ -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)