# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Sync trustee positions to accounting (Buha). Input: featureInstanceId, documentList (DataRef on processDocuments[documents] — list with one ActionDocument carrying JSON { positionIds, documentIds, ... }). Reads positionIds from the first document and calls AccountingBridge.pushBatchToAccounting. """ import json import logging from typing import Dict, Any, List from modules.datamodels.datamodelChat import ActionResult, ActionDocument from modules.datamodels.datamodelDocref import DocumentReferenceList logger = logging.getLogger(__name__) def _resolveFirstDocument(documentListParam, services) -> Dict[str, Any] | None: """Resolve the first document from either Graph-Editor output (list of dicts) or Chat references. Returns the parsed JSON dict or None. """ if isinstance(documentListParam, list) and documentListParam: first = documentListParam[0] if isinstance(first, dict) and ("documentData" in first or "documentName" in first): rawData = first.get("documentData") if rawData: try: return json.loads(rawData) if isinstance(rawData, str) else rawData except (json.JSONDecodeError, TypeError): pass chatService = getattr(services, "chat", None) if not chatService: return None try: docList = DocumentReferenceList.from_string_list( documentListParam if isinstance(documentListParam, list) else [documentListParam] ) chatDocuments = chatService.getChatDocumentsFromDocumentList(docList) if not chatDocuments: return None doc = chatDocuments[0] rawBytes = chatService.getFileData(doc.fileId) if not rawBytes: return None content = rawBytes.decode("utf-8") if isinstance(rawBytes, bytes) else rawBytes return json.loads(content) if isinstance(content, str) else content except Exception as e: logger.debug("_resolveFirstDocument chat fallback failed: %s", e) return None async def syncToAccounting(self, parameters: Dict[str, Any]) -> ActionResult: """ Push trustee positions to the configured accounting system. documentList must reference the message from processDocuments (one document with JSON { positionIds, documentIds }). """ featureInstanceId = parameters.get("featureInstanceId") or (self.services.featureInstanceId if hasattr(self.services, "featureInstanceId") else None) documentListParam = parameters.get("documentList") if not featureInstanceId: return ActionResult.isFailure(error="featureInstanceId is required") if not documentListParam: return ActionResult.isFailure(error="documentList is required (reference to processDocuments result)") try: data = _resolveFirstDocument(documentListParam, self.services) if data is None: return ActionResult.isFailure(error="No document found for documentList; ensure processDocuments ran before this action") positionIds = data.get("positionIds") or [] if not positionIds: return ActionResult.isSuccess(documents=[ ActionDocument(documentName="sync_result", documentData=json.dumps({"pushed": 0, "message": "No positionIds in document"}), mimeType="application/json") ]) from modules.features.trustee.interfaceFeatureTrustee import getInterface as getTrusteeInterface from modules.features.trustee.accounting.accountingBridge import AccountingBridge trusteeInterface = getTrusteeInterface( self.services.user, mandateId=self.services.mandateId, featureInstanceId=featureInstanceId ) bridge = AccountingBridge(trusteeInterface) results = await bridge.pushBatchToAccounting(featureInstanceId, positionIds) successCount = sum(1 for r in results if r.success) summary = {"pushed": successCount, "total": len(positionIds), "results": [{"positionId": pid, "success": r.success, "error": getattr(r, "errorMessage", None)} for pid, r in zip(positionIds, results)]} return ActionResult.isSuccess(documents=[ ActionDocument(documentName="sync_result", documentData=json.dumps(summary), mimeType="application/json") ]) except Exception as e: logger.exception("syncToAccounting failed") return ActionResult.isFailure(error=str(e))