437 lines
20 KiB
Python
437 lines
20 KiB
Python
from __future__ import annotations
|
|
|
|
from govoplan_core.core.application_status import (
|
|
CAPABILITY_APPLICATION_STATUS_PROJECTION,
|
|
application_status_projection_provider,
|
|
)
|
|
from govoplan_core.core.institutional import (
|
|
CAPABILITY_SERVICE_AVAILABILITY,
|
|
CAPABILITY_SERVICE_DEFINITIONS,
|
|
service_launch_capability,
|
|
)
|
|
from govoplan_core.core.postbox import CAPABILITY_POSTBOX_PORTAL
|
|
from govoplan_core.core.modules import (
|
|
CapabilityDocumentation,
|
|
DocumentationCondition,
|
|
DocumentationLink,
|
|
DocumentationTopic,
|
|
FrontendModule,
|
|
FrontendRoute,
|
|
ModuleContext,
|
|
ModuleInterfaceProvider,
|
|
ModuleInterfaceRequirement,
|
|
ModuleManifest,
|
|
NavItem,
|
|
PermissionDefinition,
|
|
ProductAreaContribution,
|
|
PublicFrontendRoute,
|
|
RoleTemplate,
|
|
)
|
|
from govoplan_core.core.provider_governance import (
|
|
ModuleArchitectureDeclaration,
|
|
ModuleArchitectureDocumentation,
|
|
ModuleMaturityEvidence,
|
|
)
|
|
from govoplan_portal.backend.service_directory import (
|
|
CAPABILITY_PORTAL_SERVICE_DIRECTORY,
|
|
PortalServiceDirectory,
|
|
)
|
|
from govoplan_core.core.views import ViewSurface
|
|
|
|
|
|
MODULE_ID = "portal"
|
|
MODULE_VERSION = "0.1.19"
|
|
READ_SCOPE = "portal:service:read"
|
|
SERVICE_LAUNCH_CAPABILITIES = tuple(
|
|
service_launch_capability(kind) for kind in ("case", "form", "workflow")
|
|
)
|
|
|
|
|
|
def _service_directory(context: ModuleContext) -> PortalServiceDirectory:
|
|
return PortalServiceDirectory(context.registry)
|
|
|
|
|
|
def _router(context: ModuleContext):
|
|
from govoplan_portal.backend.router import configure_registry, router
|
|
|
|
configure_registry(context.registry)
|
|
return router
|
|
|
|
|
|
def _public_tenant_resolver(request: object, session: object) -> str | None:
|
|
path = str(getattr(getattr(request, "url", None), "path", ""))
|
|
if "/portal/status/" not in path:
|
|
return None
|
|
path_params = getattr(request, "path_params", {})
|
|
tracking_id = str(
|
|
path_params.get("trackingId")
|
|
or path_params.get("tracking_id")
|
|
or path.rsplit("/", 1)[-1]
|
|
or ""
|
|
).strip()
|
|
if not tracking_id:
|
|
return None
|
|
app = getattr(request, "app", None)
|
|
registry = getattr(getattr(app, "state", None), "govoplan_registry", None)
|
|
provider = application_status_projection_provider(registry)
|
|
if provider is None:
|
|
return None
|
|
return provider.tenant_id_for_tracking_id(session, tracking_id=tracking_id)
|
|
|
|
|
|
manifest = ModuleManifest(
|
|
id=MODULE_ID,
|
|
name="Portal",
|
|
version=MODULE_VERSION,
|
|
optional_dependencies=(
|
|
"access",
|
|
"services",
|
|
"cases",
|
|
"forms",
|
|
"forms_runtime",
|
|
"workflow_engine",
|
|
"postbox",
|
|
),
|
|
optional_capabilities=(
|
|
CAPABILITY_SERVICE_DEFINITIONS,
|
|
CAPABILITY_SERVICE_AVAILABILITY,
|
|
*SERVICE_LAUNCH_CAPABILITIES,
|
|
CAPABILITY_POSTBOX_PORTAL,
|
|
CAPABILITY_APPLICATION_STATUS_PROJECTION,
|
|
),
|
|
permissions=(
|
|
PermissionDefinition(
|
|
scope=READ_SCOPE,
|
|
label="View service directory",
|
|
description="Discover services available to the current account and function assignments.",
|
|
category="Portal",
|
|
level="tenant",
|
|
module_id=MODULE_ID,
|
|
resource="service",
|
|
action="read",
|
|
),
|
|
),
|
|
role_templates=(
|
|
RoleTemplate(
|
|
slug="portal_user",
|
|
name="Portal user",
|
|
description="Discover and enter available institutional services.",
|
|
permissions=(READ_SCOPE,),
|
|
default_authenticated=True,
|
|
),
|
|
),
|
|
provides_interfaces=(
|
|
ModuleInterfaceProvider(name="portal.service_directory", version="0.1.0"),
|
|
),
|
|
requires_interfaces=(
|
|
ModuleInterfaceRequirement(name="services.definition", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True),
|
|
ModuleInterfaceRequirement(name="services.availability", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True),
|
|
*(
|
|
ModuleInterfaceRequirement(
|
|
name=capability,
|
|
version_min="0.1.0",
|
|
version_max_exclusive="0.2.0",
|
|
optional=True,
|
|
)
|
|
for capability in SERVICE_LAUNCH_CAPABILITIES
|
|
),
|
|
ModuleInterfaceRequirement(
|
|
name=CAPABILITY_POSTBOX_PORTAL,
|
|
version_min="0.1.0",
|
|
version_max_exclusive="0.2.0",
|
|
optional=True,
|
|
),
|
|
ModuleInterfaceRequirement(
|
|
name=CAPABILITY_APPLICATION_STATUS_PROJECTION,
|
|
version_min="1.0.0",
|
|
version_max_exclusive="2.0.0",
|
|
optional=True,
|
|
),
|
|
),
|
|
capability_factories={
|
|
CAPABILITY_PORTAL_SERVICE_DIRECTORY: _service_directory,
|
|
},
|
|
route_factory=_router,
|
|
public_tenant_resolver=_public_tenant_resolver,
|
|
nav_items=(
|
|
NavItem(
|
|
path="/portal",
|
|
label="Services",
|
|
icon="landmark",
|
|
required_any=(READ_SCOPE,),
|
|
order=25,
|
|
),
|
|
),
|
|
frontend=FrontendModule(
|
|
module_id=MODULE_ID,
|
|
package_name="@govoplan/portal-webui",
|
|
routes=(
|
|
FrontendRoute(
|
|
path="/portal",
|
|
component="PortalPage",
|
|
required_any=(READ_SCOPE,),
|
|
order=25,
|
|
),
|
|
),
|
|
public_routes=(
|
|
PublicFrontendRoute(
|
|
path="/portal/status/:trackingId",
|
|
component="PortalStatusPage",
|
|
order=12,
|
|
),
|
|
),
|
|
nav_items=(
|
|
NavItem(
|
|
path="/portal",
|
|
label="Services",
|
|
icon="landmark",
|
|
required_any=(READ_SCOPE,),
|
|
order=25,
|
|
),
|
|
),
|
|
product_areas=(
|
|
ProductAreaContribution(
|
|
id="services-cases",
|
|
module_id=MODULE_ID,
|
|
label="i18n:govoplan-core.product_area.services_cases",
|
|
icon="landmark",
|
|
description="i18n:govoplan-core.product_area.services_cases_description",
|
|
surface_ids=("portal.nav.portal", "portal.route.portal"),
|
|
order=20,
|
|
),
|
|
),
|
|
view_surfaces=(
|
|
ViewSurface(
|
|
id="portal.navigation",
|
|
module_id=MODULE_ID,
|
|
kind="navigation",
|
|
label="Services navigation",
|
|
order=10,
|
|
),
|
|
ViewSurface(
|
|
id="portal.directory",
|
|
module_id=MODULE_ID,
|
|
kind="route",
|
|
label="Service directory",
|
|
order=20,
|
|
),
|
|
ViewSurface(
|
|
id="portal.application-status",
|
|
module_id=MODULE_ID,
|
|
kind="route",
|
|
label="Applicant status",
|
|
order=30,
|
|
),
|
|
),
|
|
),
|
|
capability_documentation={
|
|
CAPABILITY_PORTAL_SERVICE_DIRECTORY: CapabilityDocumentation(
|
|
label="Portal service directory",
|
|
summary="Projects governed service definitions into role-aware availability entries.",
|
|
contract_version="0.1.0",
|
|
),
|
|
},
|
|
documentation=(
|
|
DocumentationTopic(
|
|
id="portal.data-subject-requests",
|
|
title="Portal data-subject request boundary",
|
|
summary="Understand why Portal has no separate privacy export and which authoritative modules own the projected data.",
|
|
body=(
|
|
"Portal persists no service-directory, service-launch, Postbox, application-status, applicant, or session records, so it deliberately publishes no duplicate data-subject request provider. Services owns definition attribution; Cases, Forms Runtime, and Workflow Engine own launch effects; Postbox owns mailbox records; and Forms Runtime owns submission and status-access data. Core and the deployment operator own authentication state, request/security logs, and infrastructure telemetry. "
|
|
"If durable personalization, analytics, saved searches, contact data, or sessions are added to Portal, a tenant-scoped privacy provider is required before release."
|
|
),
|
|
layer="available",
|
|
documentation_types=("admin", "user"),
|
|
audience=("user", "operator", "module_admin", "auditor"),
|
|
translations={
|
|
"de": {
|
|
"title": "Datenschutzgrenze des Portals",
|
|
"summary": "Verstehen, warum das Portal keinen eigenen Datenschutzexport bereitstellt und welche maßgeblichen Module die projizierten Daten verwalten.",
|
|
"body": (
|
|
"Das Portal speichert weder Dienstverzeichnis-, Dienststart-, Postfach-, Antragsstatus-, Antragsteller- noch Sitzungsdaten und stellt deshalb bewusst keinen doppelten Auskunftsanbieter bereit. "
|
|
"Services verwaltet die Zuordnung von Dienstdefinitionen; Cases, Forms Runtime und Workflow Engine verwalten die Wirkungen eines Starts; Postbox verwaltet Postfachdaten; Forms Runtime verwaltet Einreichungs- und Statuszugriffsdaten. "
|
|
"Core und der Betriebsverantwortliche verwalten Authentifizierungszustand, Anfrage- und Sicherheitsprotokolle sowie Infrastrukturtelemetrie. "
|
|
"Werden dem Portal dauerhafte Personalisierung, Analysen, gespeicherte Suchen, Kontaktdaten oder Sitzungen hinzugefügt, ist vor der Freigabe ein mandantenbezogener Datenschutzanbieter erforderlich."
|
|
),
|
|
}
|
|
},
|
|
links=(
|
|
DocumentationLink(
|
|
label="Portal ownership boundary",
|
|
href="govoplan-portal/docs/SERVICE_DIRECTORY_CONCEPT.md",
|
|
kind="repository",
|
|
),
|
|
),
|
|
related_modules=(
|
|
"core",
|
|
"services",
|
|
"cases",
|
|
"forms_runtime",
|
|
"workflow_engine",
|
|
"postbox",
|
|
),
|
|
metadata={
|
|
"kind": "reference",
|
|
"help_contexts": ["portal.data-subject-requests"],
|
|
"dsar_coverage": "not_applicable_no_persistence",
|
|
},
|
|
order=10,
|
|
),
|
|
DocumentationTopic(
|
|
id="portal.function-postboxes",
|
|
title="Portal-facing function Postboxes",
|
|
summary="Open explicitly published function Postboxes without moving their access rules into Portal.",
|
|
body=(
|
|
"When Postbox is installed, Portal can display Postboxes whose exact definition or published template revision is marked portal-visible. "
|
|
"Postbox re-evaluates the current function assignment, classification, and read authority for every projection. Portal stores no Postbox ACL, "
|
|
"does not expose vacant or inaccessible addresses, and links back to the authoritative Postbox surface."
|
|
),
|
|
layer="configured",
|
|
documentation_types=("admin", "user"),
|
|
audience=("user", "operator", "module_admin"),
|
|
translations={
|
|
"de": {
|
|
"title": "Portal-sichtbare Funktionspostfächer",
|
|
"summary": "Ausdrücklich veröffentlichte Funktionspostfächer öffnen, ohne ihre Zugriffsregeln in das Portal zu verlagern.",
|
|
"body": (
|
|
"Ist Postbox installiert, kann das Portal Postfächer anzeigen, deren exakte Definition oder veröffentlichte Vorlagenrevision als portalsichtbar markiert ist. "
|
|
"Postbox prüft für jede Projektion die aktuelle Funktionszuordnung, Klassifikation und Leseberechtigung erneut. Das Portal speichert keine Postfach-ACL, "
|
|
"zeigt keine unbesetzten oder nicht zugänglichen Adressen und verweist auf die maßgebliche Postbox-Oberfläche."
|
|
),
|
|
}
|
|
},
|
|
links=(DocumentationLink(label="Portal", href="/portal", kind="runtime"),),
|
|
related_modules=("postbox", "idm", "organizations"),
|
|
metadata={"kind": "guide", "help_contexts": ["portal.postboxes"]},
|
|
order=20,
|
|
),
|
|
DocumentationTopic(
|
|
id="portal.application-status",
|
|
title="Track an application",
|
|
summary="View the bounded public lifecycle through the access profile configured for the exact submitted Form revision.",
|
|
body=(
|
|
"Portal presents the applicant status projection owned by Forms Runtime. The service administrator chooses authenticated-only access, a short-lived email link, or a permanent public bearer link for each exact published Form revision. "
|
|
"Authenticated access is checked against the applicant account. Email-link requests return the same response for matching and non-matching details, revoke the previous link when a new one is sent, and depend on configured Notifications and Mail delivery. A permanent link does not expire or require sign-in and must therefore be handled like a bearer secret. "
|
|
"The page exposes only lifecycle states, update times, a tracking identifier, and the submission receipt. It does not display Form values, evidence, internal notes, actors, decision reasoning, or module handoff details."
|
|
),
|
|
layer="configured",
|
|
documentation_types=("admin", "user"),
|
|
audience=("public", "user", "operator", "module_admin"),
|
|
translations={
|
|
"de": {
|
|
"title": "Antrag verfolgen",
|
|
"summary": "Den begrenzten öffentlichen Verlauf über das für die genaue veröffentlichte Formularrevision konfigurierte Zugriffsprofil einsehen.",
|
|
"body": (
|
|
"Das Portal zeigt die von Forms Runtime verwaltete Antragsstatusprojektion. Für jede genaue veröffentlichte Formularrevision wählt die Dienstadministration zwischen ausschließlich authentifiziertem Zugriff, einem kurzlebigen E-Mail-Link und einem dauerhaften öffentlichen Inhaberlink. "
|
|
"Beim authentifizierten Zugriff wird das Antragstellerkonto geprüft. Anforderungen eines E-Mail-Links liefern für passende und unpassende Angaben dieselbe Antwort, widerrufen beim erneuten Versand den vorherigen Link und setzen konfigurierte Notifications- und Mail-Zustellung voraus. Ein dauerhafter Link läuft nicht ab und erfordert keine Anmeldung; er ist deshalb wie ein Inhabergeheimnis zu behandeln. "
|
|
"Die Seite zeigt ausschließlich Lebenszyklusstatus, Aktualisierungszeiten, eine Vorgangskennung und die Einreichungsbestätigung. Formularwerte, Nachweise, interne Notizen, handelnde Personen, Entscheidungsbegründungen und Details von Modulübergaben bleiben verborgen."
|
|
),
|
|
}
|
|
},
|
|
links=(
|
|
DocumentationLink(
|
|
label="Applicant status",
|
|
href="/portal/status/:trackingId",
|
|
kind="runtime",
|
|
),
|
|
),
|
|
related_modules=("forms_runtime", "notifications", "mail"),
|
|
metadata={
|
|
"kind": "guide",
|
|
"help_contexts": ["portal.application-status"],
|
|
"privacy_notes": [
|
|
"Possession of a permanent status link grants access to the bounded projection until its owner suspends the policy or revokes the grant.",
|
|
"The email request surface does not reveal whether the tracking identifier, email address, provider, or delivery attempt matched."
|
|
],
|
|
},
|
|
order=30,
|
|
),
|
|
DocumentationTopic(
|
|
id="portal.service-directory",
|
|
title="Service directory",
|
|
summary="Find services available in the configured institution and understand relevant availability limits.",
|
|
body=(
|
|
"Portal presents provider-owned, versioned service definitions. "
|
|
"Published services may be available, unavailable with a reason, "
|
|
"or undiscoverable when they do not apply to the current audience. "
|
|
"Opening a service re-evaluates that exact revision and delegates case, "
|
|
"form, or workflow startup to an installed owner capability."
|
|
),
|
|
layer="available",
|
|
documentation_types=("admin", "user"),
|
|
audience=("user", "operator", "module_admin"),
|
|
conditions=(
|
|
DocumentationCondition(
|
|
required_modules=("portal",),
|
|
required_scopes=(READ_SCOPE,),
|
|
),
|
|
),
|
|
translations={
|
|
"de": {
|
|
"title": "Dienstverzeichnis",
|
|
"summary": "Verfügbare Dienste der konfigurierten Institution finden und ihre maßgeblichen Verfügbarkeitsgrenzen verstehen.",
|
|
"body": (
|
|
"Das Portal zeigt versionierte Dienstdefinitionen ihrer jeweils verantwortlichen Anbieter. Veröffentlichte Dienste können verfügbar sein, mit einer Begründung als nicht verfügbar erscheinen oder unauffindbar bleiben, wenn sie für die aktuelle Zielgruppe nicht gelten. "
|
|
"Beim Öffnen wird die genaue Revision erneut geprüft und der Start eines Falls, Formulars oder Workflows an die installierte Besitzerfunktion übergeben."
|
|
),
|
|
}
|
|
},
|
|
links=(
|
|
DocumentationLink(
|
|
label="Service directory architecture",
|
|
href="govoplan-portal/docs/SERVICE_DIRECTORY_CONCEPT.md",
|
|
kind="repository",
|
|
),
|
|
DocumentationLink(
|
|
label="Portal interface pattern audit",
|
|
href="govoplan-portal/docs/INTERFACE_PATTERN_MIGRATION.md",
|
|
kind="repository",
|
|
),
|
|
),
|
|
metadata={
|
|
"kind": "workflow",
|
|
"help_contexts": ["portal.service-directory"],
|
|
"steps": [
|
|
"Open the service directory and select an applicable published service.",
|
|
"Review any explained availability restriction before continuing.",
|
|
"Open the service so Portal rechecks the exact revision and hands the start to its owning module.",
|
|
],
|
|
},
|
|
),
|
|
),
|
|
architecture=ModuleArchitectureDeclaration(
|
|
layer="communication_participation",
|
|
kind="domain",
|
|
maturity="vertical_slice",
|
|
evidence=(
|
|
ModuleMaturityEvidence(
|
|
kind="test",
|
|
reference="tests/test_service_directory.py",
|
|
summary="Proves provider-neutral service discovery and explained availability.",
|
|
),
|
|
ModuleMaturityEvidence(
|
|
kind="documentation",
|
|
reference="docs/SERVICE_DIRECTORY_CONCEPT.md",
|
|
summary="Defines Portal presentation and Services ownership boundaries.",
|
|
),
|
|
),
|
|
known_limits=(
|
|
"Portal does not persist service definitions; the Services provider remains authoritative.",
|
|
"Case, Forms Runtime, and Workflow Engine own launch effects. Portal keeps entries unavailable whenever the selected owner capability is absent.",
|
|
"Forms Runtime owns applicant-status access, redaction, and timeline semantics; Portal only presents that projection. Payment and decision-document actions are not yet included in the first status surface.",
|
|
"Portal owns no durable subject records and therefore has no DSAR provider; adding persistence requires introducing one before release.",
|
|
),
|
|
owned_concepts=("service discovery", "service presentation", "channel entry", "applicant status presentation"),
|
|
non_owned_concepts=("institutional service definition", "case lifecycle", "applicant status access decision"),
|
|
reference_packages=("product.service-to-decision",),
|
|
documentation=ModuleArchitectureDocumentation(
|
|
security=("docs/SERVICE_DIRECTORY_CONCEPT.md",),
|
|
operations=("docs/SERVICE_DIRECTORY_CONCEPT.md",),
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def get_manifest() -> ModuleManifest:
|
|
return manifest
|