feat(cases): expose exact revisions for records filing
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
Reference in New Issue
Block a user