256 lines
7.6 KiB
Python
256 lines
7.6 KiB
Python
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",
|
|
]
|