"""Identity-consumer ownership transfer. A consumer's ``owner_person_id`` is the accountable human for an outbound data-pull integration. When the owner offboards, the owner FK is handed to a named successor (the consumer keeps running under the new owner) — this is the single write the offboarding handover path and any admin reassign go through, so the audit trail is identical regardless of caller. """ from __future__ import annotations import logging import uuid from sqlalchemy.ext.asyncio import AsyncSession from app.audit.service import AuditService from app.core.exceptions import NotFoundError, ValidationError from app.identity.consumers.models import IdentityConsumer from app.identity.person_models import IdentityPerson logger = logging.getLogger(__name__) async def transfer_consumer_owner( db: AsyncSession, *, consumer_id: uuid.UUID, successor_person_id: uuid.UUID, trace_id: str, actor_user_id: uuid.UUID | None, ) -> IdentityConsumer: """Reassign a consumer's owner to an active successor. Idempotent on a no-op (already that owner). Raises if the consumer is gone or the successor isn't an active employee.""" consumer = await db.get(IdentityConsumer, consumer_id) if consumer is None or consumer.deleted_at is not None: raise NotFoundError("IdentityConsumer", str(consumer_id)) successor = await db.get(IdentityPerson, successor_person_id) if successor is None or successor.deleted_at is not None: raise ValidationError("successor_person_id does not reference a known person.") if successor.employment_status != "active": raise ValidationError( "Consumer owner must be an active employee." ) old_owner = consumer.owner_person_id if old_owner == successor_person_id: return consumer consumer.owner_person_id = successor_person_id await db.flush() await AuditService(db).log( trace_id=trace_id, actor_type="user" if actor_user_id else "system", actor_user_id=actor_user_id, action="identity_consumer.owner_transferred", status="success", resource_type="identity_consumer", resource_id=str(consumer.id), detail={"slug": consumer.slug, "from": str(old_owner), "to": str(successor_person_id)}, ) return consumer __all__ = ["transfer_consumer_owner"]