"""PWG Pilot Demo (April 2026) Bootstraps a complete PWG-Pilot demo environment in an empty dev/demo install: - 1 mandate "Stiftung PWG" - 1 SysAdmin demo user "pwg.demo" - 4 features: workspace, trustee (BUHA PWG), graphicalEditor (PWG Automationen), neutralization (Datenschutz) - Trustee seed-data (5 fictitious tenants with monthly rent journal lines for the current year, loaded from ``demoData/pwg/_seedTrusteeData.json``) - Pilot workflow imported from ``demoData/workflows/pwg-mietzinsbestaetigung-pilot.workflow.json`` (active=false — user activates manually after triggering once). Idempotent: ``load()`` skips anything that already exists; ``remove()`` deletes mandate, user, seed data and imported workflow cleanly. Pattern: subclass of :class:`_BaseDemoConfig`, auto-discovered by ``demoConfigs/__init__.py``. See ``investorDemo2026.py`` for the reference implementation we mirror here. """ import json import logging import uuid from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional from modules.demoConfigs._baseDemoConfig import _BaseDemoConfig logger = logging.getLogger(__name__) _DEMO_PREFIX = "demo-pwg2026" _MANDATE_PWG = { "name": "stiftung-pwg", "label": "Stiftung PWG", } _USER = { "username": "pwg.demo", "email": "pwg.demo@poweron.swiss", "fullName": "PWG Demo Sachbearbeiter", "password": "pwg.demo.2026", "language": "de", } _FEATURES_PWG = [ {"code": "workspace", "label": "Dokumentenablage PWG"}, {"code": "trustee", "label": "Buchhaltung PWG"}, {"code": "graphicalEditor", "label": "PWG Automationen"}, {"code": "neutralization", "label": "Datenschutz"}, ] # Filename markers used to identify the imported pilot workflow on remove(). _PILOT_WORKFLOW_LABEL = "PWG Pilot: Jahresmietzinsbestätigung" _PILOT_WORKFLOW_FILE = "pwg-mietzinsbestaetigung-pilot.workflow.json" _SEED_TRUSTEE_FILE = "_seedTrusteeData.json" class PwgDemo2026(_BaseDemoConfig): code = "pwg-demo-2026" label = "PWG Pilot Demo (Mietzinsbestätigungen)" description = ( "Stiftung PWG, ein Demo-Sachbearbeiter, Trustee mit fiktiven Mietern, " "Graph-Editor mit dem Pilot-Workflow für Jahresmietzinsbestätigungen " "(als File importiert, active=false). Idempotent." ) credentials = [ { "role": "Demo-Sachbearbeiter", "username": _USER["username"], "email": _USER["email"], "password": _USER["password"], } ] # ------------------------------------------------------------------ # load # ------------------------------------------------------------------ def load(self, db) -> Dict[str, Any]: summary: Dict[str, Any] = {"created": [], "skipped": [], "errors": []} try: mandateId = self._ensureMandate(db, _MANDATE_PWG, summary) userId = self._ensureUser(db, summary) self._ensurePlatformAdminFlag(db, userId, summary) if mandateId and userId: self._ensureMembership(db, userId, mandateId, _MANDATE_PWG["label"], summary) self._ensureFeatures(db, mandateId, _MANDATE_PWG["label"], _FEATURES_PWG, summary) self._ensureFeatureAccess(db, userId, mandateId, _MANDATE_PWG["label"], summary) self._ensureNeutralizationConfig(db, mandateId, userId, summary) self._ensureBilling(db, mandateId, _MANDATE_PWG["label"], summary) trusteeInstanceId = self._getFeatureInstanceId(db, mandateId, "trustee", "Buchhaltung PWG") if trusteeInstanceId: self._ensureTrusteeSeed(mandateId, trusteeInstanceId, summary) graphInstanceId = self._getFeatureInstanceId(db, mandateId, "graphicalEditor", "PWG Automationen") if graphInstanceId: self._ensurePilotWorkflow(mandateId, graphInstanceId, summary) except Exception as e: logger.error(f"PWG demo load failed: {e}", exc_info=True) summary["errors"].append(str(e)) # Surface initial credentials so the SysAdmin doesn't have to grep the # source code -- consumed by AdminDemoConfigPage to render a copyable # login box in the result banner. summary["credentials"] = list(self.credentials) return summary # ------------------------------------------------------------------ # remove # ------------------------------------------------------------------ def remove(self, db) -> Dict[str, Any]: summary: Dict[str, Any] = {"removed": [], "errors": []} from modules.datamodels.datamodelMembership import UserMandate from modules.datamodels.datamodelUam import Mandate, UserInDB try: existing = db.getRecordset(Mandate, recordFilter={"name": _MANDATE_PWG["name"]}) for m in existing: mid = m.get("id") self._removeMandateData(db, mid, _MANDATE_PWG["label"], summary) db.recordDelete(Mandate, mid) summary["removed"].append(f"Mandate {_MANDATE_PWG['label']} ({mid})") logger.info(f"Removed mandate {_MANDATE_PWG['label']} ({mid})") except Exception as e: summary["errors"].append(f"Remove mandate {_MANDATE_PWG['label']}: {e}") try: existing = db.getRecordset(UserInDB, recordFilter={"username": _USER["username"]}) for u in existing: uid = u.get("id") memberships = db.getRecordset(UserMandate, recordFilter={"userId": uid}) or [] for mem in memberships: try: db.recordDelete(UserMandate, mem.get("id")) except Exception: pass db.recordDelete(UserInDB, uid) summary["removed"].append(f"User {_USER['username']} ({uid})") logger.info(f"Removed user {_USER['username']} ({uid})") except Exception as e: summary["errors"].append(f"Remove user: {e}") return summary # ================================================================== # — load helpers (mostly mirrors of investorDemo2026.py) # ================================================================== def _ensureMandate(self, db, mandateDef: Dict, summary: Dict) -> Optional[str]: from modules.datamodels.datamodelUam import Mandate from modules.interfaces.interfaceBootstrap import copySystemRolesToMandate existing = db.getRecordset(Mandate, recordFilter={"name": mandateDef["name"]}) if existing: mid = existing[0].get("id") summary["skipped"].append(f"Mandate {mandateDef['label']} exists ({mid})") return mid mandate = Mandate(name=mandateDef["name"], label=mandateDef["label"], enabled=True) created = db.recordCreate(Mandate, mandate) mid = created.get("id") logger.info(f"Created mandate {mandateDef['label']} ({mid})") summary["created"].append(f"Mandate {mandateDef['label']}") copySystemRolesToMandate(db, mid) return mid def _ensureUser(self, db, summary: Dict) -> Optional[str]: from modules.datamodels.datamodelUam import AuthAuthority, UserInDB from passlib.context import CryptContext existing = db.getRecordset(UserInDB, recordFilter={"username": _USER["username"]}) if existing: uid = existing[0].get("id") summary["skipped"].append(f"User {_USER['username']} exists ({uid})") return uid pwdContext = CryptContext(schemes=["argon2"], deprecated="auto") user = UserInDB( username=_USER["username"], email=_USER["email"], fullName=_USER["fullName"], enabled=True, language=_USER["language"], isSysAdmin=True, authenticationAuthority=AuthAuthority.LOCAL, hashedPassword=pwdContext.hash(_USER["password"]), ) created = db.recordCreate(UserInDB, user) uid = created.get("id") logger.info(f"Created user {_USER['username']} ({uid})") summary["created"].append(f"User {_USER['fullName']}") return uid def _ensurePlatformAdminFlag(self, db, userId: Optional[str], summary: Dict): from modules.datamodels.datamodelUam import UserInDB if not userId: return existing = db.getRecord(UserInDB, userId) if not existing: summary["errors"].append(f"User {userId} not found — cannot set isPlatformAdmin") return currentFlag = bool(existing.get("isPlatformAdmin", False)) if isinstance(existing, dict) else bool(getattr(existing, "isPlatformAdmin", False)) if currentFlag: summary["skipped"].append("isPlatformAdmin already set") return db.recordModify(UserInDB, userId, {"isPlatformAdmin": True}) summary["created"].append("isPlatformAdmin flag") def _ensureMembership(self, db, userId: str, mandateId: str, mandateLabel: str, summary: Dict): from modules.datamodels.datamodelMembership import UserMandate, UserMandateRole from modules.datamodels.datamodelRbac import Role existing = db.getRecordset(UserMandate, recordFilter={"userId": userId, "mandateId": mandateId}) if existing: userMandateId = existing[0].get("id") summary["skipped"].append(f"Membership {_USER['username']} -> {mandateLabel} exists") else: um = UserMandate(userId=userId, mandateId=mandateId, enabled=True) created = db.recordCreate(UserMandate, um) userMandateId = created.get("id") summary["created"].append(f"Membership {_USER['username']} -> {mandateLabel}") adminRoles = db.getRecordset(Role, recordFilter={"mandateId": mandateId, "roleLabel": "admin"}) if adminRoles: adminRoleId = adminRoles[0].get("id") existingRole = db.getRecordset(UserMandateRole, recordFilter={"userMandateId": userMandateId, "roleId": adminRoleId}) if not existingRole: umr = UserMandateRole(userMandateId=userMandateId, roleId=adminRoleId) db.recordCreate(UserMandateRole, umr) def _ensureFeatures(self, db, mandateId: str, mandateLabel: str, featureDefs: List[Dict], summary: Dict): from modules.interfaces.interfaceFeatures import getFeatureInterface fi = getFeatureInterface(db) existingInstances = fi.getFeatureInstancesForMandate(mandateId) existingLabels = { (inst.label if hasattr(inst, "label") else inst.get("label", "")) for inst in existingInstances } for featureDef in featureDefs: code = featureDef["code"] instanceLabel = featureDef["label"] if instanceLabel in existingLabels: summary["skipped"].append(f"Feature '{instanceLabel}' in {mandateLabel} exists") continue try: fi.createFeatureInstance( featureCode=code, mandateId=mandateId, label=instanceLabel, enabled=True, copyTemplateRoles=True, ) summary["created"].append(f"Feature '{instanceLabel}' in {mandateLabel}") except Exception as e: summary["errors"].append(f"Feature '{instanceLabel}' in {mandateLabel}: {e}") def _ensureFeatureAccess(self, db, userId: str, mandateId: str, mandateLabel: str, summary: Dict): """Grant the demo user admin access on EVERY feature instance of the mandate. Without an explicit ``FeatureAccess`` + ``{code}-admin`` role the user does not see any feature tile in the UI -- so this method ALSO heals a half-broken state by re-copying the per-feature template roles if they are missing (e.g. when the instance was created via an older code path that skipped ``copyTemplateRoles``). """ from modules.datamodels.datamodelFeatures import FeatureInstance from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole from modules.datamodels.datamodelRbac import Role from modules.interfaces.interfaceFeatures import getFeatureInterface instances = db.getRecordset(FeatureInstance, recordFilter={"mandateId": mandateId}) or [] for inst in instances: instId = inst.get("id") featureCode = inst.get("featureCode", "") if not instId: continue existing = db.getRecordset(FeatureAccess, recordFilter={"userId": userId, "featureInstanceId": instId}) if existing: featureAccessId = existing[0].get("id") summary["skipped"].append(f"FeatureAccess {featureCode} in {mandateLabel} exists") else: fa = FeatureAccess(userId=userId, featureInstanceId=instId, enabled=True) created = db.recordCreate(FeatureAccess, fa) featureAccessId = created.get("id") summary["created"].append(f"FeatureAccess {featureCode} in {mandateLabel}") adminRoleLabel = f"{featureCode}-admin" adminRoles = db.getRecordset(Role, recordFilter={ "featureInstanceId": instId, "roleLabel": adminRoleLabel, }) # Self-heal: if the per-feature admin role does not exist on this # instance the template roles were never copied -- copy them now. if not adminRoles: logger.warning( "Feature instance %s (%s) is missing role '%s' -- " "re-copying template roles", instId, featureCode, adminRoleLabel, ) try: fi = getFeatureInterface(db) fi._copyTemplateRoles(featureCode, mandateId, instId) summary["created"].append( f"Repaired template roles for {featureCode} in {mandateLabel}" ) except Exception as repairErr: summary["errors"].append( f"Could not repair template roles for {featureCode} " f"in {mandateLabel}: {repairErr}" ) adminRoles = db.getRecordset(Role, recordFilter={ "featureInstanceId": instId, "roleLabel": adminRoleLabel, }) if not adminRoles: # Hard fail surfaced to UI -- without the admin role the user # would silently not see the instance. summary["errors"].append( f"Admin role '{adminRoleLabel}' not found for feature " f"instance {featureCode} in {mandateLabel} -- demo user " f"will not see this feature." ) continue adminRoleId = adminRoles[0].get("id") existingRole = db.getRecordset(FeatureAccessRole, recordFilter={ "featureAccessId": featureAccessId, "roleId": adminRoleId, }) if not existingRole: far = FeatureAccessRole(featureAccessId=featureAccessId, roleId=adminRoleId) db.recordCreate(FeatureAccessRole, far) summary["created"].append( f"Role '{adminRoleLabel}' assigned to demo user in {mandateLabel}" ) def _ensureNeutralizationConfig(self, db, mandateId: Optional[str], userId: Optional[str], summary: Dict): if not mandateId or not userId: return from modules.datamodels.datamodelFeatures import FeatureInstance instances = db.getRecordset(FeatureInstance, recordFilter={"mandateId": mandateId, "featureCode": "neutralization"}) if not instances: return instanceId = instances[0].get("id") try: from modules.features.neutralization.datamodelFeatureNeutralizer import DataNeutraliserConfig existing = db.getRecordset(DataNeutraliserConfig, recordFilter={"featureInstanceId": instanceId}) if existing: summary["skipped"].append(f"Neutralization config for mandate {mandateId} exists") return config = DataNeutraliserConfig( featureInstanceId=instanceId, mandateId=mandateId, userId=userId, enabled=True, scope="featureInstance", ) db.recordCreate(DataNeutraliserConfig, config) summary["created"].append(f"Neutralization config for mandate {mandateId}") except Exception as e: summary["errors"].append(f"Neutralization config: {e}") def _ensureBilling(self, db, mandateId: Optional[str], mandateLabel: str, summary: Dict): if not mandateId: return try: from modules.datamodels.datamodelBilling import BillingSettings from modules.interfaces.interfaceDbBilling import getRootInterface billingInterface = getRootInterface() existingSettings = billingInterface.getSettings(mandateId) if existingSettings: summary["skipped"].append(f"Billing for {mandateLabel} exists") return settings = BillingSettings( mandateId=mandateId, warningThresholdPercent=10.0, notifyOnWarning=True, ) billingInterface.db.recordCreate(BillingSettings, settings) summary["created"].append(f"Billing settings for {mandateLabel}") except Exception as e: summary["errors"].append(f"Billing for {mandateLabel}: {e}") def _getFeatureInstanceId(self, db, mandateId: str, featureCode: str, label: str) -> Optional[str]: from modules.datamodels.datamodelFeatures import FeatureInstance instances = db.getRecordset(FeatureInstance, recordFilter={ "mandateId": mandateId, "featureCode": featureCode, "label": label, }) or [] if instances: return instances[0].get("id") # fallback: any instance of that feature in the mandate instances = db.getRecordset(FeatureInstance, recordFilter={ "mandateId": mandateId, "featureCode": featureCode, }) or [] return instances[0].get("id") if instances else None # ------------------------------------------------------------------ # PWG-specific helpers — Trustee seed-data + pilot-workflow import # ------------------------------------------------------------------ def _ensureTrusteeSeed(self, mandateId: str, featureInstanceId: str, summary: Dict): """Idempotently load 5 fictitious tenants and their 12-month rent journal lines into the trustee database for this feature instance. Skips any tenant whose contact (matched by name+address) already exists, so re-running ``load()`` is safe. """ seedPath = _demoDataDir() / "pwg" / _SEED_TRUSTEE_FILE if not seedPath.is_file(): summary["errors"].append(f"PWG seed file missing: {seedPath}") return try: seed = json.loads(seedPath.read_text(encoding="utf-8")) except Exception as exc: summary["errors"].append(f"PWG seed file unreadable: {exc}") return try: trusteeDb = _openTrusteeDb() except Exception as exc: summary["errors"].append(f"Trustee DB connection failed: {exc}") return from modules.features.trustee.datamodelFeatureTrustee import ( TrusteeDataAccount, TrusteeDataContact, TrusteeDataJournalEntry, TrusteeDataJournalLine, ) rentAccountNumber = str(seed.get("rentAccount", "6000")) year = int(seed.get("year", datetime.now().year)) # 1) Ensure rent account exists once existingAccounts = trusteeDb.getRecordset(TrusteeDataAccount, recordFilter={ "featureInstanceId": featureInstanceId, "accountNumber": rentAccountNumber, }) or [] if not existingAccounts: trusteeDb.recordCreate(TrusteeDataAccount, TrusteeDataAccount( accountNumber=rentAccountNumber, label=str(seed.get("rentAccountLabel", "Mietzinsertrag")), accountType="revenue", accountGroup="rental_income", currency="CHF", isActive=True, mandateId=mandateId, featureInstanceId=featureInstanceId, )) summary["created"].append(f"Trustee account {rentAccountNumber}") # 2) Ensure contacts + monthly journal entries createdTenants = 0 skippedTenants = 0 for tenant in seed.get("tenants", []): name = tenant.get("name", "") address = tenant.get("address", "") if not name: continue existing = trusteeDb.getRecordset(TrusteeDataContact, recordFilter={ "featureInstanceId": featureInstanceId, "name": name, "address": address, }) or [] if existing: skippedTenants += 1 continue contact = TrusteeDataContact( externalId=tenant.get("contactNumber"), contactType="customer", contactNumber=tenant.get("contactNumber"), name=name, address=address, zip=tenant.get("zip"), city=tenant.get("city"), country=tenant.get("country"), email=tenant.get("email"), mandateId=mandateId, featureInstanceId=featureInstanceId, ) trusteeDb.recordCreate(TrusteeDataContact, contact) createdTenants += 1 # 12 monthly rent bookings (credit on rent account) monthlyRent = float(tenant.get("monthlyRentChf") or 0.0) if monthlyRent <= 0: continue for month in range(1, 13): from datetime import datetime as _dtCls, timezone as _tzCls bookingTs = _dtCls(year, month, 1, tzinfo=_tzCls.utc).timestamp() entryRef = f"PWG-{tenant.get('contactNumber')}-{year}{month:02d}" entry = TrusteeDataJournalEntry( externalId=entryRef, bookingDate=bookingTs, reference=entryRef, description=f"Mietzins {month:02d}/{year} {name}", currency="CHF", totalAmount=monthlyRent, mandateId=mandateId, featureInstanceId=featureInstanceId, ) createdEntry = trusteeDb.recordCreate(TrusteeDataJournalEntry, entry) line = TrusteeDataJournalLine( journalEntryId=createdEntry.get("id"), accountNumber=rentAccountNumber, debitAmount=0.0, creditAmount=monthlyRent, currency="CHF", description=f"Mietzins {month:02d}/{year} {name} ({tenant.get('contactNumber')})", mandateId=mandateId, featureInstanceId=featureInstanceId, ) trusteeDb.recordCreate(TrusteeDataJournalLine, line) if createdTenants: summary["created"].append(f"PWG seed: {createdTenants} tenants × 12 monthly journal lines") if skippedTenants: summary["skipped"].append(f"PWG seed: {skippedTenants} tenants already present") def _ensurePilotWorkflow(self, mandateId: str, featureInstanceId: str, summary: Dict): """Import the pilot workflow JSON into the graphical-editor DB. Uses the schema-aware import pipeline introduced in Phase 1 (``_workflowFileSchema.envelopeToWorkflowData`` + ``GraphicalEditorObjects.importWorkflowFromDict``). The workflow is always created with ``active=False`` so a manual trigger is required — this matches the demo-bootstrap safety default. """ envelopePath = _demoDataDir() / "workflows" / _PILOT_WORKFLOW_FILE if not envelopePath.is_file(): summary["errors"].append(f"Pilot workflow file missing: {envelopePath}") return try: envelope = json.loads(envelopePath.read_text(encoding="utf-8")) except Exception as exc: summary["errors"].append(f"Pilot workflow file unreadable: {exc}") return try: geDb = _openGraphicalEditorDb() except Exception as exc: summary["errors"].append(f"GraphicalEditor DB connection failed: {exc}") return from modules.features.graphicalEditor._workflowFileSchema import ( envelopeToWorkflowData, validateFileEnvelope, ) from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import AutoWorkflow from modules.features.graphicalEditor.nodeRegistry import STATIC_NODE_TYPES existing = geDb.getRecordset(AutoWorkflow, recordFilter={ "mandateId": mandateId, "featureInstanceId": featureInstanceId, "label": _PILOT_WORKFLOW_LABEL, }) or [] if existing: summary["skipped"].append(f"Pilot workflow already imported ({existing[0].get('id')})") return knownTypes = [n.get("id") for n in STATIC_NODE_TYPES if isinstance(n, dict) and n.get("id")] try: normalized, warnings = validateFileEnvelope(envelope, knownNodeTypes=knownTypes) except Exception as exc: summary["errors"].append(f"Pilot workflow envelope invalid: {exc}") return if warnings: summary["created"].append(f"Pilot workflow warnings: {warnings}") data = envelopeToWorkflowData( normalized, mandateId=mandateId, featureInstanceId=featureInstanceId, ) # Inject the trustee feature-instance id into the parameters so the # node runtime resolves it without manual editor cleanup. trusteeInstanceId = self._guessTrusteeInstanceId(mandateId) if trusteeInstanceId: for node in data.get("graph", {}).get("nodes", []) or []: params = node.get("parameters") or {} if "featureInstanceId" in params and not params["featureInstanceId"]: params["featureInstanceId"] = trusteeInstanceId node["parameters"] = params # Force-import: AutoWorkflow.create accepts our envelope-derived data # (graph, label, invocations, …) verbatim; we add ids/timestamps that # AutoWorkflow expects. record = AutoWorkflow( id=str(uuid.uuid4()), mandateId=mandateId, featureInstanceId=featureInstanceId, label=data.get("label") or _PILOT_WORKFLOW_LABEL, description=data.get("description") or "", tags=data.get("tags") or [], graph=data.get("graph") or {"nodes": [], "connections": []}, invocations=data.get("invocations") or [], templateScope=data.get("templateScope") or "instance", sharedReadOnly=bool(data.get("sharedReadOnly")), notifyOnFailure=bool(data.get("notifyOnFailure", True)), active=False, ) created = geDb.recordCreate(AutoWorkflow, record) summary["created"].append(f"Pilot workflow imported (active=false, id={created.get('id')})") logger.info(f"Imported pilot workflow into graphicalEditor instance {featureInstanceId}") def _guessTrusteeInstanceId(self, mandateId: str) -> Optional[str]: """Return the first trustee feature-instance id of the given mandate. The demo only ever creates one trustee feature in this mandate, so a first-hit lookup is sufficient and avoids depending on the label. """ try: from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.datamodels.datamodelFeatures import FeatureInstance from modules.shared.configuration import APP_CONFIG appDb = DatabaseConnector( dbHost=APP_CONFIG.get("DB_HOST", "localhost"), dbDatabase="poweron_app", dbUser=APP_CONFIG.get("DB_USER"), dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"), dbPort=int(APP_CONFIG.get("DB_PORT", 5432)), userId=None, ) instances = appDb.getRecordset(FeatureInstance, recordFilter={ "mandateId": mandateId, "featureCode": "trustee", }) or [] return instances[0].get("id") if instances else None except Exception as exc: logger.warning(f"Could not resolve trustee instance for mandate {mandateId}: {exc}") return None # ------------------------------------------------------------------ # remove helpers # ------------------------------------------------------------------ def _removeMandateData(self, db, mandateId: str, mandateLabel: str, summary: Dict): """Cascade-delete everything created by load() for this mandate.""" from modules.datamodels.datamodelBilling import BillingSettings from modules.datamodels.datamodelChat import ChatLog, ChatMessage, ChatWorkflow from modules.datamodels.datamodelFeatures import FeatureInstance from modules.datamodels.datamodelMembership import ( FeatureAccess, FeatureAccessRole, UserMandate, UserMandateRole, ) from modules.datamodels.datamodelRbac import AccessRule, Role instances = db.getRecordset(FeatureInstance, recordFilter={"mandateId": mandateId}) or [] for inst in instances: instId = inst.get("id") featureCode = inst.get("featureCode", "") if not instId: continue if featureCode == "graphicalEditor": self._removeGraphicalEditorData(instId, mandateId, mandateLabel, summary) if featureCode == "trustee": self._removeTrusteeSeed(instId, mandateLabel, summary) if featureCode == "neutralization": self._removeNeutralizationData(db, instId, mandateLabel, summary) chatWorkflows = db.getRecordset(ChatWorkflow, recordFilter={"featureInstanceId": instId}) or [] for wf in chatWorkflows: wfId = wf.get("id") for msg in db.getRecordset(ChatMessage, recordFilter={"workflowId": wfId}) or []: db.recordDelete(ChatMessage, msg.get("id")) for log in db.getRecordset(ChatLog, recordFilter={"workflowId": wfId}) or []: db.recordDelete(ChatLog, log.get("id")) db.recordDelete(ChatWorkflow, wfId) accesses = db.getRecordset(FeatureAccess, recordFilter={"featureInstanceId": instId}) or [] for access in accesses: for role in db.getRecordset(FeatureAccessRole, recordFilter={"featureAccessId": access.get("id")}) or []: db.recordDelete(FeatureAccessRole, role.get("id")) db.recordDelete(FeatureAccess, access.get("id")) db.recordDelete(FeatureInstance, instId) summary["removed"].append(f"FeatureInstance {featureCode} in {mandateLabel}") memberships = db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId}) or [] for um in memberships: for umr in db.getRecordset(UserMandateRole, recordFilter={"userMandateId": um.get("id")}) or []: db.recordDelete(UserMandateRole, umr.get("id")) db.recordDelete(UserMandate, um.get("id")) roles = db.getRecordset(Role, recordFilter={"mandateId": mandateId}) or [] for role in roles: for rule in db.getRecordset(AccessRule, recordFilter={"roleId": role.get("id")}) or []: db.recordDelete(AccessRule, rule.get("id")) db.recordDelete(Role, role.get("id")) try: from modules.interfaces.interfaceDbBilling import getRootInterface billingDb = getRootInterface().db billingSettings = billingDb.getRecordset(BillingSettings, recordFilter={"mandateId": mandateId}) or [] for bs in billingSettings: billingDb.recordDelete(BillingSettings, bs.get("id")) except Exception as e: summary["errors"].append(f"Billing cleanup for {mandateLabel}: {e}") def _removeGraphicalEditorData(self, featureInstanceId: str, mandateId: str, mandateLabel: str, summary: Dict): try: from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import ( AutoRun, AutoStepLog, AutoTask, AutoVersion, AutoWorkflow, ) geDb = _openGraphicalEditorDb() workflows = geDb.getRecordset(AutoWorkflow, recordFilter={ "mandateId": mandateId, "featureInstanceId": featureInstanceId, }) or [] for wf in workflows: wfId = wf.get("id") for version in geDb.getRecordset(AutoVersion, recordFilter={"workflowId": wfId}) or []: geDb.recordDelete(AutoVersion, version.get("id")) for run in geDb.getRecordset(AutoRun, recordFilter={"workflowId": wfId}) or []: runId = run.get("id") for step in geDb.getRecordset(AutoStepLog, recordFilter={"runId": runId}) or []: geDb.recordDelete(AutoStepLog, step.get("id")) geDb.recordDelete(AutoRun, runId) for task in geDb.getRecordset(AutoTask, recordFilter={"workflowId": wfId}) or []: geDb.recordDelete(AutoTask, task.get("id")) geDb.recordDelete(AutoWorkflow, wfId) if workflows: summary["removed"].append(f"{len(workflows)} AutoWorkflows in {mandateLabel}") except Exception as e: summary["errors"].append(f"GraphicalEditor cleanup for {mandateLabel}: {e}") def _removeTrusteeSeed(self, featureInstanceId: str, mandateLabel: str, summary: Dict): try: from modules.features.trustee.datamodelFeatureTrustee import ( TrusteeAccountingConfig, TrusteeDataAccount, TrusteeDataContact, TrusteeDataJournalEntry, TrusteeDataJournalLine, ) trusteeDb = _openTrusteeDb() for model in ( TrusteeDataJournalLine, TrusteeDataJournalEntry, TrusteeDataContact, TrusteeDataAccount, TrusteeAccountingConfig, ): rows = trusteeDb.getRecordset(model, recordFilter={"featureInstanceId": featureInstanceId}) or [] for row in rows: trusteeDb.recordDelete(model, row.get("id")) if rows: summary["removed"].append(f"{len(rows)} {model.__name__} in {mandateLabel}") except Exception as e: summary["errors"].append(f"Trustee cleanup for {mandateLabel}: {e}") def _removeNeutralizationData(self, db, featureInstanceId: str, mandateLabel: str, summary: Dict): try: from modules.features.neutralization.datamodelFeatureNeutralizer import DataNeutraliserConfig configs = db.getRecordset(DataNeutraliserConfig, recordFilter={"featureInstanceId": featureInstanceId}) or [] for cfg in configs: db.recordDelete(DataNeutraliserConfig, cfg.get("id")) if configs: summary["removed"].append(f"DataNeutraliserConfig in {mandateLabel}") except Exception as e: summary["errors"].append(f"Neutralization cleanup for {mandateLabel}: {e}") # ---------------------------------------------------------------------- # Module-level helpers (private) # ---------------------------------------------------------------------- def _demoDataDir() -> Path: """Return absolute path to ``gateway/demoData`` regardless of CWD.""" # __file__ = .../gateway/modules/demoConfigs/pwgDemo2026.py return Path(__file__).resolve().parents[2] / "demoData" def _openTrusteeDb(): """Open a privileged DB connection to ``poweron_trustee`` (used by both seed and remove paths so they work consistently).""" from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.shared.configuration import APP_CONFIG return DatabaseConnector( dbHost=APP_CONFIG.get("DB_HOST", "localhost"), dbDatabase="poweron_trustee", dbUser=APP_CONFIG.get("DB_USER"), dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"), dbPort=int(APP_CONFIG.get("DB_PORT", 5432)), userId=None, ) def _openGraphicalEditorDb(): """Open a privileged DB connection to ``poweron_graphicaleditor``.""" from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.shared.configuration import APP_CONFIG return DatabaseConnector( dbHost=APP_CONFIG.get("DB_HOST", "localhost"), dbDatabase="poweron_graphicaleditor", dbUser=APP_CONFIG.get("DB_USER"), dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"), dbPort=int(APP_CONFIG.get("DB_PORT", 5432)), userId=None, )