Define tenant lifecycle event contract
This commit is contained in:
@@ -32,6 +32,13 @@ from govoplan_core.tenancy.service import (
|
||||
tenant_counts,
|
||||
)
|
||||
from govoplan_tenancy.backend.db.models import Tenant
|
||||
from govoplan_tenancy.backend.lifecycle import (
|
||||
TENANT_EVENT_CREATED,
|
||||
TENANT_EVENT_DELETION_REQUESTED,
|
||||
TENANT_EVENT_ERASURE_COMPLETED,
|
||||
tenant_lifecycle_event,
|
||||
tenant_lifecycle_event_type,
|
||||
)
|
||||
|
||||
from .schemas import (
|
||||
TenantAdminItem,
|
||||
@@ -457,11 +464,19 @@ def create_tenant(
|
||||
session,
|
||||
tenant_id=tenant.id,
|
||||
user_id=principal.user.id,
|
||||
action="tenant.created",
|
||||
action=TENANT_EVENT_CREATED,
|
||||
object_type="tenant",
|
||||
object_id=tenant.id,
|
||||
scope="system",
|
||||
details={"slug": tenant.slug, "name": tenant.name, "creator_account_id": principal.account_id, "owner_account_id": owner_account_id},
|
||||
details=tenant_lifecycle_event(
|
||||
"created",
|
||||
tenant_id=tenant.id,
|
||||
tenant_slug=tenant.slug,
|
||||
tenant_name=tenant.name,
|
||||
actor_account_id=principal.account_id,
|
||||
requested_by_account_id=owner_account_id,
|
||||
details={"creator_account_id": principal.account_id, "owner_account_id": owner_account_id},
|
||||
).audit_details(),
|
||||
)
|
||||
_record_tenant_list_change(session, tenant=tenant, operation="created", principal=principal)
|
||||
session.commit()
|
||||
@@ -483,6 +498,7 @@ def update_tenant(
|
||||
tenant = session.get(Tenant, tenant_id)
|
||||
if tenant is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
|
||||
was_active = tenant.is_active
|
||||
before_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
|
||||
if payload.name is not None:
|
||||
tenant.name = payload.name.strip()
|
||||
@@ -522,6 +538,25 @@ def update_tenant(
|
||||
object_id=tenant.id,
|
||||
details=payload.model_dump(exclude_unset=True),
|
||||
)
|
||||
if payload.is_active is not None and payload.is_active != was_active:
|
||||
lifecycle_phase = "resumed" if payload.is_active else "suspended"
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=tenant.id,
|
||||
user_id=principal.user.id,
|
||||
action=tenant_lifecycle_event_type(lifecycle_phase),
|
||||
scope="system",
|
||||
object_type="tenant",
|
||||
object_id=tenant.id,
|
||||
details=tenant_lifecycle_event(
|
||||
lifecycle_phase,
|
||||
tenant_id=tenant.id,
|
||||
tenant_slug=tenant.slug,
|
||||
tenant_name=tenant.name,
|
||||
actor_account_id=principal.account_id,
|
||||
details={"previous_is_active": was_active, "is_active": tenant.is_active},
|
||||
).audit_details(),
|
||||
)
|
||||
after_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
|
||||
_record_tenant_settings_section_changes(session, tenant_id=tenant.id, before=before_sections, after=after_sections, principal=principal)
|
||||
_record_tenant_list_change(session, tenant=tenant, operation="updated", principal=principal)
|
||||
@@ -562,6 +597,26 @@ def retire_tenant(
|
||||
|
||||
if request.mode == "destroy":
|
||||
deleted_item = _tenant_item(session, tenant)
|
||||
deletion_event = tenant_lifecycle_event(
|
||||
"deletion_requested",
|
||||
tenant_id=tenant.id,
|
||||
tenant_slug=tenant.slug,
|
||||
tenant_name=tenant.name,
|
||||
actor_account_id=principal.account_id,
|
||||
reason=request.reason,
|
||||
counts=plan.counts,
|
||||
details={"mode": request.mode},
|
||||
)
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=tenant.id,
|
||||
user_id=principal.user.id,
|
||||
action=TENANT_EVENT_DELETION_REQUESTED,
|
||||
scope="system",
|
||||
object_type="tenant",
|
||||
object_id=tenant.id,
|
||||
details=deletion_event.audit_details(),
|
||||
)
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=tenant.id,
|
||||
@@ -572,6 +627,25 @@ def retire_tenant(
|
||||
object_id=tenant.id,
|
||||
details={"reason": request.reason, "counts": plan.counts},
|
||||
)
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=tenant.id,
|
||||
user_id=principal.user.id,
|
||||
action=TENANT_EVENT_ERASURE_COMPLETED,
|
||||
scope="system",
|
||||
object_type="tenant",
|
||||
object_id=tenant.id,
|
||||
details=tenant_lifecycle_event(
|
||||
"erasure_completed",
|
||||
tenant_id=tenant.id,
|
||||
tenant_slug=tenant.slug,
|
||||
tenant_name=tenant.name,
|
||||
actor_account_id=principal.account_id,
|
||||
reason=request.reason,
|
||||
counts=plan.counts,
|
||||
details={"mode": request.mode},
|
||||
).audit_details(),
|
||||
)
|
||||
_record_tenant_list_change(session, tenant=tenant, operation="deleted", principal=principal)
|
||||
session.delete(tenant)
|
||||
session.commit()
|
||||
@@ -590,6 +664,25 @@ def retire_tenant(
|
||||
tenant.settings = settings_payload
|
||||
tenant.is_active = False
|
||||
session.add(tenant)
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=tenant.id,
|
||||
user_id=principal.user.id,
|
||||
action=TENANT_EVENT_DELETION_REQUESTED,
|
||||
scope="system",
|
||||
object_type="tenant",
|
||||
object_id=tenant.id,
|
||||
details=tenant_lifecycle_event(
|
||||
"deletion_requested",
|
||||
tenant_id=tenant.id,
|
||||
tenant_slug=tenant.slug,
|
||||
tenant_name=tenant.name,
|
||||
actor_account_id=principal.account_id,
|
||||
reason=request.reason,
|
||||
counts=plan.counts,
|
||||
details={"mode": request.mode},
|
||||
).audit_details(),
|
||||
)
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=tenant.id,
|
||||
|
||||
99
src/govoplan_tenancy/backend/lifecycle.py
Normal file
99
src/govoplan_tenancy/backend/lifecycle.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
TenantLifecyclePhase = Literal[
|
||||
"created",
|
||||
"suspended",
|
||||
"resumed",
|
||||
"deletion_requested",
|
||||
"erasure_completed",
|
||||
]
|
||||
|
||||
TENANT_EVENT_CREATED = "tenant.created"
|
||||
TENANT_EVENT_SUSPENDED = "tenant.suspended"
|
||||
TENANT_EVENT_RESUMED = "tenant.resumed"
|
||||
TENANT_EVENT_DELETION_REQUESTED = "tenant.deletion_requested"
|
||||
TENANT_EVENT_ERASURE_COMPLETED = "tenant.erasure_completed"
|
||||
|
||||
TENANT_LIFECYCLE_EVENTS: tuple[str, ...] = (
|
||||
TENANT_EVENT_CREATED,
|
||||
TENANT_EVENT_SUSPENDED,
|
||||
TENANT_EVENT_RESUMED,
|
||||
TENANT_EVENT_DELETION_REQUESTED,
|
||||
TENANT_EVENT_ERASURE_COMPLETED,
|
||||
)
|
||||
|
||||
_EVENT_BY_PHASE: Mapping[TenantLifecyclePhase, str] = {
|
||||
"created": TENANT_EVENT_CREATED,
|
||||
"suspended": TENANT_EVENT_SUSPENDED,
|
||||
"resumed": TENANT_EVENT_RESUMED,
|
||||
"deletion_requested": TENANT_EVENT_DELETION_REQUESTED,
|
||||
"erasure_completed": TENANT_EVENT_ERASURE_COMPLETED,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TenantLifecycleEvent:
|
||||
event_type: str
|
||||
tenant_id: str
|
||||
tenant_slug: str | None = None
|
||||
tenant_name: str | None = None
|
||||
actor_account_id: str | None = None
|
||||
requested_by_account_id: str | None = None
|
||||
reason: str | None = None
|
||||
counts: Mapping[str, int] = field(default_factory=dict)
|
||||
occurred_at: datetime | None = None
|
||||
details: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
def audit_details(self) -> dict[str, object]:
|
||||
payload: dict[str, object] = dict(self.details)
|
||||
if self.tenant_slug is not None:
|
||||
payload["slug"] = self.tenant_slug
|
||||
if self.tenant_name is not None:
|
||||
payload["name"] = self.tenant_name
|
||||
if self.actor_account_id is not None:
|
||||
payload["actor_account_id"] = self.actor_account_id
|
||||
if self.requested_by_account_id is not None:
|
||||
payload["requested_by_account_id"] = self.requested_by_account_id
|
||||
if self.reason is not None:
|
||||
payload["reason"] = self.reason
|
||||
if self.counts:
|
||||
payload["counts"] = dict(self.counts)
|
||||
if self.occurred_at is not None:
|
||||
payload["occurred_at"] = self.occurred_at.isoformat()
|
||||
return payload
|
||||
|
||||
|
||||
def tenant_lifecycle_event_type(phase: TenantLifecyclePhase) -> str:
|
||||
return _EVENT_BY_PHASE[phase]
|
||||
|
||||
|
||||
def tenant_lifecycle_event(
|
||||
phase: TenantLifecyclePhase,
|
||||
*,
|
||||
tenant_id: str,
|
||||
tenant_slug: str | None = None,
|
||||
tenant_name: str | None = None,
|
||||
actor_account_id: str | None = None,
|
||||
requested_by_account_id: str | None = None,
|
||||
reason: str | None = None,
|
||||
counts: Mapping[str, int] | None = None,
|
||||
occurred_at: datetime | None = None,
|
||||
details: Mapping[str, object] | None = None,
|
||||
) -> TenantLifecycleEvent:
|
||||
return TenantLifecycleEvent(
|
||||
event_type=tenant_lifecycle_event_type(phase),
|
||||
tenant_id=tenant_id,
|
||||
tenant_slug=tenant_slug,
|
||||
tenant_name=tenant_name,
|
||||
actor_account_id=actor_account_id,
|
||||
requested_by_account_id=requested_by_account_id,
|
||||
reason=reason,
|
||||
counts=dict(counts or {}),
|
||||
occurred_at=occurred_at,
|
||||
details=dict(details or {}),
|
||||
)
|
||||
Reference in New Issue
Block a user