feat(cases): expose exact revisions for records filing

This commit is contained in:
2026-08-06 01:43:32 +02:00
parent cfe2389e5a
commit 2f030e56a9
4 changed files with 284 additions and 0 deletions
+55
View File
@@ -9,6 +9,7 @@ from govoplan_core.core.module_guards import (
from govoplan_core.core.institutional import CAPABILITY_PARTY_RESOLVER
from govoplan_core.core.modules import (
CapabilityDocumentation,
DocumentationCondition,
DocumentationLink,
DocumentationTopic,
FrontendModule,
@@ -46,6 +47,10 @@ from govoplan_cases.backend.service import (
CAPABILITY_CASES_REGISTRY,
SqlCaseRegistry,
)
from govoplan_cases.backend.record_source import (
CAPABILITY_RECORD_SOURCE_CASES,
create_cases_record_source,
)
from govoplan_core.db.base import Base
@@ -131,6 +136,7 @@ manifest = ModuleManifest(
"decisions",
"forms_runtime",
"workflow_engine",
"records",
),
optional_capabilities=(CAPABILITY_PARTY_RESOLVER,),
permissions=(
@@ -283,6 +289,7 @@ manifest = ModuleManifest(
ModuleInterfaceProvider(name="cases.party_context", version="0.1.0"),
ModuleInterfaceProvider(name="cases.registry", version="0.1.0"),
ModuleInterfaceProvider(name="cases.service_launcher", version="0.1.0"),
ModuleInterfaceProvider(name=CAPABILITY_RECORD_SOURCE_CASES, version="1.0.0"),
),
requires_interfaces=(
ModuleInterfaceRequirement(name="services.definition", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True),
@@ -294,6 +301,7 @@ manifest = ModuleManifest(
CAPABILITY_CASES_PARTY_CONTEXT: _party_context,
CAPABILITY_CASES_REGISTRY: _case_registry,
CAPABILITY_CASES_SERVICE_LAUNCHER: _service_launcher,
CAPABILITY_RECORD_SOURCE_CASES: create_cases_record_source,
},
capability_documentation={
CAPABILITY_CASES_SERVICE_INTAKE: CapabilityDocumentation(
@@ -316,6 +324,11 @@ manifest = ModuleManifest(
summary="Starts a replay-safe case from an exact available Service revision.",
contract_version="0.1.0",
),
CAPABILITY_RECORD_SOURCE_CASES: CapabilityDocumentation(
label="Cases record source",
summary="Resolves currently authorized immutable case revisions for Records filing.",
contract_version="1.0.0",
),
},
migration_spec=MigrationSpec(
module_id=MODULE_ID,
@@ -347,6 +360,48 @@ manifest = ModuleManifest(
resource_acl_providers=(CaseAclProvider(),),
tenant_summary_providers=(_tenant_summary,),
documentation=(
DocumentationTopic(
id="cases.workflow.file-exact-revision",
title="File an exact case revision into an eAkte",
summary="Preserve a reconstructable case snapshot in Records without moving case ownership.",
body=(
"When Records is enabled, Cases resolves an exact immutable case revision only after "
"current case permission and object-level access are checked. The filed reference includes "
"the case number, lifecycle state, represented valid interval, recorded time, and a SHA-256 "
"digest of the canonical snapshot. Cases remains authoritative for the case lifecycle."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("case_manager", "records_manager", "operator", "auditor"),
related_modules=("records",),
order=15,
conditions=(
DocumentationCondition(
required_modules=("cases", "records"),
required_scopes=("cases:case:read", "records:workspace:write"),
),
),
links=(
DocumentationLink(label="Cases", href="/cases", kind="runtime"),
DocumentationLink(label="Records", href="/records", kind="runtime"),
DocumentationLink(label="Cases concept", href="govoplan-cases/docs/CONCEPT.md", kind="repository"),
),
metadata={
"kind": "workflow",
"help_contexts": ["cases.detail", "records.action.file"],
"prerequisites": [
"You currently may read the case and file items into the destination record.",
"The exact immutable case revision has been identified.",
],
"steps": [
"Open the case and identify the revision that represents the evidence state.",
"Choose the destination record and state the access purpose and filing reason.",
"Confirm filing; Cases rechecks current access and resolves the exact revision.",
"Verify the case revision and snapshot digest in the record chronology.",
],
"outcome": "The eAkte preserves an exact case snapshot reference while Cases retains authority.",
},
),
DocumentationTopic(
id="cases.institutional-context",
title="Case institutional context",
+115
View File
@@ -0,0 +1,115 @@
from __future__ import annotations
from collections.abc import Sequence
import hashlib
import json
from urllib.parse import quote
from sqlalchemy.orm import Session
from govoplan_core.core.records import (
RecordContractError,
RecordSourceLocator,
RecordSourceReference,
)
from govoplan_cases.backend.db.models import CaseIdentity, CaseRecordRevision
from govoplan_cases.backend.service import can_access_case
CAPABILITY_RECORD_SOURCE_CASES = "records.source.cases"
class CasesRecordSource:
provider_id = "cases"
def resource_types(self) -> Sequence[str]:
return ("case_revision",)
def resolve(
self,
session: object,
principal: object,
*,
locator: RecordSourceLocator,
purpose: str,
) -> RecordSourceReference:
if not isinstance(session, Session):
raise RecordContractError(
"Case 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("Case record references cannot cross tenants.")
if locator.source_module != "cases" or locator.resource_type != "case_revision":
raise RecordContractError("Unsupported Cases record source type.")
if not str(purpose or "").strip():
raise RecordContractError("Case record references require a purpose.")
if not hasattr(principal, "has") or not (
principal.has("cases:case:read") or principal.has("cases:case:admin")
):
raise RecordContractError("Current Cases read permission is required.")
if not can_access_case(
session, principal, case_id=locator.resource_id, permission="read"
):
raise RecordContractError("The current principal cannot read this case.")
try:
revision = int(locator.source_revision)
except ValueError as exc:
raise RecordContractError("Case source revisions must be numeric.") from exc
row = (
session.query(CaseRecordRevision)
.filter(
CaseRecordRevision.tenant_id == tenant_id,
CaseRecordRevision.case_id == locator.resource_id,
CaseRecordRevision.revision == revision,
)
.one_or_none()
)
if row is None:
raise RecordContractError("The exact case revision does not exist.")
identity = (
session.query(CaseIdentity)
.filter(
CaseIdentity.tenant_id == tenant_id,
CaseIdentity.case_id == locator.resource_id,
)
.one_or_none()
)
if identity is None:
raise RecordContractError("The case identity is unavailable.")
snapshot_json = json.dumps(
row.snapshot,
sort_keys=True,
separators=(",", ":"),
default=str,
).encode("utf-8")
return RecordSourceReference(
locator=locator,
label=f"{identity.case_number} - {row.title}",
authority_mode="external_authoritative",
content_sha256=hashlib.sha256(snapshot_json).hexdigest(),
content_type="application/vnd.govoplan.case-revision+json",
size_bytes=len(snapshot_json),
valid_from=row.opened_at,
valid_to=row.closed_at,
recorded_at=row.recorded_at,
launch_url=f"/cases/{quote(row.case_id, safe='')}",
metadata={
"case_number": identity.case_number,
"case_type_key": row.case_type_key,
"status_key": row.status_key,
"access_mode": row.access_mode,
"snapshot_sha256": hashlib.sha256(snapshot_json).hexdigest(),
},
)
def create_cases_record_source(_context: object) -> CasesRecordSource:
return CasesRecordSource()
__all__ = [
"CAPABILITY_RECORD_SOURCE_CASES",
"CasesRecordSource",
"create_cases_record_source",
]