from __future__ import annotations from collections.abc import Mapping, Sequence from dataclasses import dataclass from datetime import UTC, datetime, timedelta import hashlib import json import secrets from typing import Literal import uuid from sqlalchemy.orm import Session from govoplan_core.core.institutional import ( CAPABILITY_FORM_DEFINITIONS, EvidenceReference, FormDefinition, FormDefinitionProvider, InstitutionalReference, ) from govoplan_forms_runtime.backend.db.models import ( FormIntakeProfile, FormIntakeSession, ) from govoplan_forms_runtime.backend.domain import FormInstance from govoplan_forms_runtime.backend.service import FormRuntimeError, FormRuntimeService IntakeMode = Literal["anonymous", "invitation"] INTAKE_MODES = frozenset({"anonymous", "invitation"}) DEFAULT_DRAFT_TTL_SECONDS = 30 * 24 * 60 * 60 DEFAULT_INVITATION_TTL_SECONDS = 14 * 24 * 60 * 60 DEFAULT_RATE_LIMIT_PER_MINUTE = 60 class FormIntakeError(FormRuntimeError): pass @dataclass(frozen=True, slots=True) class PublicIntakePrincipal: tenant_id: str account_id: str auth_method: str profile_id: str identity_id: str | None = None def has(self, scope: str) -> bool: del scope return False @dataclass(frozen=True, slots=True) class IntakeSessionResult: session_id: str mode: str status: str expires_at: datetime instance: FormInstance | None token: str | None = None replayed: bool = False def to_dict(self) -> dict[str, object]: return { "session_id": self.session_id, "mode": self.mode, "status": self.status, "expires_at": self.expires_at.isoformat(), "instance": self.instance.to_dict() if self.instance else None, "token": self.token, "replayed": self.replayed, } class FormIntakeService: def __init__(self, registry: object | None) -> None: self._registry = registry self._runtime = FormRuntimeService(registry) def create_profile( self, session: Session, principal: object, *, definition_ref: InstitutionalReference, mode: IntakeMode, custodian_ref: str, draft_ttl_seconds: int = DEFAULT_DRAFT_TTL_SECONDS, invitation_ttl_seconds: int = DEFAULT_INVITATION_TTL_SECONDS, rate_limit_per_minute: int = DEFAULT_RATE_LIMIT_PER_MINUTE, metadata: Mapping[str, object] | None = None, recorded_at: datetime, ) -> FormIntakeProfile: tenant_id = _principal_tenant(principal) actor_id = _principal_actor(principal) _require_aware(recorded_at, "Intake profile recorded_at") if mode not in INTAKE_MODES: raise FormIntakeError(f"Unsupported public intake mode: {mode!r}.") if not custodian_ref.strip(): raise FormIntakeError("A public intake profile requires a custodian.") if not 60 <= draft_ttl_seconds <= 365 * 24 * 60 * 60: raise FormIntakeError( "Draft expiry must be between one minute and one year." ) if not 60 <= invitation_ttl_seconds <= 90 * 24 * 60 * 60: raise FormIntakeError( "Invitation expiry must be between one minute and ninety days." ) if not 1 <= rate_limit_per_minute <= 10_000: raise FormIntakeError( "Public intake rate limit must be between 1 and 10000." ) definition = self._definition( session, principal, reference=definition_ref, effective_at=recorded_at, ) if definition.publication_state != "published": raise FormIntakeError("Only a published Form can receive public intake.") profile = FormIntakeProfile( tenant_id=tenant_id, profile_id=str(uuid.uuid4()), public_id=secrets.token_urlsafe(24), definition_id=definition.reference.object_id, definition_revision=str(definition.reference.version), mode=mode, enabled=True, revision=1, custodian_ref=custodian_ref.strip(), draft_ttl_seconds=draft_ttl_seconds, invitation_ttl_seconds=invitation_ttl_seconds, rate_limit_per_minute=rate_limit_per_minute, rate_window_started_at=None, rate_window_count=0, created_by=actor_id, updated_by=actor_id, details={ "identity_claim_allowed": False, "pseudonymous": False, **dict(metadata or {}), }, ) session.add(profile) session.flush() return profile def list_profiles( self, session: Session, principal: object, ) -> tuple[FormIntakeProfile, ...]: return tuple( session.query(FormIntakeProfile) .filter(FormIntakeProfile.tenant_id == _principal_tenant(principal)) .order_by( FormIntakeProfile.definition_id.asc(), FormIntakeProfile.mode.asc(), ) .all() ) def list_available_definitions( self, session: Session, principal: object, *, query: str = "", limit: int = 200, ) -> tuple[FormDefinition, ...]: provider = _capability(self._registry, CAPABILITY_FORM_DEFINITIONS) if not isinstance(provider, FormDefinitionProvider): raise FormIntakeError("The Forms definition provider is unavailable.") definitions = provider.list_form_definitions( session, principal, tenant_id=_principal_tenant(principal), query=query.strip(), limit=min(max(limit, 1), 200), ) return tuple( item for item in definitions if item.publication_state == "published" and item.reference.version ) def set_profile_enabled( self, session: Session, principal: object, *, profile_id: str, expected_revision: int, enabled: bool, ) -> FormIntakeProfile: profile = self._profile_for_admin( session, principal, profile_id=profile_id, lock=True, ) if profile.revision != expected_revision: raise FormIntakeError( "Intake profile revision conflict: the expected revision is stale." ) profile.enabled = bool(enabled) profile.revision += 1 profile.updated_by = _principal_actor(principal) session.add(profile) session.flush() return profile def issue_invitation( self, session: Session, principal: object, *, profile_id: str, idempotency_key: str, recorded_at: datetime, expires_at: datetime | None = None, metadata: Mapping[str, object] | None = None, ) -> IntakeSessionResult: profile = self._profile_for_admin( session, principal, profile_id=profile_id, lock=True, ) if profile.mode != "invitation" or not profile.enabled: raise FormIntakeError("This intake profile cannot issue invitations.") _require_aware(recorded_at, "Invitation recorded_at") effective_expiry = expires_at or ( recorded_at + timedelta(seconds=profile.invitation_ttl_seconds) ) _require_aware(effective_expiry, "Invitation expires_at") if effective_expiry <= recorded_at: raise FormIntakeError("Invitation expiry must be in the future.") request = { "operation": "issue_invitation", "profile_id": profile.profile_id, "expires_at": effective_expiry.isoformat(), "metadata": dict(metadata or {}), } replay = self._session_replay( session, tenant_id=profile.tenant_id, idempotency_key=idempotency_key, request_sha256=_hash(request), ) if replay is not None: return self._result(session, replay, token=None, replayed=True) token = secrets.token_urlsafe(32) intake_session = self._new_session( profile, token=token, idempotency_key=idempotency_key, request_sha256=_hash(request), expires_at=effective_expiry, created_by=_principal_actor(principal), metadata=metadata, ) session.add(intake_session) session.flush() return self._result(session, intake_session, token=token) def start_anonymous( self, session: Session, *, public_id: str, values: Mapping[str, object], idempotency_key: str, recorded_at: datetime, ) -> IntakeSessionResult: profile = self._public_profile( session, public_id=public_id, expected_mode="anonymous", now=recorded_at, ) request = { "operation": "anonymous_start", "profile_id": profile.profile_id, "values": dict(values), "recorded_at": recorded_at.isoformat(), } request_sha256 = _hash(request) replay = self._session_replay( session, tenant_id=profile.tenant_id, idempotency_key=f"anonymous:{profile.profile_id}:{idempotency_key}", request_sha256=request_sha256, ) if replay is not None: return self._result(session, replay, token=None, replayed=True) token = secrets.token_urlsafe(32) intake_session = self._new_session( profile, token=token, idempotency_key=f"anonymous:{profile.profile_id}:{idempotency_key}", request_sha256=request_sha256, expires_at=recorded_at + timedelta(seconds=profile.draft_ttl_seconds), created_by="public", ) session.add(intake_session) session.flush() instance = self._start_instance( session, profile, intake_session, values=values, recorded_at=recorded_at, ) intake_session.instance_id = instance.instance_id intake_session.status = "active" intake_session.started_at = recorded_at session.add(intake_session) session.flush() return self._result(session, intake_session, token=token, instance=instance) def start_invitation( self, session: Session, *, token: str, values: Mapping[str, object], recorded_at: datetime, ) -> IntakeSessionResult: intake_session, profile = self._public_session( session, token=token, now=recorded_at, lock=True, ) if intake_session.mode != "invitation": raise _public_unavailable() if intake_session.instance_id: return self._result(session, intake_session, replayed=True) instance = self._start_instance( session, profile, intake_session, values=values, recorded_at=recorded_at, ) intake_session.instance_id = instance.instance_id intake_session.status = "active" intake_session.started_at = recorded_at session.add(intake_session) session.flush() return self._result(session, intake_session, instance=instance) def get_public_instance( self, session: Session, *, token: str, now: datetime, ) -> tuple[FormInstance, FormDefinition]: intake_session, profile = self._public_session( session, token=token, now=now, lock=True, ) instance = self._instance(session, intake_session) definition = self._definition( session, _public_principal(intake_session, profile), reference=_profile_definition_ref(profile), effective_at=instance.recorded_at, ) return instance, definition def update_public_draft( self, session: Session, *, token: str, expected_revision: int, values: Mapping[str, object], attachment_refs: Sequence[EvidenceReference], signature_refs: Sequence[EvidenceReference], idempotency_key: str, recorded_at: datetime, change_reason: str, ) -> FormInstance: intake_session, profile = self._public_session( session, token=token, now=recorded_at, lock=True, ) principal = _public_principal(intake_session, profile) return self._runtime.update_draft( session, principal, instance_id=_session_instance_id(intake_session), expected_revision=expected_revision, values=values, attachment_refs=attachment_refs, signature_refs=signature_refs, idempotency_key=f"public:{intake_session.session_id}:{idempotency_key}", recorded_at=recorded_at, change_reason=change_reason, ) def submit_public( self, session: Session, *, token: str, expected_revision: int, values: Mapping[str, object], attachment_refs: Sequence[EvidenceReference], signature_refs: Sequence[EvidenceReference], idempotency_key: str, recorded_at: datetime, ) -> FormInstance: intake_session, profile = self._public_session( session, token=token, now=recorded_at, lock=True, allow_submitted=True, ) principal = _public_principal(intake_session, profile) instance = self._runtime.submit_instance( session, principal, instance_id=_session_instance_id(intake_session), expected_revision=expected_revision, values=values, attachment_refs=attachment_refs, signature_refs=signature_refs, idempotency_key=f"public:{intake_session.session_id}:{idempotency_key}", recorded_at=recorded_at, ) intake_session.status = "submitted" intake_session.submitted_at = recorded_at session.add(intake_session) session.flush() return instance def session_context( self, session: Session, *, token: str, now: datetime, ) -> tuple[FormIntakeSession, FormIntakeProfile, PublicIntakePrincipal]: intake_session, profile = self._public_session( session, token=token, now=now, lock=True, ) return intake_session, profile, _public_principal(intake_session, profile) def _start_instance( self, session: Session, profile: FormIntakeProfile, intake_session: FormIntakeSession, *, values: Mapping[str, object], recorded_at: datetime, ) -> FormInstance: principal = _public_principal(intake_session, profile) return self._runtime.create_instance( session, principal, definition_ref=_profile_definition_ref(profile), values=values, idempotency_key=f"public-start:{intake_session.session_id}", recorded_at=recorded_at, metadata={ "intake": { "profile_id": profile.profile_id, "mode": profile.mode, "channel": "public_web", "authenticated_identity_claimed": False, "authority_basis": "invitation" if profile.mode == "invitation" else "self_submission", } }, ) def _definition( self, session: Session, principal: object, *, reference: InstitutionalReference, effective_at: datetime, ) -> FormDefinition: provider = _capability(self._registry, CAPABILITY_FORM_DEFINITIONS) if not isinstance(provider, FormDefinitionProvider): raise FormIntakeError("The Forms definition provider is unavailable.") definition = provider.get_form_definition( session, principal, reference=reference, effective_at=effective_at, ) if definition is None or definition.reference != reference: raise FormIntakeError("The exact Form definition is unavailable.") return definition def _profile_for_admin( self, session: Session, principal: object, *, profile_id: str, lock: bool, ) -> FormIntakeProfile: query = session.query(FormIntakeProfile).filter( FormIntakeProfile.tenant_id == _principal_tenant(principal), FormIntakeProfile.profile_id == profile_id, ) if lock: query = query.with_for_update() profile = query.one_or_none() if profile is None: raise LookupError("Form intake profile not found.") return profile def _public_profile( self, session: Session, *, public_id: str, expected_mode: str, now: datetime, ) -> FormIntakeProfile: _require_aware(now, "Public intake time") profile = ( session.query(FormIntakeProfile) .filter(FormIntakeProfile.public_id == public_id) .with_for_update() .one_or_none() ) if profile is None or not profile.enabled or profile.mode != expected_mode: raise _public_unavailable() _consume_rate_limit(profile, now=now) session.add(profile) return profile def _public_session( self, session: Session, *, token: str, now: datetime, lock: bool, allow_submitted: bool = False, ) -> tuple[FormIntakeSession, FormIntakeProfile]: _require_aware(now, "Public intake time") query = session.query(FormIntakeSession).filter( FormIntakeSession.token_sha256 == _token_hash(token) ) if lock: query = query.with_for_update() intake_session = query.one_or_none() if intake_session is None: raise _public_unavailable() profile_query = session.query(FormIntakeProfile).filter( FormIntakeProfile.id == intake_session.profile_id ) if lock: profile_query = profile_query.with_for_update() profile = profile_query.one_or_none() allowed_states = {"issued", "active"} if allow_submitted: allowed_states.add("submitted") if ( profile is None or not profile.enabled or intake_session.status not in allowed_states or intake_session.revoked_at is not None or _aware(intake_session.expires_at) <= now ): raise _public_unavailable() _consume_rate_limit(profile, now=now) session.add(profile) return intake_session, profile def _session_replay( self, session: Session, *, tenant_id: str, idempotency_key: str, request_sha256: str, ) -> FormIntakeSession | None: existing = ( session.query(FormIntakeSession) .filter( FormIntakeSession.tenant_id == tenant_id, FormIntakeSession.idempotency_key == idempotency_key, ) .one_or_none() ) if existing is not None and existing.request_sha256 != request_sha256: raise FormIntakeError( "Public intake idempotency conflict: this key was used for another request." ) return existing def _new_session( self, profile: FormIntakeProfile, *, token: str, idempotency_key: str, request_sha256: str, expires_at: datetime, created_by: str, metadata: Mapping[str, object] | None = None, ) -> FormIntakeSession: session_id = str(uuid.uuid4()) return FormIntakeSession( tenant_id=profile.tenant_id, session_id=session_id, profile_id=profile.id, token_sha256=_token_hash(token), mode=profile.mode, status="issued", instance_id=None, actor_id=f"form-public:{session_id}", idempotency_key=idempotency_key, request_sha256=request_sha256, expires_at=expires_at, created_by=created_by, details=dict(metadata or {}), ) def _instance( self, session: Session, intake_session: FormIntakeSession, ) -> FormInstance: profile = session.get(FormIntakeProfile, intake_session.profile_id) if profile is None: raise _public_unavailable() instance = self._runtime.get_instance( session, _public_principal(intake_session, profile), instance_id=_session_instance_id(intake_session), ) if instance is None: raise _public_unavailable() return instance def _result( self, session: Session, intake_session: FormIntakeSession, *, token: str | None = None, instance: FormInstance | None = None, replayed: bool = False, ) -> IntakeSessionResult: if instance is None and intake_session.instance_id: instance = self._instance(session, intake_session) return IntakeSessionResult( session_id=intake_session.session_id, mode=intake_session.mode, status=intake_session.status, expires_at=_aware(intake_session.expires_at), instance=instance, token=token, replayed=replayed, ) def profile_payload(profile: FormIntakeProfile) -> dict[str, object]: return { "profile_id": profile.profile_id, "public_id": profile.public_id, "definition_ref": { "kind": "form", "owner_module": "forms", "object_id": profile.definition_id, "tenant_id": profile.tenant_id, "version": profile.definition_revision, }, "mode": profile.mode, "enabled": profile.enabled, "revision": profile.revision, "draft_ttl_seconds": profile.draft_ttl_seconds, "invitation_ttl_seconds": profile.invitation_ttl_seconds, "rate_limit_per_minute": profile.rate_limit_per_minute, "metadata": dict(profile.details), } def _consume_rate_limit(profile: FormIntakeProfile, *, now: datetime) -> None: window = ( _aware(profile.rate_window_started_at) if profile.rate_window_started_at is not None else None ) if window is None or now - window >= timedelta(minutes=1): profile.rate_window_started_at = now profile.rate_window_count = 1 return if profile.rate_window_count >= profile.rate_limit_per_minute: raise FormIntakeError("Public Form intake is temporarily rate limited.") profile.rate_window_count += 1 def _public_principal( intake_session: FormIntakeSession, profile: FormIntakeProfile, ) -> PublicIntakePrincipal: return PublicIntakePrincipal( tenant_id=intake_session.tenant_id, account_id=intake_session.actor_id, auth_method=f"form_public_{intake_session.mode}", profile_id=profile.profile_id, ) def _profile_definition_ref(profile: FormIntakeProfile) -> InstitutionalReference: return InstitutionalReference( kind="form", owner_module="forms", object_id=profile.definition_id, tenant_id=profile.tenant_id, version=profile.definition_revision, ) def _session_instance_id(intake_session: FormIntakeSession) -> str: if not intake_session.instance_id: raise _public_unavailable() return intake_session.instance_id def _public_unavailable() -> FormIntakeError: return FormIntakeError("Public Form intake is unavailable.") def _principal_tenant(principal: object) -> str: value = str(getattr(principal, "tenant_id", "") or "").strip() if not value: raise FormIntakeError("Forms Runtime requires a tenant-bound principal.") return value def _principal_actor(principal: object) -> str: for name in ("account_id", "identity_id", "membership_id"): value = str(getattr(principal, name, "") or "").strip() if value: return value raise FormIntakeError("Forms Runtime requires an acting identity.") def _capability(registry: object | None, name: str) -> object | None: if registry is None or not hasattr(registry, "has_capability"): return None if not registry.has_capability(name): return None if hasattr(registry, "require_capability"): return registry.require_capability(name) if hasattr(registry, "capability"): return registry.capability(name) return None def _token_hash(token: str) -> str: clean = str(token or "").strip() if len(clean) < 32: raise _public_unavailable() return hashlib.sha256(clean.encode("utf-8")).hexdigest() def _hash(value: Mapping[str, object]) -> str: return hashlib.sha256( json.dumps(value, sort_keys=True, separators=(",", ":"), default=str).encode( "utf-8" ) ).hexdigest() def _require_aware(value: datetime, label: str) -> None: if value.tzinfo is None or value.utcoffset() is None: raise FormIntakeError(f"{label} must include a timezone.") def _aware(value: datetime) -> datetime: return value if value.tzinfo is not None else value.replace(tzinfo=UTC) __all__ = [ "DEFAULT_DRAFT_TTL_SECONDS", "DEFAULT_INVITATION_TTL_SECONDS", "DEFAULT_RATE_LIMIT_PER_MINUTE", "FormIntakeError", "FormIntakeService", "IntakeSessionResult", "PublicIntakePrincipal", "profile_payload", ]