From 2ec09497ab6d9ab1302c4d87f8f0a50cba66cc8d Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 13 Aug 2025 13:42:51 +0200 Subject: [PATCH] Simplify organisation syncs --- lib/AppInfo/Application.php | 34 ++--- lib/Service/OrganizationSyncService.php | 170 ++++++++++++++++-------- 2 files changed, 133 insertions(+), 71 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 77ae05ca..787b180f 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -28,6 +28,7 @@ use OCA\OpenRegister\Event\ObjectLockedEvent; use OCA\OpenRegister\Event\ObjectUnlockedEvent; use OCA\OpenRegister\Event\ObjectRevertedEvent; +use OCP\IDBConnection; use OCP\IUserManager; use OCP\IGroupManager; use OCP\IAppConfig; @@ -67,13 +68,13 @@ public function __construct() * Register event listeners and services * * @param IRegistrationContext $context Registration context - * + * * @return void */ public function register(IRegistrationContext $context): void { include_once __DIR__ . '/../../vendor/autoload.php'; - + // Register the handlers as services $context->registerService('OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler', function (ContainerInterface $c) { return new \OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler( @@ -124,10 +125,10 @@ public function register(IRegistrationContext $context): void $context->registerEventListener(ObjectLockedEvent::class, SoftwareCatalogEventListener::class); $context->registerEventListener(ObjectUnlockedEvent::class, SoftwareCatalogEventListener::class); $context->registerEventListener(ObjectRevertedEvent::class, SoftwareCatalogEventListener::class); - + // Organization event listeners removed - now using cron job for organization synchronization // Contact person event listeners are still active for real-time processing - + // Register new focused services $context->registerService(\OCA\SoftwareCatalog\Service\OrganisatieService::class, function ($container) { return new \OCA\SoftwareCatalog\Service\OrganisatieService( @@ -180,7 +181,8 @@ public function register(IRegistrationContext $context): void $container->get(SymfonyEmailService::class), $container->get(IAppConfig::class), $container->get('Psr\Log\LoggerInterface'), - $container->get(SettingsService::class) + $container->get(SettingsService::class), + $container->get(IDBConnection::class) ); }); @@ -233,14 +235,14 @@ public function register(IRegistrationContext $context): void * Boot the application * * @param IBootContext $context Boot context - * + * * @return void */ public function boot(IBootContext $context): void { $container = $context->getServerContainer(); $logger = $container->get(LoggerInterface::class); - + try { $config = $container->get(IAppConfig::class); $appManager = $container->get(IAppManager::class); @@ -256,7 +258,7 @@ public function boot(IBootContext $context): void // Check if we actually have a valid configuration, not just version matching $needsInitialization = false; $initReason = ''; - + if ($lastInitializedVersion !== $currentAppVersion || empty($lastInitializedVersion)) { $needsInitialization = true; $initReason = empty($lastInitializedVersion) ? 'never_initialized' : 'version_changed'; @@ -264,7 +266,7 @@ public function boot(IBootContext $context): void // Even if version matches, check if we have valid configuration $hasValidConfig = $config->getValueString(self::APP_ID, 'voorzieningen_organisatie_schema', '') !== '' || $config->getValueString(self::APP_ID, 'organization_schema', '') !== ''; - + if (!$hasValidConfig) { $needsInitialization = true; $initReason = 'missing_configuration'; @@ -274,25 +276,25 @@ public function boot(IBootContext $context): void ]); } } - + if ($needsInitialization) { $logger->info('SoftwareCatalog boot: Starting initialization', [ 'reason' => $initReason, 'currentVersion' => $currentAppVersion, 'lastInitializedVersion' => $lastInitializedVersion ]); - + try { $settingsService = $container->get(SettingsService::class); $initResult = $settingsService->initialize(); - + $logger->info('SoftwareCatalog boot: Initialization completed', [ 'result' => $initResult, 'hasErrors' => !empty($initResult['errors']) ]); - + // Only update version if initialization was actually successful - if (empty($initResult['errors']) && + if (empty($initResult['errors']) && ($initResult['autoConfigured'] || $initResult['fullyConfigured'])) { $config->setValueString(self::APP_ID, 'last_initialized_version', $currentAppVersion); $logger->info('SoftwareCatalog boot: Version updated to ' . $currentAppVersion . ' (successful init)'); @@ -303,7 +305,7 @@ public function boot(IBootContext $context): void 'fullyConfigured' => $initResult['fullyConfigured'] ?? false ]); } - + } catch (\RuntimeException $e) { // Don't update version if OpenRegister is not available $logger->warning('SoftwareCatalog boot: OpenRegister not available during initialization', [ @@ -320,7 +322,7 @@ public function boot(IBootContext $context): void } else { $logger->debug('SoftwareCatalog boot: Skipping initialization (version unchanged and config valid)'); } - + } catch (\Exception $e) { // Log error but don't fail the boot process $logger->error('SoftwareCatalog boot error during version check: ' . $e->getMessage(), [ diff --git a/lib/Service/OrganizationSyncService.php b/lib/Service/OrganizationSyncService.php index dc192e98..383288f7 100644 --- a/lib/Service/OrganizationSyncService.php +++ b/lib/Service/OrganizationSyncService.php @@ -18,18 +18,20 @@ namespace OCA\SoftwareCatalog\Service; +use OCA\OpenRegister\Service\ObjectService; use OCA\SoftwareCatalog\Service\OrganisatieService; use OCA\SoftwareCatalog\Service\ContactpersoonService; use OCA\SoftwareCatalog\Service\SymfonyEmailService; use OCP\IAppConfig; +use OCP\IDBConnection; use Psr\Log\LoggerInterface; /** * Service for synchronizing organizations and contact persons - * + * * This service provides comprehensive synchronization between SoftwareCatalog objects * and OpenRegister entities, ensuring data consistency and proper user management. - * + * * @category Service * @package OCA\SoftwareCatalog\Service * @author Conduction b.v. @@ -96,7 +98,8 @@ public function __construct( SymfonyEmailService $emailService, IAppConfig $config, LoggerInterface $logger, - SettingsService $settingsService + SettingsService $settingsService, + private IDBConnection $db, ) { $this->organisatieService = $organisatieService; $this->contactpersoonService = $contactpersoonService; @@ -106,6 +109,60 @@ public function __construct( $this->settingsService = $settingsService; } + public function performOrganizationsSync(): array + { + // Check configuration + $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); + $register = $voorzieningenConfig['register'] ?? ''; + $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? ''; +// $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? ''; + + $stats = [ + 'organizationsProcessed' => 0, + 'entitiesCreated' => 0, + 'entitiesUpdated' => 0, + 'contactPersonsProcessed' => 0, + 'usersCreated' => 0, + 'usersUpdated' => 0, + 'errors' => [], + 'startTime' => date('Y-m-d H:i:s'), + 'endTime' => null, + 'duration' => null + ]; + + $qb = $this->db->getQueryBuilder(); + + $qb->select('o.uuid', $qb->createFunction('json_unquote(json_extract(o.object, \'$.status\')) as status'), 'o2.uuid as oreg_uuid', 'o2.active as active') + ->from('openregister_objects', 'o') + ->leftJoin(fromAlias:'o', join: 'openregister_organisations', alias: 'o2', condition: 'o.uuid = o2.uuid') + ->where($qb->expr()->eq('o.schema', $qb->createNamedParameter($organizationSchema))) + ->andWhere($qb->expr()->eq('o.register', $qb->createNamedParameter($register))) + ->andWhere($qb->expr()->orX( + $qb->expr()->neq('o2.active', $qb->createFunction('(json_unquote(json_extract(o.object, \'$.status\')) = \'actief\')')), + $qb->expr()->isNull('o2.uuid') + )); + + $sql = $qb->getSQL(); + $objects = $qb->execute()->fetchAll(); + $orgs = []; + + foreach($objects as $object) { + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + if($objectService instanceOf ObjectService === false) { + return []; + } + + $object = $objectService->find($object['uuid']); + + $org = $this->ensureOrganisationEntity($object,$stats); + + } + + return $stats; + } + + + /** * Performs comprehensive organization and contact person synchronization * @@ -113,7 +170,7 @@ public function __construct( * the specified time window with organisation entities. * * @param int $minutesBack Number of minutes to look back for changes (0 = all objects) - * + * * @return array Synchronization results and statistics */ public function performFullSync(int $minutesBack = 10): array @@ -122,7 +179,7 @@ public function performFullSync(int $minutesBack = 10): array 'minutesBack' => $minutesBack, 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental' ]); - + $stats = [ 'organizationsProcessed' => 0, 'entitiesCreated' => 0, @@ -179,7 +236,7 @@ public function performFullSync(int $minutesBack = 10): array $stats['duration'] = $endTime->diff($startTime)->format('%H:%I:%S'); $this->logger->info('OrganizationSyncService: Completed comprehensive synchronization', $stats); - + return $stats; } catch (\Exception $e) { @@ -199,14 +256,14 @@ public function performFullSync(int $minutesBack = 10): array * @param string $register The register ID * @param string $organizationSchema The organization schema ID * @param int $minutesBack Number of minutes to look back (0 = all objects) - * + * * @return array Array of organisatie objects */ private function getOrganisatieObjectsByTimeWindow(string $register, string $organizationSchema, int $minutesBack): array { try { $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); - + // Build base query for register and schema $query = [ '@self' => [ @@ -214,17 +271,17 @@ private function getOrganisatieObjectsByTimeWindow(string $register, string $org 'schema' => (int) $organizationSchema ] ]; - + // Add time-based filtering if minutesBack > 0 if ($minutesBack > 0) { $cutoffTime = new \DateTime(); $cutoffTime->sub(new \DateInterval('PT' . $minutesBack . 'M')); $cutoffTimeString = $cutoffTime->format('Y-m-d\TH:i:sP'); - + // Add time filtering to the query // Filter objects that were updated within the time window $query['@self']['updated'] = ['gte' => $cutoffTimeString]; - + $this->logger->debug('OrganizationSyncService: Using searchObjects with time-based filtering', [ 'register' => $register, 'schema' => $organizationSchema, @@ -241,10 +298,10 @@ private function getOrganisatieObjectsByTimeWindow(string $register, string $org 'query' => $query ]); } - + // Use searchObjects method for filtering $objects = $objectService->searchObjects($query); - + $this->logger->debug('OrganizationSyncService: Retrieved organisatie objects with searchObjects', [ 'register' => $register, 'schema' => $organizationSchema, @@ -261,7 +318,7 @@ private function getOrganisatieObjectsByTimeWindow(string $register, string $org 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); - + return []; } } @@ -328,24 +385,24 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta try { $objectData = $organisatieObject->getObject(); $organisatieId = $objectData['id'] ?? $organisatieObject->getId(); - + // Try to find existing organisation entity $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); - + try { $organisationEntity = $organisationMapper->findByUuid($organisatieId); - + // Entity exists - update it if needed - $beoordeling = strtolower($objectData['beoordeling'] ?? 'actief'); - $shouldBeActive = in_array($beoordeling, ['actief', 'active']); - + $status = strtolower($objectData['status'] ?? 'actief'); + $shouldBeActive = in_array($status, ['actief', 'active']); + if ($organisationEntity->getActive() !== $shouldBeActive) { $this->logger->info('OrganizationSyncService: Updating organisation entity status', [ 'organisatieId' => $organisatieId, 'oldActive' => $organisationEntity->getActive(), 'newActive' => $shouldBeActive ]); - + $organisationEntity->setActive($shouldBeActive); $organisationMapper->save($organisationEntity); $stats['entitiesUpdated']++; @@ -364,7 +421,7 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta } } } - + $this->logger->debug('OrganizationSyncService: Found existing organisation entity', [ 'organisatieId' => $organisatieId, 'entityId' => $organisationEntity->getId(), @@ -376,7 +433,7 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta $this->logger->info('OrganizationSyncService: Creating new organisation entity', [ 'organisatieId' => $organisatieId ]); - + $organisationEntity = $this->organisatieService->createOrganisationInOpenRegister($objectData); if ($organisationEntity) { $stats['entitiesCreated']++; @@ -400,7 +457,7 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta } return $organisationEntity; } - + } catch (\Exception $e) { $this->logger->error('OrganizationSyncService: Failed to ensure organisation entity', [ 'organisatieId' => $organisatieObject->getId(), @@ -409,7 +466,7 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta return null; } } - + /** * Safely sends organization registration email with error handling * @@ -429,7 +486,7 @@ private function sendOrganizationRegistrationEmail(array $organizationData): boo return false; } } - + /** * Safely sends organization activation email with error handling * @@ -468,7 +525,7 @@ private function getContactPersonsForOrganisation(string $organisatieId, string try { $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); - + // Use searchObjects for more efficient filtering on-demand $query = [ '@self' => [ @@ -477,7 +534,7 @@ private function getContactPersonsForOrganisation(string $organisatieId, string ], 'organisatie' => $organisatieId ]; - + $contactPersons = $objectService->searchObjects($query); $this->logger->debug('OrganizationSyncService: Retrieved contact persons on-demand', [ @@ -585,7 +642,7 @@ private function updateOrganisationEntityUsers(object $organisationEntity, array try { $organisationUuid = $organisationEntity->getUuid(); $currentUsers = $organisationEntity->getUsers() ?? []; - + // Add admin users to ensure they're always included $adminUsers = $this->getAdminUsers(); $allUsernames = array_unique(array_merge($usernames, $adminUsers)); @@ -605,12 +662,12 @@ private function updateOrganisationEntityUsers(object $organisationEntity, array ]); $organisationEntity->setUsers($allUsernames); - + $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); $organisationMapper->save($organisationEntity); - + $stats['entitiesUpdated']++; - + $this->logger->info('OrganizationSyncService: Successfully updated organisation entity users', [ 'organisationUuid' => $organisationUuid, 'totalUsers' => count($allUsernames) @@ -640,7 +697,7 @@ private function getAdminUsers(): array try { $groupManager = \OC::$server->get('OCP\IGroupManager'); $adminGroup = $groupManager->get('admin'); - + if ($adminGroup) { $adminUsers = $adminGroup->getUsers(); $adminUsernames = []; @@ -649,7 +706,7 @@ private function getAdminUsers(): array } return $adminUsernames; } - + return []; } catch (\Exception $e) { $this->logger->error('OrganizationSyncService: Failed to get admin users', [ @@ -663,7 +720,7 @@ private function getAdminUsers(): array * Performs a quick sync status check with prediction of objects to be processed * * @param int $minutesBack Number of minutes to look back for prediction (default: 10 for scheduled sync) - * + * * @return array Status information about sync requirements including processing predictions */ public function getSyncStatus(int $minutesBack = 10): array @@ -684,10 +741,10 @@ public function getSyncStatus(int $minutesBack = 10): array // Get total counts (all objects) $allOrganisatieObjects = $this->getOrganisatieObjectsByTimeWindow($register, $organizationSchema, 0); - + // Get incremental counts (objects to be processed in next sync) $incrementalOrganisatieObjects = $this->getOrganisatieObjectsByTimeWindow($register, $organizationSchema, $minutesBack); - + // Get organization entities count $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); $entitiesCount = 0; @@ -710,7 +767,7 @@ public function getSyncStatus(int $minutesBack = 10): array } // Calculate efficiency metrics - $efficiencyImprovement = count($allOrganisatieObjects) > 0 + $efficiencyImprovement = count($allOrganisatieObjects) > 0 ? round((1 - (count($incrementalOrganisatieObjects) / count($allOrganisatieObjects))) * 100, 1) : 0; @@ -718,31 +775,31 @@ public function getSyncStatus(int $minutesBack = 10): array 'configured' => true, 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental', 'timeWindow' => $minutesBack, - + // Total counts 'totalOrganizationObjects' => count($allOrganisatieObjects), 'totalOrganizationEntities' => $entitiesCount, - + // Processing predictions 'organizationsToProcess' => count($incrementalOrganisatieObjects), 'contactPersonsToProcess' => $predictedContactPersonsToProcess, - + // Efficiency metrics 'efficiencyImprovement' => $efficiencyImprovement . '%', 'processingReduction' => count($allOrganisatieObjects) - count($incrementalOrganisatieObjects), - + // Configuration 'contactSchemaConfigured' => !empty($contactSchema), 'lastSyncTime' => $this->config->getValueString('softwarecatalog', 'last_sync_time', 'Never'), - + // Email configuration status 'emailStatus' => $this->getEmailConfigurationStatus(), - + // Status messages - 'message' => count($incrementalOrganisatieObjects) > 0 + 'message' => count($incrementalOrganisatieObjects) > 0 ? "Ready to process {$this->formatNumber(count($incrementalOrganisatieObjects))} organizations and {$this->formatNumber($predictedContactPersonsToProcess)} contact persons" : 'No organizations to process in the current time window', - 'nextScheduledSync' => $minutesBack > 0 + 'nextScheduledSync' => $minutesBack > 0 ? "Will process organizations updated in the last {$minutesBack} minutes" : 'Will process all organizations (full sync)' ]; @@ -781,7 +838,7 @@ private function getEmailConfigurationStatus(): array * Format numbers for better readability * * @param int $number The number to format - * + * * @return string Formatted number */ private function formatNumber(int $number): string @@ -810,7 +867,7 @@ public function recordSyncTime(): void * Uses default 10-minute lookback for incremental sync. * * @param int $minutesBack Number of minutes to look back for changes (default: 10) - * + * * @return array Synchronization results with detailed logging information */ public function performScheduledSync(int $minutesBack = 10): array @@ -822,8 +879,8 @@ public function performScheduledSync(int $minutesBack = 10): array try { // Perform the core synchronization with time-based filtering - $syncResults = $this->performFullSync($minutesBack); - +// $syncResults = $this->performFullSync($minutesBack); + $syncResults = $this->performOrganizationsSync(); // Record the sync time $this->recordSyncTime(); @@ -855,7 +912,7 @@ public function performScheduledSync(int $minutesBack = 10): array 'line' => $e->getLine(), 'trace' => $e->getTraceAsString() ]); - + return [ 'organizationsProcessed' => 0, 'entitiesCreated' => 0, @@ -879,7 +936,7 @@ public function performScheduledSync(int $minutesBack = 10): array * Uses full sync (minutesBack = 0) for manual triggers by default. * * @param int $minutesBack Number of minutes to look back for changes (default: 0 for full sync) - * + * * @return array Synchronization results formatted for API response */ public function performManualSync(int $minutesBack = 0): array @@ -890,8 +947,11 @@ public function performManualSync(int $minutesBack = 0): array ]); try { + $syncResults = $this->performOrganizationsSync(); +// die; + // Perform the core synchronization with time-based filtering - $syncResults = $this->performFullSync($minutesBack); +// $syncResults = $this->performFullSync($minutesBack); // Record the sync time $this->recordSyncTime(); @@ -942,7 +1002,7 @@ public function getSyncStatusWithErrorHandling(int $minutesBack = 10): array 'minutesBack' => $minutesBack, 'exception' => $e->getMessage() ]); - + return [ 'configured' => false, 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental', @@ -951,4 +1011,4 @@ public function getSyncStatusWithErrorHandling(int $minutesBack = 10): array ]; } } -} \ No newline at end of file +}