Expose decision filing and search sources
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.institutional import InstitutionalReference
|
||||
from govoplan_core.core.records import (
|
||||
RecordContractError,
|
||||
RecordSourceLocator,
|
||||
RecordSourceReference,
|
||||
)
|
||||
from govoplan_decisions.backend.db.models import FormalDecisionRevision
|
||||
from govoplan_decisions.backend.service import SqlDecisionRegistry
|
||||
|
||||
|
||||
CAPABILITY_RECORD_SOURCE_DECISIONS = "records.source.decisions"
|
||||
|
||||
|
||||
class DecisionsRecordSource:
|
||||
provider_id = "decisions"
|
||||
|
||||
def resource_types(self) -> Sequence[str]:
|
||||
return ("decision_revision",)
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
locator: RecordSourceLocator,
|
||||
purpose: str,
|
||||
) -> RecordSourceReference:
|
||||
if not isinstance(session, Session):
|
||||
raise RecordContractError(
|
||||
"Decision record references require a database session."
|
||||
)
|
||||
tenant_id = str(getattr(principal, "tenant_id", "") or "").strip()
|
||||
if not tenant_id or locator.tenant_id != tenant_id:
|
||||
raise RecordContractError(
|
||||
"Decision record references cannot cross tenants."
|
||||
)
|
||||
if (
|
||||
locator.source_module != "decisions"
|
||||
or locator.resource_type != "decision_revision"
|
||||
):
|
||||
raise RecordContractError("Unsupported Decisions record source type.")
|
||||
if not str(purpose or "").strip():
|
||||
raise RecordContractError("Decision record references require a purpose.")
|
||||
if not _has(principal, "decisions:decision:read"):
|
||||
raise RecordContractError("Current Decision read permission is required.")
|
||||
if not (
|
||||
_has(principal, "decisions:decision:read_sensitive")
|
||||
or _has(principal, "decisions:decision:admin")
|
||||
):
|
||||
raise RecordContractError(
|
||||
"Protected Decision read permission is required to file the complete formal outcome."
|
||||
)
|
||||
row = (
|
||||
session.query(FormalDecisionRevision)
|
||||
.filter(
|
||||
FormalDecisionRevision.tenant_id == tenant_id,
|
||||
FormalDecisionRevision.decision_id == locator.resource_id,
|
||||
FormalDecisionRevision.revision == locator.source_revision,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
if row is None:
|
||||
raise RecordContractError("The exact Decision revision does not exist.")
|
||||
decision = SqlDecisionRegistry().get_decision(
|
||||
session,
|
||||
principal,
|
||||
reference=_decision_reference(row),
|
||||
)
|
||||
if decision is None:
|
||||
raise RecordContractError("The exact Decision revision is unavailable.")
|
||||
snapshot_json = json.dumps(
|
||||
row.payload,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
default=str,
|
||||
).encode("utf-8")
|
||||
return RecordSourceReference(
|
||||
locator=locator,
|
||||
label=f"Decision {decision.decision_type} - {decision.reference.object_id}",
|
||||
authority_mode="external_authoritative",
|
||||
content_sha256=hashlib.sha256(snapshot_json).hexdigest(),
|
||||
content_type="application/vnd.govoplan.formal-decision-revision+json",
|
||||
size_bytes=len(snapshot_json),
|
||||
valid_from=decision.temporal.valid_from,
|
||||
valid_to=decision.temporal.valid_to,
|
||||
recorded_at=decision.temporal.recorded_at,
|
||||
metadata={
|
||||
"decision_type": decision.decision_type,
|
||||
"state": decision.state,
|
||||
"assurance_level": decision.assurance_level,
|
||||
"subject_count": len(decision.subject_refs),
|
||||
"evidence_count": len(decision.fact_evidence),
|
||||
"protected_snapshot": True,
|
||||
"snapshot_sha256": hashlib.sha256(snapshot_json).hexdigest(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def create_decisions_record_source(_context: object) -> DecisionsRecordSource:
|
||||
return DecisionsRecordSource()
|
||||
|
||||
|
||||
def _decision_reference(row: FormalDecisionRevision) -> InstitutionalReference:
|
||||
return InstitutionalReference(
|
||||
kind="decision",
|
||||
owner_module="decisions",
|
||||
object_id=row.decision_id,
|
||||
tenant_id=row.tenant_id,
|
||||
version=row.revision,
|
||||
)
|
||||
|
||||
|
||||
def _has(principal: object, scope: str) -> bool:
|
||||
return bool(hasattr(principal, "has") and principal.has(scope))
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_RECORD_SOURCE_DECISIONS",
|
||||
"DecisionsRecordSource",
|
||||
"create_decisions_record_source",
|
||||
]
|
||||
Reference in New Issue
Block a user