from __future__ import annotations from dataclasses import dataclass from datetime import datetime from typing import Literal from govoplan_core.core.institutional import ( CAPABILITY_PARTY_RESOLVER, EvidenceReference, InstitutionalContextError, InstitutionalReference, PartyRepresentation, PartyResolver, PartySubjectReference, ProcedureParty, TemporalRevision, ) CAPABILITY_CASES_PARTY_CONTEXT = "cases.party_context" CasePartySource = Literal["provider", "compatibility"] @dataclass(frozen=True, slots=True) class CasePartyCompatibilityRecord: party_id: str role: str subject: PartySubjectReference valid_from: datetime valid_to: datetime | None = None preferred_channels: tuple[str, ...] = () permitted_channels: tuple[str, ...] = () delivery_recipient: bool = False contact_snapshot_refs: tuple[str, ...] = () evidence: tuple[EvidenceReference, ...] = () @dataclass(frozen=True, slots=True) class CasePartySet: case_ref: InstitutionalReference effective_at: datetime source: CasePartySource parties: tuple[ProcedureParty, ...] @dataclass(frozen=True, slots=True) class CasePartyDeliveryTarget: party_ref: InstitutionalReference subject: PartySubjectReference role: str channel: str contact_snapshot_refs: tuple[str, ...] represented_party_refs: tuple[InstitutionalReference, ...] = () evidence: tuple[EvidenceReference, ...] = () class CasePartyContext: """Resolve case-local participation without copying subject master data.""" def __init__(self, registry: object | None = None) -> None: self._registry = registry def resolve( self, session: object, principal: object, *, case_ref: InstitutionalReference, effective_at: datetime, compatibility: tuple[CasePartyCompatibilityRecord, ...] = (), ) -> CasePartySet: if case_ref.kind != "case": raise InstitutionalContextError( "Case party resolution requires a case reference." ) _require_aware(effective_at) provider = _capability(self._registry, CAPABILITY_PARTY_RESOLVER) if isinstance(provider, PartyResolver): candidates = tuple( provider.list_procedure_parties( session, principal, procedure_ref=case_ref, effective_at=effective_at, ) ) source: CasePartySource = "provider" else: candidates = tuple( _compatibility_party(case_ref, item) for item in compatibility ) source = "compatibility" effective = tuple( item for item in candidates if item.status == "active" and item.temporal.effective_at(effective_at) ) for item in effective: if not _same_object(item.procedure_ref, case_ref): raise InstitutionalContextError( "Party provider returned a party for another procedure." ) keys = tuple( ( item.role, item.subject.kind, item.subject.provider, item.subject.subject_id, ) for item in effective ) if len(keys) != len(set(keys)): raise InstitutionalContextError( "Party resolution returned conflicting active role assignments." ) return CasePartySet( case_ref=case_ref, effective_at=effective_at, source=source, parties=effective, ) def delivery_targets( self, party_set: CasePartySet, *, channel: str, ) -> tuple[CasePartyDeliveryTarget, ...]: if not channel.strip(): raise InstitutionalContextError("Delivery channel is required.") targets: list[CasePartyDeliveryTarget] = [] for party in party_set.parties: if not party.delivery_recipient or channel not in party.permitted_channels: continue if not party.contact_snapshot_refs: raise InstitutionalContextError( "A case delivery target requires frozen contact snapshots." ) represented = tuple( item.represented_party_ref for item in party.representations if _representation_effective( item, effective_at=party_set.effective_at, action="receive", ) ) targets.append( CasePartyDeliveryTarget( party_ref=party.reference, subject=party.subject, role=party.role, channel=channel, contact_snapshot_refs=party.contact_snapshot_refs, represented_party_refs=represented, evidence=party.evidence, ) ) return tuple(targets) def _compatibility_party( case_ref: InstitutionalReference, item: CasePartyCompatibilityRecord, ) -> ProcedureParty: if item.subject.tenant_id != case_ref.tenant_id: raise InstitutionalContextError( "Compatibility party subject belongs to another tenant." ) return ProcedureParty( reference=InstitutionalReference( kind="party", owner_module="cases", object_id=item.party_id, tenant_id=case_ref.tenant_id, version="compatibility-1", valid_at=item.valid_from, ), procedure_ref=case_ref, role=item.role, subject=item.subject, temporal=TemporalRevision( revision="compatibility-1", valid_from=item.valid_from, valid_to=item.valid_to, recorded_at=item.valid_from, change_reason="Cases compatibility party projection.", ), preferred_channels=item.preferred_channels, permitted_channels=item.permitted_channels, delivery_recipient=item.delivery_recipient, contact_snapshot_refs=item.contact_snapshot_refs, evidence=item.evidence, ) def _representation_effective( representation: PartyRepresentation, *, effective_at: datetime, action: str, ) -> bool: return ( action in representation.permitted_actions and representation.temporal.effective_at(effective_at) and ( representation.revoked_at is None or representation.revoked_at > effective_at ) ) def _same_object( left: InstitutionalReference, right: InstitutionalReference, ) -> bool: return ( left.kind, left.owner_module, left.object_id, left.tenant_id, ) == ( right.kind, right.owner_module, right.object_id, right.tenant_id, ) def _require_aware(value: datetime) -> None: if value.tzinfo is None or value.utcoffset() is None: raise InstitutionalContextError( "Case party effective time must include a timezone." ) def _capability(registry: object | None, name: str) -> object | None: if ( registry is None or not hasattr(registry, "has_capability") or not hasattr(registry, "capability") or not registry.has_capability(name) ): return None return registry.capability(name) __all__ = [ "CAPABILITY_CASES_PARTY_CONTEXT", "CasePartyCompatibilityRecord", "CasePartyContext", "CasePartyDeliveryTarget", "CasePartySet", ]