diff --git a/appinfo/routes.php b/appinfo/routes.php index bf6d028b..938793ee 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -140,5 +140,15 @@ ['name' => 'view#getApiDocumentation', 'url' => '/api/views/docs', 'verb' => 'GET'], ['name' => 'view#getView', 'url' => '/api/views/{viewId}', 'verb' => 'GET'], + // ======================================================================== + // AANGEBODEN GEBRUIK API ENDPOINTS - Custom Objects API for Gebruiks + // ======================================================================== + + // AangebodenGebruik API endpoints for filtering gebruiks by organization involvement + ['name' => 'aangebodenGebruik#getGebruiksWhereAfnemer', 'url' => '/api/aangeboden-gebruik/afnemer', 'verb' => 'GET'], + ['name' => 'aangebodenGebruik#getGebruiksWhereDeelnemers', 'url' => '/api/aangeboden-gebruik/deelnemers', 'verb' => 'GET'], + ['name' => 'aangebodenGebruik#setGebruikSelfToActiveOrg', 'url' => '/api/aangeboden-gebruik/{gebruikId}/set-self', 'verb' => 'PUT'], + ['name' => 'aangebodenGebruik#getApiDocumentation', 'url' => '/api/aangeboden-gebruik/docs', 'verb' => 'GET'], + ], ]; diff --git a/lib/Controller/AangebodenGebruikController.php b/lib/Controller/AangebodenGebruikController.php new file mode 100644 index 00000000..7490f022 --- /dev/null +++ b/lib/Controller/AangebodenGebruikController.php @@ -0,0 +1,504 @@ + + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version 1.0.0 + * @link https://github.com/ConductionNL/SoftwareCatalog + */ + +namespace OCA\SoftwareCatalog\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCA\SoftwareCatalog\Service\AangebodenGebruikService; +use Psr\Log\LoggerInterface; + +/** + * Controller for handling offered usage (aangeboden gebruik) API operations + * + * This controller provides REST API endpoints for managing gebruiks objects where + * the active organization is involved either as afnemer (consumer) or in deelnemers + * (participants), and for updating the @self property of gebruiks objects. + * + * @category Controller + * @package OCA\SoftwareCatalog\Controller + * @author Conduction b.v. + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version 1.0.0 + * @link https://github.com/ConductionNL/SoftwareCatalog + */ +class AangebodenGebruikController extends Controller +{ + /** + * Constructor for AangebodenGebruikController + * + * @param string $appName The name of the app + * @param IRequest $request The HTTP request object + * @param AangebodenGebruikService $aangebodenGebruikService The business logic service + * @param LoggerInterface $logger The logger service for debugging and error reporting + */ + public function __construct( + string $appName, + IRequest $request, + private readonly AangebodenGebruikService $aangebodenGebruikService, + private readonly LoggerInterface $logger + ) { + parent::__construct($appName, $request); + } + + /** + * Get all gebruiks objects where the active organization is the afnemer (consumer) + * + * API Endpoint: GET /api/aangeboden-gebruik/afnemer + * + * Query Parameters: + * - limit (int): Maximum number of results to return + * - offset (int): Number of results to skip for pagination + * - status (string): Filter by usage status + * - product (string): Filter by product ID + * - startDate (string): Filter by start date (ISO 8601 format) + * - endDate (string): Filter by end date (ISO 8601 format) + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * @return JSONResponse JSON response with gebruiks array where org is afnemer + */ + public function getGebruiksWhereAfnemer(): JSONResponse + { + $this->logger->info('API: Getting gebruiks where active org is afnemer', [ + 'endpoint' => '/api/aangeboden-gebruik/afnemer', + 'method' => 'GET', + 'query_params' => $this->request->getParams() + ]); + + try { + // Parse query parameters for filtering options + $options = $this->parseQueryOptions(); + + // Get gebruiks from service where org is afnemer + $result = $this->aangebodenGebruikService->getGebruiksWhereAfnemer($options); + + // Determine appropriate HTTP status code + $statusCode = $result['success'] ? 200 : 500; + + $this->logger->info('API: Afnemer gebruiks request completed', [ + 'success' => $result['success'], + 'gebruiks_count' => $result['count'] ?? 0, + 'organisation' => $result['organisation'] ?? 'unknown' + ]); + + return new JSONResponse($result, $statusCode); + + } catch (\Exception $e) { + $this->logger->error('API: Failed to get afnemer gebruiks', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return new JSONResponse([ + 'success' => false, + 'error' => 'Internal server error: ' . $e->getMessage(), + 'gebruiks' => [], + 'count' => 0 + ], 500); + } + } + + /** + * Get all gebruiks objects where the active organization is in deelnemers (participants) + * + * API Endpoint: GET /api/aangeboden-gebruik/deelnemers + * + * Query Parameters: + * - limit (int): Maximum number of results to return + * - offset (int): Number of results to skip for pagination + * - status (string): Filter by usage status + * - product (string): Filter by product ID + * - startDate (string): Filter by start date (ISO 8601 format) + * - endDate (string): Filter by end date (ISO 8601 format) + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * @return JSONResponse JSON response with gebruiks array where org is in deelnemers + */ + public function getGebruiksWhereDeelnemers(): JSONResponse + { + $this->logger->info('API: Getting gebruiks where active org is in deelnemers', [ + 'endpoint' => '/api/aangeboden-gebruik/deelnemers', + 'method' => 'GET', + 'query_params' => $this->request->getParams() + ]); + + try { + // Parse query parameters for filtering options + $options = $this->parseQueryOptions(); + + // Get gebruiks from service where org is in deelnemers + $result = $this->aangebodenGebruikService->getGebruiksWhereDeelnemers($options); + + // Determine appropriate HTTP status code + $statusCode = $result['success'] ? 200 : 500; + + $this->logger->info('API: Deelnemers gebruiks request completed', [ + 'success' => $result['success'], + 'gebruiks_count' => $result['count'] ?? 0, + 'organisation' => $result['organisation'] ?? 'unknown' + ]); + + return new JSONResponse($result, $statusCode); + + } catch (\Exception $e) { + $this->logger->error('API: Failed to get deelnemers gebruiks', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return new JSONResponse([ + 'success' => false, + 'error' => 'Internal server error: ' . $e->getMessage(), + 'gebruiks' => [], + 'count' => 0 + ], 500); + } + } + + /** + * Set the @self property of a gebruik to the active organization + * + * API Endpoint: PUT /api/aangeboden-gebruik/{gebruikId}/set-self + * + * This endpoint allows setting the @self.organisation property of a specific gebruik + * object to the active organization, but only if the active organization is the + * afnemer (consumer) for that gebruik. + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * @param string $gebruikId The UUID of the gebruik object to update + * @return JSONResponse JSON response with success status and updated object + */ + public function setGebruikSelfToActiveOrg(string $gebruikId): JSONResponse + { + $this->logger->info('API: Setting gebruik @self property to active org', [ + 'endpoint' => "/api/aangeboden-gebruik/{$gebruikId}/set-self", + 'method' => 'PUT', + 'gebruik_id' => $gebruikId + ]); + + try { + // Validate input + if (empty($gebruikId)) { + return new JSONResponse([ + 'success' => false, + 'error' => 'Gebruik ID is required', + 'gebruik' => null + ], 400); + } + + // Parse any additional options from request body + $options = []; + $requestBody = $this->request->getParams(); + if (!empty($requestBody)) { + $options = array_filter($requestBody, function($key) { + return !in_array($key, ['gebruikId']); // Exclude path parameters + }, ARRAY_FILTER_USE_KEY); + } + + // Update gebruik @self property via service + $result = $this->aangebodenGebruikService->setGebruikSelfToActiveOrg($gebruikId, $options); + + // Determine appropriate HTTP status code + $statusCode = $result['success'] ? 200 : ($result['error'] === 'Gebruik object not found' ? 404 : + ($result['error'] === 'Operation not allowed: active organization is not the afnemer' ? 403 : 500)); + + $this->logger->info('API: Set gebruik @self property request completed', [ + 'gebruik_id' => $gebruikId, + 'success' => $result['success'], + 'status_code' => $statusCode + ]); + + return new JSONResponse($result, $statusCode); + + } catch (\Exception $e) { + $this->logger->error('API: Failed to set gebruik @self property', [ + 'gebruik_id' => $gebruikId, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return new JSONResponse([ + 'success' => false, + 'error' => 'Internal server error: ' . $e->getMessage(), + 'gebruik' => null + ], 500); + } + } + + /** + * Get API documentation for AangebodenGebruik endpoints + * + * API Endpoint: GET /api/aangeboden-gebruik/docs + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * @return JSONResponse JSON response with API documentation + */ + public function getApiDocumentation(): JSONResponse + { + $documentation = [ + 'api_version' => '1.0.0', + 'description' => 'SoftwareCatalog AangebodenGebruik API - Manage gebruiks objects where active organization is involved', + 'base_url' => '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/api/aangeboden-gebruik', + 'endpoints' => [ + [ + 'method' => 'GET', + 'path' => '/api/aangeboden-gebruik/afnemer', + 'description' => 'Get all gebruiks objects where the active organization is the afnemer (consumer)', + 'parameters' => [ + [ + 'name' => 'limit', + 'type' => 'integer', + 'required' => false, + 'description' => 'Maximum number of results to return' + ], + [ + 'name' => 'offset', + 'type' => 'integer', + 'required' => false, + 'description' => 'Number of results to skip for pagination' + ], + [ + 'name' => 'status', + 'type' => 'string', + 'required' => false, + 'description' => 'Filter by usage status' + ], + [ + 'name' => 'product', + 'type' => 'string', + 'required' => false, + 'description' => 'Filter by product ID' + ], + [ + 'name' => 'startDate', + 'type' => 'string', + 'required' => false, + 'description' => 'Filter by start date (ISO 8601 format)' + ], + [ + 'name' => 'endDate', + 'type' => 'string', + 'required' => false, + 'description' => 'Filter by end date (ISO 8601 format)' + ] + ], + 'response_example' => [ + 'success' => true, + 'gebruiks' => [ + [ + 'id' => 'usage-uuid-123', + 'afnemer' => 'org-uuid', + 'product' => 'product-uuid', + 'status' => 'actief', + '_filter_type' => 'afnemer', + '_schema_id' => 'schema-id' + ] + ], + 'count' => 1, + 'filter_type' => 'afnemer', + 'organisation' => 'org-uuid' + ] + ], + [ + 'method' => 'GET', + 'path' => '/api/aangeboden-gebruik/deelnemers', + 'description' => 'Get all gebruiks objects where the active organization is in deelnemers (participants)', + 'parameters' => [ + [ + 'name' => 'limit', + 'type' => 'integer', + 'required' => false, + 'description' => 'Maximum number of results to return' + ], + [ + 'name' => 'offset', + 'type' => 'integer', + 'required' => false, + 'description' => 'Number of results to skip for pagination' + ], + [ + 'name' => 'status', + 'type' => 'string', + 'required' => false, + 'description' => 'Filter by usage status' + ], + [ + 'name' => 'product', + 'type' => 'string', + 'required' => false, + 'description' => 'Filter by product ID' + ], + [ + 'name' => 'startDate', + 'type' => 'string', + 'required' => false, + 'description' => 'Filter by start date (ISO 8601 format)' + ], + [ + 'name' => 'endDate', + 'type' => 'string', + 'required' => false, + 'description' => 'Filter by end date (ISO 8601 format)' + ] + ], + 'response_example' => [ + 'success' => true, + 'gebruiks' => [ + [ + 'id' => 'usage-uuid-456', + 'afnemer' => 'other-org-uuid', + 'deelnemers' => ['org-uuid', 'another-org-uuid'], + 'product' => 'product-uuid', + 'status' => 'actief', + '_filter_type' => 'deelnemers', + '_schema_id' => 'schema-id' + ] + ], + 'count' => 1, + 'filter_type' => 'deelnemers', + 'organisation' => 'org-uuid' + ] + ], + [ + 'method' => 'PUT', + 'path' => '/api/aangeboden-gebruik/{gebruikId}/set-self', + 'description' => 'Set the @self property of a gebruik to the active organization (only allowed if active org is afnemer)', + 'parameters' => [ + [ + 'name' => 'gebruikId', + 'type' => 'string', + 'required' => true, + 'description' => 'The UUID of the gebruik object to update (in URL path)' + ] + ], + 'response_example' => [ + 'success' => true, + 'message' => 'Gebruik @self property updated successfully', + 'gebruik' => [ + 'id' => 'usage-uuid-123', + 'afnemer' => 'org-uuid', + '@self' => [ + 'organisation' => 'org-uuid', + 'register' => 'register-id', + 'schema' => 'schema-id' + ] + ], + 'updated_fields' => ['@self.organisation'] + ] + ], + [ + 'method' => 'GET', + 'path' => '/api/aangeboden-gebruik/docs', + 'description' => 'Get this API documentation', + 'parameters' => [], + 'response_example' => '(this response)' + ] + ], + 'security' => [ + 'afnemer_filtering' => 'Uses standard RBAC filtering based on organization association', + 'deelnemers_filtering' => 'Uses RBAC-disabled search to find participation records', + 'self_update_permission' => 'Only allowed if active organization is the afnemer for the specific gebruik' + ], + 'error_codes' => [ + 400 => 'Bad Request - Invalid parameters or missing required fields', + 403 => 'Forbidden - Operation not allowed (e.g., org is not afnemer for @self update)', + 404 => 'Not Found - Gebruik object not found', + 500 => 'Internal Server Error - Server-side error occurred' + ] + ]; + + return new JSONResponse($documentation, 200); + } + + /** + * Parse query parameters into options array + * + * This method extracts and validates query parameters for filtering, + * pagination, and other options. + * + * @return array Parsed options array + */ + private function parseQueryOptions(): array + { + $options = []; + + // Parse pagination parameters + $limit = $this->request->getParam('limit'); + if ($limit !== null && is_numeric($limit)) { + $options['limit'] = (int)$limit; + } + + $offset = $this->request->getParam('offset'); + if ($offset !== null && is_numeric($offset)) { + $options['offset'] = (int)$offset; + } + + // Parse filter parameters + $status = $this->request->getParam('status'); + if ($status !== null && !empty(trim($status))) { + $options['status'] = trim($status); + } + + $product = $this->request->getParam('product'); + if ($product !== null && !empty(trim($product))) { + $options['product'] = trim($product); + } + + $startDate = $this->request->getParam('startDate'); + if ($startDate !== null && !empty(trim($startDate))) { + $options['startDate'] = trim($startDate); + } + + $endDate = $this->request->getParam('endDate'); + if ($endDate !== null && !empty(trim($endDate))) { + $options['endDate'] = trim($endDate); + } + + $this->logger->debug('Parsed query options for AangebodenGebruik', [ + 'raw_params' => [ + 'limit' => $limit, + 'offset' => $offset, + 'status' => $status, + 'product' => $product, + 'startDate' => $startDate, + 'endDate' => $endDate + ], + 'parsed_options' => $options + ]); + + return $options; + } +} + + diff --git a/lib/Service/AangebodenGebruikService.php b/lib/Service/AangebodenGebruikService.php new file mode 100644 index 00000000..ae2735c2 --- /dev/null +++ b/lib/Service/AangebodenGebruikService.php @@ -0,0 +1,487 @@ + + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version 1.0.0 + * @link https://github.com/ConductionNL/SoftwareCatalog + */ + +namespace OCA\SoftwareCatalog\Service; + +use OCA\OpenRegister\Service\ObjectService; +use OCP\App\IAppManager; +use OCP\IAppConfig; +use OCP\IUserSession; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Exception; + +/** + * Service for managing offered usage (aangeboden gebruik) operations + * + * This service provides operations for querying gebruiks objects where the active + * organization is involved either as the afnemer (consumer) or in the deelnemers + * (participants) array, and for updating the @self property of gebruiks objects. + * + * @category Service + * @package OCA\SoftwareCatalog\Service + * @author Conduction b.v. + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version 1.0.0 + * @link https://github.com/ConductionNL/SoftwareCatalog + */ +class AangebodenGebruikService +{ + /** + * Constructor for AangebodenGebruikService + * + * @param IAppConfig $config Nextcloud app configuration service + * @param IAppManager $appManager App manager service for checking available apps + * @param ContainerInterface $container PSR-11 container interface for dependency injection + * @param LoggerInterface $logger Logger service for debugging and error reporting + * @param SettingsService $settingsService Settings service for retrieving configuration + * @param IUserSession $userSession User session service for current user context + */ + public function __construct( + private readonly IAppConfig $config, + private readonly IAppManager $appManager, + private readonly ContainerInterface $container, + private readonly LoggerInterface $logger, + private readonly SettingsService $settingsService, + private readonly IUserSession $userSession + ) { + } + + /** + * Get all gebruiks objects where the active organization is the afnemer (consumer) + * + * This method retrieves all gebruiks objects where the active organization + * appears as the afnemer using standard RBAC filtering. + * + * @param array $options Additional query options (limit, offset, filters, etc.) + * @return array Array with success status, gebruiks data, and metadata + * @throws Exception When OpenRegister service is not available + */ + public function getGebruiksWhereAfnemer(array $options = []): array + { + $this->logger->info('Getting gebruiks objects where active org is afnemer', [ + 'options' => $options + ]); + + try { + // Get ObjectService from OpenRegister + $objectService = $this->getObjectService(); + + // Get current organization + $currentOrg = $this->getCurrentOrganisation(); + if (!$currentOrg) { + $this->logger->warning('No current organization available for afnemer filtering'); + return [ + 'success' => true, + 'gebruiks' => [], + 'count' => 0, + 'message' => 'No current organization available' + ]; + } + + // Get configuration for gebruiks register/schema + $gebruiksConfig = $this->getGebruiksConfiguration(); + + $allGebruiks = []; + + // Search each configured schema for gebruiks where org is afnemer + foreach ($gebruiksConfig['schemas'] as $schemaId) { + if (!$schemaId) continue; + + try { + // Build query for afnemer filtering with RBAC enabled + $query = [ + '@self' => [ + 'register' => $gebruiksConfig['register_id'], + 'schema' => $schemaId, + 'organisation' => $currentOrg // Standard RBAC filtering + ] + ]; + + // Add additional filters from options + $query = $this->addQueryFilters($query, $options); + + // Execute search with RBAC enabled (default behavior) + $gebruikItems = $objectService->searchObjects($query); + + // Process and add to results + foreach ($gebruikItems as $gebruik) { + $gebruik['_filter_type'] = 'afnemer'; + $gebruik['_schema_id'] = $schemaId; + $allGebruiks[] = $gebruik; + } + + $this->logger->debug('Retrieved afnemer gebruiks from schema', [ + 'schema_id' => $schemaId, + 'count' => count($gebruikItems), + 'organisation' => $currentOrg + ]); + + } catch (Exception $e) { + $this->logger->warning('Failed to get afnemer gebruiks from schema', [ + 'schema_id' => $schemaId, + 'error' => $e->getMessage() + ]); + } + } + + return [ + 'success' => true, + 'gebruiks' => $allGebruiks, + 'count' => count($allGebruiks), + 'filter_type' => 'afnemer', + 'organisation' => $currentOrg + ]; + + } catch (Exception $e) { + $this->logger->error('Failed to get afnemer gebruiks', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'error' => 'Failed to retrieve gebruiks: ' . $e->getMessage(), + 'gebruiks' => [], + 'count' => 0 + ]; + } + } + + /** + * Get all gebruiks objects where the active organization is in deelnemers (participants) + * + * This method retrieves all gebruiks objects where the active organization + * appears in the deelnemers array, using RBAC-disabled search. + * + * @param array $options Additional query options (limit, offset, filters, etc.) + * @return array Array with success status, gebruiks data, and metadata + * @throws Exception When OpenRegister service is not available + */ + public function getGebruiksWhereDeelnemers(array $options = []): array + { + $this->logger->info('Getting gebruiks objects where active org is in deelnemers', [ + 'options' => $options + ]); + + try { + // Get ObjectService from OpenRegister + $objectService = $this->getObjectService(); + + // Get current organization + $currentOrg = $this->getCurrentOrganisation(); + if (!$currentOrg) { + $this->logger->warning('No current organization available for deelnemers filtering'); + return [ + 'success' => true, + 'gebruiks' => [], + 'count' => 0, + 'message' => 'No current organization available' + ]; + } + + // Get configuration for gebruiks register/schema + $gebruiksConfig = $this->getGebruiksConfiguration(); + + $allGebruiks = []; + + // Search each configured schema for gebruiks where org is in deelnemers + foreach ($gebruiksConfig['schemas'] as $schemaId) { + if (!$schemaId) continue; + + try { + // Build query for deelnemers filtering + $query = [ + '@self' => [ + 'register' => $gebruiksConfig['register_id'], + 'schema' => $schemaId + ], + 'deelnemers' => $currentOrg // Search where current org is in deelnemers + ]; + + // Add additional filters from options + $query = $this->addQueryFilters($query, $options); + + // Execute search with RBAC disabled to find deelnemers + $gebruikItems = $objectService->searchObjects($query, rbac: false); + + // Process and add to results + foreach ($gebruikItems as $gebruik) { + $gebruik['_filter_type'] = 'deelnemers'; + $gebruik['_schema_id'] = $schemaId; + $allGebruiks[] = $gebruik; + } + + $this->logger->debug('Retrieved deelnemers gebruiks from schema', [ + 'schema_id' => $schemaId, + 'count' => count($gebruikItems), + 'organisation_in_deelnemers' => $currentOrg + ]); + + } catch (Exception $e) { + $this->logger->warning('Failed to get deelnemers gebruiks from schema', [ + 'schema_id' => $schemaId, + 'error' => $e->getMessage() + ]); + } + } + + return [ + 'success' => true, + 'gebruiks' => $allGebruiks, + 'count' => count($allGebruiks), + 'filter_type' => 'deelnemers', + 'organisation' => $currentOrg + ]; + + } catch (Exception $e) { + $this->logger->error('Failed to get deelnemers gebruiks', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'error' => 'Failed to retrieve gebruiks: ' . $e->getMessage(), + 'gebruiks' => [], + 'count' => 0 + ]; + } + } + + /** + * Set the @self property of a gebruik to the active organization + * + * This method updates the @self.organisation property of a specific gebruik + * object, but only if the active organization is the afnemer for that gebruik. + * + * @param string $gebruikId The UUID of the gebruik object to update + * @param array $options Additional update options + * @return array Result with success status and updated object data + * @throws Exception When OpenRegister service is not available or operation fails + */ + public function setGebruikSelfToActiveOrg(string $gebruikId, array $options = []): array + { + $this->logger->info('Setting gebruik @self property to active organisation', [ + 'gebruik_id' => $gebruikId, + 'options' => $options + ]); + + try { + // Validate input + if (empty($gebruikId)) { + return [ + 'success' => false, + 'error' => 'Gebruik ID is required', + 'gebruik' => null + ]; + } + + // Get ObjectService from OpenRegister + $objectService = $this->getObjectService(); + + // Get current organization + $currentOrg = $this->getCurrentOrganisation(); + if (!$currentOrg) { + return [ + 'success' => false, + 'error' => 'No current organization available', + 'gebruik' => null + ]; + } + + // Get the existing gebruik object + $existingGebruik = $objectService->getObject($gebruikId); + if (!$existingGebruik) { + return [ + 'success' => false, + 'error' => 'Gebruik object not found', + 'gebruik' => null + ]; + } + + // Verify that the active organization is the afnemer + $gebruikData = $existingGebruik->getObject(); + $afnemerInfo = $gebruikData['afnemer'] ?? null; + + // Check various ways the afnemer might be stored (UUID, object, or string) + $afnemerId = null; + if (is_array($afnemerInfo) && isset($afnemerInfo['id'])) { + $afnemerId = $afnemerInfo['id']; + } elseif (is_string($afnemerInfo)) { + $afnemerId = $afnemerInfo; + } + + if (!$afnemerId || $afnemerId !== $currentOrg) { + return [ + 'success' => false, + 'error' => 'Operation not allowed: active organization is not the afnemer', + 'gebruik' => null, + 'debug' => [ + 'afnemer_in_object' => $afnemerInfo, + 'resolved_afnemer_id' => $afnemerId, + 'current_org' => $currentOrg + ] + ]; + } + + // Update the @self.organisation property + $selfData = $gebruikData['@self'] ?? []; + $selfData['organisation'] = $currentOrg; + $gebruikData['@self'] = $selfData; + + // Save the updated object + $existingGebruik->setObject($gebruikData); + $updatedGebruik = $objectService->saveObject($existingGebruik); + + $this->logger->info('Successfully updated gebruik @self property', [ + 'gebruik_id' => $gebruikId, + 'organisation' => $currentOrg, + 'afnemer_verified' => $afnemerId + ]); + + return [ + 'success' => true, + 'message' => 'Gebruik @self property updated successfully', + 'gebruik' => $updatedGebruik->getObject(), + 'updated_fields' => ['@self.organisation'] + ]; + + } catch (Exception $e) { + $this->logger->error('Failed to update gebruik @self property', [ + 'gebruik_id' => $gebruikId, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'error' => 'Failed to update gebruik: ' . $e->getMessage(), + 'gebruik' => null + ]; + } + } + + /** + * Get current active organisation for filtering + * + * @return string|null Current organisation identifier or null if no user session + */ + private function getCurrentOrganisation(): ?string + { + $user = $this->userSession->getUser(); + if (!$user) { + return null; + } + + // Get user's organization from configuration or session + // This follows the same pattern as ViewService + $userOrg = $this->config->getUserValue( + $user->getUID(), + 'softwarecatalog', + 'organisation', + null + ); + + return $userOrg; + } + + /** + * Get ObjectService from OpenRegister app + * + * @return ObjectService The OpenRegister object service + * @throws Exception When OpenRegister service is not available + */ + private function getObjectService(): ObjectService + { + if (!in_array('openregister', $this->appManager->getInstalledApps())) { + throw new Exception('OpenRegister app is not installed'); + } + + try { + return $this->container->get('OCA\OpenRegister\Service\ObjectService'); + } catch (Exception $e) { + throw new Exception('Failed to get OpenRegister service: ' . $e->getMessage()); + } + } + + /** + * Get configuration for gebruiks objects (register ID and schema IDs) + * + * @return array Configuration with register_id and schemas array + */ + private function getGebruiksConfiguration(): array + { + // Get AMEF configuration which includes gebruiks schemas + $amefConfig = $this->settingsService->getAmefConfig(); + + return [ + 'register_id' => $amefConfig['register_id'] ?? null, + 'schemas' => $amefConfig['gebruik_schemas'] ?? [] + ]; + } + + /** + * Add query filters from options to the base query + * + * This method processes additional filter options and adds them to the query. + * Supported filters: limit, offset, status, product, etc. + * + * @param array $baseQuery The base query to extend + * @param array $options Filter options to apply + * @return array Extended query with additional filters + */ + private function addQueryFilters(array $baseQuery, array $options): array + { + // Add limit if specified + if (isset($options['limit']) && is_numeric($options['limit'])) { + $baseQuery['@limit'] = (int)$options['limit']; + } + + // Add offset if specified + if (isset($options['offset']) && is_numeric($options['offset'])) { + $baseQuery['@offset'] = (int)$options['offset']; + } + + // Add status filter if specified + if (isset($options['status']) && !empty($options['status'])) { + $baseQuery['status'] = $options['status']; + } + + // Add product filter if specified + if (isset($options['product']) && !empty($options['product'])) { + $baseQuery['product'] = $options['product']; + } + + // Add date filters if specified + if (isset($options['startDate']) && !empty($options['startDate'])) { + $baseQuery['startDate'] = $options['startDate']; + } + + if (isset($options['endDate']) && !empty($options['endDate'])) { + $baseQuery['endDate'] = $options['endDate']; + } + + return $baseQuery; + } +} + + diff --git a/lib/Service/ContactpersoonService.php b/lib/Service/ContactpersoonService.php index 89548a11..422441c6 100644 --- a/lib/Service/ContactpersoonService.php +++ b/lib/Service/ContactpersoonService.php @@ -133,6 +133,9 @@ public function processContactpersoon(object $contactpersoonObject, bool $isUpda // Link user to organization entity $this->contactPersonHandler->addUserToOrganizationEntity($contactpersoonObject, $username); + // Update contactpersoon object owner to user UID + $this->updateContactpersoonObjectOwner($contactpersoonObject, $username); + $this->logger->info('ContactpersoonService: Successfully created user account', [ 'contactId' => $contactId, 'username' => $username @@ -466,4 +469,77 @@ public function getContactPersonsForOrganization(string $organizationUuid): arra return []; } } + + /** + * Updates the contactpersoon object's @self metadata to set owner to the user UID + * + * @param object $contactObject The contactpersoon object to update + * @param string $userUID The user UID to set as owner + * @return void + */ + private function updateContactpersoonObjectOwner(object $contactObject, string $userUID): void + { + try { + $contactId = $contactObject->getUuid(); + + // Get configuration for register and schema + $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); + $register = $voorzieningenConfig['register'] ?? ''; + $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? ''; + + if (empty($register) || empty($contactSchema)) { + $this->logger->warning('ContactpersoonService: Cannot update object owner - missing configuration', [ + 'contactId' => $contactId, + 'register' => $register, + 'contactSchema' => $contactSchema + ]); + return; + } + + $this->logger->info('ContactpersoonService: Updating contactpersoon object owner', [ + 'contactId' => $contactId, + 'userUID' => $userUID, + 'register' => $register, + 'schema' => $contactSchema + ]); + + // Get the current object data + $currentObject = $contactObject->getObject(); + + // Get current @self metadata or create new + $selfMetadata = $currentObject['@self'] ?? []; + + // Update the owner field to the user UID + $selfMetadata['owner'] = $userUID; + + // Update the object with the new @self metadata + $currentObject['@self'] = $selfMetadata; + $contactObject->setObject($currentObject); + + // Save the updated object using ObjectService + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + $objectService->saveObject( + object: $contactObject, + register: $register, + schema: $contactSchema, + rbac: false, + multi: false + ); + + $this->logger->info('ContactpersoonService: Successfully updated contactpersoon object owner', [ + 'contactId' => $contactId, + 'userUID' => $userUID, + 'ownerSet' => $selfMetadata['owner'] + ]); + + } catch (\Exception $e) { + $this->logger->error('ContactpersoonService: Failed to update contactpersoon object owner', [ + 'contactId' => $contactObject->getUuid(), + 'userUID' => $userUID, + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + } } \ No newline at end of file diff --git a/lib/Service/OrganizationSyncService.php b/lib/Service/OrganizationSyncService.php index 705786dc..eeb83042 100644 --- a/lib/Service/OrganizationSyncService.php +++ b/lib/Service/OrganizationSyncService.php @@ -534,6 +534,11 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta 'status' => $objectData['status'] ?? 'Unknown' ]); + // Get configuration for object updates + $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); + $register = $voorzieningenConfig['register'] ?? ''; + $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? ''; + // Try to find existing organisation entity $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); @@ -584,6 +589,9 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta } } + // Update organisatie object owner to organisation entity UUID + $this->updateOrganisatieObjectOwner($organisatieObject, $organisationEntity, $register, $organizationSchema); + return $organisationEntity; } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { @@ -619,6 +627,9 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta 'organisatieId' => $organisatieId ]); } + + // Update organisatie object owner to organisation entity UUID + $this->updateOrganisatieObjectOwner($organisatieObject, $organisationEntity, $register, $organizationSchema); } else { $this->logger->error('❌ ORGANISATION ENTITY CREATION FAILED', [ 'app' => 'softwarecatalog', @@ -1401,6 +1412,9 @@ private function createOrUpdateContactPersonObject(array $contactData, string $o // Add user to organization entity in database $this->contactpersonHandler->addUserToOrganizationEntity($contactObject, $user->getUID()); + // Update contactpersoon object owner to user UID + $this->updateContactpersoonObjectOwner($contactObject, $user->getUID(), $register, $contactSchema); + $this->logger->critical('🎉 USER ACCOUNT CREATED SUCCESS', [ 'app' => 'softwarecatalog', 'contactId' => $contactObject->getUuid(), @@ -1521,6 +1535,9 @@ public function processSpecificContactPerson($contactObject): array // Add user to organization entity in database $this->contactpersonHandler->addUserToOrganizationEntity($contactObject, $user->getUID()); + // Update contactpersoon object owner to user UID + $this->updateContactpersoonObjectOwner($contactObject, $user->getUID(), $register, $contactSchema); + $stats['usersCreated']++; } else { $this->logger->info('[EVENT] OrganizationSyncService: Skipping user creation - organization not active or not found in entity table', [ @@ -1819,4 +1836,127 @@ public function getSyncStatusWithErrorHandling(int $minutesBack = 10): array ]; } } + + /** + * Updates the organisatie object's @self metadata to set owner to the organisation entity UUID + * + * @param object $organisatieObject The organisatie object to update + * @param object $organisationEntity The organisation entity + * @param string $register The register ID + * @param string $organizationSchema The organization schema ID + * @return void + */ + private function updateOrganisatieObjectOwner(object $organisatieObject, object $organisationEntity, string $register, string $organizationSchema): void + { + try { + $organisatieId = $organisatieObject->getUuid(); + $organisationEntityUuid = $organisationEntity->getUuid(); + + $this->logger->info('OrganizationSyncService: Updating organisatie object owner', [ + 'organisatieId' => $organisatieId, + 'organisationEntityUuid' => $organisationEntityUuid, + 'register' => $register, + 'schema' => $organizationSchema + ]); + + // Get the current object data + $currentObject = $organisatieObject->getObject(); + + // Get current @self metadata or create new + $selfMetadata = $currentObject['@self'] ?? []; + + // Update the owner field to the organisation entity UUID + $selfMetadata['owner'] = $organisationEntityUuid; + + // Update the object with the new @self metadata + $currentObject['@self'] = $selfMetadata; + $organisatieObject->setObject($currentObject); + + // Save the updated object using ObjectService + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + $objectService->saveObject( + object: $organisatieObject, + register: $register, + schema: $organizationSchema, + rbac: false, + multi: false + ); + + $this->logger->info('OrganizationSyncService: Successfully updated organisatie object owner', [ + 'organisatieId' => $organisatieId, + 'organisationEntityUuid' => $organisationEntityUuid, + 'ownerSet' => $selfMetadata['owner'] + ]); + + } catch (\Exception $e) { + $this->logger->error('OrganizationSyncService: Failed to update organisatie object owner', [ + 'organisatieId' => $organisatieObject->getUuid(), + 'organisationEntityUuid' => $organisationEntity->getUuid(), + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + } + + /** + * Updates the contactpersoon object's @self metadata to set owner to the user UID + * + * @param object $contactObject The contactpersoon object to update + * @param string $userUID The user UID to set as owner + * @param string $register The register ID + * @param string $contactSchema The contact schema ID + * @return void + */ + private function updateContactpersoonObjectOwner(object $contactObject, string $userUID, string $register, string $contactSchema): void + { + try { + $contactId = $contactObject->getUuid(); + + $this->logger->info('OrganizationSyncService: Updating contactpersoon object owner', [ + 'contactId' => $contactId, + 'userUID' => $userUID, + 'register' => $register, + 'schema' => $contactSchema + ]); + + // Get the current object data + $currentObject = $contactObject->getObject(); + + // Get current @self metadata or create new + $selfMetadata = $currentObject['@self'] ?? []; + + // Update the owner field to the user UID + $selfMetadata['owner'] = $userUID; + + // Update the object with the new @self metadata + $currentObject['@self'] = $selfMetadata; + $contactObject->setObject($currentObject); + + // Save the updated object using ObjectService + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + $objectService->saveObject( + object: $contactObject, + register: $register, + schema: $contactSchema, + rbac: false, + multi: false + ); + + $this->logger->info('OrganizationSyncService: Successfully updated contactpersoon object owner', [ + 'contactId' => $contactId, + 'userUID' => $userUID, + 'ownerSet' => $selfMetadata['owner'] + ]); + + } catch (\Exception $e) { + $this->logger->error('OrganizationSyncService: Failed to update contactpersoon object owner', [ + 'contactId' => $contactObject->getUuid(), + 'userUID' => $userUID, + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + } } diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index 337f4318..68b10d7f 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -627,7 +627,8 @@ "objectDescriptionField": "beschrijvingLang", "objectImageField": "logo", "allowFiles": true, - "allowedTags": ["DPIA", "Handleiding"] + "allowedTags": ["DPIA", "Handleiding"], + "autoPublish": true } }, "dienst": { @@ -882,7 +883,8 @@ "ISO-27001", "ISO-16075", "Verklaring van toepasselijkheid" - ] + ], + "autoPublish": true } }, "kwetsbaarheid": { @@ -1037,12 +1039,12 @@ "contactpersoon": { "uri": null, "slug": "contactpersoon", - "title": "Contact Persoon", + "title": "Contactpersoon", "description": "Contactgegevens van een persoon", "version": "0.0.22", "summary": "", "icon": "AccountMultiple", - "required": ["organisatie", "e-mailadres"], + "required": ["e-mailadres"], "properties": { "voornaam": { "type": "string", @@ -1111,7 +1113,6 @@ "handling": "related-object" }, "$ref": "#/components/schemas/organisatie", - "required": true, "order": 9 }, "username": { @@ -1214,9 +1215,7 @@ "enum": [ "Aanbod-beheerder", "Gebruik-beheerder", - "Gebruik-raadpleger", "Functioneel-beheerder", - "VNG-raadpleger", "Organisatie-beheerder" ], "example": "Bijvoorbeeld: [\"Aanbod-beheerder\", \"Functioneel-beheerder\"]" @@ -1321,7 +1320,8 @@ "deleted": null, "configuration": { "objectNameField": "achternaam", - "objectDescriptionField": "functie" + "objectDescriptionField": "functie", + "autoPublish": true } }, "organisatie": { @@ -1705,7 +1705,8 @@ "title": "Status", "type": "string", "default": "concept", - "visible": true, + "visible": false, + "hideOnCollection": true, "facetable": false, "order": 17, "minLength": null, @@ -1734,7 +1735,8 @@ "description": "Type samenwerking van de organisatie", "title": "Samenwerkingstype", "type": "string", - "visible": true, + "visible": false, + "hideOnCollection": true, "facetable": true, "order": 14, "minLength": null, @@ -1870,7 +1872,8 @@ "BRL-certificaten", "CE-markering documenten", "Aanbestedingsdocumentatie" - ] + ], + "autoPublish": true } }, "gebruik": { @@ -2705,7 +2708,8 @@ "deleted": null, "configuration": { "objectNameField": "type", - "objectDescriptionField": "beschrijvingKort" + "objectDescriptionField": "beschrijvingKort", + "autoPublish": true } }, "beoordeeling": { @@ -4604,7 +4608,8 @@ "objectDescriptionField": "beschrijvingLang", "objectImageField": "logo", "allowFiles": true, - "allowedTags": ["Documentatie", "Handleiding", "Technische specificatie"] + "allowedTags": ["Documentatie", "Handleiding", "Technische specificatie"], + "autoPublish": true } }, "compliancy": { @@ -4680,7 +4685,8 @@ "objectSummaryField": "standaardversie", "objectDescriptionField": "module", "allowFiles": true, - "allowedTags": ["testraport"] + "allowedTags": ["testraport"], + "autoPublish": true } }, "moduleVersie": { @@ -4801,7 +4807,8 @@ "configuration": { "objectNameField": "versie", "objectSummaryField": "beschrijvingKort", - "objectDescriptionField": "beschrijvingLang" + "objectDescriptionField": "beschrijvingLang", + "autoPublish": true } } }, diff --git a/test_aangeboden_gebruik_api.sh b/test_aangeboden_gebruik_api.sh new file mode 100644 index 00000000..61f3f4dc --- /dev/null +++ b/test_aangeboden_gebruik_api.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# Test script for AangebodenGebruik API endpoints +# This script tests the new custom objects endpoint for managing gebruiks objects + +echo "===================================" +echo "Testing AangebodenGebruik API" +echo "===================================" + +BASE_URL="http://localhost/index.php/apps/softwarecatalog/api/aangeboden-gebruik" +AUTH="admin:admin" + +echo "" +echo "1. Testing API Documentation endpoint..." +echo "GET ${BASE_URL}/docs" +docker-compose exec nextcloud curl -s -X GET "${BASE_URL}/docs" \ + -H "Content-Type: application/json" \ + -u ${AUTH} | jq . + +echo "" +echo "2. Testing Afnemer Gebruiks endpoint..." +echo "GET ${BASE_URL}/afnemer" +docker-compose exec nextcloud curl -s -X GET "${BASE_URL}/afnemer" \ + -H "Content-Type: application/json" \ + -u ${AUTH} | jq . + +echo "" +echo "3. Testing Afnemer Gebruiks endpoint with limit..." +echo "GET ${BASE_URL}/afnemer?limit=5" +docker-compose exec nextcloud curl -s -X GET "${BASE_URL}/afnemer?limit=5" \ + -H "Content-Type: application/json" \ + -u ${AUTH} | jq . + +echo "" +echo "4. Testing Deelnemers Gebruiks endpoint..." +echo "GET ${BASE_URL}/deelnemers" +docker-compose exec nextcloud curl -s -X GET "${BASE_URL}/deelnemers" \ + -H "Content-Type: application/json" \ + -u ${AUTH} | jq . + +echo "" +echo "5. Testing Deelnemers Gebruiks endpoint with status filter..." +echo "GET ${BASE_URL}/deelnemers?status=actief" +docker-compose exec nextcloud curl -s -X GET "${BASE_URL}/deelnemers?status=actief" \ + -H "Content-Type: application/json" \ + -u ${AUTH} | jq . + +echo "" +echo "6. Testing Set @self Property endpoint (will need a valid UUID)..." +echo "Note: Replace USAGE_UUID with an actual usage UUID from the previous responses" +echo "PUT ${BASE_URL}/USAGE_UUID/set-self" +echo "Example command (uncomment and replace UUID):" +echo "# docker-compose exec nextcloud curl -s -X PUT \"${BASE_URL}/USAGE_UUID/set-self\" \\" +echo "# -H \"Content-Type: application/json\" \\" +echo "# -u ${AUTH} | jq ." + +echo "" +echo "===================================" +echo "Testing Complete" +echo "===================================" +echo "" +echo "To test the @self property update:" +echo "1. Look for a 'gebruikId' or 'id' in the afnemer response above" +echo "2. Replace USAGE_UUID in the curl command with that ID" +echo "3. Run the command manually" +echo "" +echo "Expected behaviors:" +echo "- Afnemer endpoint: Returns gebruiks where active org is the consumer" +echo "- Deelnemers endpoint: Returns gebruiks where active org is a participant" +echo "- Set @self endpoint: Only works if active org is the afnemer" + + diff --git a/website/docs/aangeboden-gebruik-api.md b/website/docs/aangeboden-gebruik-api.md new file mode 100644 index 00000000..a94e0a60 --- /dev/null +++ b/website/docs/aangeboden-gebruik-api.md @@ -0,0 +1,250 @@ +# AangebodenGebruik API Documentation + +## Overview + +The AangebodenGebruik API provides endpoints to manage gebruiks (usage) objects where the active organization is involved either as an afnemer (consumer) or in the deelnemers (participants) list. This API allows organizations to: + +1. Retrieve gebruiks objects where they are the afnemer +2. Retrieve gebruiks objects where they are listed in deelnemers +3. Update the '@self' property of a gebruik to claim ownership (only if they are the afnemer) + +## Base URL + +All endpoints are prefixed with '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/api/aangeboden-gebruik' + +## Authentication + +These endpoints require user authentication and use standard Nextcloud RBAC (Role-Based Access Control) for the afnemer endpoints. The deelnemers endpoint uses RBAC-disabled search to find participation records. + +## Endpoints + +### 1. Get Gebruiks Where Active Organization is Afnemer + +**Endpoint:** 'GET /api/aangeboden-gebruik/afnemer' + +**Description:** Returns all gebruiks objects where the active organization is the afnemer (consumer). + +**Query Parameters:** +- 'limit' (integer, optional): Maximum number of results to return +- 'offset' (integer, optional): Number of results to skip for pagination +- 'status' (string, optional): Filter by usage status +- 'product' (string, optional): Filter by product ID +- 'startDate' (string, optional): Filter by start date (ISO 8601 format) +- 'endDate' (string, optional): Filter by end date (ISO 8601 format) + +**Response Example:** +```json +{ + 'success': true, + 'gebruiks': [ + { + 'id': 'usage-uuid-123', + 'afnemer': 'org-uuid', + 'product': 'product-uuid', + 'status': 'actief', + '_filter_type': 'afnemer', + '_schema_id': 'schema-id' + } + ], + 'count': 1, + 'filter_type': 'afnemer', + 'organisation': 'org-uuid' +} +``` + +### 2. Get Gebruiks Where Active Organization is in Deelnemers + +**Endpoint:** 'GET /api/aangeboden-gebruik/deelnemers' + +**Description:** Returns all gebruiks objects where the active organization appears in the deelnemers (participants) array. + +**Query Parameters:** +- 'limit' (integer, optional): Maximum number of results to return +- 'offset' (integer, optional): Number of results to skip for pagination +- 'status' (string, optional): Filter by usage status +- 'product' (string, optional): Filter by product ID +- 'startDate' (string, optional): Filter by start date (ISO 8601 format) +- 'endDate' (string, optional): Filter by end date (ISO 8601 format) + +**Response Example:** +```json +{ + 'success': true, + 'gebruiks': [ + { + 'id': 'usage-uuid-456', + 'afnemer': 'other-org-uuid', + 'deelnemers': ['org-uuid', 'another-org-uuid'], + 'product': 'product-uuid', + 'status': 'actief', + '_filter_type': 'deelnemers', + '_schema_id': 'schema-id' + } + ], + 'count': 1, + 'filter_type': 'deelnemers', + 'organisation': 'org-uuid' +} +``` + +### 3. Set Gebruik @self Property to Active Organization + +**Endpoint:** 'PUT /api/aangeboden-gebruik/{gebruikId}/set-self' + +**Description:** Sets the '@self.organisation' property of a specific gebruik object to the active organization. This operation is only allowed if the active organization is the afnemer for that gebruik. + +**Path Parameters:** +- 'gebruikId' (string, required): The UUID of the gebruik object to update + +**Security:** This endpoint verifies that the active organization is the afnemer before allowing the update. + +**Response Example (Success):** +```json +{ + 'success': true, + 'message': 'Gebruik @self property updated successfully', + 'gebruik': { + 'id': 'usage-uuid-123', + 'afnemer': 'org-uuid', + '@self': { + 'organisation': 'org-uuid', + 'register': 'register-id', + 'schema': 'schema-id' + } + }, + 'updated_fields': ['@self.organisation'] +} +``` + +**Response Example (Permission Denied):** +```json +{ + 'success': false, + 'error': 'Operation not allowed: active organization is not the afnemer', + 'gebruik': null +} +``` + +### 4. Get API Documentation + +**Endpoint:** 'GET /api/aangeboden-gebruik/docs' + +**Description:** Returns comprehensive API documentation including all endpoints, parameters, and examples. + +## Error Handling + +The API uses standard HTTP status codes: + +- **200 OK**: Request successful +- **400 Bad Request**: Invalid parameters or missing required fields +- **403 Forbidden**: Operation not allowed (e.g., organization is not afnemer for @self update) +- **404 Not Found**: Gebruik object not found +- **500 Internal Server Error**: Server-side error occurred + +## Security Model + +### Afnemer Filtering +- Uses standard RBAC filtering based on organization association +- Only returns gebruiks where the active organization has proper access rights + +### Deelnemers Filtering +- Uses RBAC-disabled search to find participation records +- Searches across all gebruiks to find those where the active organization appears in the deelnemers array + +### @self Update Permission +- Verifies that the active organization is the afnemer before allowing updates +- Prevents unauthorized modification of gebruik ownership + +## Usage Examples + +### Get Gebruiks as Afnemer with Pagination +```bash +curl -X GET 'http://localhost/index.php/apps/softwarecatalog/api/aangeboden-gebruik/afnemer?limit=10&offset=0' \ + -H 'Content-Type: application/json' \ + -u admin:admin +``` + +### Get Gebruiks as Deelnemers with Status Filter +```bash +curl -X GET 'http://localhost/index.php/apps/softwarecatalog/api/aangeboden-gebruik/deelnemers?status=actief' \ + -H 'Content-Type: application/json' \ + -u admin:admin +``` + +### Set Gebruik @self Property +```bash +curl -X PUT 'http://localhost/index.php/apps/softwarecatalog/api/aangeboden-gebruik/usage-uuid-123/set-self' \ + -H 'Content-Type: application/json' \ + -u admin:admin +``` + +## Docker Testing Commands + +When testing in the Docker environment, use the docker-compose exec command: + +### Test Afnemer Endpoint +```bash +cd /home/rubenlinde/nextcloud-docker-dev +docker-compose exec nextcloud curl -X GET 'http://localhost/index.php/apps/softwarecatalog/api/aangeboden-gebruik/afnemer' \ + -H 'Content-Type: application/json' \ + -u admin:admin +``` + +### Test Deelnemers Endpoint +```bash +docker-compose exec nextcloud curl -X GET 'http://localhost/index.php/apps/softwarecatalog/api/aangeboden-gebruik/deelnemers' \ + -H 'Content-Type: application/json' \ + -u admin:admin +``` + +### Test Set @self Property +```bash +docker-compose exec nextcloud curl -X PUT 'http://localhost/index.php/apps/softwarecatalog/api/aangeboden-gebruik/USAGE_UUID/set-self' \ + -H 'Content-Type: application/json' \ + -u admin:admin +``` + +Replace 'USAGE_UUID' with an actual usage object UUID from your system. + +## Configuration Requirements + +The AangebodenGebruik API relies on the following configuration: + +1. **OpenRegister App**: Must be installed and available +2. **AMEF Configuration**: Must be configured with proper register_id and gebruik_schemas +3. **User Organization**: Active user must have an organization associated with their account + +## Technical Implementation + +### Service Layer +- **AangebodenGebruikService**: Handles business logic for filtering gebruiks and updating @self properties +- **SettingsService**: Provides configuration data for gebruiks schemas and register IDs + +### Controller Layer +- **AangebodenGebruikController**: Handles HTTP requests and responses, parameter validation, and error handling + +### Data Flow +1. Controller receives HTTP request and parses parameters +2. Service layer retrieves configuration and current organization +3. Service queries OpenRegister with appropriate filters (RBAC enabled/disabled) +4. Results are processed and returned via controller + +## Troubleshooting + +### Common Issues + +**No results returned**: +- Verify that the active user has an organization configured +- Check that gebruiks objects exist with proper afnemer or deelnemers relationships +- Ensure AMEF configuration includes valid gebruik_schemas + +**Permission denied on @self update**: +- Verify that the active organization is the afnemer for the specific gebruik +- Check that the gebruik object exists and is accessible + +**Configuration errors**: +- Ensure OpenRegister app is installed and enabled +- Verify AMEF configuration in SoftwareCatalog settings +- Check that register_id and schema IDs are correctly configured + +